Skip to content

Commit

Permalink
Projection analyzers
Browse files Browse the repository at this point in the history
  • Loading branch information
mhelleborg committed Mar 6, 2024
1 parent 7c3adf5 commit 419f541
Show file tree
Hide file tree
Showing 7 changed files with 515 additions and 10 deletions.
21 changes: 21 additions & 0 deletions Source/Analyzers/AnalysisExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,27 @@ public static bool IsAggregateRoot(this INamedTypeSymbol typeSymbol)

return false;
}

/// <summary>
/// Checks if base class of the type is Dolittle.SDK.Projections.ProjectionBase
/// </summary>
/// <param name="typeSymbol">The checked class</param>
/// <returns></returns>
public static bool IsProjection(this INamedTypeSymbol typeSymbol)
{
var baseType = typeSymbol.BaseType;
while (baseType != null)
{
if (baseType.ToString() == DolittleTypes.ProjectionBaseClass)
{
return true;
}

baseType = baseType.BaseType;
}

return false;
}

public static bool HasEventTypeAttribute(this ITypeSymbol type) => type.HasAttribute(DolittleTypes.EventTypeAttribute);
public static bool HasAggregateRootAttribute(this ITypeSymbol type) => type.HasAttribute(DolittleTypes.AggregateRootAttribute);
Expand Down
56 changes: 56 additions & 0 deletions Source/Analyzers/DescriptorRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,60 @@ internal static class Aggregate
isEnabledByDefault: true
);
}

internal static class Projection
{
internal static readonly DiagnosticDescriptor MissingAttribute =
new(
DiagnosticIds.ProjectionMissingAttributeRuleId,
title: "Class does not have the correct identifying ProjectionAttribute",
messageFormat: "'{0}' is missing ProjectionAttribute",
DiagnosticCategories.Sdk,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Mark the class with an attribute to assign an identifier to it");

// ProjectionMissingBaseClassRuleId

internal static readonly DiagnosticDescriptor MissingBaseClass =
new(
DiagnosticIds.ProjectionMissingBaseClassRuleId,
title: "Projection does not inherit from ProjectionBase",
messageFormat: "'{0}' does not inherit from ProjectionBase",
DiagnosticCategories.Sdk,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Inherit from ProjectionBase.");

internal static readonly DiagnosticDescriptor InvalidOnMethodParameters =
new(
DiagnosticIds.ProjectionInvalidOnMethodParametersRuleId,
title: "Invalid On-method",
messageFormat: "'{0}' is invalid",
DiagnosticCategories.Sdk,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Change the On-method to match the required signature. The method should take an event as first parameter and a ProjectionContext as the second parameter.");

internal static readonly DiagnosticDescriptor InvalidOnMethodReturnType =
new(
DiagnosticIds.ProjectionInvalidOnMethodReturnTypeRuleId,
title: "Invalid On-method return type",
messageFormat: "'{0}' returns an invalid type",
DiagnosticCategories.Sdk,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Change the On-method to return void, ProjectionResultType or ProjectionResult<TReadModel>.");

internal static readonly DiagnosticDescriptor EventTypeAlreadyHandled =
new(
DiagnosticIds.ProjectionDuplicateEventHandler,
title: "Event type already handled",
messageFormat: "'{0}' is already handled",
DiagnosticCategories.Sdk,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The event type is already handled by another On-method.");
}

}
19 changes: 12 additions & 7 deletions Source/Analyzers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static class DiagnosticIds
/// Attribute missing the required ID.
/// </summary>
public const string EventMissingAttributeRuleId = "SDK0002";

/// <summary>
/// Identity is shared between multiple targets.
/// </summary>
Expand All @@ -24,16 +24,16 @@ public static class DiagnosticIds
/// Invalid timestamp.
/// </summary>
public const string InvalidTimestampParameter = "SDK0004";

/// <summary>
/// Invalid timestamp.
/// </summary>
public const string InvalidStartStopTime = "SDK0005";

public const string InvalidAccessibility = "SDK0006";

public const string EventHandlerMissingEventContext = "SDK0007";

/// <summary>
/// Aggregate missing the required Attribute.
/// </summary>
Expand All @@ -53,17 +53,22 @@ public static class DiagnosticIds
/// Aggregate On-method has an incorrect number of parameters
/// </summary>
public const string AggregateMutationShouldBePrivateRuleId = "AGG0004";

/// <summary>
/// Apply can not be used in an On-method.
/// </summary>
public const string AggregateMutationsCannotProduceEvents = "AGG0005";

/// <summary>
/// Public methods can not mutate the state of an aggregate.
/// All mutations need to be done in On-methods.
/// </summary>
public const string PublicMethodsCannotMutateAggregateState = "AGG0006";



public const string ProjectionMissingAttributeRuleId = "PROJ0001";
public const string ProjectionMissingBaseClassRuleId = "PROJ0002";
public const string ProjectionInvalidOnMethodParametersRuleId = "PROJ0003";
public const string ProjectionInvalidOnMethodReturnTypeRuleId = "PROJ0004";
public const string ProjectionDuplicateEventHandler = "PROJ0005";
}
9 changes: 6 additions & 3 deletions Source/Analyzers/DolittleTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ static class DolittleTypes
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 ICommitEventsInterface = "Dolittle.SDK.Events.Store.ICommitEvents";

public const string EventContext = "Dolittle.SDK.Events.EventContext";


public const string ProjectionBaseClass = "Dolittle.SDK.Projections.ProjectionBase";
public const string ProjectionAttribute = "Dolittle.SDK.Projections.ProjectionAttribute";
public const string ProjectionResultType = "Dolittle.SDK.Projections.ProjectionResultType";
public const string ProjectionContextType = "Dolittle.SDK.Projections.ProjectionContext";
}
172 changes: 172 additions & 0 deletions Source/Analyzers/ProjectionsAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Dolittle.SDK.Analyzers;

#pragma warning disable CS1574, CS1584, CS1581, CS1580
/// <summary>
/// Analyzer for <see cref="Dolittle.SDK.Projections"/>.
/// </summary>
#pragma warning restore CS1574, CS1584, CS1581, CS1580
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ProjectionsAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc />
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(
// DescriptorRules.DuplicateIdentity,
DescriptorRules.Events.MissingAttribute,
DescriptorRules.Projection.MissingAttribute,
DescriptorRules.Projection.MissingBaseClass,
DescriptorRules.Projection.InvalidOnMethodParameters,
DescriptorRules.Projection.InvalidOnMethodReturnType,
DescriptorRules.Projection.EventTypeAlreadyHandled
);

/// <inheritdoc />
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeProjections, ImmutableArray.Create(SyntaxKind.ClassDeclaration));
}


