Repository: MessagePack-CSharp/MessagePack-CSharp

Issue:
Code analysis and fix don't work for Nullable and generics in 3.0 prerelease
#### Bug description

1. Code fix doesn't add `MessagePackObjectAttribute` and `KeyAttribute` to `A`
```cs
public struct A
{
    public int Id;
}

[MessagePackObject]
public class B
{
    [Key(0)]
    public A? Member;
}
```

2. Code analysis doesn't report that `A` is missing `MessagePackObjectAttribute`
```cs
public struct A
{
    public int Id;
}

[MessagePackObject]
public class B
{
    [Key(0)]
    public List<A> Member;
}
```

- Version used: 3.0.238-rc.1
- Runtime: net6.0

#### Additional context

v2.5.168 works fine for both cases.



Hints:

Patch:
diff --git a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
index 0ed26db03..067853098 100644
--- a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
+++ b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
@@ -190,7 +190,7 @@ public class TypeCollector
     private readonly ITypeSymbol? targetType;
 
     // visitor workspace:
-    private readonly Dictionary<ITypeSymbol, bool> alreadyCollected = new(SymbolEqualityComparer.Default);
+    private readonly HashSet<ITypeSymbol> alreadyCollected = new(SymbolEqualityComparer.Default);
     private readonly ImmutableSortedSet<ObjectSerializationInfo>.Builder collectedObjectInfo = ImmutableSortedSet.CreateBuilder<ObjectSerializationInfo>(ResolverRegisterInfoComparer.Default);
     private readonly ImmutableSortedSet<EnumSerializationInfo>.Builder collectedEnumInfo = ImmutableSortedSet.CreateBuilder<EnumSerializationInfo>(ResolverRegisterInfoComparer.Default);
     private readonly ImmutableSortedSet<GenericSerializationInfo>.Builder collectedGenericInfo = ImmutableSortedSet.CreateBuilder<GenericSerializationInfo>(ResolverRegisterInfoComparer.Default);
@@ -271,111 +271,92 @@ public FullModel Collect()
     }
 
     // Gate of recursive collect
-    private bool CollectCore(ITypeSymbol typeSymbol)
+    private void CollectCore(ITypeSymbol typeSymbol, ISymbol? callerSymbol = null)
     {
         this.cancellationToken.ThrowIfCancellationRequested();
 
-        if (this.alreadyCollected.TryGetValue(typeSymbol, out bool result))
+        if (!this.alreadyCollected.Add(typeSymbol))
         {
-            return result;
+            return;
         }
 
         var typeSymbolString = typeSymbol.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToString();
         if (!string.IsNullOrEmpty(typeSymbolString) && EmbeddedTypes.Contains(typeSymbolString))
         {
-            result = true;
-            this.alreadyCollected.Add(typeSymbol, result);
-            return result;
+            return;
         }
 
         FormattableType formattableType = new(typeSymbol, null);
         if (this.options.AssumedFormattableTypes.Contains(formattableType) || this.options.AssumedFormattableTypes.Contains(formattableType with { IsFormatterInSameAssembly = true }))
         {
-            result = true;
-            this.alreadyCollected.Add(typeSymbol, result);
-            return result;
+            return;
         }
 
         if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol)
         {
-            return RecursiveProtection(() => this.CollectArray((IArrayTypeSymbol)this.ToTupleUnderlyingType(arrayTypeSymbol)));
+            this.CollectArray((IArrayTypeSymbol)this.ToTupleUnderlyingType(arrayTypeSymbol), callerSymbol);
+            return;
         }
 
         if (typeSymbol is ITypeParameterSymbol)
         {
-            result = true;
-            this.alreadyCollected.Add(typeSymbol, result);
-            return result;
+            return;
         }
 
         if (!IsAllowAccessibility(typeSymbol))
         {
-            result = false;
-            this.alreadyCollected.Add(typeSymbol, result);
-            return result;
+            return;
         }
 
         if (!(typeSymbol is INamedTypeSymbol type))
         {
-            result = false;
-            this.alreadyCollected.Add(typeSymbol, result);
-            return result;
+            return;
         }
 
         var customFormatterAttr = typeSymbol.GetAttributes().FirstOrDefault(x => x.AttributeClass.ApproximatelyEqual(this.typeReferences.FormatterAttribute));
         if (customFormatterAttr != null)
         {
             this.CheckValidMessagePackFormatterAttribute(customFormatterAttr);
-            result = true;
-            this.alreadyCollected.Add(typeSymbol, result);
-            return result;
+            return;
         }
 
         if (type.EnumUnderlyingType != null)
         {
-            return RecursiveProtection(() => this.CollectEnum(type, type.EnumUnderlyingType));
+            this.CollectEnum(type, type.EnumUnderlyingType);
+            return;
         }
 
         if (type.IsGenericType)
         {
-            return RecursiveProtection(() => this.CollectGeneric((INamedTypeSymbol)this.ToTupleUnderlyingType(type)));
+            this.CollectGeneric((INamedTypeSymbol)this.ToTupleUnderlyingType(type), callerSymbol);
+            return;
         }
 
         if (type.Locations[0].IsInMetadata)
         {
-            result = true;
-            this.alreadyCollected.Add(typeSymbol, result);
-            return result;
+            return;
         }
 
         if (type.TypeKind == TypeKind.Interface || (type.TypeKind == TypeKind.Class && type.IsAbstract))
         {
-            return RecursiveProtection(() => this.CollectUnion(type));
+            this.CollectUnion(type);
+            return;
         }
 
