Skip to content

Commit

Permalink
Added ability to extend custom events to redact specific events, in a…
Browse files Browse the repository at this point in the history
…ddition to the built-in ones.

Added analyzer to ensure the extended type is annotated correctly
  • Loading branch information
mhelleborg committed Oct 21, 2024
1 parent eb7c367 commit 52ab322
Show file tree
Hide file tree
Showing 16 changed files with 509 additions and 143 deletions.
8 changes: 4 additions & 4 deletions Source/Analyzers/AggregateAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ static HashSet<ITypeSymbol> CheckOnMethods(SyntaxNodeAnalysisContext context, IN
context.ReportDiagnostic(Diagnostic.Create(
DescriptorRules.Events.MissingAttribute,
parameters[0].DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax().GetLocation(),
eventType.ToTargetClassAndAttributeProps(DolittleTypes.EventTypeAttribute),
eventType.ToTargetClassAndAttributeProps(DolittleConstants.Types.EventTypeAttribute),
eventType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))
);
}
Expand Down Expand Up @@ -187,14 +187,14 @@ static void CheckAggregateRootAttributePresent(SyntaxNodeAnalysisContext context
var hasAttribute = aggregateClass.GetAttributes()
.Any(attribute =>
attribute.AttributeClass?.ToDisplayString()
.Equals(DolittleTypes.AggregateRootAttribute, StringComparison.Ordinal) == true);
.Equals(DolittleConstants.Types.AggregateRootAttribute, StringComparison.Ordinal) == true);

if (!hasAttribute)
{
context.ReportDiagnostic(Diagnostic.Create(
DescriptorRules.Aggregate.MissingAttribute,
aggregateClass.Locations[0],
aggregateClass.ToTargetClassAndAttributeProps(DolittleTypes.AggregateRootAttribute),
aggregateClass.ToTargetClassAndAttributeProps(DolittleConstants.Types.AggregateRootAttribute),
aggregateClass.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)
));
}
Expand All @@ -216,7 +216,7 @@ static void CheckApplyInvocations(SyntaxNodeAnalysisContext context, ClassDeclar
{
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Events.MissingAttribute,
invocation.GetLocation(),
type.ToTargetClassAndAttributeProps(DolittleTypes.EventTypeAttribute), type.ToString()));
type.ToTargetClassAndAttributeProps(DolittleConstants.Types.EventTypeAttribute), type.ToString()));
}

