diff --git a/README.md b/README.md index 3a9937a0e..5878d56ed 100755 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0171](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0171.md)|Usage|Use pattern matching instead of HasValue for Nullable\ check|ℹ️|❌|✔️| |[MA0172](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0172.md)|Usage|Both sides of the logical operation are identical|⚠️|❌|❌| |[MA0173](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0173.md)|Design|Use LazyInitializer.EnsureInitialize|ℹ️|✔️|✔️| -|[MA0174](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0174.md)|Style|Record should use explicit 'class' keyword|ℹ️|❌|❌| +|[MA0174](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0174.md)|Style|Record should use explicit 'class' keyword|ℹ️|❌|✔️| |[MA0175](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0175.md)|Style|Record should not use explicit 'class' keyword|ℹ️|❌|✔️| |[MA0176](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0176.md)|Performance|Optimize guid creation|ℹ️|✔️|✔️| |[MA0177](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0177.md)|Style|Use single-line XML comment syntax when possible|ℹ️|❌|✔️| diff --git a/docs/README.md b/docs/README.md index 273a8c80b..ce0501f79 100755 --- a/docs/README.md +++ b/docs/README.md @@ -172,7 +172,7 @@ |[MA0171](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0171.md)|Usage|Use pattern matching instead of HasValue for Nullable\ check|ℹ️|❌|✔️| |[MA0172](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0172.md)|Usage|Both sides of the logical operation are identical|⚠️|❌|❌| |[MA0173](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0173.md)|Design|Use LazyInitializer.EnsureInitialize|ℹ️|✔️|✔️| -|[MA0174](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0174.md)|Style|Record should use explicit 'class' keyword|ℹ️|❌|❌| +|[MA0174](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0174.md)|Style|Record should use explicit 'class' keyword|ℹ️|❌|✔️| |[MA0175](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0175.md)|Style|Record should not use explicit 'class' keyword|ℹ️|❌|✔️| |[MA0176](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0176.md)|Performance|Optimize guid creation|ℹ️|✔️|✔️| |[MA0177](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0177.md)|Style|Use single-line XML comment syntax when possible|ℹ️|❌|✔️| diff --git a/docs/Rules/MA0174.md b/docs/Rules/MA0174.md index d8ca8384c..99b4d3247 100644 --- a/docs/Rules/MA0174.md +++ b/docs/Rules/MA0174.md @@ -1,6 +1,6 @@ # MA0174 - Record should use explicit 'class' keyword -Source: [RecordClassDeclarationShouldBeExplicitAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/RecordClassDeclarationShouldBeExplicitAnalyzer.cs) +Sources: [RecordClassDeclarationShouldBeExplicitAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/RecordClassDeclarationShouldBeExplicitAnalyzer.cs), [RecordClassDeclarationShouldBeExplicitFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/RecordClassDeclarationShouldBeExplicitFixer.cs) This rule suggests adding the explicit `class` keyword to record declarations that don't specify it. diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/RecordClassDeclarationShouldBeExplicitFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/RecordClassDeclarationShouldBeExplicitFixer.cs new file mode 100644 index 000000000..4cae713bc --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/RecordClassDeclarationShouldBeExplicitFixer.cs @@ -0,0 +1,51 @@ +#if CSHARP10_OR_GREATER +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class RecordClassDeclarationShouldBeExplicitFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.RecordClassDeclarationShouldBeExplicit); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root?.FindNode(context.Span, getInnermostNodeForTie: true) is not RecordDeclarationSyntax nodeToFix) + return; + + var title = "Add 'class' keyword"; + var codeAction = CodeAction.Create( + title, + ct => AddClassKeyword(context.Document, nodeToFix, ct), + equivalenceKey: title); + + context.RegisterCodeFix(codeAction, context.Diagnostics); + } + + private static async Task AddClassKeyword(Document document, RecordDeclarationSyntax nodeToFix, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + + var classKeyword = SyntaxFactory.Token(SyntaxKind.ClassKeyword) + .WithLeadingTrivia(nodeToFix.Keyword.TrailingTrivia) + .WithTrailingTrivia(SyntaxFactory.Space); + + var newNode = nodeToFix + .WithKeyword(nodeToFix.Keyword.WithTrailingTrivia()) + .WithClassOrStructKeyword(classKeyword); + + editor.ReplaceNode(nodeToFix, newNode); + return editor.GetChangedDocument(); + } +} +#endif diff --git a/tests/Meziantou.Analyzer.Test/Rules/RecordClassDeclarationShouldBeExplicitAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/RecordClassDeclarationShouldBeExplicitAnalyzerTests.cs index f2f980d1f..ef4b35306 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/RecordClassDeclarationShouldBeExplicitAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/RecordClassDeclarationShouldBeExplicitAnalyzerTests.cs @@ -11,6 +11,7 @@ private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() .WithAnalyzer() + .WithCodeFixProvider() .WithTargetFramework(TargetFramework.NetLatest); } @@ -150,5 +151,44 @@ public record class Target : BaseRecord { } """) .ValidateAsync(); } + + [Fact] + public async Task ImplicitRecordClass_CodeFix_ShouldAddClassKeyword() + { + await CreateProjectBuilder() + .WithSourceCode(""" + public [|record|] Target { } + """) + .ShouldFixCodeWith(""" + public record class Target { } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ImplicitRecordClass_WithModifiers_CodeFix_ShouldAddClassKeyword() + { + await CreateProjectBuilder() + .WithSourceCode(""" + public sealed [|record|] Target { } + """) + .ShouldFixCodeWith(""" + public sealed record class Target { } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ImplicitRecordClass_WithParameters_CodeFix_ShouldAddClassKeyword() + { + await CreateProjectBuilder() + .WithSourceCode(""" + public [|record|] Target(int Id) { } + """) + .ShouldFixCodeWith(""" + public record class Target(int Id) { } + """) + .ValidateAsync(); + } } #endif \ No newline at end of file