Repository: MessagePack-CSharp/MessagePack-CSharp

Issue:
Offer code fix for MsgPack004: attribute all members
As folks migrate to v3, non-public fields and properties on all data types will be flagged as requiring a `Key` or `IgnoreMember` attribute.
A bulk code fix should be offered that adds `IgnoreMember` everywhere necessary to ease the migration.


Hints:

Patch:
diff --git a/src/MessagePack.Analyzers.CodeFixes/CodeFixes/MessagePackCodeFixProvider.cs b/src/MessagePack.Analyzers.CodeFixes/CodeFixes/MessagePackCodeFixProvider.cs
index a0491a2e4..90c0e83e4 100644
--- a/src/MessagePack.Analyzers.CodeFixes/CodeFixes/MessagePackCodeFixProvider.cs
+++ b/src/MessagePack.Analyzers.CodeFixes/CodeFixes/MessagePackCodeFixProvider.cs
@@ -6,22 +6,25 @@ namespace MessagePack.Analyzers.CodeFixes;
 [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MessagePackCodeFixProvider)), Shared]
 public class MessagePackCodeFixProvider : CodeFixProvider
 {
-    public sealed override ImmutableArray<string> FixableDiagnosticIds
+    public const string AddKeyAttributeEquivanceKey = "MessagePackAnalyzer.AddKeyAttribute";
+    public const string AddIgnoreMemberAttributeEquivalenceKey = "MessagePackAnalyzer.AddIgnoreMemberAttribute";
+
+    public override ImmutableArray<string> FixableDiagnosticIds
     {
         get
         {
             return ImmutableArray.Create(
-                MsgPack00xMessagePackAnalyzer.PublicMemberNeedsKey.Id,
+                MsgPack00xMessagePackAnalyzer.MemberNeedsKey.Id,
                 MsgPack00xMessagePackAnalyzer.TypeMustBeMessagePackObject.Id);
         }
     }
 
-    public sealed override FixAllProvider GetFixAllProvider()
+    public override FixAllProvider GetFixAllProvider()
     {
         return WellKnownFixAllProviders.BatchFixer;
     }
 
-    public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
+    public override async Task RegisterCodeFixesAsync(CodeFixContext context)
     {
         var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) as CompilationUnitSyntax;
         if (root is null)
@@ -112,29 +115,43 @@ myTypeInfo.Type as INamedTypeSymbol ??
             }
         }
 
-        var action = CodeAction.Create("Add MessagePack KeyAttribute", c => AddKeyAttributeAsync(context.Document, namedSymbol, c), "MessagePackAnalyzer.AddKeyAttribute");
+        CodeAction addKeyAction = CodeAction.Create("Add MessagePack KeyAttribute", c => AddKeyAttributeAsync(context.Document, namedSymbol, c), AddKeyAttributeEquivanceKey);
+        context.RegisterCodeFix(addKeyAction, context.Diagnostics.First());
 
-        context.RegisterCodeFix(action, context.Diagnostics.First()); // use single.
+        foreach (Diagnostic diagnostic in context.Diagnostics)
+        {
+            SyntaxNode? diagnosticTargetNode = root.FindNode(diagnostic.Location.SourceSpan);
+            if (diagnosticTargetNode?.AncestorsAndSelf().FirstOrDefault(n => n is FieldDeclarationSyntax or PropertyDeclarationSyntax) is MemberDeclarationSyntax fieldOrProperty)
+            {
+                context.RegisterCodeFix(
+                    CodeAction.Create(
+                        "Add MessagePack [IgnoreMember]",
+                        ct => AddIgnoreMemberAttributeAsync(context.Document, fieldOrProperty, ct),
+                        AddIgnoreMemberAttributeEquivalenceKey),
+                    diagnostic);
+            }
+        }
     }
 
