Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AK1004 - Add ScheduleTell to IWithTimers analyzer #81

Merged

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions src/Akka.Analyzers.Tests/Utility/AkkaVerifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,31 @@ public static Task VerifyCodeFix(string before, string after, string fixerAction
return test.RunAsync();
}

public static Task VerifyCodeFix(
string before,
string after,
string fixerActionKey,
int incrementalIterations,
CodeFixTestBehaviors codeFixBehaviors,
DiagnosticResult[] diagnostics,
DiagnosticResult[] fixedDiagnostics)
{
Guard.AssertIsNotNull(before);
Guard.AssertIsNotNull(after);

var test = new AkkaTest
{
TestCode = before,
FixedCode = after,
CodeActionEquivalenceKey = fixerActionKey,
NumberOfIncrementalIterations = incrementalIterations,
CodeFixTestBehaviors = codeFixBehaviors
};
test.TestState.ExpectedDiagnostics.AddRange(diagnostics);
test.FixedState.ExpectedDiagnostics.AddRange(fixedDiagnostics);
return test.RunAsync();
}

private sealed class AkkaTest() : TestBase(ReferenceAssembliesHelper.CurrentAkka);

private class TestBase : CSharpCodeFixTest<TAnalyzer, EmptyCodeFixProvider, DefaultVerifier>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// -----------------------------------------------------------------------
// <copyright file="ShouldUseIWithTimerInsteadOfITellScheduler.cs" company="Akka.NET Project">
// Copyright (C) 2013-2024 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

using Akka.Analyzers.Context;
using Akka.Analyzers.Context.Core.Actor;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Akka.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ShouldUseIWithTimersInsteadOfScheduleTellAnalyzer(): AkkaDiagnosticAnalyzer(RuleDescriptors.Ak1004ShouldUseIWithTimersInsteadOfScheduleTell)
{
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;

var classDeclaration = invocationExpr.FirstAncestorOrSelf<ClassDeclarationSyntax>();
if (classDeclaration is null)
return;
var classBase = semanticModel.GetDeclaredSymbol(classDeclaration)?.BaseType;
var coreContext = akkaContext.AkkaCore;

// Check that the class declaration inherits from ActorBase
if (classBase is null || !classBase.IsDerivedOrImplements(coreContext.Actor.ActorBaseType!))
return;

// invocation must be a member access expression
if (invocationExpr.Expression is not MemberAccessExpressionSyntax memberAccessExpr)
return;

// Get the member symbol from the invocation expression
if(semanticModel.GetSymbolInfo(memberAccessExpr).Symbol is not IMethodSymbol methodSymbol)
return;

// Check if the method name is `ScheduleTellOnce` or `ScheduleTellRepeatedly`
ArgumentSyntax? receiver = null;
ArgumentSyntax? sender = null;
var refSymbols = akkaContext.AkkaCore.Actor.ITellScheduler.ScheduleTellOnce;
if (refSymbols.Any(s => ReferenceEquals(methodSymbol, s)))
{
receiver = invocationExpr.ArgumentList.Arguments[1];
sender = invocationExpr.ArgumentList.Arguments[3];
}
else
{
refSymbols = akkaContext.AkkaCore.Actor.ITellScheduler.ScheduleTellRepeatedly;
if (refSymbols.Any(s => ReferenceEquals(methodSymbol, s)))
{
receiver = invocationExpr.ArgumentList.Arguments[2];
sender = invocationExpr.ArgumentList.Arguments[4];
}
}

// Check that both receiver and sender is Self or if sender is Nobody or NoSender
if (!IsReferenceToSelf(receiver, semanticModel, coreContext.Actor) ||
!IsReferenceToSelfOrNobody(sender, semanticModel, coreContext.Actor))
return;

var diagnostic = Diagnostic.Create(
descriptor: RuleDescriptors.Ak1004ShouldUseIWithTimersInsteadOfScheduleTell,
location: invocationExpr.GetLocation(),
"ScheduleTell invocation");
ctx.ReportDiagnostic(diagnostic);

}, SyntaxKind.InvocationExpression);
}

