Repository: MessagePack-CSharp/MessagePack-CSharp

Issue:
MsgPack011 issues when flagging nesting types
When MsgPack011 reports a diagnostic on a nest_ing_ type, it works at the command line. But in the IDE, the error tends to disappear from the error list, and the code fix isn't offered in the quick actions menu.

A unit test to reproduce the problem throws with "Code fix is attempting to provide a fix for a non-local analyzer diagnostic".


Hints:

Patch:
diff --git a/src/MessagePack.Analyzers.CodeFixes/CodeFixes/FormatterCodeFixProvider.cs b/src/MessagePack.Analyzers.CodeFixes/CodeFixes/FormatterCodeFixProvider.cs
index 3c52fa540..9e37611da 100644
--- a/src/MessagePack.Analyzers.CodeFixes/CodeFixes/FormatterCodeFixProvider.cs
+++ b/src/MessagePack.Analyzers.CodeFixes/CodeFixes/FormatterCodeFixProvider.cs
@@ -1,6 +1,8 @@
 ﻿// Copyright (c) All contributors. All rights reserved.
 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
 
+using System.Diagnostics.CodeAnalysis;
+
 namespace MessagePack.Analyzers.CodeFixes;
 
 [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SimpleBaseTypeSyntax)), Shared]
@@ -75,9 +77,21 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
 
     private static Task<Document> AddPartialModifierAsync(Document document, SyntaxNode syntaxRoot, BaseTypeDeclarationSyntax typeDecl, Diagnostic diagnostic, CancellationToken cancellationToken)
     {
-        SyntaxNode? modifiedSyntax = syntaxRoot.ReplaceNode(
-            typeDecl,
-            typeDecl.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword)));
+        SyntaxNode? modifiedSyntax = syntaxRoot;
+        if (TryAddPartialModifier(typeDecl, out BaseTypeDeclarationSyntax? modified))
+        {
+            modifiedSyntax = modifiedSyntax.ReplaceNode(typeDecl, modified);
+        }
+
+        foreach (Location addlLocation in diagnostic.AdditionalLocations)
+        {
+            BaseTypeDeclarationSyntax? addlType = modifiedSyntax.FindNode(addlLocation.SourceSpan) as BaseTypeDeclarationSyntax;
+            if (addlType is not null && TryAddPartialModifier(addlType, out modified))
+            {
+                modifiedSyntax = modifiedSyntax.ReplaceNode(addlType, modified);
+            }
+        }
+
         document = document.WithSyntaxRoot(modifiedSyntax);
         return Task.FromResult(document);
     }
@@ -145,4 +159,16 @@ private static SyntaxTokenList AddInternalVisibility(SyntaxTokenList modifiers)
         SyntaxToken ReplaceModifier(SyntaxToken original) => internalKeywordToken.WithLeadingTrivia(original.LeadingTrivia).WithTrailingTrivia(original.TrailingTrivia);
         SyntaxTokenList ReplaceModifierInList(int index) => modifiers.Replace(modifiers[index], ReplaceModifier(modifiers[index]));
     }
+
+    private static bool TryAddPartialModifier(BaseTypeDeclarationSyntax typeDeclaration, [NotNullWhen(true)] out BaseTypeDeclarationSyntax? modified)
+    {
+        if (typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword))
+        {
+            modified = null;
+            return false;
+        }
+
+        modified = typeDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword));
+        return true;
+    }
 }
diff --git a/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs b/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs
index 1c2d5e612..fe898a4d8 100644
--- a/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs
+++ b/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs
@@ -25,10 +25,10 @@ public class MsgPack00xMessagePackAnalyzer : DiagnosticAnalyzer
 
     internal const string Category = "Usage";
 