static void AnalyzeProjections(SyntaxNodeAnalysisContext context)
{
// Check if the symbol has the projection root base class
var projectionSyntax = (ClassDeclarationSyntax)context.Node;
// Check if the symbol has the projection root base class
var projectionSymbol = context.SemanticModel.GetDeclaredSymbol(projectionSyntax);
if (projectionSymbol?.IsProjection() != true) return;

CheckProjectionAttributePresent(context, projectionSymbol);
CheckOnMethods(context, projectionSymbol);
}


static void CheckOnMethods(SyntaxNodeAnalysisContext context, INamedTypeSymbol projectionType)
{
var members = projectionType.GetMembers();
var onMethods = members.Where(_ => _.Name.Equals("On")).OfType<IMethodSymbol>().ToArray();
var eventTypesHandled = new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default);

foreach (var onMethod in onMethods)
{
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.Projection.MutationShouldBePrivate, syntax.GetLocation(),
// onMethod.ToDisplayString()));
// }

var parameters = onMethod.Parameters;
if (parameters.Length is not 1 and not 2)
{
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Projection.InvalidOnMethodParameters, syntax.GetLocation(),
onMethod.ToDisplayString()));
}

if (parameters.Length > 0)
{
var eventType = parameters[0].Type;
if(!eventTypesHandled.Add(eventType))
{
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Projection.EventTypeAlreadyHandled, syntax.GetLocation(),
eventType.ToDisplayString()));
}

if (!eventType.HasEventTypeAttribute())
{
context.ReportDiagnostic(Diagnostic.Create(
DescriptorRules.Events.MissingAttribute,
parameters[0].DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax().GetLocation(),
eventType.ToTargetClassAndAttributeProps(DolittleTypes.EventTypeAttribute),
eventType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))
);
}
}

if (parameters.Length > 1)
{
var secondParameterTypeSymbol = parameters[1].Type;
var contextType = secondParameterTypeSymbol.ToDisplayString();
if (!contextType.Equals(DolittleTypes.ProjectionContextType, StringComparison.Ordinal)
// && !contextType.Equals(DolittleTypes.EventContext, StringComparison.Ordinal)
)
{
var loc = parameters[1].DeclaringSyntaxReferences.First().GetSyntax().GetLocation();
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Projection.InvalidOnMethodParameters, loc,
onMethod.ToDisplayString()));
}

}

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

static void CheckOnReturnType(SyntaxNodeAnalysisContext context, INamedTypeSymbol projectionType, IMethodSymbol onMethod,
MethodDeclarationSyntax syntax)
{
// Check for valid return type. Valid types are void, ProjectionResultType and ProjectionResult<>
var returnType = onMethod.ReturnType;
if(returnType.SpecialType == SpecialType.System_Void)
{
return; // void is valid
}

if (returnType is not INamedTypeSymbol namedReturnType)
{
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Projection.InvalidOnMethodReturnType, syntax.GetLocation(),
onMethod.ToDisplayString()));
return;
}

if (namedReturnType.IsGenericType)
{
var genericType = namedReturnType.TypeArguments[0];
if (!genericType.Equals(projectionType))
{
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Projection.InvalidOnMethodReturnType, syntax.GetLocation(),
onMethod.ToDisplayString()));
}
}
else
{
if (namedReturnType.ToDisplayString() != DolittleTypes.ProjectionResultType)
{
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Projection.InvalidOnMethodReturnType, syntax.GetLocation(),
onMethod.ToDisplayString()));
}
}
}

static void CheckProjectionAttributePresent(SyntaxNodeAnalysisContext context, INamedTypeSymbol projectionClass)
{
var hasAttribute = projectionClass.GetAttributes()
.Any(attribute => attribute.AttributeClass?.ToDisplayString().Equals(DolittleTypes.ProjectionAttribute, StringComparison.Ordinal) == true);

if (!hasAttribute)
{
context.ReportDiagnostic(Diagnostic.Create(
DescriptorRules.Projection.MissingAttribute,
projectionClass.Locations[0],
projectionClass.ToTargetClassAndAttributeProps(DolittleTypes.ProjectionAttribute),
projectionClass.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)
));
}
}
}
4 changes: 4 additions & 0 deletions Source/Testing/Testing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@
<ItemGroup>
<PackageReference Include="moq" Version="4.18.3" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<Folder Include="Projections\" />
</ItemGroup>

</Project>
Loading

0 comments on commit 419f541

Please sign in to comment.