-        return RecursiveProtection(() => this.CollectObject(type));
-
-        bool RecursiveProtection(Func<bool> func)
-        {
-            this.alreadyCollected.Add(typeSymbol, true);
-            bool result = func();
-            this.alreadyCollected[typeSymbol] = result;
-            return result;
-        }
+        this.CollectObject(type, callerSymbol);
     }
 
-    private bool CollectEnum(INamedTypeSymbol type, ISymbol enumUnderlyingType)
+    private void CollectEnum(INamedTypeSymbol type, ISymbol enumUnderlyingType)
     {
         this.collectedEnumInfo.Add(EnumSerializationInfo.Create(type, enumUnderlyingType, this.options.Generator.Resolver));
-        return true;
     }
 
-    private bool CollectUnion(INamedTypeSymbol type)
+    private void CollectUnion(INamedTypeSymbol type)
     {
         if (!options.IsGeneratingSource)
         {
             // In analyzer-only mode, this method doesn't work.
-            return true;
+            return;
         }
 
         ImmutableArray<TypedConstant>[] unionAttrs = type.GetAttributes().Where(x => x.AttributeClass.ApproximatelyEqual(this.typeReferences.UnionAttribute)).Select(x => x.ConstructorArguments).ToArray();
@@ -405,7 +386,6 @@ private bool CollectUnion(INamedTypeSymbol type)
             this.options.Generator.Resolver);
 
         this.collectedUnionInfo.Add(info);
-        return true;
     }
 
     private void CollectGenericUnion(INamedTypeSymbol type)
@@ -420,7 +400,7 @@ private void CollectGenericUnion(INamedTypeSymbol type)
         do
         {
             var x = enumerator.Current;
-            if (x[1] is { Value: INamedTypeSymbol unionType } && !this.alreadyCollected.ContainsKey(unionType))
+            if (x[1] is { Value: INamedTypeSymbol unionType } && !this.alreadyCollected.Contains(unionType))
             {
                 this.CollectCore(unionType);
             }
@@ -428,18 +408,15 @@ private void CollectGenericUnion(INamedTypeSymbol type)
         while (enumerator.MoveNext());
     }
 
