Skip to content

Commit

Permalink
Add analyzer for invalid use of ITimerScheduler inside PreRestart and…
Browse files Browse the repository at this point in the history
… AroundPreRestart (#85)
  • Loading branch information
Arkatufus authored Mar 25, 2024
1 parent 4ff3a14 commit 988b4f1
Show file tree
Hide file tree
Showing 9 changed files with 751 additions and 1 deletion.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// -----------------------------------------------------------------------
// <copyright file="MustNotUseIWithTimersInPreRestartAnalyzer.cs" company="Akka.NET Project">
// Copyright (C) 2013-2024 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

using System.Collections.Immutable;
using Akka.Analyzers.Context;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Akka.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MustNotUseIWithTimersInPreRestartAnalyzer(): AkkaDiagnosticAnalyzer(RuleDescriptors.Ak1007MustNotUseIWithTimersInPreRestart)
{
public override void AnalyzeCompilation(CompilationStartAnalysisContext context, AkkaContext akkaContext)
{
Guard.AssertIsNotNull(context);
Guard.AssertIsNotNull(akkaContext);

context.RegisterSyntaxNodeAction(ctx =>
{
var invocationExpr = (InvocationExpressionSyntax)ctx.Node;
var semanticModel = ctx.SemanticModel;

if (semanticModel.GetSymbolInfo(invocationExpr).Symbol is not IMethodSymbol methodInvocationSymbol)
return;

// Invocation expression must be either `ITimerScheduler.StartPeriodicTimer()` or `ITimerScheduler.StartSingleTimer()`
var iWithTimers = akkaContext.AkkaCore.Actor.ITimerScheduler;
var refMethods = iWithTimers.StartPeriodicTimer.AddRange(iWithTimers.StartSingleTimer);
if (!methodInvocationSymbol.MatchesAny(refMethods))
return;

// Grab the enclosing method declaration
var methodDeclaration = invocationExpr.FirstAncestorOrSelf<MethodDeclarationSyntax>();
if (methodDeclaration is null)
return;

var methodDeclarationSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration);
if(methodDeclarationSymbol is null)
return;

// Method declaration must be `ActorBase.PreRestart()` or `ActorBase.AroundPreRestart()`
var actorBase = akkaContext.AkkaCore.Actor.ActorBase;
refMethods = new[] { actorBase.PreRestart!, actorBase.AroundPreRestart! }.ToImmutableArray();
if (!methodDeclarationSymbol.OverridesAny(refMethods))
return;

var diagnostic = Diagnostic.Create(
descriptor: RuleDescriptors.Ak1007MustNotUseIWithTimersInPreRestart,
location: invocationExpr.GetLocation(),
messageArgs: [methodInvocationSymbol.Name, methodDeclarationSymbol.Name]);
ctx.ReportDiagnostic(diagnostic);

}, SyntaxKind.InvocationExpression);
}
}
4 changes: 4 additions & 0 deletions src/Akka.Analyzers/Context/Core/Actor/ActorSymbolFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ public static class ActorSymbolFactory
public static INamedTypeSymbol? ActorRefs(Compilation compilation)
=> Guard.AssertIsNotNull(compilation)
.GetTypeByMetadataName("Akka.Actor.ActorRefs");

public static INamedTypeSymbol? TimerSchedulerInterface(Compilation compilation)
=> Guard.AssertIsNotNull(compilation)
.GetTypeByMetadataName($"{AkkaActorNamespace}.ITimerScheduler");
}
10 changes: 10 additions & 0 deletions src/Akka.Analyzers/Context/Core/Actor/AkkaCoreActorContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ private EmptyAkkaCoreActorContext() { }
public INamedTypeSymbol? GracefulStopSupportType => null;
public INamedTypeSymbol? ITellSchedulerType => null;
public INamedTypeSymbol? ActorRefsType => null;
public INamedTypeSymbol? ITimerSchedulerType => null;

public IGracefulStopSupportContext GracefulStopSupportSupport => EmptyGracefulStopSupportContext.Instance;
public IIndirectActorProducerContext IIndirectActorProducer => EmptyIndirectActorProducerContext.Instance;
Expand All @@ -31,6 +32,7 @@ private EmptyAkkaCoreActorContext() { }
public IPropsContext Props => EmptyPropsContext.Instance;
public ITellSchedulerInterfaceContext ITellScheduler => EmptyTellSchedulerInterfaceContext.Instance;
public IActorRefsContext ActorRefs => EmptyActorRefsContext.Empty;
public ITimerSchedulerContext ITimerScheduler => EmptyTimerSchedulerContext.Instance;
}