if (!handledEventTypes.Contains(type))
Expand Down
10 changes: 5 additions & 5 deletions Source/Analyzers/AnalysisExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static bool IsAggregateRoot(this INamedTypeSymbol typeSymbol)
var baseType = typeSymbol.BaseType;
while (baseType != null)
{
if (baseType.ToString() == DolittleTypes.AggregateRootBaseClass)
if (baseType.ToString() == DolittleConstants.Types.AggregateRootBaseClass)
{
return true;
}
Expand All @@ -56,7 +56,7 @@ public static bool IsProjection(this INamedTypeSymbol typeSymbol)
var baseType = typeSymbol.BaseType;
while (baseType != null)
{
if (baseType.ToString() == DolittleTypes.ReadModelClass)
if (baseType.ToString() == DolittleConstants.Types.ReadModelClass)
{
return true;
}
Expand All @@ -67,9 +67,9 @@ public static bool IsProjection(this INamedTypeSymbol typeSymbol)
return false;
}

public static bool HasEventTypeAttribute(this ITypeSymbol type) => type.HasAttribute(DolittleTypes.EventTypeAttribute);
public static bool HasAggregateRootAttribute(this ITypeSymbol type) => type.HasAttribute(DolittleTypes.AggregateRootAttribute);
public static bool HasEventHandlerAttribute(this ITypeSymbol type) => type.HasAttribute(DolittleTypes.EventHandlerAttribute);
public static bool HasEventTypeAttribute(this ITypeSymbol type) => type.HasAttribute(DolittleConstants.Types.EventTypeAttribute);
public static bool HasAggregateRootAttribute(this ITypeSymbol type) => type.HasAttribute(DolittleConstants.Types.AggregateRootAttribute);
public static bool HasEventHandlerAttribute(this ITypeSymbol type) => type.HasAttribute(DolittleConstants.Types.EventHandlerAttribute);

public static bool HasAttribute(this ITypeSymbol type, string attributeName)
{
Expand Down
99 changes: 77 additions & 22 deletions Source/Analyzers/AttributeIdentityAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,23 @@ public class AttributeIdentityAnalyzer : DiagnosticAnalyzer

static readonly ImmutableDictionary<string, string?> _missingProjectionBaseClassProperties =
ImmutableDictionary<string, string?>.Empty
.Add(BaseclassKey, DolittleTypes.ReadModelClass);
.Add(BaseclassKey, DolittleConstants.Types.ReadModelClass);

static readonly ImmutableDictionary<string, string?> _missingAggregateBaseClassProperties =
ImmutableDictionary<string, string?>.Empty
.Add(BaseclassKey, DolittleTypes.AggregateRootBaseClass);
.Add(BaseclassKey, DolittleConstants.Types.AggregateRootBaseClass);

readonly ConcurrentDictionary<(string type, Guid id), AttributeSyntax> _identities = new();

/// <inheritdoc />
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(DescriptorRules.InvalidIdentity, DescriptorRules.DuplicateIdentity, DescriptorRules.MissingBaseClass, DescriptorRules.InvalidTimespan);
ImmutableArray.Create(
DescriptorRules.InvalidIdentity,
DescriptorRules.DuplicateIdentity,
DescriptorRules.MissingBaseClass,
DescriptorRules.InvalidTimespan,
DescriptorRules.IncorrectRedactedEventTypePrefix
);

/// <inheritdoc />
public override void Initialize(AnalysisContext context)
Expand All @@ -60,37 +66,45 @@ void CheckAttribute(SyntaxNodeAnalysisContext context)
break;
case "AggregateRootAttribute":
CheckAttributeIdentity(attribute, symbol, context);
CheckHasBaseClass(context, DolittleTypes.AggregateRootBaseClass, _missingAggregateBaseClassProperties);
CheckHasBaseClass(context, DolittleConstants.Types.AggregateRootBaseClass,
_missingAggregateBaseClassProperties);
break;

case "ProjectionAttribute":
CheckAttributeIdentity(attribute, symbol, context);
CheckAttributeParseAbleIfPresent(attribute, symbol, context, "idleUnloadTimeout", IsValidTimespan, DescriptorRules.InvalidTimespan);
CheckHasBaseClass(context, DolittleTypes.ReadModelClass, _missingProjectionBaseClassProperties);
CheckAttributeParseAbleIfPresent(attribute, symbol, context, "idleUnloadTimeout", IsValidTimespan,
DescriptorRules.InvalidTimespan);
CheckHasBaseClass(context, DolittleConstants.Types.ReadModelClass,
_missingProjectionBaseClassProperties);
break;
}
}

void CheckAttributeParseAbleIfPresent(AttributeSyntax attribute, IMethodSymbol symbol, SyntaxNodeAnalysisContext context, string parameterName,
void CheckAttributeParseAbleIfPresent(AttributeSyntax attribute, IMethodSymbol symbol,
SyntaxNodeAnalysisContext context, string parameterName,
Func<string, bool> isParseAble, DiagnosticDescriptor descriptor)
{
var parameter = symbol.Parameters.FirstOrDefault(_ => _.Name == parameterName);
if (parameter is null || !attribute.TryGetArgumentValue(parameter, out var value)) return;
if (!isParseAble(value.GetText().ToString().Trim('\"')))
{
var properties = ImmutableDictionary<string, string?>.Empty.Add("parameterName", parameterName);
context.ReportDiagnostic(Diagnostic.Create(descriptor, attribute.GetLocation(), properties, attribute.Name.ToString(), parameterName));
context.ReportDiagnostic(Diagnostic.Create(descriptor, attribute.GetLocation(), properties,
attribute.Name.ToString(), parameterName));
}
}

void CheckHasBaseClass(SyntaxNodeAnalysisContext context, string expectedBaseClass, ImmutableDictionary<string, string?> properties)
void CheckHasBaseClass(SyntaxNodeAnalysisContext context, string expectedBaseClass,
ImmutableDictionary<string, string?> properties)
{
if (context.Node.FirstAncestorOrSelf<ClassDeclarationSyntax>() is not { } classDeclaration) return;

if (classDeclaration.BaseList is null || classDeclaration.BaseList.Types.Count == 0 || !TypeExtends(classDeclaration, expectedBaseClass, context))
if (classDeclaration.BaseList is null || classDeclaration.BaseList.Types.Count == 0 ||
!TypeExtends(classDeclaration, expectedBaseClass, context))
{
var className = classDeclaration.Identifier.ToString();
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.MissingBaseClass, classDeclaration.GetLocation(), properties, className,
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.MissingBaseClass, classDeclaration.GetLocation(),
properties, className,
expectedBaseClass));
}
}
Expand Down Expand Up @@ -131,31 +145,70 @@ void CheckAttributeIdentity(AttributeSyntax attribute, IMethodSymbol symbol, Syn
if (!TryGetStringValue(attribute, identityParameter, context, out var identityText)) return;
var attributeName = attribute.Name.ToString();

if (FlagRedactionIdentity(symbol, attribute, context, identityText!)) return;

if (!Guid.TryParse(identityText!.Trim('"'), out var identifier))
{
var properties = ImmutableDictionary<string, string?>.Empty.Add("identityParameter", identityParameter.Name);
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.InvalidIdentity, attribute.GetLocation(), properties,
var properties =
ImmutableDictionary<string, string?>.Empty.Add("identityParameter", identityParameter.Name);
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.InvalidIdentity, attribute.GetLocation(),
properties,
attributeName, identityParameter.Name, identityText));
return;
}
else