-    private bool CollectArray(IArrayTypeSymbol array)
+    private void CollectArray(IArrayTypeSymbol array, ISymbol? callerSymbol)
     {
         ITypeSymbol elemType = array.ElementType;
 
-        if (!this.CollectCore(elemType))
-        {
-            return false;
-        }
+        this.CollectCore(elemType, callerSymbol);
 
         if (elemType is ITypeParameterSymbol || (elemType is INamedTypeSymbol symbol && IsOpenGenericTypeRecursively(symbol)))
         {
-            return true;
+            return;
         }
 
         QualifiedTypeName elementTypeName = QualifiedTypeName.Create(elemType);
@@ -454,7 +431,7 @@ private bool CollectArray(IArrayTypeSymbol array)
         if (formatterName is null)
         {
             ////this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.AotArrayRankTooHigh));
-            return false;
+            return;
         }
 
         var info = ResolverRegisterInfo.CreateArray(array, this.options.Generator.Resolver) with
@@ -468,7 +445,6 @@ private bool CollectArray(IArrayTypeSymbol array)
             },
         };
         this.collectedArrayInfo.Add(info);
-        return true;
     }
 
     private ITypeSymbol ToTupleUnderlyingType(ITypeSymbol typeSymbol)
@@ -493,7 +469,7 @@ private ITypeSymbol ToTupleUnderlyingType(ITypeSymbol typeSymbol)
         return namedType;
     }
 
-    private bool CollectGeneric(INamedTypeSymbol type)
+    private void CollectGeneric(INamedTypeSymbol type, ISymbol? callerSymbol)
     {
         INamedTypeSymbol typeDefinition = type.ConstructUnboundGenericType();
         var genericTypeDefinitionString = typeDefinition.ToDisplayString();
@@ -506,21 +482,18 @@ private bool CollectGeneric(INamedTypeSymbol type)
         // special case
         if (fullName == "global::System.ArraySegment<byte>" || fullName == "global::System.ArraySegment<byte>?")
         {
-            return true;
+            return;
         }
 
         // nullable
         if (genericTypeDefinitionString == "T?")
         {
             var firstTypeArgument = type.TypeArguments[0];
-            if (!this.CollectCore(firstTypeArgument))
-            {
-                return false;
-            }
+            this.CollectCore(firstTypeArgument, callerSymbol);
 
             if (EmbeddedTypes.Contains(firstTypeArgument.ToString()!))
             {
-                return true;
+                return;
             }
 
             if (!isOpenGenericType)
@@ -537,7 +510,7 @@ private bool CollectGeneric(INamedTypeSymbol type)
                 this.collectedGenericInfo.Add(info);
             }
 
-            return true;
+            return;
         }
 
         // collection
@@ -546,14 +519,14 @@ private bool CollectGeneric(INamedTypeSymbol type)
         {
             foreach (ITypeSymbol item in type.TypeArguments)
             {
-                this.CollectCore(item);
+                this.CollectCore(item, callerSymbol);
             }
 
             GenericSerializationInfo info;
 
             if (isOpenGenericType)
             {
-                return true;
+                return;
             }
             else
             {
@@ -575,7 +548,7 @@ private bool CollectGeneric(INamedTypeSymbol type)
 
             if (genericTypeDefinitionString != "System.Linq.ILookup<,>")
             {
-                return true;
+                return;
             }
 
             formatterName = KnownGenericTypes["System.Linq.IGrouping<,>"];
@@ -607,17 +580,17 @@ private bool CollectGeneric(INamedTypeSymbol type)
                 Formatter = enumerableInfo.Formatter with { Name = formatterName },
             };
             this.collectedGenericInfo.Add(enumerableInfo);
-            return true;
+            return;
         }
 
         if (type.AllInterfaces.FirstOrDefault(x => !x.IsUnboundGenericType && x.ConstructUnboundGenericType() is { Name: nameof(ICollection<int>) }) is INamedTypeSymbol collectionIface
             && type.InstanceConstructors.Any(ctor => ctor.Parameters.Length == 0))
         {
-            this.CollectCore(collectionIface.TypeArguments[0]);
+            this.CollectCore(collectionIface.TypeArguments[0], callerSymbol);
 
             if (isOpenGenericType)
             {
-                return true;
+                return;
             }
             else
             {
@@ -633,6 +606,7 @@ private bool CollectGeneric(INamedTypeSymbol type)
                 };
 
                 this.collectedGenericInfo.Add(info);
+                return;
             }
         }
 