+    private static IEnumerable<ISymbol> FindMembersMissingAttributes(INamedTypeSymbol type) => type.GetAllMembers()
+        .Where(x => x.Kind == SymbolKind.Property || x.Kind == SymbolKind.Field)
+        .Where(x => x.GetAttributes().FindAttributeShortName(MsgPack00xMessagePackAnalyzer.IgnoreShortName) == null && x.GetAttributes().FindAttributeShortName(MsgPack00xMessagePackAnalyzer.IgnoreDataMemberShortName) == null)
+        .Where(x => !x.IsStatic)
+        .Where(x =>
+        {
+            return x switch
+            {
+                IPropertySymbol p => p.ExplicitInterfaceImplementations.Length == 0,
+                IFieldSymbol f => !f.IsImplicitlyDeclared,
+                _ => throw new NotSupportedException("Unsupported member type."),
+            };
+        });
+
     private static async Task<Solution> AddKeyAttributeAsync(Document document, INamedTypeSymbol type, CancellationToken cancellationToken)
     {
         var solutionEditor = new SolutionEditor(document.Project.Solution);
 
-        var targets = type.GetAllMembers()
-            .Where(x => x.Kind == SymbolKind.Property || x.Kind == SymbolKind.Field)
-            .Where(x => x.GetAttributes().FindAttributeShortName(MsgPack00xMessagePackAnalyzer.IgnoreShortName) == null && x.GetAttributes().FindAttributeShortName(MsgPack00xMessagePackAnalyzer.IgnoreDataMemberShortName) == null)
-            .Where(x => !x.IsStatic)
-            .Where(x =>
-            {
-                return x switch
-                {
-                    IPropertySymbol p => p.ExplicitInterfaceImplementations.Length == 0,
-                    IFieldSymbol f => !f.IsImplicitlyDeclared,
-                    _ => throw new NotSupportedException("Unsupported member type."),
-                };
-            })
-            .ToArray();
+        var targets = FindMembersMissingAttributes(type).ToArray();
 
         var startOrder = targets
             .Select(x => x.GetAttributes().FindAttributeShortName(MsgPack00xMessagePackAnalyzer.KeyAttributeShortName))
@@ -174,4 +191,15 @@ private static async Task<Solution> AddKeyAttributeAsync(Document document, INam
 
         return solutionEditor.GetChangedSolution();
     }
+
+    private static async Task<Solution> AddIgnoreMemberAttributeAsync(Document document, MemberDeclarationSyntax memberDecl, CancellationToken cancellationToken)
+    {
+        SolutionEditor solutionEditor = new(document.Project.Solution);
+        DocumentEditor documentEditor = await solutionEditor.GetDocumentEditorAsync(document.Id, cancellationToken);
+        SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(documentEditor.OriginalDocument);
+
+        documentEditor.AddAttribute(memberDecl, syntaxGenerator.Attribute("MessagePack.IgnoreMemberAttribute"));
+
+        return solutionEditor.GetChangedSolution();
+    }
 }
diff --git a/src/MessagePack.SourceGenerator/AnalyzerReleases.Unshipped.md b/src/MessagePack.SourceGenerator/AnalyzerReleases.Unshipped.md
index fa6a05e42..3f9ba4001 100644
--- a/src/MessagePack.SourceGenerator/AnalyzerReleases.Unshipped.md
+++ b/src/MessagePack.SourceGenerator/AnalyzerReleases.Unshipped.md
@@ -8,7 +8,7 @@ Rule ID | Category | Severity | Notes
 MsgPack001 | Reliability | Disabled | MsgPack001SpecifyOptionsAnalyzer
 MsgPack002 | Reliability | Disabled | MsgPack002UseConstantOptionsAnalyzer
 MsgPack003 | Usage | Error | MsgPack00xMessagePackAnalyzer
