From 698582c5e8aef5eabdc29a0de87bb3c8ffe25e26 Mon Sep 17 00:00:00 2001 From: danigutsch Date: Sun, 18 Aug 2024 13:37:46 +0200 Subject: [PATCH 1/5] Adds new analyzer ID. --- src/Vogen/Diagnostics/RuleIdentifiers.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Vogen/Diagnostics/RuleIdentifiers.cs b/src/Vogen/Diagnostics/RuleIdentifiers.cs index 537d955013..86a2271578 100644 --- a/src/Vogen/Diagnostics/RuleIdentifiers.cs +++ b/src/Vogen/Diagnostics/RuleIdentifiers.cs @@ -40,4 +40,5 @@ public static class RuleIdentifiers public const string EfCoreTargetMustExplicitlySpecifyItsPrimitive = "VOG030"; public const string EfCoreTargetMustBeAVo = "VOG031"; public const string DoNotThrowFromUserCode = "VOG032"; + public const string UseReadonlyStructInsteadOfStruct = "VOG033"; } \ No newline at end of file From 2e2fb9af9c89e87c95e14e7ab17ce42ddd630546 Mon Sep 17 00:00:00 2001 From: danigutsch Date: Sun, 18 Aug 2024 13:38:44 +0200 Subject: [PATCH 2/5] Add PreferReadonlyStructAnalyzer to enforce readonly structs Introduced a new analyzer class `PreferReadonlyStructAnalyzer` to enforce the use of `readonly struct`. Applys to `struct` and `record struct` --- .../Rules/PreferReadonlyStructAnalyzer.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/Vogen/Rules/PreferReadonlyStructAnalyzer.cs diff --git a/src/Vogen/Rules/PreferReadonlyStructAnalyzer.cs b/src/Vogen/Rules/PreferReadonlyStructAnalyzer.cs new file mode 100644 index 0000000000..e4191780ae --- /dev/null +++ b/src/Vogen/Rules/PreferReadonlyStructAnalyzer.cs @@ -0,0 +1,79 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; +using System.Linq; +using Vogen.Diagnostics; + +namespace Vogen.Rules; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PreferReadonlyStructAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor _rule = new( + RuleIdentifiers.UseReadonlyStructInsteadOfStruct, + "Use readonly struct instead of struct", + "Type '{0}' should be a readonly struct", + RuleCategories.Usage, + DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: + "The struct is not readonly. This can lead to invalid value objects in your domain. Use readonly struct instead."); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(_rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.StructDeclaration, SyntaxKind.RecordStructDeclaration); + } + + private static void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + if (context.Node is not TypeDeclarationSyntax typeDeclaration) + { + return; + } + + var symbol = context.SemanticModel.GetDeclaredSymbol(typeDeclaration); + if (symbol is null) + { + return; + } + + // readonly struct became available in C# 7.2 + var languageVersion = GetLanguageVersion(context); + if (languageVersion < LanguageVersion.CSharp7_2) + { + return; + } + + if (!VoFilter.IsTarget(symbol)) + { + return; + } + + if (symbol.IsReadOnly) + { + return; + } + + ReportDiagnostic(context, symbol); + } + + private static void ReportDiagnostic(SyntaxNodeAnalysisContext context, INamedTypeSymbol symbol) + { + var diagnostic = Diagnostic.Create(_rule, symbol.Locations[0], symbol.Name); + context.ReportDiagnostic(diagnostic); + } + + private static LanguageVersion GetLanguageVersion(SyntaxNodeAnalysisContext context) + { + var compilation = context.SemanticModel.Compilation; + var parseOptions = (CSharpParseOptions)compilation.SyntaxTrees.First().Options; + return parseOptions.LanguageVersion; + } +} \ No newline at end of file From 4395fcc67d8648b6baacd3726c88ac190ef76d79 Mon Sep 17 00:00:00 2001 From: danigutsch Date: Sun, 18 Aug 2024 13:40:15 +0200 Subject: [PATCH 3/5] Add MakeStructReadonlyCodeFixProvider class Introduce a new code fix provider class `MakeStructReadonlyCodeFixProvider` to automatically make structs readonly when the diagnostic created by `PreferReadonlyStructAnalyzer` is triggered. --- .../MakeStructReadonlyCodeFixProvider.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/Vogen.CodeFixers/Rules/MakeStructReadonlyFixers/MakeStructReadonlyCodeFixProvider.cs diff --git a/src/Vogen.CodeFixers/Rules/MakeStructReadonlyFixers/MakeStructReadonlyCodeFixProvider.cs b/src/Vogen.CodeFixers/Rules/MakeStructReadonlyFixers/MakeStructReadonlyCodeFixProvider.cs new file mode 100644 index 0000000000..3550075a60 --- /dev/null +++ b/src/Vogen.CodeFixers/Rules/MakeStructReadonlyFixers/MakeStructReadonlyCodeFixProvider.cs @@ -0,0 +1,75 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; +using System.Composition; +using Vogen.Diagnostics; + +namespace Vogen.Rules.MakeStructReadonlyFixers; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MakeStructReadonlyCodeFixProvider)), Shared] +public sealed class MakeStructReadonlyCodeFixProvider : CodeFixProvider +{ + private const string _title = "Make struct readonly"; + + public override ImmutableArray FixableDiagnosticIds => + ImmutableArray.Create(RuleIdentifiers.UseReadonlyStructInsteadOfStruct); + + 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 is null) + { + return; + } + + var diagnostic = context.Diagnostics[0]; + var diagnosticSpan = diagnostic.Location.SourceSpan; + + var typeDeclarationSyntax = root.FindToken(diagnosticSpan.Start) + .Parent? + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(syntax => syntax is StructDeclarationSyntax or RecordDeclarationSyntax); + + if (typeDeclarationSyntax is null) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + _title, + c => MakeStructReadonlyAsync(context.Document, typeDeclarationSyntax, c), + _title), + diagnostic); + } + + private static async Task MakeStructReadonlyAsync(Document document, TypeDeclarationSyntax typeDeclarationSyntax, CancellationToken cancellationToken) + { + var readonlyModifier = SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword); + + var newModifiers = typeDeclarationSyntax.Modifiers; + + // Ensure that the readonly keyword is inserted before the partial keyword + if (newModifiers.Any(SyntaxKind.PartialKeyword)) + { + var partialIndex = newModifiers.IndexOf(SyntaxKind.PartialKeyword); + newModifiers = newModifiers.Insert(partialIndex, readonlyModifier); + } + else + { + newModifiers = newModifiers.Add(readonlyModifier); + } + + var newStructDeclaration = typeDeclarationSyntax.WithModifiers(newModifiers); + + var root = await document.GetSyntaxRootAsync(cancellationToken); + var newRoot = root!.ReplaceNode(typeDeclarationSyntax, newStructDeclaration); + return document.WithSyntaxRoot(newRoot); + } +} From b06b5915cc7d10d87d9795690a899d743fff3c57 Mon Sep 17 00:00:00 2001 From: danigutsch Date: Sun, 18 Aug 2024 13:40:55 +0200 Subject: [PATCH 4/5] Add unit tests for PreferReadonlyStructAnalyzer and MakeStructReadonlyCodeFixProvider. --- .../PreferReadonlyStructsAnalyzerTests.cs | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/AnalyzerTests/PreferReadonlyStructsAnalyzerTests.cs diff --git a/tests/AnalyzerTests/PreferReadonlyStructsAnalyzerTests.cs b/tests/AnalyzerTests/PreferReadonlyStructsAnalyzerTests.cs new file mode 100644 index 0000000000..89380056c3 --- /dev/null +++ b/tests/AnalyzerTests/PreferReadonlyStructsAnalyzerTests.cs @@ -0,0 +1,116 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using System.Threading.Tasks; +using VerifyCS = AnalyzerTests.Verifiers.CSharpCodeFixVerifier; + +namespace AnalyzerTests; + +public class PreferReadonlyStructsAnalyzerTests +{ + [Theory] + [InlineData("class")] + [InlineData("record")] + [InlineData("record class")] + public async Task Does_not_trigger_if_not_struct(string type) + { + var source = $$""" + using Vogen; + + namespace Whatever; + + [ValueObject] + public partial {{type}} CustomerId { } + """; + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { source } + }, + CompilerDiagnostics = CompilerDiagnostics.Suggestions, + ReferenceAssemblies = References.Net80AndOurs.Value, + }; + + test.DisabledDiagnostics.Add("CS1591"); + + await test.RunAsync(); + } + + [Theory] + [InlineData("struct")] + [InlineData("record struct")] + public async Task Does_not_trigger_when_struct_is_readonly(string type) + { + var source = $$""" + using Vogen; + + namespace Whatever; + + [ValueObject] + public readonly partial {{type}} CustomerId { } + """; + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { source } + }, + CompilerDiagnostics = CompilerDiagnostics.Suggestions, + ReferenceAssemblies = References.Net80AndOurs.Value, + }; + + test.DisabledDiagnostics.Add("CS1591"); + + await test.RunAsync(); + } + + [Theory] + [InlineData("struct", "")] + [InlineData("record struct", "")] + [InlineData("struct", "")] + [InlineData("record struct", "")] + [InlineData("struct", "")] + [InlineData("record struct", "")] + public async Task Triggers_when_struct_is_not_partial(string modifier, string genericType) + { + var source = $$""" + using Vogen; + namespace Whatever; + + [ValueObject{{genericType}}] + public partial {{modifier}} {|#0:DocumentId|} { } + """; + + var fixedCode = $$""" + using Vogen; + namespace Whatever; + + [ValueObject{{genericType}}] + public readonly partial {{modifier}} {|#0:DocumentId|} { } + """; + + var expectedDiagnostic = VerifyCS + .Diagnostic("VOG033") + .WithSeverity(DiagnosticSeverity.Info) + .WithLocation(0) + .WithArguments("DocumentId"); + + var test = new VerifyCS.Test + { + TestState = + { + Sources = { source } + }, + CompilerDiagnostics = CompilerDiagnostics.Suggestions, + ReferenceAssemblies = References.Net80AndOurs.Value, + ExpectedDiagnostics = { expectedDiagnostic }, + FixedCode = fixedCode + }; + + test.DisabledDiagnostics.Add("CS1591"); + + await test.RunAsync(); + } +} From 464f3536a8f7bf4fb5181e4d56b98b41d698026a Mon Sep 17 00:00:00 2001 From: danigutsch Date: Sun, 18 Aug 2024 13:41:14 +0200 Subject: [PATCH 5/5] Add new rule VOG033 to AnalyzerReleases.Unshipped.md Added a new rule to the `AnalyzerReleases.Unshipped.md` file: - **Rule ID**: VOG033 - **Category**: Usage - **Severity**: Info - **Notes**: UseReadonlyStructInsteadOfStructAnalyzer --- src/Vogen/AnalyzerReleases.Unshipped.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Vogen/AnalyzerReleases.Unshipped.md b/src/Vogen/AnalyzerReleases.Unshipped.md index 5cc732a2d1..51b3f4ac49 100644 --- a/src/Vogen/AnalyzerReleases.Unshipped.md +++ b/src/Vogen/AnalyzerReleases.Unshipped.md @@ -5,4 +5,5 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- +VOG033 | Usage | Info | UseReadonlyStructInsteadOfStructAnalyzer