@@ -640,29 +614,23 @@ private bool CollectGeneric(INamedTypeSymbol type)
         if (type.IsDefinition)
         {
             this.CollectGenericUnion(type);
-            return this.CollectObject(type);
+            this.CollectObject(type, callerSymbol);
         }
         else
         {
             // Collect substituted types for the properties and fields.
             // NOTE: It is used to register formatters from nested generic type.
             //       However, closed generic types such as `Foo<string>` are not registered as a formatter.
-            this.GetObjectInfo(type);
+            this.GetObjectInfo(type, callerSymbol);
 
             // Collect generic type definition, that is not collected when it is defined outside target project.
-            if (!this.CollectCore(type.OriginalDefinition))
-            {
-                return false;
-            }
+            this.CollectCore(type.OriginalDefinition, callerSymbol);
         }
 
         // Collect substituted types for the type parameters (e.g. Bar in Foo<Bar>)
         foreach (var item in type.TypeArguments)
         {
-            if (!this.CollectCore(item))
-            {
-                return false;
-            }
+            this.CollectCore(item, callerSymbol);
         }
 
         if (!isOpenGenericType)
@@ -676,22 +644,18 @@ static ImmutableArray<GenericTypeParameterInfo> GetTypeParameters(QualifiedTypeN
 
         static ImmutableArray<QualifiedTypeName> GetTypeArguments(QualifiedTypeName qtn)
             => qtn is QualifiedNamedTypeName named ? named.TypeArguments : ImmutableArray<QualifiedTypeName>.Empty;
-
-        return true;
     }
 
-    private bool CollectObject(INamedTypeSymbol type)
+    private void CollectObject(INamedTypeSymbol type, ISymbol? callerSymbol)
     {
-        ObjectSerializationInfo? info = this.GetObjectInfo(type);
+        ObjectSerializationInfo? info = this.GetObjectInfo(type, callerSymbol);
         if (info is not null)
         {
             this.collectedObjectInfo.Add(info);
         }
-
-        return info is not null;
     }
 
-    private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttribute)
+    private void CheckValidMessagePackFormatterAttribute(AttributeData formatterAttribute)
     {
         if (formatterAttribute.ConstructorArguments[0].Value is ITypeSymbol formatterType)
         {
@@ -703,14 +667,10 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
                 ImmutableDictionary<string, string?> typeInfo = ImmutableDictionary.Create<string, string?>().Add("type", formatterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
                 this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.MessageFormatterMustBeMessagePackFormatter, location, typeInfo));
             }
-
-            return isMessagePackFormatter;
         }
-
-        return false;
     }
 
-    private ObjectSerializationInfo? GetObjectInfo(INamedTypeSymbol formattedType)
+    private ObjectSerializationInfo? GetObjectInfo(INamedTypeSymbol formattedType, ISymbol? callerSymbol)
     {
         var isClass = !formattedType.IsValueType;
         bool nestedFormatterRequired = false;
@@ -851,7 +811,7 @@ void ReportNonUniqueNameIfApplicable(ISymbol item, string stringKey)
 
                             if (specialFormatter is null)
                             {
-                                this.CollectCore(item.Type); // recursive collect
+                                this.CollectCore(item.Type, item); // recursive collect
                             }
                         }
 
