diff --git a/src/Akka.Analyzers.Tests/Analyzers/AK1000/MustNotUseIWithTimersInPreRestartAnalyzerSpecs.cs b/src/Akka.Analyzers.Tests/Analyzers/AK1000/MustNotUseIWithTimersInPreRestartAnalyzerSpecs.cs new file mode 100644 index 0000000..6664c97 --- /dev/null +++ b/src/Akka.Analyzers.Tests/Analyzers/AK1000/MustNotUseIWithTimersInPreRestartAnalyzerSpecs.cs @@ -0,0 +1,529 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013-2024 .NET Foundation +// +// ----------------------------------------------------------------------- + +using Microsoft.CodeAnalysis; +using Verify = Akka.Analyzers.Tests.Utility.AkkaVerifier; + +namespace Akka.Analyzers.Tests.Analyzers.AK1000; + +public class MustNotUseIWithTimersInPreRestartAnalyzerSpecs +{ + public static readonly TheoryData SuccessCases = new() + { + // ReceiveActor calling ITimerScheduler methods outside of AroundPreRestart() and PreRestart() +""" +// 01 +using System; +using Akka.Actor; + +public class MyActor: ReceiveActor, IWithTimers +{ + public MyActor() + { + ReceiveAny(_ => + { + Timers!.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + LocalFunction(); + NonOverrideMethod(); + }); + + Timers!.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + + return; + + void LocalFunction() + { + Timers!.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + } + + private void NonOverrideMethod() + { + Timers!.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + public override void AroundPostRestart(Exception cause, object message) + { + base.AroundPostRestart(cause, message); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + protected override void PostRestart(Exception reason) + { + base.PostRestart(reason); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + public override void AroundPostStop() + { + base.AroundPostStop(); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + protected override void PostStop() + { + base.PostStop(); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + public override void AroundPreStart() + { + base.AroundPreStart(); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + protected override void PreStart() + { + base.PreStart(); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + public ITimerScheduler Timers { get; set; } = null!; +} +""", + + // UntypedActor calling ITimerScheduler methods outside of AroundPreRestart() and PreRestart() +""" +// 02 +using System; +using Akka.Actor; + +public class MyActor: UntypedActor, IWithTimers +{ + protected override void OnReceive(object message) + { + Timers!.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + LocalFunction(); + NonOverrideMethod(); + + return; + + void LocalFunction() + { + Timers!.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + } + + private void NonOverrideMethod() + { + Timers!.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + public override void AroundPostRestart(Exception cause, object message) + { + base.AroundPostRestart(cause, message); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + protected override void PostRestart(Exception reason) + { + base.PostRestart(reason); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + public override void AroundPostStop() + { + base.AroundPostStop(); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + protected override void PostStop() + { + base.PostStop(); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + public override void AroundPreStart() + { + base.AroundPreStart(); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + protected override void PreStart() + { + base.PreStart(); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + public ITimerScheduler Timers { get; set; } = null!; +} +""", + + // ReceiveActor without ITimerScheduler method calls +""" +// 03 +using Akka.Actor; + +public class MyActor: ReceiveActor, IWithTimers +{ + public MyActor() { } + + public ITimerScheduler Timers { get; set; } = null!; +} + +""", + + // UntypedActor without ITimerScheduler method calls +""" +// 04 +using Akka.Actor; + +public class MyActor: UntypedActor, IWithTimers +{ + public MyActor() { } + + protected override void OnReceive(object message) { } + + public ITimerScheduler Timers { get; set; } = null!; +} +""", + + // Non-Actor class that implements IWithTimers and have the same `AroundPreRestart()` and `PreRestart()` + // methods fingerprints, we're not responsible for this. +""" +// 05 +using System; +using Akka.Actor; + +public class MyNonActorClass: MyNonActorBaseClass, IWithTimers +{ + public MyNonActorClass(ITimerScheduler scheduler) + { + Timers = scheduler; + } + + public override void AroundPreRestart(Exception cause, object message) + { + base.AroundPreRestart(cause, message); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + protected override void PreRestart(Exception reason, object message) + { + base.PreRestart(reason, message); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + public ITimerScheduler Timers { get; set; } +} + +public abstract class MyNonActorBaseClass +{ + public virtual void AroundPreRestart(Exception cause, object message) { } + + protected virtual void PreRestart(Exception reason, object message) { } +} +""", + }; + + public static readonly + TheoryData<(string testData, (int startLine, int startColumn, int endLine, int endColumn) spanData, object[] arguments)> + FailureCases = new() + { + // ReceiveActor with ITimerScheduler.StartSingleTimer call inside AroundPreStart + ( +""" +// 01 +using System; +using Akka.Actor; + +public sealed class MyActor : ReceiveActor, IWithTimers +{ + public override void AroundPreRestart(Exception cause, object message) + { + base.AroundPreRestart(cause, message); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + } + + public ITimerScheduler Timers { get; set; } +} +""", (10, 9, 10, 73), ["StartSingleTimer", "AroundPreRestart"]), + + // ReceiveActor with ITimerScheduler.StartPeriodicTimer call inside AroundPreStart, variant 1 + ( +""" +// 02 +using System; +using Akka.Actor; + +public sealed class MyActor : ReceiveActor, IWithTimers +{ + public override void AroundPreRestart(Exception cause, object message) + { + base.AroundPreRestart(cause, message); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + } + + public ITimerScheduler Timers { get; set; } +} +""", (10, 9, 10, 75), ["StartPeriodicTimer", "AroundPreRestart"]), + + // ReceiveActor with ITimerScheduler.StartPeriodicTimer call inside AroundPreStart, variant 2 + ( +""" +// 03 +using System; +using Akka.Actor; + +public sealed class MyActor : ReceiveActor, IWithTimers +{ + public override void AroundPreRestart(Exception cause, object message) + { + base.AroundPreRestart(cause, message); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + public ITimerScheduler Timers { get; set; } +} +""", (10, 9, 10, 100), ["StartPeriodicTimer", "AroundPreRestart"]), + + // ReceiveActor with ITimerScheduler.StartSingleTimer call inside PreStart + ( +""" +// 04 +using System; +using Akka.Actor; + +public sealed class MyActor : ReceiveActor, IWithTimers +{ + protected override void PreRestart(Exception reason, object message) + { + base.PreRestart(reason, message); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + } + + public ITimerScheduler Timers { get; set; } +} +""", (10, 9, 10, 73), ["StartSingleTimer", "PreRestart"]), + + // ReceiveActor with ITimerScheduler.StartPeriodicTimer call inside PreStart, variant 1 + ( +""" +// 05 +using System; +using Akka.Actor; + +public sealed class MyActor : ReceiveActor, IWithTimers +{ + protected override void PreRestart(Exception reason, object message) + { + base.PreRestart(reason, message); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + } + + public ITimerScheduler Timers { get; set; } +} +""", (10, 9, 10, 75), ["StartPeriodicTimer", "PreRestart"]), + + // ReceiveActor with ITimerScheduler.StartPeriodicTimer call inside PreStart, variant 2 + ( +""" +// 06 +using System; +using Akka.Actor; + +public sealed class MyActor : ReceiveActor, IWithTimers +{ + protected override void PreRestart(Exception reason, object message) + { + base.PreRestart(reason, message); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + public ITimerScheduler Timers { get; set; } +} +""", (10, 9, 10, 100), ["StartPeriodicTimer", "PreRestart"]), + + // UntypedActor with ITimerScheduler.StartSingleTimer call inside AroundPreStart + ( +""" +// 07 +using System; +using Akka.Actor; + +public sealed class MyActor : UntypedActor, IWithTimers +{ + public override void AroundPreRestart(Exception cause, object message) + { + base.AroundPreRestart(cause, message); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + } + + protected override void OnReceive(object message) { } + + public ITimerScheduler Timers { get; set; } +} +""", (10, 9, 10, 73), ["StartSingleTimer", "AroundPreRestart"]), + + // UntypedActor with ITimerScheduler.StartPeriodicTimer call inside AroundPreStart, variant 1 + ( +""" +// 08 +using System; +using Akka.Actor; + +public sealed class MyActor : UntypedActor, IWithTimers +{ + public override void AroundPreRestart(Exception cause, object message) + { + base.AroundPreRestart(cause, message); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + } + + protected override void OnReceive(object message) { } + + public ITimerScheduler Timers { get; set; } +} +""", (10, 9, 10, 75), ["StartPeriodicTimer", "AroundPreRestart"]), + + // UntypedActor with ITimerScheduler.StartPeriodicTimer call inside AroundPreStart, variant 2 + ( +""" +// 09 +using System; +using Akka.Actor; + +public sealed class MyActor : UntypedActor, IWithTimers +{ + public override void AroundPreRestart(Exception cause, object message) + { + base.AroundPreRestart(cause, message); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + protected override void OnReceive(object message) { } + + public ITimerScheduler Timers { get; set; } +} +""", (10, 9, 10, 100), ["StartPeriodicTimer", "AroundPreRestart"]), + + // UntypedActor with ITimerScheduler.StartSingleTimer call inside PreStart + ( +""" +// 10 +using System; +using Akka.Actor; + +public sealed class MyActor : UntypedActor, IWithTimers +{ + protected override void PreRestart(Exception reason, object message) + { + base.PreRestart(reason, message); + Timers.StartSingleTimer("test", "test", TimeSpan.FromMinutes(3)); + } + + protected override void OnReceive(object message) { } + + public ITimerScheduler Timers { get; set; } +} +""", (10, 9, 10, 73), ["StartSingleTimer", "PreRestart"]), + + // UntypedActor with ITimerScheduler.StartPeriodicTimer call inside PreStart, variant 1 + ( +""" +// 11 +using System; +using Akka.Actor; + +public sealed class MyActor : UntypedActor, IWithTimers +{ + protected override void PreRestart(Exception reason, object message) + { + base.PreRestart(reason, message); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3)); + } + + protected override void OnReceive(object message) { } + + public ITimerScheduler Timers { get; set; } +} +""", (10, 9, 10, 75), ["StartPeriodicTimer", "PreRestart"]), + + // UntypedActor with ITimerScheduler.StartPeriodicTimer call inside PreStart, variant 2 + ( +""" +// 12 +using System; +using Akka.Actor; + +public sealed class MyActor : UntypedActor, IWithTimers +{ + protected override void PreRestart(Exception reason, object message) + { + base.PreRestart(reason, message); + Timers.StartPeriodicTimer("test", "test", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3)); + } + + protected override void OnReceive(object message) { } + + public ITimerScheduler Timers { get; set; } +} +""", (10, 9, 10, 100), ["StartPeriodicTimer", "PreRestart"]), + }; + + [Theory] + [MemberData(nameof(SuccessCases))] + public async Task SuccessCase(string testCode) + { + await Verify.VerifyAnalyzer(testCode).ConfigureAwait(true); + } + + [Theory] + [MemberData(nameof(FailureCases))] + public Task FailureCase( + (string testCode, (int startLine, int startColumn, int endLine, int endColumn) spanData, object[] arguments) d) + { + var expected = Verify.Diagnostic() + .WithSpan(d.spanData.startLine, d.spanData.startColumn, d.spanData.endLine, d.spanData.endColumn) + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments(d.arguments); + + return Verify.VerifyAnalyzer(d.testCode, expected); + } + +} \ No newline at end of file diff --git a/src/Akka.Analyzers/AK1000/MustNotUseIWithTimersInPreRestartAnalyzer.cs b/src/Akka.Analyzers/AK1000/MustNotUseIWithTimersInPreRestartAnalyzer.cs new file mode 100644 index 0000000..9c3fbbc --- /dev/null +++ b/src/Akka.Analyzers/AK1000/MustNotUseIWithTimersInPreRestartAnalyzer.cs @@ -0,0 +1,61 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013-2024 .NET Foundation +// +// ----------------------------------------------------------------------- + +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(); + 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); + } +} \ No newline at end of file diff --git a/src/Akka.Analyzers/Context/Core/Actor/ActorSymbolFactory.cs b/src/Akka.Analyzers/Context/Core/Actor/ActorSymbolFactory.cs index 1b59f08..fddd99c 100644 --- a/src/Akka.Analyzers/Context/Core/Actor/ActorSymbolFactory.cs +++ b/src/Akka.Analyzers/Context/Core/Actor/ActorSymbolFactory.cs @@ -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"); } \ No newline at end of file diff --git a/src/Akka.Analyzers/Context/Core/Actor/AkkaCoreActorContext.cs b/src/Akka.Analyzers/Context/Core/Actor/AkkaCoreActorContext.cs index 00cbb21..b3f25bb 100644 --- a/src/Akka.Analyzers/Context/Core/Actor/AkkaCoreActorContext.cs +++ b/src/Akka.Analyzers/Context/Core/Actor/AkkaCoreActorContext.cs @@ -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; @@ -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 @@ -44,6 +46,8 @@ public sealed class AkkaCoreActorContext : IAkkaCoreActorContext private readonly Lazy _lazyGracefulStopSupportType; private readonly Lazy _lazyTellSchedulerInterface; private readonly Lazy _lazyActorRefsType; + private readonly Lazy _lazyITimerSchedulerType; + private readonly Lazy _lazyGracefulStopSupport; private readonly Lazy _lazyIIndirectActorProducer; private readonly Lazy _lazyReceiveActor; @@ -62,6 +66,8 @@ private AkkaCoreActorContext(Compilation compilation) _lazyGracefulStopSupportType = new Lazy(() => ActorSymbolFactory.GracefulStopSupport(compilation)); _lazyTellSchedulerInterface = new Lazy(() => ActorSymbolFactory.TellSchedulerInterface(compilation)); _lazyActorRefsType = new Lazy(() => ActorSymbolFactory.ActorRefs(compilation)); + _lazyITimerSchedulerType = new Lazy(() => ActorSymbolFactory.TimerSchedulerInterface(compilation)); + _lazyGracefulStopSupport = new Lazy(() => GracefulStopSupportContext.Get(this)); _lazyIIndirectActorProducer = new Lazy(() => IndirectActorProducerContext.Get(this)); _lazyReceiveActor = new Lazy(() => ReceiveActorContext.Get(this)); @@ -70,6 +76,7 @@ private AkkaCoreActorContext(Compilation compilation) _lazyProps = new Lazy(() => PropsContext.Get(this)); ITellScheduler = TellSchedulerInterfaceContext.Get(compilation); ActorRefs = ActorRefsContext.Get(this); + ITimerScheduler = TimerSchedulerContext.Get(this); } public INamedTypeSymbol? ActorBaseType => _lazyActorBaseType.Value; @@ -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; @@ -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); diff --git a/src/Akka.Analyzers/Context/Core/Actor/BaseActorContext.cs b/src/Akka.Analyzers/Context/Core/Actor/BaseActorContext.cs index febc070..50ed947 100644 --- a/src/Akka.Analyzers/Context/Core/Actor/BaseActorContext.cs +++ b/src/Akka.Analyzers/Context/Core/Actor/BaseActorContext.cs @@ -11,7 +11,24 @@ 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 @@ -19,19 +36,66 @@ 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 _lazySelf; + private readonly Lazy _lazyAroundPreRestart; + private readonly Lazy _lazyAroundPreStart; + private readonly Lazy _lazyPreStart; + private readonly Lazy _lazyAroundPostRestart; + private readonly Lazy _lazyPreRestart; + private readonly Lazy _lazyPostRestart; + private readonly Lazy _lazyAroundPostStop; + private readonly Lazy _lazyPostStop; private ActorBaseContext(AkkaCoreActorContext context) { + Guard.AssertIsNotNull(context.ActorBaseType); + _lazySelf = new Lazy(() => (IPropertySymbol) context.ActorBaseType!.GetMembers("Self").First()); + + _lazyAroundPreRestart = new Lazy(() => (IMethodSymbol) context.ActorBaseType! + .GetMembers(nameof(AroundPreRestart)).First()); + _lazyAroundPreStart = new Lazy(() => (IMethodSymbol) context.ActorBaseType! + .GetMembers(nameof(AroundPreStart)).First()); + _lazyPreStart = new Lazy(() => (IMethodSymbol) context.ActorBaseType! + .GetMembers(nameof(PreStart)).First()); + _lazyAroundPostRestart = new Lazy(() => (IMethodSymbol) context.ActorBaseType! + .GetMembers(nameof(AroundPostRestart)).First()); + _lazyPreRestart = new Lazy(() => (IMethodSymbol) context.ActorBaseType! + .GetMembers(nameof(PreRestart)).First()); + _lazyPostRestart = new Lazy(() => (IMethodSymbol) context.ActorBaseType! + .GetMembers(nameof(PostRestart)).First()); + _lazyAroundPostStop = new Lazy(() => (IMethodSymbol) context.ActorBaseType! + .GetMembers(nameof(AroundPostStop)).First()); + _lazyPostStop = new Lazy(() => (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); + } } \ No newline at end of file diff --git a/src/Akka.Analyzers/Context/Core/Actor/IAkkaCoreActorContext.cs b/src/Akka.Analyzers/Context/Core/Actor/IAkkaCoreActorContext.cs index 2302884..0ab403e 100644 --- a/src/Akka.Analyzers/Context/Core/Actor/IAkkaCoreActorContext.cs +++ b/src/Akka.Analyzers/Context/Core/Actor/IAkkaCoreActorContext.cs @@ -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; } @@ -30,4 +31,5 @@ public interface IAkkaCoreActorContext public IPropsContext Props { get; } public ITellSchedulerInterfaceContext ITellScheduler { get; } public IActorRefsContext ActorRefs { get; } + public ITimerSchedulerContext ITimerScheduler { get; } } diff --git a/src/Akka.Analyzers/Context/Core/Actor/ITimerSchedulerContext.cs b/src/Akka.Analyzers/Context/Core/Actor/ITimerSchedulerContext.cs new file mode 100644 index 0000000..39b1da5 --- /dev/null +++ b/src/Akka.Analyzers/Context/Core/Actor/ITimerSchedulerContext.cs @@ -0,0 +1,50 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013-2024 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Akka.Analyzers.Context.Core.Actor; + +public interface ITimerSchedulerContext +{ + public ImmutableArray StartPeriodicTimer { get; } + public ImmutableArray StartSingleTimer { get; } +} + +public sealed class EmptyTimerSchedulerContext : ITimerSchedulerContext +{ + public static readonly EmptyTimerSchedulerContext Instance = new(); + private EmptyTimerSchedulerContext() { } + public ImmutableArray StartPeriodicTimer => ImmutableArray.Empty; + public ImmutableArray StartSingleTimer => ImmutableArray.Empty; +} + +public sealed class TimerSchedulerContext : ITimerSchedulerContext +{ + private readonly Lazy> _lazyStartPeriodicTimer; + private readonly Lazy> _lazyStartSingleTimer; + + private TimerSchedulerContext(AkkaCoreActorContext context) + { + Guard.AssertIsNotNull(context); + + _lazyStartPeriodicTimer = new Lazy>(() => context.ITimerSchedulerType! + .GetMembers(nameof(StartPeriodicTimer)).Select(m => (IMethodSymbol)m).ToImmutableArray()); + _lazyStartSingleTimer = new Lazy>(() => context.ITimerSchedulerType! + .GetMembers(nameof(StartSingleTimer)).Select(m => (IMethodSymbol)m).ToImmutableArray()); + } + + public ImmutableArray StartPeriodicTimer => _lazyStartPeriodicTimer.Value; + public ImmutableArray StartSingleTimer => _lazyStartSingleTimer.Value; + + public static TimerSchedulerContext Get(AkkaCoreActorContext context) + { + Guard.AssertIsNotNull(context); + + return new TimerSchedulerContext(context); + } +} \ No newline at end of file diff --git a/src/Akka.Analyzers/Utility/CodeAnalysisExtensions.cs b/src/Akka.Analyzers/Utility/CodeAnalysisExtensions.cs index d2f1597..ad8c140 100644 --- a/src/Akka.Analyzers/Utility/CodeAnalysisExtensions.cs +++ b/src/Akka.Analyzers/Utility/CodeAnalysisExtensions.cs @@ -4,6 +4,7 @@ // // ----------------------------------------------------------------------- +using System.Collections.Immutable; using System.Runtime.CompilerServices; using Akka.Analyzers.Context.Core; using Microsoft.CodeAnalysis; @@ -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 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 refMethods) + { + while (!ReferenceEquals(methodSymbol, methodSymbol.ConstructedFrom)) + methodSymbol = methodSymbol.ConstructedFrom; + + return refMethods.Any(m => ReferenceEquals(m, methodSymbol)); + } + } \ No newline at end of file diff --git a/src/Akka.Analyzers/Utility/RuleDescriptors.cs b/src/Akka.Analyzers/Utility/RuleDescriptors.cs index 7c1f570..e39fc9b 100644 --- a/src/Akka.Analyzers/Utility/RuleDescriptors.cs +++ b/src/Akka.Analyzers/Utility/RuleDescriptors.cs @@ -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