Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2272406
Code fixer for converting SubscribeXEvents to attributes
Tayrtahn Jul 7, 2026
8336a43
Tweaks and docs
Tayrtahn Jul 7, 2026
3be2b13
Test method descriptions
Tayrtahn Jul 7, 2026
34d2d8d
Add test of multiple invocations
Tayrtahn Jul 8, 2026
e5e8f56
Ignore anonymous delegates
Tayrtahn Jul 8, 2026
587565a
Fix event handler method symbol lookup
Tayrtahn Jul 8, 2026
21510d2
Fix whitespace weirdness with multiple invocations
Tayrtahn Jul 8, 2026
ab1b3a9
Ignore any methods that contain conditional preprocessor directives
Tayrtahn Jul 27, 2026
0e8a16a
Expand preprocessor directive filtering to check the entire class.
Tayrtahn Jul 28, 2026
0b64608
Add test that subscriptions in if statement blocks are not flagged.
Tayrtahn Jul 28, 2026
189bf0d
doc tweak
Tayrtahn Jul 28, 2026
1856896
Ignore invocation using generic type parameters as type args
Tayrtahn Jul 28, 2026
08d6f2a
Ignore subscriptions where the handler method is generic.
Tayrtahn Jul 29, 2026
dbb86b9
Skip compilations without the subscription attribute
Tayrtahn Jul 29, 2026
7099298
Merge branch 'master' into analyzer/entitysystem-subscription-converter
Tayrtahn Jul 29, 2026
ce788fb
Ignore virtual and abstract target methods
Tayrtahn Jul 29, 2026
da40fc9
Add using directive if needed
Tayrtahn Jul 29, 2026
31dd718
Oops, I guess that's not a 1:1 mapping after all.
Tayrtahn Jul 30, 2026
607c324
A little bit of cleanup
Tayrtahn Jul 30, 2026
2d1741e
Add global usings file to Robust.Shared.IntegrationTests project.
Tayrtahn Jul 30, 2026
84ed1ed
Break up and better document the automatic using directive stuff
Tayrtahn Jul 30, 2026
59e1ce5
Remove the requirement for the method to be named Initialize
Tayrtahn Jul 31, 2026
2ae349d
Ignore event handlers with abstract event types
Tayrtahn Jul 31, 2026
987d46b
Move class symbol lookup back into the code action.
Tayrtahn Jul 31, 2026
060d609
Fancier warning message
Tayrtahn Jul 31, 2026
dfd2814
Reuse a single document editor if possible
Tayrtahn Aug 3, 2026
fe68dfc
Filter for EntitySystems using checking for IEntitySystem instead of …
Tayrtahn Aug 3, 2026
9b6f90f
Docs cleanup and tweaks
Tayrtahn Aug 3, 2026
8eda4b9
Merge branch 'master' into analyzer/entitysystem-subscription-converter
Tayrtahn Aug 5, 2026
0f5f95c
First pass adding before/after support
Tayrtahn Aug 5, 2026
75a5e0d
Cleanup and docs
Tayrtahn Aug 6, 2026
f937ca9
Don't add empty type arrays
Tayrtahn Aug 6, 2026
3b171db
More tests
Tayrtahn Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.EntitySystemSubscriptionConversionAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;

namespace Robust.Analyzers.Tests;

[TestOf(typeof(EntitySystemSubscriptionConversionAnalyzer))]
public sealed class EntitySystemSubscriptionConversionAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<EntitySystemSubscriptionConversionAnalyzer, DefaultVerifier>()
{
TestState =
{
Sources = { code }
},
};

test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));

// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);

return test.RunAsync();
}

private const string TestTypeDefs = """
using Robust.Shared.GameObjects;
using System;

namespace Robust.Shared.GameObjects
{
public interface IComponent;
public abstract class Component : IComponent;

public readonly struct EntityUid;

public delegate void ComponentEventRefHandler<in TComp, TEvent>(EntityUid uid, TComp component, ref TEvent args)
where TComp : IComponent
where TEvent : notnull;

public delegate void ComponentEventHandler<in TComp, in TEvent>(EntityUid uid, TComp component, TEvent args)
where TComp : IComponent
where TEvent : notnull;

public interface IEntitySystem;
public abstract class EntitySystem : IEntitySystem
{
public virtual void Initialize() { }
public void SubscribeLocalEvent<TComp, TEvent>(
ComponentEventRefHandler<TComp, TEvent> handler,
Type[]? before = null,
Type[]? after = null)
where TComp : IComponent
where TEvent : notnull
{ }

public void SubscribeLocalEvent<TComp, TEvent>(
ComponentEventHandler<TComp, TEvent> handler,
Type[]? before = null,
Type[]? after = null)
where TComp : IComponent
where TEvent : notnull
{ }
}
}

namespace Robust.Shared.Analyzers
{
public sealed class SubscribeLocalEventAttribute : Attribute;
}

public readonly struct TestEvent;
public readonly struct TestEvent2;
public readonly struct TestEvent3;
public sealed partial class TestComponent : IComponent;
""";

