dolittle/DotNET.SDK

View on GitHub
Source/Analyzers/EventHandlerAnalyzer.cs

Summary

Maintainability
A
30 mins
Test Coverage
using System;
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;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class EventHandlerAnalyzer : DiagnosticAnalyzer
{
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
        DescriptorRules.InvalidTimestamp,
        DescriptorRules.InvalidStartStopTimestamp,
        DescriptorRules.InvalidAccessibility,
        DescriptorRules.Events.MissingAttribute,
        DescriptorRules.Events.MissingEventContext
        );

    const int StartFromTimestampOffset = 6;
    const int StopAtTimestampOffset = 7;

    public override void Initialize(AnalysisContext context)
    {
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.RegisterSyntaxNodeAction(AnalyzeEventHandler, ImmutableArray.Create(SyntaxKind.ClassDeclaration));
    }

    void AnalyzeEventHandler(SyntaxNodeAnalysisContext context)
    {
        var classSyntax = (ClassDeclarationSyntax)context.Node;
        var classSymbol = context.SemanticModel.GetDeclaredSymbol(classSyntax);
        if (classSymbol is null || !classSymbol.HasAttribute(DolittleTypes.EventHandlerAttribute))
        {
            return;
        }

        AnalyzeEventHandlerAttribute(context, classSymbol);
        AnalyzeHandleMethods(context, classSymbol);
    }

    void AnalyzeHandleMethods(SyntaxNodeAnalysisContext context, INamedTypeSymbol classSymbol)
    {
        var handleMethods = classSymbol.GetMembers()
            .OfType<IMethodSymbol>()
            .Where(method => method.Name == "Handle");

        foreach (var handleMethod in handleMethods)
        {
            AnalyzeHandleMethod(context, handleMethod);
        }
    }

    void AnalyzeHandleMethod(SyntaxNodeAnalysisContext context, IMethodSymbol handleMethod)
    {
        // Check that it is public
        if (!handleMethod.DeclaredAccessibility.HasFlag(Accessibility.Public))
        {
            var diagnostic = Diagnostic.Create(DescriptorRules.InvalidAccessibility, handleMethod.Locations[0], handleMethod.Name, Accessibility.Public);
            context.ReportDiagnostic(diagnostic);
        }
        
        // Get the first parameter and get the type
        var parameter = handleMethod.Parameters.FirstOrDefault();
        if (parameter is null)
        {
            return;
        }
        // Check if the type has the EventType attribute
        if (!parameter.Type.HasEventTypeAttribute())
        {
            var diagnostic = Diagnostic.Create(DescriptorRules.Events.MissingAttribute, parameter.Locations[0],
                parameter.Type.ToTargetClassAndAttributeProps(DolittleTypes.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)
        {
            var diagnostic = Diagnostic.Create(DescriptorRules.Events.MissingEventContext, handleMethod.Locations[0], handleMethod.Name);
            context.ReportDiagnostic(diagnostic);
        }
    }

    static void AnalyzeEventHandlerAttribute(SyntaxNodeAnalysisContext context, INamedTypeSymbol classSymbol)
    {
        var eventHandlerAttribute = classSymbol.GetAttributes()
            .Single(attribute => attribute.AttributeClass?.ToDisplayString() == DolittleTypes.EventHandlerAttribute);
        if (eventHandlerAttribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken) is not AttributeSyntax attributeSyntaxNode)
        {
            return;
        }

        AnalyzeEventHandlerStartFromStopAt(context, eventHandlerAttribute, attributeSyntaxNode);
    }


    static void AnalyzeEventHandlerStartFromStopAt(SyntaxNodeAnalysisContext context, AttributeData eventHandlerAttribute, AttributeSyntax syntaxNode)
    {
        var startFrom = GetAttributeArgument(eventHandlerAttribute, syntaxNode, StartFromTimestampOffset, "startFromTimestamp");
        if (startFrom?.value is not null && !DateTimeOffset.TryParse(startFrom.Value.value.ToString(), out var parsedStartFrom))
        {
            var diagnostic = Diagnostic.Create(DescriptorRules.InvalidTimestamp, startFrom.Value.location, "startFromTimestamp");
            context.ReportDiagnostic(diagnostic);
        }

        var stopAt = GetAttributeArgument(eventHandlerAttribute, syntaxNode, StopAtTimestampOffset, "stopAtTimestamp");
        if (stopAt?.value is not null && !DateTimeOffset.TryParse(stopAt.Value.value.ToString(), out var parsedStopAt))
        {
            var diagnostic = Diagnostic.Create(DescriptorRules.InvalidTimestamp, stopAt.Value.location, "stopAtTimestamp");
            context.ReportDiagnostic(diagnostic);
        }
        
        if(parsedStartFrom > DateTimeOffset.MinValue && parsedStopAt > DateTimeOffset.MinValue && parsedStartFrom >= parsedStopAt)
        {
            var diagnostic = Diagnostic.Create(DescriptorRules.InvalidStartStopTimestamp, syntaxNode.GetLocation(), "startFromTimestamp", "stopAtTimestamp");
            context.ReportDiagnostic(diagnostic);
        }
    }

    public static (object? value, Location location)? GetAttributeArgument(AttributeData eventHandlerAttribute, AttributeSyntax syntaxNode, int offset,
        string argumentName)
    {
        if (syntaxNode.ArgumentList is null)
        {
            return null;
        }

        // Handle positional arguments
        if (offset > eventHandlerAttribute.ConstructorArguments.Length)
        {
            return null;
        }

        var argument = eventHandlerAttribute.ConstructorArguments[offset];

        foreach (var argumentSyntax in syntaxNode.ArgumentList.Arguments)
        {
            if (argumentSyntax.NameColon?.Name?.Identifier.Text.Equals(argumentName, StringComparison.OrdinalIgnoreCase) == true)
            {
                return (argument.Value, argumentSyntax.GetLocation());
            }
        }

        if (syntaxNode.ArgumentList.Arguments.Count > offset)
        {
            var positionalSyntax = syntaxNode.ArgumentList.Arguments[offset];
            return (argument.Value, positionalSyntax.GetLocation());
        }
        
        return (argument.Value, syntaxNode.GetLocation());
    }
}