@@ -882,7 +842,7 @@ void ReportNonUniqueNameIfApplicable(ISymbol item, string stringKey)
 
                             if (specialFormatter is null)
                             {
-                                this.CollectCore(item.Type); // recursive collect
+                                this.CollectCore(item.Type, item); // recursive collect
                             }
                         }
 
@@ -1053,19 +1013,7 @@ void ReportNonUniqueNameIfApplicable(ISymbol item, string stringKey)
                     if (messagePackFormatter == null)
                     {
                         // recursive collect
-                        if (!this.CollectCore(item.Type))
-                        {
-                            var syntax = item.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax();
-
-                            var typeSyntax = (syntax as PropertyDeclarationSyntax)?.Type
-                                ?? (syntax as ParameterSyntax)?.Type; // for primary constructor
-
-                            // TODO: add the declaration of the referenced type as an additional location.
-                            this.reportDiagnostic?.Invoke(Diagnostic.Create(
-                                MsgPack00xMessagePackAnalyzer.TypeMustBeMessagePackObject,
-                                typeSyntax?.GetLocation(),
-                                item.Type.ToDisplayString(ShortTypeNameFormat)));
-                        }
+                        this.CollectCore(item.Type, item);
                     }
                 }
             }
@@ -1198,18 +1146,7 @@ void ReportNonUniqueNameIfApplicable(ISymbol item, string stringKey)
                     if (messagePackFormatter == null)
                     {
                         // recursive collect
-                        if (!this.CollectCore(item.Type))
-                        {
-                            var syntax = item.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax();
-
-                            var typeSyntax = ((syntax as VariableDeclaratorSyntax)?.Parent as VariableDeclarationSyntax)?.Type;
-
-                            // TODO: add the declaration of the referenced type as an additional location.
-                            this.reportDiagnostic?.Invoke(Diagnostic.Create(
-                                MsgPack00xMessagePackAnalyzer.TypeMustBeMessagePackObject,
-                                typeSyntax?.GetLocation(),
-                                item.Type.ToDisplayString(ShortTypeNameFormat)));
-                        }
+                        this.CollectCore(item.Type, item);
                     }
                 }
             }
@@ -1372,6 +1309,15 @@ void ReportNonUniqueNameIfApplicable(ISymbol item, string stringKey)
 
         if (contractAttr is null)
         {
+            if (formattedType.IsDefinition)
+            {
+                Location location = callerSymbol is not null ? callerSymbol.Locations[0] : formattedType.Locations[0];
+                var targetName = callerSymbol is not null ? callerSymbol.ContainingType.Name + "." + callerSymbol.Name : formattedType.Name;
+
+                ImmutableDictionary<string, string?> typeInfo = ImmutableDictionary.Create<string, string?>().Add("type", formattedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+                this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.TypeMustBeMessagePackObject, location, typeInfo, targetName));
+            }
+
             // Indicate to our caller that we don't have a valid object.
             return null;
         }


Test patch:
diff --git a/tests/MessagePack.SourceGenerator.Tests/MsgPack00xAnalyzerTests.cs b/tests/MessagePack.SourceGenerator.Tests/MsgPack00xAnalyzerTests.cs
index a0191b047..d39d74ef9 100644
--- a/tests/MessagePack.SourceGenerator.Tests/MsgPack00xAnalyzerTests.cs
+++ b/tests/MessagePack.SourceGenerator.Tests/MsgPack00xAnalyzerTests.cs
@@ -318,7 +318,7 @@ public class Foo
             public class Bar
             {
                 [MessagePack.Key(0)]
-                public {|MsgPack003:Foo|} Member { get; set; }
+                public Foo {|MsgPack003:Member|} { get; set; }
             }
             """;
 
@@ -361,7 +361,7 @@ public class Foo
             public class Bar
             {
                 [MessagePack.Key(0)]
-                public {|MsgPack003:Foo|} Member;
+                public Foo {|MsgPack003:Member|};
             }
             """;
 