-MsgPack004 | Usage | Error | MsgPack00xMessagePackAnalyzer
+MsgPack004 | Usage | Error | Member needs Key or IgnoreMember attribute
 MsgPack005 | Usage | Error | MsgPack00xMessagePackAnalyzer
 MsgPack006 | Usage | Error | MsgPack00xMessagePackAnalyzer
 MsgPack007 | Usage | Error | MsgPack00xMessagePackAnalyzer
diff --git a/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs b/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs
index b47e81019..eaab08734 100644
--- a/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs
+++ b/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs
@@ -53,7 +53,7 @@ public class MsgPack00xMessagePackAnalyzer : DiagnosticAnalyzer
         isEnabledByDefault: true,
         helpLinkUri: AnalyzerUtilities.GetHelpLink(MessagePackFormatterMustBeMessagePackFormatterId));
 
-    public static readonly DiagnosticDescriptor PublicMemberNeedsKey = new DiagnosticDescriptor(
+    public static readonly DiagnosticDescriptor MemberNeedsKey = new DiagnosticDescriptor(
         id: AttributeMessagePackObjectMembersId,
         title: "Attribute properties and fields of MessagePack objects",
         category: Category,
@@ -258,7 +258,7 @@ public class MsgPack00xMessagePackAnalyzer : DiagnosticAnalyzer
     public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
         TypeMustBeMessagePackObject,
         MessageFormatterMustBeMessagePackFormatter,
-        PublicMemberNeedsKey,
+        MemberNeedsKey,
         BaseTypeContainsUnattributedPublicMembers,
         InvalidMessagePackObject,
         BothStringAndIntKeyAreNull,
diff --git a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
index 47b27f731..cc38d870d 100644
--- a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
+++ b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
@@ -779,7 +779,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
                             var syntax = item.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax();
                             var identifier = (syntax as PropertyDeclarationSyntax)?.Identifier ?? (syntax as ParameterSyntax)?.Identifier;
 
-                            this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.PublicMemberNeedsKey, identifier?.GetLocation(), formattedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Name));
+                            this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.MemberNeedsKey, identifier?.GetLocation(), formattedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Name));
                         }
                         else if (formattedType.BaseType is not null)
                         {
@@ -881,7 +881,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
                 {
                     if (contractAttr is not null)
                     {
-                        this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.PublicMemberNeedsKey, item.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(), formattedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Name));
+                        this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.MemberNeedsKey, item.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(), formattedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Name));
                     }
                 }
                 else


Test patch:
diff --git a/tests/MessagePack.SourceGenerator.Tests/MessagePackAnalyzerTests.cs b/tests/MessagePack.SourceGenerator.Tests/MessagePackAnalyzerTests.cs
index e356fe63a..91cca522c 100644
--- a/tests/MessagePack.SourceGenerator.Tests/MessagePackAnalyzerTests.cs
+++ b/tests/MessagePack.SourceGenerator.Tests/MessagePackAnalyzerTests.cs
@@ -1,7 +1,7 @@
 ﻿// 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 MessagePack.Analyzers.CodeFixes;
 using Microsoft.CodeAnalysis.Testing;
 using VerifyCS = CSharpCodeFixVerifier<MessagePack.SourceGenerator.Analyzers.MsgPack00xMessagePackAnalyzer, MessagePack.Analyzers.CodeFixes.MessagePackCodeFixProvider>;
 
@@ -118,6 +118,43 @@ public class Foo
             TestCode = input,
             FixedCode = output,
             MarkupOptions = MarkupOptions.UseFirstDescriptor,