var key = (attributeName, identifier);
if (!_identities.TryAdd(key, attribute))
{
// Only reports secondary sightings, not the first one
ReportDuplicateIdentity(attribute, context, identifier);
}
}

bool FlagRedactionIdentity(IMethodSymbol symbol, AttributeSyntax attribute, SyntaxNodeAnalysisContext context, string identifier)
{
// Only relevant for EventTypeAttribute on a class extending PersonalDataRedacted
if (symbol.ReceiverType?.Name != "EventTypeAttribute") return false;
if (context.Node.FirstAncestorOrSelf<ClassDeclarationSyntax>() is not { } classDeclaration) return false;
if (!TypeExtends(classDeclaration, DolittleConstants.Types.RedactedEvent, context)) return false;

// At this point we know that the attribute is an EventTypeAttribute on a class extending PersonalDataRedacted
// If the identifier does not contain the redaction prefix, we report an error

if (Guid.TryParse(identifier.Trim('"'), out var guid))
{
var key = (attributeName, identifier);
if (!_identities.TryAdd(key, attribute))
if (guid.ToString()
.StartsWith(DolittleConstants.Identifiers.RedactionIdentityPrefix,
StringComparison.InvariantCultureIgnoreCase))
{
// Only reports secondary sightings, not the first one
ReportDuplicateIdentity(attribute, context, identifier);
return false;
}

context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.IncorrectRedactedEventTypePrefix,
attribute.GetLocation(), identifier));
return true;
}


if (!identifier.StartsWith(DolittleConstants.Identifiers.RedactionIdentityPrefix,
StringComparison.InvariantCultureIgnoreCase))
{
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.IncorrectRedactedEventTypePrefix,
attribute.GetLocation(), identifier));
}

return true;
}

static bool TryGetStringValue(AttributeSyntax attribute, IParameterSymbol parameter, SyntaxNodeAnalysisContext context, out string? argumentString)
static bool TryGetStringValue(AttributeSyntax attribute, IParameterSymbol parameter,
SyntaxNodeAnalysisContext context, out string? argumentString)
{
if (!attribute.TryGetArgumentValue(parameter, out var expression))
{
argumentString = null;
return true;
}

// Check if the argument is a string literal or a constant
if (expression is LiteralExpressionSyntax { Token.Value: string value })
{
Expand All @@ -179,9 +232,11 @@ static bool TryGetStringValue(AttributeSyntax attribute, IParameterSymbol parame
return false;
}

static void ReportDuplicateIdentity(AttributeSyntax attribute, SyntaxNodeAnalysisContext context, Guid identifier) =>
static void ReportDuplicateIdentity(AttributeSyntax attribute, SyntaxNodeAnalysisContext context,
Guid identifier) =>
context.ReportDiagnostic(
Diagnostic.Create(DescriptorRules.DuplicateIdentity, attribute.GetLocation(), attribute.Name.ToString(), identifier.ToString()));
Diagnostic.Create(DescriptorRules.DuplicateIdentity, attribute.GetLocation(), attribute.Name.ToString(),
identifier.ToString()));

static bool IsValidTimespan(string value) => TimeSpan.TryParse(value, out _);
}
9 changes: 9 additions & 0 deletions Source/Analyzers/DescriptorRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ static class DescriptorRules
isEnabledByDefault: true,
description: "The generic type must match the property type.");

internal static readonly DiagnosticDescriptor IncorrectRedactedEventTypePrefix =
new(
DiagnosticIds.RedactionEventIncorrectPrefix,
title: "Redaction events must have the correct prefix",
messageFormat: "The prefix for redaction events types should be 'de1e7e17-bad5-da7a', was '{0}'",
DiagnosticCategories.Sdk,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "In order for redaction events to be recognized by the runtime, it must have the correct prefix.");