[Test]
[Description("Tests that a SubscribeLocalEvent invocation in an EntitySystem Intialize method is flagged as elligible for conversion.")]
public async Task FlagSubscribeLocalEvent()
{
const string code = """
using Robust.Shared.GameObjects;

public sealed partial class InitalizeBasedSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<TestComponent, TestEvent>(OnTest);
}

private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
{
// Do something
}
}
""";

await Verifier(code,
// /0/Test0.cs(9,9): info RA0058: Event subscription using SubscribeLocalEvent can be converted to use SubscribeLocalEventAttribute
VerifyCS.Diagnostic().WithSpan(9, 9, 9, 62).WithArguments("SubscribeLocalEvent", "SubscribeLocalEventAttribute")
);
}

[Test]
[Description("Tests that a subscription using an anonymous delegate is not flagged as elligible for conversion.")]
public async Task IgnoreAnonymousDelegate()
{
const string code = """
using Robust.Shared.GameObjects;

public sealed partial class InitalizeBasedSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<TestComponent, TestEvent>((u, c, ref _) => OnTest(u, c));
}

private void OnTest(EntityUid uid, TestComponent comp) { }
}
""";

await Verifier(code, []);
}

[Test]
[Description("Tests that a subscription in a method containing preprocessor directives is not flagged as elligible for conversion.")]
public async Task IgnoreWithPreprocessorDirectives()
{
const string code = """

using Robust.Shared.GameObjects;

public sealed partial class InitalizeBasedSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();

#if DEBUG
SubscribeLocalEvent<TestComponent, TestEvent>(OnTest);
#else
SubscribeLocalEvent<TestComponent, TestEvent>(OnTest2);
#endif
}

private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args) { }
private void OnTest2(EntityUid uid, TestComponent comp, ref TestEvent args) { }
}
""";

await Verifier(code, []);
}

[Test]
[Description("Tests that a subscription using a generic type parameter is not flagged as elligible for conversion.")]
public async Task IgnoreWithGenericComponent()
{
const string code = """

using Robust.Shared.GameObjects;

public sealed partial class InitalizeBasedSystem<TComp> : EntitySystem
where TComp : Component
{
public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<TComp, TestEvent>(OnTest);
}

private void OnTest(EntityUid uid, TComp comp, ref TestEvent args) { }
}
""";

await Verifier(code, []);
}

[Test]
[Description("Tests that subscriptions using generic methods as event handlers are not flagged as elligible for conversion.")]
public async Task IgnoreWithGenericHandler()
{
const string code = """

using Robust.Shared.GameObjects;

public sealed partial class InitalizeBasedSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<TestComponent, TestEventClassA>(OnTest);
SubscribeLocalEvent<TestComponent, TestEventClassB>(OnTest);
}

private void OnTest<T>(EntityUid uid, TestComponent comp, ref T args) where T : TestEventArgs { }
}

public class TestEventArgs;
public sealed class TestEventClassA : TestEventArgs;
public sealed class TestEventClassB : TestEventArgs;
""";

await Verifier(code, []);
}

[Test]
[Description("Tests that subscriptions using event handlers with abstract event types are not flagged as elligible for conversion.")]
public async Task IgnoreWithAbstractHandler()
{
const string code = """

using Robust.Shared.GameObjects;

public sealed partial class InitalizeBasedSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<TestComponent, TestEventClassA>(OnTest);
SubscribeLocalEvent<TestComponent, TestEventClassB>(OnTest);
}

private void OnTest(EntityUid uid, TestComponent comp, TestEventArgs args) { }
}

public abstract class TestEventArgs;
public sealed class TestEventClassA : TestEventArgs;
public sealed class TestEventClassB : TestEventArgs;
""";

await Verifier(code, []);
}

[Test]
[Description("Tests that subscriptions within if statement blocks are not flagged as elligible for conversion.")]
public async Task IgnoreWithIfStatement()
{
const string code = """

using Robust.Shared.GameObjects;

public sealed partial class InitalizeBasedSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();

if (true)
SubscribeLocalEvent<TestComponent, TestEvent>(OnTest);
else
SubscribeLocalEvent<TestComponent, TestEvent>(OnTest2);
}

private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args) { }
private void OnTest2(EntityUid uid, TestComponent comp, ref TestEvent args) { }
}
""";

await Verifier(code, []);
}
}
Loading
Loading