Skip to content

Commit

Permalink
Merge pull request #467 from TimeWarpEngineering/Cramer/2024-08-13/St…
Browse files Browse the repository at this point in the history
…ateReadOnlyPublicPropertiesAnalyzer

Add state read only public properties analyzer
  • Loading branch information
StevenTCramer authored Aug 13, 2024
2 parents 4914b69 + f778172 commit 5cb2e52
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 9 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<!-- Set common properties regarding assembly information and nuget packages -->
<PropertyGroup>
<TimeWarpStateVersion>11.0.0-beta.82+8.0.303</TimeWarpStateVersion>
<TimeWarpStateVersion>11.0.0-beta.83+8.0.303</TimeWarpStateVersion>
<Authors>Steven T. Cramer</Authors>
<Product>TimeWarp State</Product>
<PackageVersion>$(TimeWarpStateVersion)</PackageVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
StateInheritanceRule | Design | Error | StateInheritanceAnalyzer
StateReadOnlyPublicPropertiesRule | Design | Error | StateReadOnlyPublicPropertiesAnalyzer
TW0001 | TimeWarp.State | Error | TimeWarpStateActionAnalyzer
TWD001 | Debug | Info | TimeWarpStateActionAnalyzer
5 changes: 5 additions & 0 deletions Source/TimeWarp.State.Analyzer/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 0 additions & 4 deletions Source/TimeWarp.State.Analyzer/StateInheritanceAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.Error, isEnabledByDefault: true, description: Description);

public override ImmutableArray<DiagnosticDescriptor> 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);
}
}
}
4 changes: 0 additions & 4 deletions Source/TimeWarp.State.Analyzer/TimeWarpStateActionAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SampleState>
// {
// 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<StateReadOnlyPublicPropertiesAnalyzer, FixieVerifier>
// {
// 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<SampleState>
// {
// 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<StateReadOnlyPublicPropertiesAnalyzer, FixieVerifier>
// {
// 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<SampleState>
// {
// public int PublicProperty { get; private set; }
//
// public override void Initialize() { }
// }
// """;
//
// var analyzerTest = new CSharpAnalyzerTest<StateReadOnlyPublicPropertiesAnalyzer, FixieVerifier>
// {
// 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<SampleState>
// {
// public int PublicProperty { get; }
//
// public override void Initialize() { }
// }
// """;
//
// var analyzerTest = new CSharpAnalyzerTest<StateReadOnlyPublicPropertiesAnalyzer, FixieVerifier>
// {
// TestCode = TestCode
// };
//
// const string TimeWarpStateAssemblyPath = @"TimeWarp.State.dll";
// analyzerTest.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(TimeWarpStateAssemblyPath));
//
// await analyzerTest.RunAsync();
// }
// }

0 comments on commit 5cb2e52

Please sign in to comment.