internal static class Events
{
Expand Down
1 change: 1 addition & 0 deletions Source/Analyzers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public static class DiagnosticIds

public const string NonNullableRedactableField = "SDK0011";
public const string RedactableFieldIncorrectType = "SDK0012";
public const string RedactionEventIncorrectPrefix = "SDK0013";

/// <summary>
/// Aggregate missing the required Attribute.
Expand Down
30 changes: 30 additions & 0 deletions Source/Analyzers/DolittleConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Dolittle.SDK.Analyzers;

static class DolittleConstants
{
public static class Types
{
public const string AggregateRootBaseClass = "Dolittle.SDK.Aggregates.AggregateRoot";
public const string EventTypeAttribute = "Dolittle.SDK.Events.EventTypeAttribute";
public const string AggregateRootAttribute = "Dolittle.SDK.Aggregates.AggregateRootAttribute";
public const string EventHandlerAttribute = "Dolittle.SDK.Events.Handling.EventHandlerAttribute";

public const string CommitEventsInterface = "Dolittle.SDK.Events.Store.ICommitEvents";

public const string EventContext = "Dolittle.SDK.Events.EventContext";
public const string RedactedEvent = "Dolittle.SDK.Events.Redaction.PersonalDataRedacted";

public const string ReadModelClass = "Dolittle.SDK.Projections.ReadModel";
public const string ProjectionAttribute = "Dolittle.SDK.Projections.ProjectionAttribute";
public const string ProjectionResultType = "Dolittle.SDK.Projections.ProjectionResultType";
public const string ProjectionContextType = "Dolittle.SDK.Projections.ProjectionContext";
}

public static class Identifiers
{
public const string RedactionIdentityPrefix = "de1e7e17-bad5-da7a";
}
}
21 changes: 0 additions & 21 deletions Source/Analyzers/DolittleTypes.cs

This file was deleted.

8 changes: 4 additions & 4 deletions Source/Analyzers/EventHandlerAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ void AnalyzeEventHandler(SyntaxNodeAnalysisContext context)
{
var classSyntax = (ClassDeclarationSyntax)context.Node;
var classSymbol = context.SemanticModel.GetDeclaredSymbol(classSyntax);
if (classSymbol is null || !classSymbol.HasAttribute(DolittleTypes.EventHandlerAttribute))
if (classSymbol is null || !classSymbol.HasAttribute(DolittleConstants.Types.EventHandlerAttribute))
{
return;
}
Expand Down Expand Up @@ -73,13 +73,13 @@ void AnalyzeHandleMethod(SyntaxNodeAnalysisContext context, IMethodSymbol handle
if (!parameter.Type.HasEventTypeAttribute())
{
var diagnostic = Diagnostic.Create(DescriptorRules.Events.MissingAttribute, parameter.Locations[0],
parameter.Type.ToTargetClassAndAttributeProps(DolittleTypes.EventTypeAttribute), parameter.Type.Name);
parameter.Type.ToTargetClassAndAttributeProps(DolittleConstants.Types.EventTypeAttribute), parameter.Type.Name);
context.ReportDiagnostic(diagnostic);
}

// Check that the method takes an EventContext as the second parameter
var secondParameter = handleMethod.Parameters.Skip(1).FirstOrDefault();
if(secondParameter is null || secondParameter.Type.ToString() != DolittleTypes.EventContext)
if(secondParameter is null || secondParameter.Type.ToString() != DolittleConstants.Types.EventContext)
{
var diagnostic = Diagnostic.Create(DescriptorRules.Events.MissingEventContext, handleMethod.Locations[0], handleMethod.Name);
context.ReportDiagnostic(diagnostic);
Expand All @@ -89,7 +89,7 @@ void AnalyzeHandleMethod(SyntaxNodeAnalysisContext context, IMethodSymbol handle
static void AnalyzeEventHandlerAttribute(SyntaxNodeAnalysisContext context, INamedTypeSymbol classSymbol)
{
var eventHandlerAttribute = classSymbol.GetAttributes()
.Single(attribute => attribute.AttributeClass?.ToDisplayString() == DolittleTypes.EventHandlerAttribute);
.Single(attribute => attribute.AttributeClass?.ToDisplayString() == DolittleConstants.Types.EventHandlerAttribute);
if (eventHandlerAttribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken) is not AttributeSyntax attributeSyntaxNode)
{
return;
Expand Down
Loading

0 comments on commit 52ab322

Please sign in to comment.