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