private static bool IsReferenceToSelfOrNobody(ArgumentSyntax? argument, SemanticModel semanticModel, IAkkaCoreActorContext context)
{
if (argument is null)
return false;

var expression = argument.Expression;

// null is considered as NoSender
if (expression is LiteralExpressionSyntax literal && literal.Kind() == SyntaxKind.NullLiteralExpression)
return true;

// argument must be an identifier
var identifier = expression.DescendantNodesAndSelf(node => node is IdentifierNameSyntax).FirstOrDefault();
if (identifier is null)
return false;

// Check for field symbols
if (semanticModel.GetSymbolInfo(identifier).Symbol is IFieldSymbol fieldSymbol)
{
// Argument is `ActorRefs.Nobody`
if (ReferenceEquals(fieldSymbol, context.ActorRefs.Nobody))
return true;

// Argument is `ActorRefs.NoSender`
if (ReferenceEquals(fieldSymbol, context.ActorRefs.NoSender))
return true;
}

// identifier must be a property
if (semanticModel.GetSymbolInfo(identifier).Symbol is not IPropertySymbol propertySymbol)
return false;

// Argument is `ActorBase.Self`
if (ReferenceEquals(propertySymbol, context.ActorBase.Self))
return true;

// Argument is `IActorRef.Self`
if (ReferenceEquals(propertySymbol, context.IActorContext.Self))
return true;

return false;
}

private static bool IsReferenceToSelf(ArgumentSyntax? argument, SemanticModel semanticModel, IAkkaCoreActorContext context)
{
if (argument is null)
return false;

var expression = argument.Expression;

// argument must be an identifier
var identifier = expression.DescendantNodesAndSelf(node => node is IdentifierNameSyntax).FirstOrDefault();
if (identifier is null)
return false;

// identifier must be a property
if (semanticModel.GetSymbolInfo(identifier).Symbol is not IPropertySymbol propertySymbol)
return false;

// Argument is `ActorBase.Self`
if (ReferenceEquals(propertySymbol, context.ActorBase.Self))
return true;

// Argument is `IActorRef.Self`
if (ReferenceEquals(propertySymbol, context.IActorContext.Self))
return true;

return false;
}
}
45 changes: 45 additions & 0 deletions src/Akka.Analyzers/Context/Core/Actor/ActorRefsContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// -----------------------------------------------------------------------
// <copyright file="ActorRefsContext.cs" company="Akka.NET Project">
// Copyright (C) 2013-2024 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

using Microsoft.CodeAnalysis;

namespace Akka.Analyzers.Context.Core.Actor;

public interface IActorRefsContext
{
public IFieldSymbol? Nobody { get; }
public IFieldSymbol? NoSender { get; }
}

public sealed class EmptyActorRefsContext : IActorRefsContext
{
public static readonly EmptyActorRefsContext Empty = new();

private EmptyActorRefsContext() { }

public IFieldSymbol? Nobody => null;
public IFieldSymbol? NoSender => null;
}

public sealed class ActorRefsContext : IActorRefsContext
{
private readonly Lazy<IFieldSymbol> _lazyNobody;
private readonly Lazy<IFieldSymbol> _lazyNoSender;

private ActorRefsContext(IAkkaCoreActorContext context)
{
_lazyNobody = new Lazy<IFieldSymbol>(() => (IFieldSymbol) context.ActorRefsType!
.GetMembers("Nobody").First());
_lazyNoSender = new Lazy<IFieldSymbol>(() => (IFieldSymbol) context.ActorRefsType!
.GetMembers("NoSender").First());
}

public IFieldSymbol? Nobody => _lazyNobody.Value;
public IFieldSymbol? NoSender => _lazyNoSender.Value;

public static ActorRefsContext Get(IAkkaCoreActorContext context)
=> new(context);
}
8 changes: 8 additions & 0 deletions src/Akka.Analyzers/Context/Core/Actor/ActorSymbolFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,12 @@ public static class ActorSymbolFactory
public static INamedTypeSymbol? GracefulStopSupport(Compilation compilation)
=> Guard.AssertIsNotNull(compilation)
.GetTypeByMetadataName($"{AkkaActorNamespace}.GracefulStopSupport");

public static INamedTypeSymbol? TellSchedulerInterface(Compilation compilation)
=> Guard.AssertIsNotNull(compilation)
.GetTypeByMetadataName("Akka.Actor.ITellScheduler");

