From f5734e88f3a0819112bfca95e7419435a35a0227 Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Tue, 13 Aug 2024 18:56:07 +0700 Subject: [PATCH 1/5] Rename Test File --- ...tionAnalyser_Tests.cs => TimeWarpStateActionAnalyser_Tests.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Tests/TimeWarp.State.Analyzer.Tests/{BlazorStateActionAnalyser_Tests.cs => TimeWarpStateActionAnalyser_Tests.cs} (100%) diff --git a/Tests/TimeWarp.State.Analyzer.Tests/BlazorStateActionAnalyser_Tests.cs b/Tests/TimeWarp.State.Analyzer.Tests/TimeWarpStateActionAnalyser_Tests.cs similarity index 100% rename from Tests/TimeWarp.State.Analyzer.Tests/BlazorStateActionAnalyser_Tests.cs rename to Tests/TimeWarp.State.Analyzer.Tests/TimeWarpStateActionAnalyser_Tests.cs From f0569ddd26a94c3ba650d4abfac5433a0c4681cb Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Tue, 13 Aug 2024 18:57:50 +0700 Subject: [PATCH 2/5] Add StateReadOnlyPublicPropertiesAnalyzer --- .../AnalyzerReleases.Unshipped.md | 1 + .../TimeWarp.State.Analyzer/GlobalUsings.cs | 5 ++ .../StateInheritanceAnalyzer.cs | 4 - .../StateReadOnlyPublicPropertiesAnalyzer.cs | 73 +++++++++++++++++++ .../TimeWarpStateActionAnalyzer.cs | 4 - 5 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 Source/TimeWarp.State.Analyzer/GlobalUsings.cs create mode 100644 Source/TimeWarp.State.Analyzer/StateReadOnlyPublicPropertiesAnalyzer.cs diff --git a/Source/TimeWarp.State.Analyzer/AnalyzerReleases.Unshipped.md b/Source/TimeWarp.State.Analyzer/AnalyzerReleases.Unshipped.md index 92c2decd8..51db989f6 100644 --- a/Source/TimeWarp.State.Analyzer/AnalyzerReleases.Unshipped.md +++ b/Source/TimeWarp.State.Analyzer/AnalyzerReleases.Unshipped.md @@ -6,5 +6,6 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- StateInheritanceRule | Design | Error | StateInheritanceAnalyzer +StateReadOnlyPublicPropertiesRule | Design | Warning | StateReadOnlyPublicPropertiesAnalyzer TW0001 | TimeWarp.State | Error | TimeWarpStateActionAnalyzer TWD001 | Debug | Info | TimeWarpStateActionAnalyzer diff --git a/Source/TimeWarp.State.Analyzer/GlobalUsings.cs b/Source/TimeWarp.State.Analyzer/GlobalUsings.cs new file mode 100644 index 000000000..0035fdd49 --- /dev/null +++ b/Source/TimeWarp.State.Analyzer/GlobalUsings.cs @@ -0,0 +1,5 @@ +global using Microsoft.CodeAnalysis; +global using Microsoft.CodeAnalysis.CSharp.Syntax; +global using Microsoft.CodeAnalysis.Diagnostics; +global using System.Collections.Immutable; +global using System.Linq; diff --git a/Source/TimeWarp.State.Analyzer/StateInheritanceAnalyzer.cs b/Source/TimeWarp.State.Analyzer/StateInheritanceAnalyzer.cs index b6c4e6ba0..6f120bf57 100644 --- a/Source/TimeWarp.State.Analyzer/StateInheritanceAnalyzer.cs +++ b/Source/TimeWarp.State.Analyzer/StateInheritanceAnalyzer.cs @@ -1,10 +1,6 @@ namespace TimeWarp.State.Analyzer; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; -using System.Collections.Immutable; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class StateInheritanceAnalyzer : DiagnosticAnalyzer diff --git a/Source/TimeWarp.State.Analyzer/StateReadOnlyPublicPropertiesAnalyzer.cs b/Source/TimeWarp.State.Analyzer/StateReadOnlyPublicPropertiesAnalyzer.cs new file mode 100644 index 000000000..63ab8fafb --- /dev/null +++ b/Source/TimeWarp.State.Analyzer/StateReadOnlyPublicPropertiesAnalyzer.cs @@ -0,0 +1,73 @@ +namespace TimeWarp.State.Analyzer; + +using Microsoft.CodeAnalysis.CSharp; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class StateReadOnlyPublicPropertiesAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "StateReadOnlyPublicPropertiesRule"; + + private static readonly LocalizableString Title = "Public property in State class should be read-only"; + private static readonly LocalizableString MessageFormat = "The public property '{0}' in State-derived class should be read-only"; + private static readonly LocalizableString Description = "Public properties in classes inheriting from State should be read-only to enforce immutability."; + private const string Category = "Design"; + + private static readonly DiagnosticDescriptor Rule = new(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } } + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration); + } + + private static void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + + if (!InheritsFromState(classDeclaration, context.SemanticModel)) + return; + + foreach (MemberDeclarationSyntax member in classDeclaration.Members) + { + if (member is PropertyDeclarationSyntax propertyDeclaration) + { + AnalyzeProperty(propertyDeclaration, context); + } + } + } + + private static bool InheritsFromState(ClassDeclarationSyntax classDeclaration, SemanticModel semanticModel) + { + INamedTypeSymbol? classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration); + if (classSymbol == null) + return false; + + INamedTypeSymbol? baseType = classSymbol.BaseType; + while (baseType != null) + { + if (baseType.Name == "State" && baseType.TypeArguments.Length == 1) + return true; + baseType = baseType.BaseType; + } + + return false; + } + + private static void AnalyzeProperty(PropertyDeclarationSyntax propertyDeclaration, SyntaxNodeAnalysisContext context) + { + if (!propertyDeclaration.Modifiers.Any(SyntaxKind.PublicKeyword)) return; + + AccessorDeclarationSyntax? setter = + propertyDeclaration.AccessorList?.Accessors + .FirstOrDefault(a => a.IsKind(SyntaxKind.SetAccessorDeclaration)); + + if (setter != null && !setter.Modifiers.Any(SyntaxKind.PrivateKeyword)) + { + var diagnostic = Diagnostic.Create(Rule, propertyDeclaration.Identifier.GetLocation(), propertyDeclaration.Identifier.Text); + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/Source/TimeWarp.State.Analyzer/TimeWarpStateActionAnalyzer.cs b/Source/TimeWarp.State.Analyzer/TimeWarpStateActionAnalyzer.cs index 8e4bea8bd..bb817bc8d 100644 --- a/Source/TimeWarp.State.Analyzer/TimeWarpStateActionAnalyzer.cs +++ b/Source/TimeWarp.State.Analyzer/TimeWarpStateActionAnalyzer.cs @@ -1,10 +1,6 @@ namespace TimeWarp.State.Analyzer; -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.CSharp.Syntax; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class TimeWarpStateActionAnalyzer : DiagnosticAnalyzer From a8977da8586e63563605b2c6b190a7690597e054 Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Tue, 13 Aug 2024 19:50:09 +0700 Subject: [PATCH 3/5] Change from warning to Error --- Source/TimeWarp.State.Analyzer/AnalyzerReleases.Unshipped.md | 2 +- .../StateReadOnlyPublicPropertiesAnalyzer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/TimeWarp.State.Analyzer/AnalyzerReleases.Unshipped.md b/Source/TimeWarp.State.Analyzer/AnalyzerReleases.Unshipped.md index 51db989f6..9ddfc4036 100644 --- a/Source/TimeWarp.State.Analyzer/AnalyzerReleases.Unshipped.md +++ b/Source/TimeWarp.State.Analyzer/AnalyzerReleases.Unshipped.md @@ -6,6 +6,6 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- StateInheritanceRule | Design | Error | StateInheritanceAnalyzer -StateReadOnlyPublicPropertiesRule | Design | Warning | StateReadOnlyPublicPropertiesAnalyzer +StateReadOnlyPublicPropertiesRule | Design | Error | StateReadOnlyPublicPropertiesAnalyzer TW0001 | TimeWarp.State | Error | TimeWarpStateActionAnalyzer TWD001 | Debug | Info | TimeWarpStateActionAnalyzer diff --git a/Source/TimeWarp.State.Analyzer/StateReadOnlyPublicPropertiesAnalyzer.cs b/Source/TimeWarp.State.Analyzer/StateReadOnlyPublicPropertiesAnalyzer.cs index 63ab8fafb..6c09ed462 100644 --- a/Source/TimeWarp.State.Analyzer/StateReadOnlyPublicPropertiesAnalyzer.cs +++ b/Source/TimeWarp.State.Analyzer/StateReadOnlyPublicPropertiesAnalyzer.cs @@ -12,7 +12,7 @@ public class StateReadOnlyPublicPropertiesAnalyzer : DiagnosticAnalyzer private static readonly LocalizableString Description = "Public properties in classes inheriting from State should be read-only to enforce immutability."; private const string Category = "Design"; - private static readonly DiagnosticDescriptor Rule = new(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); + private static readonly DiagnosticDescriptor Rule = new(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); public override ImmutableArray SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } } From 5963b78ebf07c4754b74676688d8ddb31fc06b8d Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Tue, 13 Aug 2024 19:52:26 +0700 Subject: [PATCH 4/5] Bump version --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 7b2a5e57b..c1fe51d9d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 11.0.0-beta.82+8.0.303 + 11.0.0-beta.83+8.0.303 Steven T. Cramer TimeWarp State $(TimeWarpStateVersion) From f778172e7b204fe608e5c637b70548bf92bd4ddd Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Tue, 13 Aug 2024 19:53:11 +0700 Subject: [PATCH 5/5] Commented out tests get errror that I don't understand analyzer was tested manually and seems to work. --- ...eReadOnlyPublicPropertiesAnalyzer_Tests.cs | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 Tests/TimeWarp.State.Analyzer.Tests/StateReadOnlyPublicPropertiesAnalyzer_Tests.cs diff --git a/Tests/TimeWarp.State.Analyzer.Tests/StateReadOnlyPublicPropertiesAnalyzer_Tests.cs b/Tests/TimeWarp.State.Analyzer.Tests/StateReadOnlyPublicPropertiesAnalyzer_Tests.cs new file mode 100644 index 000000000..980284049 --- /dev/null +++ b/Tests/TimeWarp.State.Analyzer.Tests/StateReadOnlyPublicPropertiesAnalyzer_Tests.cs @@ -0,0 +1,121 @@ +// // ReSharper disable InconsistentNaming +// namespace StateReadOnlyPublicPropertiesAnalyzer_; +// +// public class Should_Trigger_StateReadOnlyPublicPropertiesRule +// { +// public static async Task Given_PublicPropertyWithPublicSetter() +// { +// const string TestCode = +// """ +// using System.Threading.Tasks; +// using TimeWarp.State; +// +// public class SampleState : State +// { +// public int PublicProperty { get; set; } +// +// public override void Initialize() { } +// } +// """; +// +// var expectedDiagnostic = new DiagnosticResult("StateReadOnlyPublicPropertiesRule", DiagnosticSeverity.Warning) +// .WithSpan(6, 16, 6, 30) +// .WithArguments("PublicProperty"); +// +// var analyzerTest = new CSharpAnalyzerTest +// { +// TestCode = TestCode +// }; +// +// analyzerTest.ExpectedDiagnostics.Add(expectedDiagnostic); +// +// const string TimeWarpStateAssemblyPath = @"TimeWarp.State.dll"; +// analyzerTest.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(TimeWarpStateAssemblyPath)); +// +// await analyzerTest.RunAsync(); +// } +// +// public static async Task Given_PublicPropertyWithProtectedSetter() +// { +// const string TestCode = +// """ +// using System.Threading.Tasks; +// using TimeWarp.State; +// +// public class SampleState : State +// { +// public int PublicProperty { get; protected set; } +// +// public override void Initialize() { } +// } +// """; +// +// var expectedDiagnostic = new DiagnosticResult("StateReadOnlyPublicPropertiesRule", DiagnosticSeverity.Warning) +// .WithSpan(6, 16, 6, 30) +// .WithArguments("PublicProperty"); +// +// var analyzerTest = new CSharpAnalyzerTest +// { +// TestCode = TestCode +// }; +// +// analyzerTest.ExpectedDiagnostics.Add(expectedDiagnostic); +// +// const string TimeWarpStateAssemblyPath = @"TimeWarp.State.dll"; +// analyzerTest.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(TimeWarpStateAssemblyPath)); +// +// await analyzerTest.RunAsync(); +// } +// +// public static async Task Given_PublicPropertyWithPrivateSetter() +// { +// const string TestCode = +// """ +// using System.Threading.Tasks; +// using TimeWarp.State; +// +// public class SampleState : State +// { +// public int PublicProperty { get; private set; } +// +// public override void Initialize() { } +// } +// """; +// +// var analyzerTest = new CSharpAnalyzerTest +// { +// TestCode = TestCode +// }; +// +// const string TimeWarpStateAssemblyPath = @"TimeWarp.State.dll"; +// analyzerTest.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(TimeWarpStateAssemblyPath)); +// +// await analyzerTest.RunAsync(); +// } +// +// public static async Task Given_PublicReadOnlyProperty() +// { +// const string TestCode = +// """ +// using System.Threading.Tasks; +// using TimeWarp.State; +// +// public class SampleState : State +// { +// public int PublicProperty { get; } +// +// public override void Initialize() { } +// } +// """; +// +// var analyzerTest = new CSharpAnalyzerTest +// { +// TestCode = TestCode +// }; +// +// const string TimeWarpStateAssemblyPath = @"TimeWarp.State.dll"; +// analyzerTest.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(TimeWarpStateAssemblyPath)); +// +// await analyzerTest.RunAsync(); +// } +// }