+            CodeActionEquivalenceKey = MessagePackCodeFixProvider.AddKeyAttributeEquivanceKey,
+        }.RunAsync();
+    }
+
+    [Fact]
+    public async Task AddIgnoreAttributesToMembers()
+    {
+        string input = Preamble + @"
+[MessagePackObject]
+public class Foo
+{
+    internal string {|MsgPack004:member1 = null|};
+    internal string {|MsgPack004:member2 = null|};
+    [Key(0)]
+    public string Member3 { get; set; }
+}
+";
+
+        string output = Preamble + @"
+[MessagePackObject]
+public class Foo
+{
+    [IgnoreMember]
+    internal string member1 = null;
+    [IgnoreMember]
+    internal string member2 = null;
+    [Key(0)]
+    public string Member3 { get; set; }
+}
+";
+
+        await new VerifyCS.Test
+        {
+            TestCode = input,
+            FixedCode = output,
+            MarkupOptions = MarkupOptions.UseFirstDescriptor,
+            CodeActionEquivalenceKey = MessagePackCodeFixProvider.AddIgnoreMemberAttributeEquivalenceKey,
         }.RunAsync();
     }
 
@@ -148,6 +185,7 @@ public class Foo
             TestCode = input,
             FixedCode = output,
             MarkupOptions = MarkupOptions.UseFirstDescriptor,
+            CodeActionEquivalenceKey = MessagePackCodeFixProvider.AddKeyAttributeEquivanceKey,
         }.RunAsync();
     }
 
@@ -185,7 +223,13 @@ public class Bar
 }
 ";
 
-        await VerifyCS.VerifyCodeFixAsync(input, output);
+        await new VerifyCS.Test
+        {
+            TestCode = input,
+            FixedCode = output,
+            MarkupOptions = MarkupOptions.UseFirstDescriptor,
+            CodeActionEquivalenceKey = MessagePackCodeFixProvider.AddKeyAttributeEquivanceKey,
+        }.RunAsync();
     }
 
     [Fact]
@@ -222,7 +266,13 @@ public record Bar
 }
 ";
 
-        await VerifyCS.VerifyCodeFixAsync(input, output);
+        await new VerifyCS.Test
+        {
+            TestCode = input,
+            FixedCode = output,
+            MarkupOptions = MarkupOptions.UseFirstDescriptor,
+            CodeActionEquivalenceKey = MessagePackCodeFixProvider.AddKeyAttributeEquivanceKey,
+        }.RunAsync();
     }
 
     [Fact]
@@ -259,7 +309,13 @@ public record Bar
 }
 ";
 
-        await VerifyCS.VerifyCodeFixAsync(input, output);
+        await new VerifyCS.Test
+        {
+            TestCode = input,
+            FixedCode = output,
+            MarkupOptions = MarkupOptions.UseFirstDescriptor,
+            CodeActionEquivalenceKey = MessagePackCodeFixProvider.AddKeyAttributeEquivanceKey,
+        }.RunAsync();
     }
 
     [Fact]
@@ -302,7 +358,13 @@ internal static class IsExternalInit
 }
 ";
 
-        await VerifyCS.VerifyCodeFixAsync(input, output);
+        await new VerifyCS.Test
+        {
+            TestCode = input,
+            FixedCode = output,
+            MarkupOptions = MarkupOptions.UseFirstDescriptor,
+            CodeActionEquivalenceKey = MessagePackCodeFixProvider.AddKeyAttributeEquivanceKey,
+        }.RunAsync();
     }
 
     [Fact]
@@ -353,6 +415,7 @@ public class Bar : Foo
                 Sources = { output1, output2 },
             },
             MarkupOptions = MarkupOptions.UseFirstDescriptor,
+            CodeActionEquivalenceKey = MessagePackCodeFixProvider.AddKeyAttributeEquivanceKey,
         }.RunAsync();
     }
 
@@ -396,6 +459,12 @@ public class Bar
             }
             """;
 
-        await VerifyCS.VerifyCodeFixAsync(input, output);
+        await new VerifyCS.Test
+        {
+            TestCode = input,
+            FixedCode = output,
+            MarkupOptions = MarkupOptions.UseFirstDescriptor,
+            CodeActionEquivalenceKey = MessagePackCodeFixProvider.AddKeyAttributeEquivanceKey,
+        }.RunAsync();
     }
 }