public static INamedTypeSymbol? ActorRefs(Compilation compilation)
=> Guard.AssertIsNotNull(compilation)
.GetTypeByMetadataName("Akka.Actor.ActorRefs");
}
14 changes: 14 additions & 0 deletions src/Akka.Analyzers/Context/Core/Actor/AkkaCoreActorContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@ private EmptyAkkaCoreActorContext() { }
public INamedTypeSymbol? IIndirectActorProducerType => null;
public INamedTypeSymbol? ReceiveActorType => null;
public INamedTypeSymbol? GracefulStopSupportType => null;
public INamedTypeSymbol? ITellSchedulerType => null;
public INamedTypeSymbol? ActorRefsType => null;

public IGracefulStopSupportContext GracefulStopSupportSupport => EmptyGracefulStopSupportContext.Instance;
public IIndirectActorProducerContext IIndirectActorProducer => EmptyIndirectActorProducerContext.Instance;
public IReceiveActorContext ReceiveActor => EmptyReceiveActorContext.Instance;
public IActorBaseContext ActorBase => EmptyActorBaseContext.Instance;
public IActorContextContext IActorContext => EmptyActorContextContext.Instance;
public IPropsContext Props => EmptyPropsContext.Instance;
public ITellSchedulerInterfaceContext ITellScheduler => EmptyTellSchedulerInterfaceContext.Instance;
public IActorRefsContext ActorRefs => EmptyActorRefsContext.Empty;
}

public sealed class AkkaCoreActorContext : IAkkaCoreActorContext
Expand All @@ -38,6 +42,8 @@ public sealed class AkkaCoreActorContext : IAkkaCoreActorContext
private readonly Lazy<INamedTypeSymbol?> _lazyIIndirectActorProducerType;
private readonly Lazy<INamedTypeSymbol?> _lazyReceiveActorType;
private readonly Lazy<INamedTypeSymbol?> _lazyGracefulStopSupportType;
private readonly Lazy<INamedTypeSymbol?> _lazyTellSchedulerInterface;
private readonly Lazy<INamedTypeSymbol?> _lazyActorRefsType;
private readonly Lazy<IGracefulStopSupportContext> _lazyGracefulStopSupport;
private readonly Lazy<IIndirectActorProducerContext> _lazyIIndirectActorProducer;
private readonly Lazy<IReceiveActorContext> _lazyReceiveActor;
Expand All @@ -54,12 +60,16 @@ private AkkaCoreActorContext(Compilation compilation)
_lazyIIndirectActorProducerType = new Lazy<INamedTypeSymbol?>(() => ActorSymbolFactory.IndirectActorProducer(compilation));
_lazyReceiveActorType = new Lazy<INamedTypeSymbol?>(() => ActorSymbolFactory.ReceiveActor(compilation));
_lazyGracefulStopSupportType = new Lazy<INamedTypeSymbol?>(() => ActorSymbolFactory.GracefulStopSupport(compilation));
_lazyTellSchedulerInterface = new Lazy<INamedTypeSymbol?>(() => ActorSymbolFactory.TellSchedulerInterface(compilation));
_lazyActorRefsType = new Lazy<INamedTypeSymbol?>(() => ActorSymbolFactory.ActorRefs(compilation));
_lazyGracefulStopSupport = new Lazy<IGracefulStopSupportContext>(() => GracefulStopSupportContext.Get(this));
_lazyIIndirectActorProducer = new Lazy<IIndirectActorProducerContext>(() => IndirectActorProducerContext.Get(this));
_lazyReceiveActor = new Lazy<IReceiveActorContext>(() => ReceiveActorContext.Get(this));
_lazyActorBase = new Lazy<IActorBaseContext>(() => ActorBaseContext.Get(this));
_lazyActorContext = new Lazy<IActorContextContext>(() => ActorContextContext.Get(this));
_lazyProps = new Lazy<IPropsContext>(() => PropsContext.Get(this));
ITellScheduler = TellSchedulerInterfaceContext.Get(compilation);
ActorRefs = ActorRefsContext.Get(this);
}

