From 172d974af9c30e7621ea5d62328220b27d0f1587 Mon Sep 17 00:00:00 2001 From: Stuart Turner Date: Wed, 12 Aug 2026 10:56:36 -0500 Subject: [PATCH 1/4] Improve `[Validate]` codefixer behavior --- .../AddValidateAttributeCodefixProvider.cs | 39 ++++++++++++++----- ...ddValidateAttributeCodefixProviderTests.cs | 30 ++++++++++++++ 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs b/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs index 78e19ee..0f794fe 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,44 @@ 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 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 newDecl = typeDeclaration + .WithoutLeadingTrivia() .WithAttributeLists( typeDeclaration.AttributeLists - .Add(AttributeList( - SingletonSeparatedList( - Attribute( - IdentifierName("Validate")))))); + .Add( + AttributeList( + SingletonSeparatedList( + Attribute( + IdentifierName("Validate") + ) + ) + ) + .WithLeadingTrivia(typeDeclaration.GetLeadingTrivia()) + .WithTrailingTrivia(ElasticCarriageReturnLineFeed) + ) + ) + .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, annotation) + .WithAdditionalAnnotations(Formatter.Annotation); var newRoot = root.ReplaceNode(typeDeclaration, newDecl); - var newDocument = document.WithSyntaxRoot(newRoot); - return Task.FromResult(newDocument); + return document.WithSyntaxRoot(newRoot); } } diff --git a/tests/Immediate.Validations.Tests/CodeFixTests/AddValidateAttributeCodefixProviderTests.cs b/tests/Immediate.Validations.Tests/CodeFixTests/AddValidateAttributeCodefixProviderTests.cs index eb91425..775c7d3 100644 --- a/tests/Immediate.Validations.Tests/CodeFixTests/AddValidateAttributeCodefixProviderTests.cs +++ b/tests/Immediate.Validations.Tests/CodeFixTests/AddValidateAttributeCodefixProviderTests.cs @@ -32,4 +32,34 @@ public sealed record Data : IValidationTarget } """ ).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() => []; + public ValidationResult Validate(ValidationResult errors) => []; + public static ValidationResult Validate(Data target) => []; + public static ValidationResult Validate(Data target, ValidationResult errors) => []; + } + """, + """ + namespace Immediate.Validations.Shared; + + /// documentation + [Validate] + public sealed record 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); } From 4a315b7b1c8185286e7b11ce1b0f6577ce025283 Mon Sep 17 00:00:00 2001 From: Stuart Turner Date: Wed, 12 Aug 2026 11:14:07 -0500 Subject: [PATCH 2/4] Verify and fix codefixer works with other existing attributes --- .../AddValidateAttributeCodefixProvider.cs | 47 +++++++---- ...ddValidateAttributeCodefixProviderTests.cs | 82 +++++++++++++++++++ 2 files changed, 111 insertions(+), 18 deletions(-) diff --git a/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs b/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs index 0f794fe..4c52a1d 100644 --- a/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs +++ b/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs @@ -3,7 +3,6 @@ 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; @@ -51,24 +50,36 @@ private static async Task AddValidateAttribute(Document document, Comp var referenceId = DocumentationCommentId.CreateReferenceId(validateSymbol); var annotation = new SyntaxAnnotation("SymbolId", referenceId); - var newDecl = typeDeclaration - .WithoutLeadingTrivia() - .WithAttributeLists( - typeDeclaration.AttributeLists - .Add( - AttributeList( - SingletonSeparatedList( - Attribute( - IdentifierName("Validate") - ) - ) - ) - .WithLeadingTrivia(typeDeclaration.GetLeadingTrivia()) - .WithTrailingTrivia(ElasticCarriageReturnLineFeed) - ) + var validateAttribute = AttributeList( + SingletonSeparatedList( + Attribute( + IdentifierName("Validate") + ) ) - .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, annotation) - .WithAdditionalAnnotations(Formatter.Annotation); + ) + .WithTrailingTrivia(ElasticCarriageReturnLineFeed); + + var newDecl = typeDeclaration.AttributeLists switch + { + [] => + typeDeclaration + .WithoutLeadingTrivia() + .WithAttributeLists( + typeDeclaration.AttributeLists + .Add(validateAttribute.WithLeadingTrivia(typeDeclaration.GetLeadingTrivia())) + ) + .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, annotation) + .WithAdditionalAnnotations(Formatter.Annotation), + + _ => + typeDeclaration + .WithAttributeLists( + typeDeclaration.AttributeLists + .Add(validateAttribute.WithLeadingTrivia(ElasticCarriageReturnLineFeed)) + ) + .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, annotation) + .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 775c7d3..8f33007 100644 --- a/tests/Immediate.Validations.Tests/CodeFixTests/AddValidateAttributeCodefixProviderTests.cs +++ b/tests/Immediate.Validations.Tests/CodeFixTests/AddValidateAttributeCodefixProviderTests.cs @@ -33,6 +33,46 @@ public sealed record Data : IValidationTarget """ ).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( @@ -62,4 +102,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); } From 34045b1bdbe98b7e871a93136c58ca47577b6c27 Mon Sep 17 00:00:00 2001 From: Stuart Turner Date: Wed, 12 Aug 2026 11:55:18 -0500 Subject: [PATCH 3/4] Fix line-endings --- .../AddValidateAttributeCodefixProvider.cs | 27 ++++++++++++------- ...ddValidateAttributeCodefixProviderTests.cs | 26 +++++++++--------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs b/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs index 4c52a1d..c6e1765 100644 --- a/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs +++ b/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs @@ -3,6 +3,7 @@ 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; @@ -50,6 +51,12 @@ private static async Task AddValidateAttribute(Document document, Comp 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( @@ -57,7 +64,7 @@ private static async Task AddValidateAttribute(Document document, Comp ) ) ) - .WithTrailingTrivia(ElasticCarriageReturnLineFeed); + .WithTrailingTrivia(newLineSyntax); var newDecl = typeDeclaration.AttributeLists switch { @@ -67,21 +74,23 @@ private static async Task AddValidateAttribute(Document document, Comp .WithAttributeLists( typeDeclaration.AttributeLists .Add(validateAttribute.WithLeadingTrivia(typeDeclaration.GetLeadingTrivia())) - ) - .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, annotation) - .WithAdditionalAnnotations(Formatter.Annotation), + ), _ => typeDeclaration .WithAttributeLists( typeDeclaration.AttributeLists - .Add(validateAttribute.WithLeadingTrivia(ElasticCarriageReturnLineFeed)) - ) - .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, annotation) - .WithAdditionalAnnotations(Formatter.Annotation), + .Add(validateAttribute) + ), }; - var newRoot = root.ReplaceNode(typeDeclaration, newDecl); + var newRoot = root.ReplaceNode( + typeDeclaration, + newDecl + .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, annotation) + .WithAdditionalAnnotations(Formatter.Annotation) + ); + return document.WithSyntaxRoot(newRoot); } } diff --git a/tests/Immediate.Validations.Tests/CodeFixTests/AddValidateAttributeCodefixProviderTests.cs b/tests/Immediate.Validations.Tests/CodeFixTests/AddValidateAttributeCodefixProviderTests.cs index 8f33007..6de1c52 100644 --- a/tests/Immediate.Validations.Tests/CodeFixTests/AddValidateAttributeCodefixProviderTests.cs +++ b/tests/Immediate.Validations.Tests/CodeFixTests/AddValidateAttributeCodefixProviderTests.cs @@ -9,26 +9,28 @@ public sealed class AddValidateAttributeCodefixProviderTests public async Task AddValidateAttribute() => await CodeFixTestHelper.CreateCodeFixTest( """ - namespace Immediate.Validations.Shared; + namespace Immediate.Validations.Testing; - public sealed record {|IV0012:Data|} : IValidationTarget + public sealed record {|IV0012:Data|} : Immediate.Validations.Shared.IValidationTarget { - public ValidationResult Validate() => []; - public ValidationResult Validate(ValidationResult errors) => []; - public static ValidationResult Validate(Data target) => []; - public static ValidationResult Validate(Data target, ValidationResult errors) => []; + 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) => []; } """, """ - namespace Immediate.Validations.Shared; + using Immediate.Validations.Shared; + + namespace Immediate.Validations.Testing; [Validate] - public sealed record Data : IValidationTarget + public sealed record Data : Immediate.Validations.Shared.IValidationTarget { - public ValidationResult Validate() => []; - public ValidationResult Validate(ValidationResult errors) => []; - public static ValidationResult Validate(Data target) => []; - public static ValidationResult Validate(Data target, ValidationResult errors) => []; + 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); From 77fbc4d591f8977a46181a8203b8291e0428d345 Mon Sep 17 00:00:00 2001 From: Stuart Turner Date: Wed, 12 Aug 2026 12:10:38 -0500 Subject: [PATCH 4/4] Limit formatting to new syntax node --- .../AddValidateAttributeCodefixProvider.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs b/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs index c6e1765..9228e7b 100644 --- a/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs +++ b/src/Immediate.Validations.CodeFixes/AddValidateAttributeCodefixProvider.cs @@ -64,6 +64,7 @@ private static async Task AddValidateAttribute(Document document, Comp ) ) ) + .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, annotation) .WithTrailingTrivia(newLineSyntax); var newDecl = typeDeclaration.AttributeLists switch @@ -73,22 +74,27 @@ private static async Task AddValidateAttribute(Document document, Comp .WithoutLeadingTrivia() .WithAttributeLists( typeDeclaration.AttributeLists - .Add(validateAttribute.WithLeadingTrivia(typeDeclaration.GetLeadingTrivia())) + .Add( + validateAttribute + .WithLeadingTrivia(typeDeclaration.GetLeadingTrivia()) + .WithAdditionalAnnotations(Formatter.Annotation) + ) ), _ => typeDeclaration .WithAttributeLists( typeDeclaration.AttributeLists - .Add(validateAttribute) + .Add( + validateAttribute + .WithAdditionalAnnotations(Formatter.Annotation) + ) ), }; var newRoot = root.ReplaceNode( typeDeclaration, newDecl - .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, annotation) - .WithAdditionalAnnotations(Formatter.Annotation) ); return document.WithSyntaxRoot(newRoot);