Skip to content

Commit

Permalink
AK1004 - Add ScheduleTell to IWithTimers analyzer (#81)
Browse files Browse the repository at this point in the history
* AK1004 - Add ScheduleTell to IWithTimers analyzer

* Fix merge problems

* Implement fix (unit test not working)

* Fix fixer code

* Remove code fixer

* Clean up solution files

* Cleanup solution files

---------

Co-authored-by: Aaron Stannard <[email protected]>
  • Loading branch information
Arkatufus and Aaronontheweb authored Mar 13, 2024
1 parent 9266142 commit 4ff3a14
Show file tree
Hide file tree
Showing 11 changed files with 1,214 additions and 1 deletion.

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

0 comments on commit 4ff3a14

Please sign in to comment.