public sealed class AkkaCoreActorContext : IAkkaCoreActorContext
Expand All @@ -44,6 +46,8 @@ public sealed class AkkaCoreActorContext : IAkkaCoreActorContext
private readonly Lazy<INamedTypeSymbol?> _lazyGracefulStopSupportType;
private readonly Lazy<INamedTypeSymbol?> _lazyTellSchedulerInterface;
private readonly Lazy<INamedTypeSymbol?> _lazyActorRefsType;
private readonly Lazy<INamedTypeSymbol?> _lazyITimerSchedulerType;

private readonly Lazy<IGracefulStopSupportContext> _lazyGracefulStopSupport;
private readonly Lazy<IIndirectActorProducerContext> _lazyIIndirectActorProducer;
private readonly Lazy<IReceiveActorContext> _lazyReceiveActor;
Expand All @@ -62,6 +66,8 @@ private AkkaCoreActorContext(Compilation compilation)
_lazyGracefulStopSupportType = new Lazy<INamedTypeSymbol?>(() => ActorSymbolFactory.GracefulStopSupport(compilation));
_lazyTellSchedulerInterface = new Lazy<INamedTypeSymbol?>(() => ActorSymbolFactory.TellSchedulerInterface(compilation));
_lazyActorRefsType = new Lazy<INamedTypeSymbol?>(() => ActorSymbolFactory.ActorRefs(compilation));
_lazyITimerSchedulerType = new Lazy<INamedTypeSymbol?>(() => ActorSymbolFactory.TimerSchedulerInterface(compilation));

_lazyGracefulStopSupport = new Lazy<IGracefulStopSupportContext>(() => GracefulStopSupportContext.Get(this));
_lazyIIndirectActorProducer = new Lazy<IIndirectActorProducerContext>(() => IndirectActorProducerContext.Get(this));
_lazyReceiveActor = new Lazy<IReceiveActorContext>(() => ReceiveActorContext.Get(this));
Expand All @@ -70,6 +76,7 @@ private AkkaCoreActorContext(Compilation compilation)
_lazyProps = new Lazy<IPropsContext>(() => PropsContext.Get(this));
ITellScheduler = TellSchedulerInterfaceContext.Get(compilation);
ActorRefs = ActorRefsContext.Get(this);
ITimerScheduler = TimerSchedulerContext.Get(this);
}

public INamedTypeSymbol? ActorBaseType => _lazyActorBaseType.Value;
Expand All @@ -81,6 +88,8 @@ private AkkaCoreActorContext(Compilation compilation)
public INamedTypeSymbol? ITellSchedulerType => _lazyTellSchedulerInterface.Value;
public INamedTypeSymbol? ActorRefsType => _lazyActorRefsType.Value;
public INamedTypeSymbol? GracefulStopSupportType => _lazyGracefulStopSupportType.Value;
public INamedTypeSymbol? ITimerSchedulerType => _lazyITimerSchedulerType.Value;

public IGracefulStopSupportContext GracefulStopSupportSupport => _lazyGracefulStopSupport.Value;
public IIndirectActorProducerContext IIndirectActorProducer => _lazyIIndirectActorProducer.Value;
public IReceiveActorContext ReceiveActor => _lazyReceiveActor.Value;
Expand All @@ -89,6 +98,7 @@ private AkkaCoreActorContext(Compilation compilation)
public IPropsContext Props => _lazyProps.Value;
public ITellSchedulerInterfaceContext ITellScheduler { get; }
public IActorRefsContext ActorRefs { get; }
public ITimerSchedulerContext ITimerScheduler { get; }

public static IAkkaCoreActorContext Get(Compilation compilation)
=> new AkkaCoreActorContext(compilation);
Expand Down
66 changes: 65 additions & 1 deletion src/Akka.Analyzers/Context/Core/Actor/BaseActorContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,91 @@ namespace Akka.Analyzers.Context.Core.Actor;

public interface IActorBaseContext
{
#region Properties

public IPropertySymbol? Self { get; }

#endregion

#region Methods

public IMethodSymbol? AroundPreRestart { get; }
public IMethodSymbol? AroundPreStart { get; }
public IMethodSymbol? PreStart { get; }
public IMethodSymbol? AroundPostRestart { get; }
public IMethodSymbol? PreRestart { get; }
public IMethodSymbol? PostRestart { get; }
public IMethodSymbol? AroundPostStop { get; }
public IMethodSymbol? PostStop { get; }

#endregion
}