@@ -390,6 +390,92 @@ public class Bar
         }.RunAsync();
     }
 
+    [Fact]
+    public async Task AddAttributeToType_Nullable()
+    {
+        // Don't use Preamble because we want to test that it works without a using statement at the top.
+        string input = /* lang=c#-test */ """
+            public struct Foo
+            {
+                public string Member;
+            }
+
+            [MessagePack.MessagePackObject]
+            public class Bar
+            {
+                [MessagePack.Key(0)]
+                public Foo? {|MsgPack003:Member|};
+            }
+            """;
+
+        string output = /* lang=c#-test */ """
+            [MessagePack.MessagePackObject]
+            public struct Foo
+            {
+                [MessagePack.Key(0)]
+                public string Member;
+            }
+
+            [MessagePack.MessagePackObject]
+            public class Bar
+            {
+                [MessagePack.Key(0)]
+                public Foo? Member;
+            }
+            """;
+
+        await new VerifyCS.Test
+        {
+            TestCode = input,
+            FixedCode = output,
+            MarkupOptions = MarkupOptions.UseFirstDescriptor,
+            CodeActionEquivalenceKey = MessagePackCodeFixProvider.AddKeyAttributeEquivanceKey,
+        }.RunAsync();
+    }
+
+    [Fact]
+    public async Task AddAttributeToType_Generic()
+    {
+        // Don't use Preamble because we want to test that it works without a using statement at the top.
+        string input = /* lang=c#-test */ """
+            public struct Foo
+            {
+                public string Member;
+            }
+
+            [MessagePack.MessagePackObject]
+            public class Bar
+            {
+                [MessagePack.Key(0)]
+                public System.Collections.Generic.List<Foo> {|MsgPack003:Member|};
+            }
+            """;
+
+        string output = /* lang=c#-test */ """
+            [MessagePack.MessagePackObject]
+            public struct Foo
+            {
+                [MessagePack.Key(0)]
+                public string Member;
+            }
+
+            [MessagePack.MessagePackObject]
+            public class Bar
+            {
+                [MessagePack.Key(0)]
+                public System.Collections.Generic.List<Foo> Member;
+            }
+            """;
+
+        await new VerifyCS.Test
+        {
+            TestCode = input,
+            FixedCode = output,
+            MarkupOptions = MarkupOptions.UseFirstDescriptor,
+            CodeActionEquivalenceKey = MessagePackCodeFixProvider.AddKeyAttributeEquivanceKey,
+        }.RunAsync();
+    }
+
     [Fact]
     public async Task AddAttributeToTypeForRecord1()
     {
@@ -404,7 +490,7 @@ public class Foo
 public record Bar
 {
     [MessagePack.Key(0)]
-    public {|MsgPack003:Foo|} Member { get; set; }
+    public Foo {|MsgPack003:Member|} { get; set; }
 }
 ";
 
@@ -447,7 +533,7 @@ public record Foo
 public record Bar
 {
     [MessagePack.Key(0)]
-    public {|MsgPack003:Foo|} Member { get; set; }
+    public Foo {|MsgPack003:Member|} { get; set; }
 }
 ";
 
@@ -487,7 +573,7 @@ public class Foo
 }
 
 [MessagePack.MessagePackObject]
-public record Bar([property: MessagePack.Key(0)] {|MsgPack003:Foo|} Member);
+public record Bar([property: MessagePack.Key(0)] Foo {|MsgPack003:Member|});
 
 namespace System.Runtime.CompilerServices
 {
@@ -590,7 +676,7 @@ public class Foo<T>
             public class Bar
             {
                 [Key(0)]
-                public {|MsgPack003:Foo<int>|} MemberUserGeneric { get; set; }
+                public Foo<int> {|MsgPack003:MemberUserGeneric|} { get; set; }
 
                 [Key(1)]
                 public System.Collections.Generic.List<int> MemberKnownGeneric { get; set; }