-    internal const string MessagePackObjectAttributeShortName = Constants.MessagePackObjectAttributeName;
-    internal const string KeyAttributeShortName = "KeyAttribute";
-    internal const string IgnoreShortName = "IgnoreMemberAttribute";
-    internal const string IgnoreDataMemberShortName = "IgnoreDataMemberAttribute";
+    public const string MessagePackObjectAttributeShortName = Constants.MessagePackObjectAttributeName;
+    public const string KeyAttributeShortName = "KeyAttribute";
+    public const string IgnoreShortName = "IgnoreMemberAttribute";
+    public const string IgnoreDataMemberShortName = "IgnoreDataMemberAttribute";
 
     private const string InvalidMessagePackObjectTitle = "MessagePackObject validation";
     private const DiagnosticSeverity InvalidMessagePackObjectSeverity = DiagnosticSeverity.Error;
@@ -239,8 +239,8 @@ public class MsgPack00xMessagePackAnalyzer : DiagnosticAnalyzer
         id: PartialTypeRequiredId,
         title: "Partial type required",
         category: Category,
-        messageFormat: "Types with private, serializable members must be declared as partial",
-        description: "When a data type has serializable members that may only be accessible to the class itself (e.g. private or protected members), the type must be declared as partial to allow source generation of the formatter as a nested type.",
+        messageFormat: "Types with private, serializable members must be declared as partial, including nesting types",
+        description: "When a data type has serializable members that may only be accessible to the class itself (e.g. private or protected members), the type must be declared as partial to allow source generation of the formatter as a nested type. When a data type is itself a nested type, its declaring types must also be partial.",
         defaultSeverity: DiagnosticSeverity.Error,
         isEnabledByDefault: true,
         helpLinkUri: AnalyzerUtilities.GetHelpLink(PartialTypeRequiredId));
diff --git a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
index 587545fd1..38a2f5020 100644
--- a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
+++ b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
@@ -1093,9 +1093,13 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
             // 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
             // as required in order to access the private members.
             bool anyNonPartialTypesFound = false;
-            foreach (BaseTypeDeclarationSyntax? decl in FindNonPartialTypes(formattedType))
+            BaseTypeDeclarationSyntax[] nonPartialTypes = FindNonPartialTypes(formattedType).ToArray();
+            if (nonPartialTypes.Length > 0)
             {
-                this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.PartialTypeRequired, decl.Identifier.GetLocation()));
+                BaseTypeDeclarationSyntax? targetType = formattedType.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as BaseTypeDeclarationSyntax;
+                Location? primaryLocation = targetType?.Identifier.GetLocation();
+                Location[]? addlLocations = nonPartialTypes.Where(t => t != targetType).Select(t => t.Identifier.GetLocation()).ToArray();
+                this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.PartialTypeRequired, primaryLocation, (IEnumerable<Location>?)addlLocations));
                 anyNonPartialTypesFound = true;
             }
 
diff --git a/src/MessagePack.SourceGenerator/Properties/AssemblyInfo.cs b/src/MessagePack.SourceGenerator/Properties/AssemblyInfo.cs
deleted file mode 100644
index 9ae3dbb6b..000000000
--- a/src/MessagePack.SourceGenerator/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-﻿// Copyright (c) All contributors. All rights reserved.
-// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-
-using System.Runtime.CompilerServices;
-
-[assembly: InternalsVisibleTo("MessagePack.Analyzers.CodeFixes, PublicKey=" + ThisAssembly.PublicKey)]
-[assembly: InternalsVisibleTo("MessagePack.Analyzers.CodeFixes.Unity, PublicKey=" + ThisAssembly.PublicKey)]


Test patch:
diff --git a/tests/MessagePack.SourceGenerator.Tests/MsgPack011PartialTypeRequiredTests.cs b/tests/MessagePack.SourceGenerator.Tests/MsgPack011PartialTypeRequiredTests.cs
index 258bbdc1e..82f94ea3a 100644
--- a/tests/MessagePack.SourceGenerator.Tests/MsgPack011PartialTypeRequiredTests.cs
+++ b/tests/MessagePack.SourceGenerator.Tests/MsgPack011PartialTypeRequiredTests.cs
@@ -1,6 +1,8 @@
 // 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.SourceGenerator.Analyzers;