public INamedTypeSymbol? ActorBaseType => _lazyActorBaseType.Value;
Expand All @@ -68,13 +78,17 @@ private AkkaCoreActorContext(Compilation compilation)
public INamedTypeSymbol? IActorContextType => _lazyActorContextType.Value;
public INamedTypeSymbol? IIndirectActorProducerType => _lazyIIndirectActorProducerType.Value;
public INamedTypeSymbol? ReceiveActorType => _lazyReceiveActorType.Value;
public INamedTypeSymbol? ITellSchedulerType => _lazyTellSchedulerInterface.Value;
public INamedTypeSymbol? ActorRefsType => _lazyActorRefsType.Value;
public INamedTypeSymbol? GracefulStopSupportType => _lazyGracefulStopSupportType.Value;
public IGracefulStopSupportContext GracefulStopSupportSupport => _lazyGracefulStopSupport.Value;
public IIndirectActorProducerContext IIndirectActorProducer => _lazyIIndirectActorProducer.Value;
public IReceiveActorContext ReceiveActor => _lazyReceiveActor.Value;
public IActorBaseContext ActorBase => _lazyActorBase.Value;
public IActorContextContext IActorContext => _lazyActorContext.Value;
public IPropsContext Props => _lazyProps.Value;
public ITellSchedulerInterfaceContext ITellScheduler { get; }
public IActorRefsContext ActorRefs { get; }

public static IAkkaCoreActorContext Get(Compilation compilation)
=> new AkkaCoreActorContext(compilation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ public interface IAkkaCoreActorContext
public INamedTypeSymbol? IIndirectActorProducerType { get; }
public INamedTypeSymbol? ReceiveActorType { get; }
public INamedTypeSymbol? GracefulStopSupportType { get; }
public INamedTypeSymbol? ITellSchedulerType { get; }
public INamedTypeSymbol? ActorRefsType { get; }

public IGracefulStopSupportContext GracefulStopSupportSupport { get; }
public IIndirectActorProducerContext IIndirectActorProducer { get; }
public IReceiveActorContext ReceiveActor { get; }
public IActorBaseContext ActorBase { get; }
public IActorContextContext IActorContext { get; }
public IPropsContext Props { get; }
public ITellSchedulerInterfaceContext ITellScheduler { get; }
public IActorRefsContext ActorRefs { get; }
}
23 changes: 23 additions & 0 deletions src/Akka.Analyzers/Context/Core/Actor/ITellSchedulerFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// -----------------------------------------------------------------------
// <copyright file="ITellSchedulerFactory.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.Core;
using Microsoft.CodeAnalysis;

namespace Akka.Analyzers.Core.Actor;

// ReSharper disable once InconsistentNaming
public static class ITellSchedulerFactory
{
public static ImmutableArray<ISymbol> ScheduleTellOnce(Compilation compilation)
=> ActorSymbolFactory.TellSchedulerInterface(compilation)!
.GetMembers("ScheduleTellOnce");

public static ImmutableArray<ISymbol> ScheduleTellRepeatedly(Compilation compilation)
=> ActorSymbolFactory.TellSchedulerInterface(compilation)!
.GetMembers("ScheduleTellRepeatedly");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// -----------------------------------------------------------------------
// <copyright file="TellSchedulerContext.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.Core.Actor;
using Microsoft.CodeAnalysis;

namespace Akka.Analyzers.Context.Core.Actor;

public interface ITellSchedulerInterfaceContext
{
public ImmutableArray<ISymbol> ScheduleTellOnce { get; }
public ImmutableArray<ISymbol> ScheduleTellRepeatedly { get; }
}

public class EmptyTellSchedulerInterfaceContext : ITellSchedulerInterfaceContext
{
private EmptyTellSchedulerInterfaceContext() { }

public static readonly EmptyTellSchedulerInterfaceContext Instance = new();

public ImmutableArray<ISymbol> ScheduleTellOnce => new();
public ImmutableArray<ISymbol> ScheduleTellRepeatedly => new();
}

public class TellSchedulerInterfaceContext: ITellSchedulerInterfaceContext
{
private readonly Lazy<ImmutableArray<ISymbol>> _lazyScheduleTellOnce;
private readonly Lazy<ImmutableArray<ISymbol>> _lazyScheduleTellRepeatedly;

private TellSchedulerInterfaceContext(Compilation compilation)
{
_lazyScheduleTellOnce = new Lazy<ImmutableArray<ISymbol>>(() => ITellSchedulerFactory.ScheduleTellOnce(compilation));
_lazyScheduleTellRepeatedly = new Lazy<ImmutableArray<ISymbol>>(() => ITellSchedulerFactory.ScheduleTellRepeatedly(compilation));
}

public ImmutableArray<ISymbol> ScheduleTellOnce => _lazyScheduleTellOnce.Value;
public ImmutableArray<ISymbol> ScheduleTellRepeatedly => _lazyScheduleTellRepeatedly.Value;

public static ITellSchedulerInterfaceContext Get(Compilation compilation)
=> new TellSchedulerInterfaceContext(compilation);
}
Loading