Skip to content

Commit

Permalink
Exception analyzers / test helpers (#244)
Browse files Browse the repository at this point in the history
* VerifyThrows additions

* Analyzers for exception throwing in mutations
  • Loading branch information
mhelleborg authored Jun 20, 2024
1 parent 8ad1d02 commit d129a46
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 52 deletions.
134 changes: 90 additions & 44 deletions Source/Analyzers/AggregateAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ namespace Dolittle.SDK.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AggregateAnalyzer : DiagnosticAnalyzer
{
static readonly ImmutableDictionary<string, string?> _targetVisibilityPrivate = ImmutableDictionary.Create<string,string?>()
static readonly ImmutableDictionary<string, string?> _targetVisibilityPrivate = ImmutableDictionary
.Create<string, string?>()
.Add("targetVisibility", "private");

/// <inheritdoc />
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(
DescriptorRules.ExceptionInMutation,
DescriptorRules.Aggregate.MissingAttribute,
DescriptorRules.Aggregate.MissingMutation,
DescriptorRules.Aggregate.MutationShouldBePrivate,
Expand Down Expand Up @@ -71,24 +73,29 @@ static HashSet<ITypeSymbol> CheckOnMethods(SyntaxNodeAnalysisContext context, IN

foreach (var onMethod in onMethods)
{
if (onMethod.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is not MethodDeclarationSyntax syntax) continue;
if (onMethod.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is not MethodDeclarationSyntax syntax)
continue;

if (syntax.Modifiers.Any(SyntaxKind.PublicKeyword)
|| syntax.Modifiers.Any(SyntaxKind.InternalKeyword)
|| syntax.Modifiers.Any(SyntaxKind.ProtectedKeyword))
{
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Aggregate.MutationShouldBePrivate, syntax.GetLocation(),
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Aggregate.MutationShouldBePrivate,
syntax.GetLocation(),
_targetVisibilityPrivate, onMethod.ToDisplayString()));
}

var parameters = onMethod.Parameters;
if (parameters.Length != 1)
{
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Aggregate.MutationHasIncorrectNumberOfParameters, syntax.GetLocation(),
context.ReportDiagnostic(Diagnostic.Create(
DescriptorRules.Aggregate.MutationHasIncorrectNumberOfParameters, syntax.GetLocation(),
onMethod.ToDisplayString()));
}

EnsureMutationDoesNotAccessCurrentTime(context, syntax);

EnsureMutationDoesNotThrowExceptions(context, syntax);


if (parameters.Length > 0)
{
Expand All @@ -110,13 +117,42 @@ static HashSet<ITypeSymbol> CheckOnMethods(SyntaxNodeAnalysisContext context, IN
return eventTypesHandled;
}

static void EnsureMutationDoesNotThrowExceptions(SyntaxNodeAnalysisContext context,
MethodDeclarationSyntax onMethod)
{
var throwStatements = onMethod.DescendantNodes().OfType<ThrowStatementSyntax>().ToArray();
foreach (var throwStatement in throwStatements)
{
context.ReportDiagnostic(Diagnostic.Create(
DescriptorRules.ExceptionInMutation,
throwStatement.GetLocation()
));
}

var throwIfMethods = onMethod.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.Where(invocation =>
invocation.Expression is MemberAccessExpressionSyntax { Name: IdentifierNameSyntax identifier } &&
identifier.Identifier.ValueText.StartsWith("ThrowIf", StringComparison.Ordinal))
.ToArray();

foreach (var throwIfMethod in throwIfMethods)
{
context.ReportDiagnostic(Diagnostic.Create(
DescriptorRules.ExceptionInMutation,
throwIfMethod.GetLocation()
));
}
}

/// <summary>
/// Checks if the method gets the current time via DateTime or DateTimeOffset
/// Since this is not allowed for the mutations, we need to report a diagnostic
/// </summary>
/// <param name="context"></param>
/// <param name="onMethod"></param>
static void EnsureMutationDoesNotAccessCurrentTime(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax onMethod)
static void EnsureMutationDoesNotAccessCurrentTime(SyntaxNodeAnalysisContext context,
MethodDeclarationSyntax onMethod)
{
var currentTimeInvocations = onMethod.DescendantNodes()
.OfType<MemberAccessExpressionSyntax>()
Expand All @@ -129,11 +165,11 @@ static void EnsureMutationDoesNotAccessCurrentTime(SyntaxNodeAnalysisContext con
{
return false;
}
var typeInfo = context.SemanticModel.GetTypeInfo(memberAccess.Expression);
// Check if the type is DateTime or DateTimeOffset
return typeInfo.Type?.ToDisplayString() == "System.DateTime" || typeInfo.Type?.ToDisplayString() == "System.DateTimeOffset";
return typeInfo.Type?.ToDisplayString() == "System.DateTime" ||
typeInfo.Type?.ToDisplayString() == "System.DateTimeOffset";
}).ToArray();

foreach (var currentTimeInvocation in currentTimeInvocations)
Expand All @@ -149,7 +185,9 @@ static void EnsureMutationDoesNotAccessCurrentTime(SyntaxNodeAnalysisContext con
static void CheckAggregateRootAttributePresent(SyntaxNodeAnalysisContext context, INamedTypeSymbol aggregateClass)
{
var hasAttribute = aggregateClass.GetAttributes()
.Any(attribute => attribute.AttributeClass?.ToDisplayString().Equals(DolittleTypes.AggregateRootAttribute, StringComparison.Ordinal) == true);
.Any(attribute =>
attribute.AttributeClass?.ToDisplayString()
.Equals(DolittleTypes.AggregateRootAttribute, StringComparison.Ordinal) == true);

if (!hasAttribute)
{
Expand All @@ -176,18 +214,20 @@ static void CheckApplyInvocations(SyntaxNodeAnalysisContext context, ClassDeclar
if (typeInfo.Type is not { } type) continue;
if (!type.HasEventTypeAttribute())
{
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Events.MissingAttribute, invocation.GetLocation(),
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Events.MissingAttribute,
invocation.GetLocation(),
type.ToTargetClassAndAttributeProps(DolittleTypes.EventTypeAttribute), type.ToString()));
}

if (!handledEventTypes.Contains(type))
{
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Aggregate.MissingMutation, invocation.GetLocation(), type.ToMinimalTypeNameProps(),
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Aggregate.MissingMutation,
invocation.GetLocation(), type.ToMinimalTypeNameProps(),
type.ToString()));
}
}
}

static void CheckApplyInvocationsInOnMethods(SyntaxNodeAnalysisContext context, INamedTypeSymbol aggregateType)
{
var onMethods = aggregateType
Expand All @@ -209,7 +249,7 @@ static void CheckApplyInvocationsInOnMethods(SyntaxNodeAnalysisContext context,
.OfType<InvocationExpressionSyntax>()
.Where(invocation => invocation.Expression is IdentifierNameSyntax { Identifier.Text: "Apply" })
.ToArray();

foreach (var applyInvocation in applyInvocations)
{
context.ReportDiagnostic(Diagnostic.Create(
Expand All @@ -218,14 +258,15 @@ static void CheckApplyInvocationsInOnMethods(SyntaxNodeAnalysisContext context,
new[] { onMethod.ToDisplayString() }
));
}

var memberApplyInvocations = syntax
.DescendantNodes()
.OfType<MemberAccessExpressionSyntax>()
.Where(memberAccess => memberAccess.Name is IdentifierNameSyntax { Identifier.Text: "Apply" })
.Where(memberAccess => memberAccess.Name is IdentifierNameSyntax { Identifier.Text: "Apply" } && memberAccess.Expression is ThisExpressionSyntax or BaseExpressionSyntax)
.Where(memberAccess => memberAccess.Name is IdentifierNameSyntax { Identifier.Text: "Apply" } &&
memberAccess.Expression is ThisExpressionSyntax or BaseExpressionSyntax)
.ToArray();

foreach (var invocation in memberApplyInvocations)
{
context.ReportDiagnostic(Diagnostic.Create(
Expand All @@ -236,9 +277,9 @@ static void CheckApplyInvocationsInOnMethods(SyntaxNodeAnalysisContext context,
}
}
}
static void CheckMutationsInPublicMethods(SyntaxNodeAnalysisContext context, INamedTypeSymbol aggregateType)
{

static void CheckMutationsInPublicMethods(SyntaxNodeAnalysisContext context, INamedTypeSymbol aggregateType)
{
var publicMethods = aggregateType
.GetMembers()
.Where(member => !member.Name.Equals("On"))
Expand All @@ -249,6 +290,7 @@ static void CheckMutationsInPublicMethods(SyntaxNodeAnalysisContext context, INa
{
return;
}

var walker = new MutationWalker(context, aggregateType);

foreach (var onMethod in publicMethods)
Expand All @@ -257,41 +299,45 @@ static void CheckMutationsInPublicMethods(SyntaxNodeAnalysisContext context, INa
{
continue;
}

walker.Visit(syntax);
}
}

class MutationWalker : CSharpSyntaxWalker
class MutationWalker : CSharpSyntaxWalker
{
readonly SyntaxNodeAnalysisContext _context;
readonly INamedTypeSymbol _aggregateType;

public MutationWalker(SyntaxNodeAnalysisContext context, INamedTypeSymbol aggregateType)
{
readonly SyntaxNodeAnalysisContext _context;
readonly INamedTypeSymbol _aggregateType;
_context = context;
_aggregateType = aggregateType;
}

public MutationWalker(SyntaxNodeAnalysisContext context, INamedTypeSymbol aggregateType)
{
_context = context;
_aggregateType = aggregateType;
}
public override void VisitAssignmentExpression(AssignmentExpressionSyntax node)
{
var leftExpression = node.Left;

public override void VisitAssignmentExpression(AssignmentExpressionSyntax node)
if (leftExpression is IdentifierNameSyntax || leftExpression is MemberAccessExpressionSyntax)
{
var leftExpression = node.Left;

if (leftExpression is IdentifierNameSyntax || leftExpression is MemberAccessExpressionSyntax)
var symbolInfo = _context.SemanticModel.GetSymbolInfo(leftExpression);
if (symbolInfo.Symbol is IFieldSymbol || symbolInfo.Symbol is IPropertySymbol)
{
var symbolInfo = _context.SemanticModel.GetSymbolInfo(leftExpression);
if (symbolInfo.Symbol is IFieldSymbol || symbolInfo.Symbol is IPropertySymbol)
var containingType = symbolInfo.Symbol.ContainingType;
if (containingType != null && SymbolEqualityComparer.Default.Equals(_aggregateType, containingType))
{
var containingType = symbolInfo.Symbol.ContainingType;
if (containingType != null && SymbolEqualityComparer.Default.Equals(_aggregateType, containingType))
{
var diagnostic = Diagnostic.Create(DescriptorRules.Aggregate.PublicMethodsCannotMutateAggregateState, leftExpression.GetLocation());
_context.ReportDiagnostic(diagnostic);
}
var diagnostic =
Diagnostic.Create(DescriptorRules.Aggregate.PublicMethodsCannotMutateAggregateState,
leftExpression.GetLocation());
_context.ReportDiagnostic(diagnostic);
}
}

base.VisitAssignmentExpression(node);
}

// You can also add other types of mutations like increments, decrements, method calls etc.
}}
base.VisitAssignmentExpression(node);
}

// You can also add other types of mutations like increments, decrements, method calls etc.
}
}
11 changes: 11 additions & 0 deletions Source/Analyzers/DescriptorRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ static class DescriptorRules
isEnabledByDefault: true,
description: "Change the accessibility level to '{1}'.");


internal static readonly DiagnosticDescriptor ExceptionInMutation =
new(
DiagnosticIds.ExceptionInMutation,
title: "Exceptions can not be thrown from mutation methods",
messageFormat: "On-methods can not throw exceptions",
DiagnosticCategories.Sdk,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "On-methods can not throw exceptions. This will prevent the system from being able to replay events.");

internal static class Events
{
internal static readonly DiagnosticDescriptor MissingAttribute =
Expand Down
5 changes: 4 additions & 1 deletion Source/Analyzers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,15 @@ public static class DiagnosticIds
public const string EventHandlerMissingEventContext = "SDK0007";

public const string MissingBaseClassRuleId = "SDK0008";

/// <summary>
/// Invalid timespan.
/// </summary>
public const string InvalidTimeSpanParameter = "SDK0009";


public const string ExceptionInMutation = "SDK0010";

/// <summary>
/// Aggregate missing the required Attribute.
/// </summary>
Expand Down
30 changes: 29 additions & 1 deletion Source/Analyzers/ProjectionsAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class ProjectionsAnalyzer : DiagnosticAnalyzer
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(
// DescriptorRules.DuplicateIdentity,
DescriptorRules.ExceptionInMutation,
DescriptorRules.Events.MissingAttribute,
DescriptorRules.Projection.MissingAttribute,
DescriptorRules.Projection.MissingBaseClass,
Expand Down Expand Up @@ -114,14 +115,41 @@ static void CheckOnMethods(SyntaxNodeAnalysisContext context, INamedTypeSymbol p
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Projection.InvalidOnMethodParameters, loc,
onMethod.ToDisplayString()));
}

}

CheckOnReturnType(context, projectionType, onMethod, syntax);
EnsureMutationDoesNotAccessCurrentTime(context, syntax);
EnsureMutationDoesNotThrowExceptions(context, syntax);
}
}

static void EnsureMutationDoesNotThrowExceptions(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax onMethod)
{
var throwStatements = onMethod.DescendantNodes().OfType<ThrowStatementSyntax>().ToArray();
foreach (var throwStatement in throwStatements)
{
context.ReportDiagnostic(Diagnostic.Create(
DescriptorRules.ExceptionInMutation,
throwStatement.GetLocation()
));
}

var throwIfMethods = onMethod.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.Where(invocation =>
invocation.Expression is MemberAccessExpressionSyntax { Name: IdentifierNameSyntax identifier } &&
identifier.Identifier.ValueText.StartsWith("ThrowIf", StringComparison.Ordinal))
.ToArray();

foreach (var throwIfMethod in throwIfMethods)
{
context.ReportDiagnostic(Diagnostic.Create(
DescriptorRules.ExceptionInMutation,
throwIfMethod.GetLocation()
));
}
}

static void CheckOnReturnType(SyntaxNodeAnalysisContext context, INamedTypeSymbol projectionType, IMethodSymbol onMethod,
MethodDeclarationSyntax syntax)
{
Expand Down
Loading

0 comments on commit d129a46

Please sign in to comment.