diff --git a/src/MessagePackAnalyzer/MessagePackAnalyzer.cs b/src/MessagePackAnalyzer/MessagePackAnalyzer.cs index e903c57ad..e7b6281a9 100644 --- a/src/MessagePackAnalyzer/MessagePackAnalyzer.cs +++ b/src/MessagePackAnalyzer/MessagePackAnalyzer.cs @@ -80,7 +80,14 @@ public override void Initialize(AnalysisContext context) { if (ReferenceSymbols.TryCreate(ctxt.Compilation, out ReferenceSymbols? typeReferences)) { - ctxt.RegisterSyntaxNodeAction(c => Analyze(c, typeReferences), SyntaxKind.ClassDeclaration, SyntaxKind.StructDeclaration, SyntaxKind.InterfaceDeclaration); + var relevantSyntaxKinds = new[] + { + SyntaxKind.ClassDeclaration, + SyntaxKind.StructDeclaration, + SyntaxKind.InterfaceDeclaration, + SyntaxKind.RecordDeclaration, + }; + ctxt.RegisterSyntaxNodeAction(c => Analyze(c, typeReferences), relevantSyntaxKinds); } }); } diff --git a/src/MessagePackAnalyzer/MessagePackCodeFixProvider.cs b/src/MessagePackAnalyzer/MessagePackCodeFixProvider.cs index c4b7c1028..57ce0ede5 100644 --- a/src/MessagePackAnalyzer/MessagePackCodeFixProvider.cs +++ b/src/MessagePackAnalyzer/MessagePackCodeFixProvider.cs @@ -154,12 +154,26 @@ private static async Task AddKeyAttributeAsync(Document document, INam foreach (ISymbol member in targets) { - if (member.GetAttributes().FindAttributeShortName(MessagePackAnalyzer.KeyAttributeShortName) is null) + if (!member.IsImplicitlyDeclared && + member.GetAttributes().FindAttributeShortName(MessagePackAnalyzer.KeyAttributeShortName) is null) { SyntaxNode node = await member.DeclaringSyntaxReferences[0].GetSyntaxAsync(cancellationToken).ConfigureAwait(false); var documentEditor = await solutionEditor.GetDocumentEditorAsync(document.Project.Solution.GetDocumentId(node.SyntaxTree), cancellationToken).ConfigureAwait(false); var syntaxGenerator = SyntaxGenerator.GetGenerator(documentEditor.OriginalDocument); - documentEditor.AddAttribute(node, syntaxGenerator.Attribute("MessagePack.KeyAttribute", syntaxGenerator.LiteralExpression(startOrder++))); + AttributeListSyntax attributeList = (AttributeListSyntax)syntaxGenerator.Attribute("MessagePack.KeyAttribute", syntaxGenerator.LiteralExpression(startOrder++)); + if (node is ParameterSyntax parameter) + { + // The primary constructor requires special target on the attribute list. + attributeList = attributeList.WithTarget( + SyntaxFactory.AttributeTargetSpecifier(SyntaxFactory.Token(SyntaxKind.PropertyKeyword))) + .WithLeadingTrivia(parameter.GetLeadingTrivia()); + ParameterSyntax attributedParameter = parameter.AddAttributeLists(attributeList); + documentEditor.ReplaceNode(parameter, attributedParameter); + } + else + { + documentEditor.AddAttribute(node, attributeList); + } } } diff --git a/tests/MessagePackAnalyzer.Tests/Helpers/CSharpCodeFixVerifier`2.cs b/tests/MessagePackAnalyzer.Tests/Helpers/CSharpCodeFixVerifier`2.cs index 2fcb42a1a..ac4e942fb 100644 --- a/tests/MessagePackAnalyzer.Tests/Helpers/CSharpCodeFixVerifier`2.cs +++ b/tests/MessagePackAnalyzer.Tests/Helpers/CSharpCodeFixVerifier`2.cs @@ -1,6 +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 System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeFixes; @@ -22,26 +23,26 @@ public static DiagnosticResult Diagnostic(string diagnosticId) public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) => new DiagnosticResult(descriptor); - public static Task VerifyAnalyzerWithoutMessagePackReferenceAsync(string source) + public static Task VerifyAnalyzerWithoutMessagePackReferenceAsync([StringSyntax("c#-test")] string source) { var test = new Test { TestCode = source, ReferenceAssemblies = ReferenceAssemblies.NetFramework.Net472.Default }; return test.RunAsync(); } - public static Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + public static Task VerifyAnalyzerAsync([StringSyntax("c#-test")] string source, params DiagnosticResult[] expected) { var test = new Test { TestCode = source }; test.ExpectedDiagnostics.AddRange(expected); return test.RunAsync(); } - public static Task VerifyCodeFixAsync(string source, string fixedSource) + public static Task VerifyCodeFixAsync([StringSyntax("c#-test")] string source, [StringSyntax("c#-test")] string fixedSource) => VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); - public static Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource) + public static Task VerifyCodeFixAsync([StringSyntax("c#-test")] string source, DiagnosticResult expected, [StringSyntax("c#-test")] string fixedSource) => VerifyCodeFixAsync(source, new[] { expected }, fixedSource); - public static Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource) + public static Task VerifyCodeFixAsync([StringSyntax("c#-test")] string source, DiagnosticResult[] expected, [StringSyntax("c#-test")] string fixedSource) { var test = new Test { @@ -53,7 +54,7 @@ public static Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected return test.RunAsync(); } - public static Task VerifyCodeFixAsync(string[] source, string[] fixedSource) + public static Task VerifyCodeFixAsync([StringSyntax("c#-test")] string[] source, [StringSyntax("c#-test")] string[] fixedSource) { var test = new Test { diff --git a/tests/MessagePackAnalyzer.Tests/MessagePackAnalyzerTests.cs b/tests/MessagePackAnalyzer.Tests/MessagePackAnalyzerTests.cs index 8fc4c65f9..a22e5e62e 100644 --- a/tests/MessagePackAnalyzer.Tests/MessagePackAnalyzerTests.cs +++ b/tests/MessagePackAnalyzer.Tests/MessagePackAnalyzerTests.cs @@ -1,8 +1,10 @@ // 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.Collections.Immutable; using System.Threading.Tasks; -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Testing; using Xunit; using VerifyCS = @@ -10,14 +12,14 @@ public class MessagePackAnalyzerTests { - private const string Preamble = @" + private const string Preamble = /* lang=c#-test */ @" using MessagePack; "; [Fact] public async Task NoMessagePackReference() { - string input = @" + string input = /* lang=c#-test */ @" public class Foo { } @@ -29,7 +31,7 @@ public class Foo [Fact] public async Task MessageFormatterAttribute() { - string input = Preamble + @"using MessagePack.Formatters; + string input = Preamble + /* lang=c#-test */ @"using MessagePack.Formatters; public class FooFormatter : IMessagePackFormatter { public void Serialize(ref MessagePackWriter writer, Foo value, MessagePackSerializerOptions options) {} @@ -55,7 +57,7 @@ public class SomeClass { [Fact] public async Task InvalidMessageFormatterType() { - string input = Preamble + @"using MessagePack.Formatters; + string input = Preamble + /* lang=c#-test */ @"using MessagePack.Formatters; public class InvalidMessageFormatter { } @@ -77,7 +79,7 @@ public class SomeClass { [Fact] public async Task NullStringKey() { - string input = Preamble + @" + string input = Preamble + /* lang=c#-test */ @" [MessagePackObject] public class Foo { @@ -92,7 +94,7 @@ public class Foo [Fact] public async Task AddAttributesToMembers() { - string input = Preamble + @" + string input = Preamble + /* lang=c#-test */ @" [MessagePackObject] public class Foo { @@ -101,7 +103,7 @@ public class Foo } "; - string output = Preamble + @" + string output = Preamble + /* lang=c#-test */ @" [MessagePackObject] public class Foo { @@ -119,7 +121,7 @@ public class Foo public async Task AddAttributeToType() { // Don't use Preamble because we want to test that it works without a using statement at the top. - string input = @" + string input = /* lang=c#-test */ @" public class Foo { public string Member { get; set; } @@ -133,7 +135,7 @@ public class Bar } "; - string output = @" + string output = /* lang=c#-test */ @" [MessagePack.MessagePackObject] public class Foo { @@ -157,13 +159,13 @@ public async Task CodeFixAppliesAcrossFiles() { var inputs = new string[] { - @" + /* lang=c#-test */ @" public class Foo { public int {|MsgPack004:Member1|} { get; set; } } ", - @"using MessagePack; + /* lang=c#-test */ @"using MessagePack; [MessagePackObject] public class Bar : Foo @@ -174,14 +176,14 @@ public class Bar : Foo }; var outputs = new string[] { - @" + /* lang=c#-test */ @" public class Foo { [MessagePack.Key(1)] public int Member1 { get; set; } } ", - @"using MessagePack; + /* lang=c#-test */ @"using MessagePack; [MessagePackObject] public class Bar : Foo @@ -194,4 +196,75 @@ public class Bar : Foo await VerifyCS.VerifyCodeFixAsync(inputs, outputs); } + + [Fact] + public async Task AddAttributesToMembersOfRecord() + { + string input = Preamble + /* lang=c#-test */ @" +[MessagePackObject] +public record Foo +{ + public string {|MsgPack004:Member1|} { get; set; } + public string {|MsgPack004:Member2|} { get; set; } +} +"; + + string output = Preamble + /* lang=c#-test */ @" +[MessagePackObject] +public record Foo +{ + [Key(0)] + public string Member1 { get; set; } + [Key(1)] + public string Member2 { get; set; } +} +"; + await new VerifyCS.Test + { + ReferenceAssemblies = ReferenceAssemblies.Net.Net60.WithPackages(ImmutableArray.Create(new PackageIdentity("MessagePack", "2.0.335"))), + CompilerDiagnostics = CompilerDiagnostics.Errors, + SolutionTransforms = + { + static (solution, projectId) => + { + return solution.WithProjectParseOptions(projectId, new CSharpParseOptions(languageVersion: LanguageVersion.CSharp11)); + }, + }, + TestCode = input, + FixedCode = output, + }.RunAsync(); + } + + [Fact] + public async Task AddAttributesToMembersOfRecordWithPrimaryCtor() + { + string input = Preamble + /* lang=c#-test */ @" +[MessagePackObject] +public record Foo( + string {|MsgPack004:Member1|}, + string {|MsgPack004:Member2|}); +"; + + string output = Preamble + /* lang=c#-test */ @" +[MessagePackObject] +public record Foo( + [property: Key(0)] string {|MsgPack004:Member1|}, + [property: Key(1)] string {|MsgPack004:Member2|}); +"; + + await new VerifyCS.Test + { + ReferenceAssemblies = ReferenceAssemblies.Net.Net60.WithPackages(ImmutableArray.Create(new PackageIdentity("MessagePack", "2.0.335"))), + CompilerDiagnostics = CompilerDiagnostics.Errors, + SolutionTransforms = + { + static (solution, projectId) => + { + return solution.WithProjectParseOptions(projectId, new CSharpParseOptions(languageVersion: LanguageVersion.CSharp11)); + }, + }, + TestCode = input, + FixedCode = output, + }.RunAsync(); + } }