diff --git a/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs b/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs index 78e19ee..9228e7b 100644 --- a/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs +++ b/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs @@ -3,7 +3,10 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Immediate.Validations.CodeFixes; @@ -30,26 +33,70 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) context.RegisterCodeFix( CodeAction.Create( "Add `[Validate]`", - createChangedDocument: _ => - AddValidateAttribute(context.Document, root, typeDeclaration), + createChangedDocument: token => + AddValidateAttribute(context.Document, root, typeDeclaration, token), equivalenceKey: nameof(AddValidateAttributeCodefixProvider) ), diagnostic ); } - private static Task AddValidateAttribute(Document document, CompilationUnitSyntax root, TypeDeclarationSyntax typeDeclaration) + private static async Task AddValidateAttribute(Document document, CompilationUnitSyntax root, TypeDeclarationSyntax typeDeclaration, CancellationToken token) { - var newDecl = typeDeclaration - .WithAttributeLists( - typeDeclaration.AttributeLists - .Add(AttributeList( - SingletonSeparatedList( - Attribute( - IdentifierName("Validate")))))); - - var newRoot = root.ReplaceNode(typeDeclaration, newDecl); - var newDocument = document.WithSyntaxRoot(newRoot); - return Task.FromResult(newDocument); + var model = await document.GetSemanticModelAsync(token); + + var validateSymbol = model?.Compilation + .GetTypeByMetadataName("Immediate.Validations.Shared.ValidateAttribute")!; + + var referenceId = DocumentationCommentId.CreateReferenceId(validateSymbol); + var annotation = new SyntaxAnnotation("SymbolId", referenceId); + + var newLineSyntax = typeDeclaration.DescendantTrivia() + .FirstOrDefault(t => t.IsKind(SyntaxKind.EndOfLineTrivia)); + + if (newLineSyntax == default) + newLineSyntax = ElasticLineFeed; + + var validateAttribute = AttributeList( + SingletonSeparatedList( + Attribute( + IdentifierName("Validate") + ) + ) + ) + .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, annotation) + .WithTrailingTrivia(newLineSyntax); + + var newDecl = typeDeclaration.AttributeLists switch + { + [] => + typeDeclaration + .WithoutLeadingTrivia() + .WithAttributeLists( + typeDeclaration.AttributeLists + .Add( + validateAttribute + .WithLeadingTrivia(typeDeclaration.GetLeadingTrivia()) + .WithAdditionalAnnotations(Formatter.Annotation) + ) + ), + + _ => + typeDeclaration + .WithAttributeLists( + typeDeclaration.AttributeLists + .Add( + validateAttribute + .WithAdditionalAnnotations(Formatter.Annotation) + ) + ), + }; + + var newRoot = root.ReplaceNode( + typeDeclaration, + newDecl + ); + + return document.WithSyntaxRoot(newRoot); } } diff --git a/tests/Immediate.Validations.Tests/CodeFixTests/AddValidateAttributeCodefixProviderTests.cs b/tests/Immediate.Validations.Tests/CodeFixTests/AddValidateAttributeCodefixProviderTests.cs index eb91425..6de1c52 100644 --- a/tests/Immediate.Validations.Tests/CodeFixTests/AddValidateAttributeCodefixProviderTests.cs +++ b/tests/Immediate.Validations.Tests/CodeFixTests/AddValidateAttributeCodefixProviderTests.cs @@ -7,10 +7,81 @@ public sealed class AddValidateAttributeCodefixProviderTests { [Fact] public async Task AddValidateAttribute() => + await CodeFixTestHelper.CreateCodeFixTest( + """ + namespace Immediate.Validations.Testing; + + public sealed record {|IV0012:Data|} : Immediate.Validations.Shared.IValidationTarget + { + public Immediate.Validations.Shared.ValidationResult Validate() => []; + public Immediate.Validations.Shared.ValidationResult Validate(Immediate.Validations.Shared.ValidationResult errors) => []; + public static Immediate.Validations.Shared.ValidationResult Validate(Data target) => []; + public static Immediate.Validations.Shared.ValidationResult Validate(Data target, Immediate.Validations.Shared.ValidationResult errors) => []; + } + """, + """ + using Immediate.Validations.Shared; + + namespace Immediate.Validations.Testing; + + [Validate] + public sealed record Data : Immediate.Validations.Shared.IValidationTarget + { + public Immediate.Validations.Shared.ValidationResult Validate() => []; + public Immediate.Validations.Shared.ValidationResult Validate(Immediate.Validations.Shared.ValidationResult errors) => []; + public static Immediate.Validations.Shared.ValidationResult Validate(Data target) => []; + public static Immediate.Validations.Shared.ValidationResult Validate(Data target, Immediate.Validations.Shared.ValidationResult errors) => []; + } + """ + ).RunAsync(TestContext.Current.CancellationToken); + + [Fact] + public async Task AddValidateAttributeWorksCorrectlyWithOtherAttributes() => + await CodeFixTestHelper.CreateCodeFixTest( + """ + using System; + + namespace Immediate.Validations.Shared; + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] + public sealed class MyStuffAttribute : Attribute; + + [MyStuff] + public sealed record {|IV0012:Data|} : IValidationTarget + { + public ValidationResult Validate() => []; + public ValidationResult Validate(ValidationResult errors) => []; + public static ValidationResult Validate(Data target) => []; + public static ValidationResult Validate(Data target, ValidationResult errors) => []; + } + """, + """ + using System; + + namespace Immediate.Validations.Shared; + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] + public sealed class MyStuffAttribute : Attribute; + + [MyStuff] + [Validate] + public sealed record {|IV0012:Data|} : IValidationTarget + { + public ValidationResult Validate() => []; + public ValidationResult Validate(ValidationResult errors) => []; + public static ValidationResult Validate(Data target) => []; + public static ValidationResult Validate(Data target, ValidationResult errors) => []; + } + """ + ).RunAsync(TestContext.Current.CancellationToken); + + [Fact] + public async Task AddValidateAttributeWorksCorrectlyWithXmlDocs() => await CodeFixTestHelper.CreateCodeFixTest( """ namespace Immediate.Validations.Shared; + /// documentation public sealed record {|IV0012:Data|} : IValidationTarget { public ValidationResult Validate() => []; @@ -22,6 +93,7 @@ public sealed record {|IV0012:Data|} : IValidationTarget """ namespace Immediate.Validations.Shared; + /// documentation [Validate] public sealed record Data : IValidationTarget { @@ -32,4 +104,46 @@ public sealed record Data : IValidationTarget } """ ).RunAsync(TestContext.Current.CancellationToken); + + [Fact] + public async Task AddValidateAttributeWorksCorrectlyWithXmlDocsAndOtherAttributes() => + await CodeFixTestHelper.CreateCodeFixTest( + """ + using System; + + namespace Immediate.Validations.Shared; + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] + public sealed class MyStuffAttribute : Attribute; + + /// documentation + [MyStuff] + public sealed record {|IV0012:Data|} : IValidationTarget + { + public ValidationResult Validate() => []; + public ValidationResult Validate(ValidationResult errors) => []; + public static ValidationResult Validate(Data target) => []; + public static ValidationResult Validate(Data target, ValidationResult errors) => []; + } + """, + """ + using System; + + namespace Immediate.Validations.Shared; + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] + public sealed class MyStuffAttribute : Attribute; + + /// documentation + [MyStuff] + [Validate] + public sealed record {|IV0012:Data|} : IValidationTarget + { + public ValidationResult Validate() => []; + public ValidationResult Validate(ValidationResult errors) => []; + public static ValidationResult Validate(Data target) => []; + public static ValidationResult Validate(Data target, ValidationResult errors) => []; + } + """ + ).RunAsync(TestContext.Current.CancellationToken); }