public sealed class EmptyActorBaseContext : IActorBaseContext
{
public static readonly EmptyActorBaseContext Instance = new();
private EmptyActorBaseContext() { }
public IPropertySymbol? Self => null;
public IMethodSymbol? AroundPreRestart => null;
public IMethodSymbol? AroundPreStart => null;
public IMethodSymbol? PreStart => null;
public IMethodSymbol? AroundPostRestart => null;
public IMethodSymbol? PreRestart => null;
public IMethodSymbol? PostRestart => null;
public IMethodSymbol? AroundPostStop => null;
public IMethodSymbol? PostStop => null;
}

public sealed class ActorBaseContext : IActorBaseContext
{
private readonly Lazy<IPropertySymbol> _lazySelf;
private readonly Lazy<IMethodSymbol> _lazyAroundPreRestart;
private readonly Lazy<IMethodSymbol> _lazyAroundPreStart;
private readonly Lazy<IMethodSymbol> _lazyPreStart;
private readonly Lazy<IMethodSymbol> _lazyAroundPostRestart;
private readonly Lazy<IMethodSymbol> _lazyPreRestart;
private readonly Lazy<IMethodSymbol> _lazyPostRestart;
private readonly Lazy<IMethodSymbol> _lazyAroundPostStop;
private readonly Lazy<IMethodSymbol> _lazyPostStop;

private ActorBaseContext(AkkaCoreActorContext context)
{
Guard.AssertIsNotNull(context.ActorBaseType);

_lazySelf = new Lazy<IPropertySymbol>(() => (IPropertySymbol) context.ActorBaseType!.GetMembers("Self").First());

_lazyAroundPreRestart = new Lazy<IMethodSymbol>(() => (IMethodSymbol) context.ActorBaseType!
.GetMembers(nameof(AroundPreRestart)).First());
_lazyAroundPreStart = new Lazy<IMethodSymbol>(() => (IMethodSymbol) context.ActorBaseType!
.GetMembers(nameof(AroundPreStart)).First());
_lazyPreStart = new Lazy<IMethodSymbol>(() => (IMethodSymbol) context.ActorBaseType!
.GetMembers(nameof(PreStart)).First());
_lazyAroundPostRestart = new Lazy<IMethodSymbol>(() => (IMethodSymbol) context.ActorBaseType!
.GetMembers(nameof(AroundPostRestart)).First());
_lazyPreRestart = new Lazy<IMethodSymbol>(() => (IMethodSymbol) context.ActorBaseType!
.GetMembers(nameof(PreRestart)).First());
_lazyPostRestart = new Lazy<IMethodSymbol>(() => (IMethodSymbol) context.ActorBaseType!
.GetMembers(nameof(PostRestart)).First());
_lazyAroundPostStop = new Lazy<IMethodSymbol>(() => (IMethodSymbol) context.ActorBaseType!
.GetMembers(nameof(AroundPostStop)).First());
_lazyPostStop = new Lazy<IMethodSymbol>(() => (IMethodSymbol) context.ActorBaseType!
.GetMembers(nameof(PostStop)).First());
}

public IPropertySymbol? Self => _lazySelf.Value;
public IMethodSymbol? AroundPreRestart => _lazyAroundPreRestart.Value;
public IMethodSymbol? AroundPreStart => _lazyAroundPreStart.Value;
public IMethodSymbol? PreStart => _lazyPreStart.Value;
public IMethodSymbol? AroundPostRestart => _lazyAroundPostRestart.Value;
public IMethodSymbol? PreRestart => _lazyPreRestart.Value;
public IMethodSymbol? PostRestart => _lazyPostRestart.Value;
public IMethodSymbol? AroundPostStop => _lazyAroundPostStop.Value;
public IMethodSymbol? PostStop => _lazyPostStop.Value;

public static ActorBaseContext Get(AkkaCoreActorContext context)
=> new(context);
{
Guard.AssertIsNotNull(context);

return new ActorBaseContext(context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public interface IAkkaCoreActorContext
public INamedTypeSymbol? GracefulStopSupportType { get; }
public INamedTypeSymbol? ITellSchedulerType { get; }
public INamedTypeSymbol? ActorRefsType { get; }
public INamedTypeSymbol? ITimerSchedulerType { get; }

public IGracefulStopSupportContext GracefulStopSupportSupport { get; }
public IIndirectActorProducerContext IIndirectActorProducer { get; }
Expand All @@ -30,4 +31,5 @@ public interface IAkkaCoreActorContext
public IPropsContext Props { get; }
public ITellSchedulerInterfaceContext ITellScheduler { get; }
public IActorRefsContext ActorRefs { get; }
public ITimerSchedulerContext ITimerScheduler { get; }
}
50 changes: 50 additions & 0 deletions src/Akka.Analyzers/Context/Core/Actor/ITimerSchedulerContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// -----------------------------------------------------------------------
// <copyright file="ITimerSchedulerContext.cs" company="Akka.NET Project">
// Copyright (C) 2013-2024 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;

namespace Akka.Analyzers.Context.Core.Actor;

public interface ITimerSchedulerContext
{
public ImmutableArray<IMethodSymbol> StartPeriodicTimer { get; }
public ImmutableArray<IMethodSymbol> StartSingleTimer { get; }
}

public sealed class EmptyTimerSchedulerContext : ITimerSchedulerContext
{
public static readonly EmptyTimerSchedulerContext Instance = new();
private EmptyTimerSchedulerContext() { }
public ImmutableArray<IMethodSymbol> StartPeriodicTimer => ImmutableArray<IMethodSymbol>.Empty;
public ImmutableArray<IMethodSymbol> StartSingleTimer => ImmutableArray<IMethodSymbol>.Empty;
}

public sealed class TimerSchedulerContext : ITimerSchedulerContext
{
private readonly Lazy<ImmutableArray<IMethodSymbol>> _lazyStartPeriodicTimer;
private readonly Lazy<ImmutableArray<IMethodSymbol>> _lazyStartSingleTimer;

private TimerSchedulerContext(AkkaCoreActorContext context)
{
Guard.AssertIsNotNull(context);

_lazyStartPeriodicTimer = new Lazy<ImmutableArray<IMethodSymbol>>(() => context.ITimerSchedulerType!
.GetMembers(nameof(StartPeriodicTimer)).Select(m => (IMethodSymbol)m).ToImmutableArray());
_lazyStartSingleTimer = new Lazy<ImmutableArray<IMethodSymbol>>(() => context.ITimerSchedulerType!
.GetMembers(nameof(StartSingleTimer)).Select(m => (IMethodSymbol)m).ToImmutableArray());
}

public ImmutableArray<IMethodSymbol> StartPeriodicTimer => _lazyStartPeriodicTimer.Value;
public ImmutableArray<IMethodSymbol> StartSingleTimer => _lazyStartSingleTimer.Value;

public static TimerSchedulerContext Get(AkkaCoreActorContext context)
{
Guard.AssertIsNotNull(context);

return new TimerSchedulerContext(context);
}
}
22 changes: 22 additions & 0 deletions src/Akka.Analyzers/Utility/CodeAnalysisExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// </copyright>
// -----------------------------------------------------------------------

using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using Akka.Analyzers.Context.Core;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -183,4 +184,25 @@ public static bool IsDerivedOrImplements(this ITypeSymbol typeSymbol, ITypeSymbo
return false;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool OverridesAny(this IMethodSymbol methodSymbol, IReadOnlyCollection<IMethodSymbol> refMethods)
{
if (!methodSymbol.IsOverride)
return false;

while (methodSymbol.OverriddenMethod != null)
methodSymbol = methodSymbol.OverriddenMethod;

return methodSymbol.MatchesAny(refMethods);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool MatchesAny(this IMethodSymbol methodSymbol, IReadOnlyCollection<IMethodSymbol> refMethods)
{
while (!ReferenceEquals(methodSymbol, methodSymbol.ConstructedFrom))
methodSymbol = methodSymbol.ConstructedFrom;

return refMethods.Any(m => ReferenceEquals(m, methodSymbol));
}

}
8 changes: 8 additions & 0 deletions src/Akka.Analyzers/Utility/RuleDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ private static DiagnosticDescriptor Rule(
"can cause memory leak and unnecessary CPU usage if they are not canceled properly inside PostStop(). " +
"Consider implementing the IWithTimers interface and use the Timers.StartSingleTimer() or " +
"Timers.StartPeriodicTimer() instead.");

public static DiagnosticDescriptor Ak1007MustNotUseIWithTimersInPreRestart { get; } = Rule(
id: "AK1007",
title: "Timers.StartSingleTimer() and Timers.StartPeriodicTimer() must not be used inside AroundPreRestart() or PreRestart()",
category: AnalysisCategory.ActorDesign,
defaultSeverity: DiagnosticSeverity.Error,
messageFormat: "Creating timer registration using `{0}()` in `{1}()` will not be honored because they will be " +
"cleared immediately. Move timer creation to `PostRestart()` instead.");
#endregion

#region AK2000 Rules
Expand Down

0 comments on commit 988b4f1

Please sign in to comment.