+using Microsoft.CodeAnalysis.Testing;
 using VerifyCS = CSharpCodeFixVerifier<MessagePack.SourceGenerator.Analyzers.MsgPack00xMessagePackAnalyzer, MessagePack.Analyzers.CodeFixes.FormatterCodeFixProvider>;
 
 public class MsgPack011PartialTypeRequiredTests
@@ -34,4 +36,102 @@ partial class MyObject
 
         await VerifyCS.VerifyCodeFixAsync(testSource, fixedSource);
     }
+
+    [Fact]
+    public async Task NestedPartialTypeRequiresPartialSelfAndDeclaringType()
+    {
+        string testSource = """
+            using MessagePack;
+
+            [MessagePackObject]
+            class {|#1:Outer|}
+            {
+                [MessagePackObject]
+                internal class {|#0:Inner|}
+                {
+                    [Key(0)]
+                    private Outer Value { get; set; }
+                }
+            }
+            """;
+
+        string fixedSource = """
+            using MessagePack;
+
+            [MessagePackObject]
+            partial class Outer
+            {
+                [MessagePackObject]
+                internal partial class Inner
+                {
+                    [Key(0)]
+                    private Outer Value { get; set; }
+                }
+            }
+            """;
+
+        await new VerifyCS.Test
+        {
+            TestState =
+            {
+                Sources = { testSource },
+                ExpectedDiagnostics =
+                {
+                    DiagnosticResult.CompilerError(MsgPack00xMessagePackAnalyzer.PartialTypeRequired.Id)
+                        .WithLocation(0)
+                        .WithLocation(1),
+                },
+            },
+            FixedCode = fixedSource,
+        }.RunAsync();
+    }
+
+    [Fact]
+    public async Task NestedPartialTypeRequiresPartialDeclaringType()
+    {
+        string testSource = """
+            using MessagePack;
+
+            [MessagePackObject]
+            class {|#1:Outer|}
+            {
+                [MessagePackObject]
+                internal partial class {|#0:Inner|}
+                {
+                    [Key(0)]
+                    private Outer Value { get; set; }
+                }
+            }
+            """;
+
+        string fixedSource = """
+            using MessagePack;
+
+            [MessagePackObject]
+            partial class Outer
+            {
+                [MessagePackObject]
+                internal partial class Inner
+                {
+                    [Key(0)]
+                    private Outer Value { get; set; }
+                }
+            }
+            """;
+
+        await new VerifyCS.Test
+        {
+            TestState =
+            {
+                Sources = { testSource },
+                ExpectedDiagnostics =
+                {
+                    DiagnosticResult.CompilerError(MsgPack00xMessagePackAnalyzer.PartialTypeRequired.Id)
+                        .WithLocation(0)
+                        .WithLocation(1),
+                },
+            },
+            FixedCode = fixedSource,
+        }.RunAsync();
+    }
 }
diff --git a/tests/MessagePack.SourceGenerator.Tests/PrivateMemberAccessTests.cs b/tests/MessagePack.SourceGenerator.Tests/PrivateMemberAccessTests.cs
index b9c001256..59f99d5f7 100644
--- a/tests/MessagePack.SourceGenerator.Tests/PrivateMemberAccessTests.cs
+++ b/tests/MessagePack.SourceGenerator.Tests/PrivateMemberAccessTests.cs
@@ -90,10 +90,10 @@ public async Task FormatterForClassWithPrivateMembers_Nested_NonPartialTypes(str
             using System;
             using MessagePack;
 
-            {{type}} {|MsgPack011:Outer|}
+            {{type}} Outer
             {
                 [MessagePackObject]
-                internal partial {{type}} MyObject
+                internal partial {{type}} {|MsgPack011:MyObject|}
                 {
                     [Key(0)]
                     private int value;