forked from Space-Station-Multiverse/RobustToolbox
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Source gen reorganizations + component unpause generator. (space-wiza…
…rds#4896) * Source gen reorganizations + component unpause generator. This commit (and subsequent commits) aims to clean up our Roslyn plugin (source gens + analyzers) stack to more sanely re-use common code I also built a new source-gen that automatically generates unpausing implementations for components, incrementing attributed TimeSpan field when unpaused. * Fix warnings in all Roslyn projects
- Loading branch information
Showing
30 changed files
with
1,318 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// OH BOY. TURNS OUT IT GETS EVEN MORE CURSED. | ||
// | ||
// So because we're compiling a copy of Robust.Roslyn.Shared into every analyzer project, | ||
// the test project sees multiple copies of it. This would make it impossible to use. | ||
// UNLESS you use this obscure C# feature called "extern alias" | ||
// that I guarantee you you've never heard of before, and are now concerned about. | ||
|
||
extern alias SerializationGenerator; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,340 @@ | ||
extern alias SerializationGenerator; | ||
using System.Linq; | ||
using System.Reflection; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.Text; | ||
using NUnit.Framework; | ||
using SerializationGenerator::Robust.Roslyn.Shared; | ||
using SerializationGenerator::Robust.Serialization.Generator; | ||
|
||
namespace Robust.Analyzers.Tests; | ||
|
||
[TestFixture] | ||
[TestOf(typeof(ComponentPauseGenerator))] | ||
[Parallelizable(ParallelScope.All)] | ||
public sealed class ComponentPauseGeneratorTest | ||
{ | ||
private const string TypesCode = """ | ||
global using System; | ||
global using Robust.Shared.Analyzers; | ||
global using Robust.Shared.GameObjects; | ||
namespace Robust.Shared.Analyzers | ||
{ | ||
[AttributeUsage(AttributeTargets.Class, Inherited = false)] | ||
public sealed class AutoGenerateComponentPauseAttribute : Attribute | ||
{ | ||
public bool Dirty = false; | ||
} | ||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] | ||
public sealed class AutoPausedFieldAttribute : Attribute; | ||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] | ||
public sealed class AutoNetworkedFieldAttribute : Attribute | ||
{ | ||
} | ||
} | ||
namespace Robust.Shared.GameObjects | ||
{ | ||
public interface IComponent; | ||
} | ||
"""; | ||
|
||
[Test] | ||
public void TestBasic() | ||
{ | ||
var result = RunGenerator(""" | ||
[AutoGenerateComponentPause] | ||
public sealed partial class FooComponent : IComponent | ||
{ | ||
[AutoPausedField] | ||
public TimeSpan Foo; | ||
} | ||
"""); | ||
|
||
ExpectNoDiagnostics(result); | ||
ExpectSource( | ||
result, | ||
""" | ||
// <auto-generated /> | ||
using Robust.Shared.GameObjects; | ||
public partial class FooComponent | ||
{ | ||
[RobustAutoGenerated] | ||
public sealed class FooComponent_AutoPauseSystem : EntitySystem | ||
{ | ||
public override void Initialize() | ||
{ | ||
SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused); | ||
} | ||
private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args) | ||
{ | ||
component.Foo += args.PausedTime; | ||
} | ||
} | ||
} | ||
"""); | ||
} | ||
|
||
[Test] | ||
public void TestNullable() | ||
{ | ||
var result = RunGenerator(""" | ||
[AutoGenerateComponentPause] | ||
public sealed partial class FooComponent : IComponent | ||
{ | ||
[AutoPausedField] | ||
public TimeSpan? Foo; | ||
} | ||
"""); | ||
|
||
ExpectNoDiagnostics(result); | ||
ExpectSource( | ||
result, | ||
""" | ||
// <auto-generated /> | ||
using Robust.Shared.GameObjects; | ||
public partial class FooComponent | ||
{ | ||
[RobustAutoGenerated] | ||
public sealed class FooComponent_AutoPauseSystem : EntitySystem | ||
{ | ||
public override void Initialize() | ||
{ | ||
SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused); | ||
} | ||
private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args) | ||
{ | ||
if (component.Foo.HasValue) | ||
component.Foo = component.Foo.Value + args.PausedTime; | ||
} | ||
} | ||
} | ||
"""); | ||
} | ||
|
||
[Test] | ||
public void TestAutoState() | ||
{ | ||
var result = RunGenerator(""" | ||
[AutoGenerateComponentPause] | ||
public sealed partial class FooComponent : IComponent | ||
{ | ||
[AutoPausedField, AutoNetworkedField] | ||
public TimeSpan Foo; | ||
} | ||
"""); | ||
|
||
ExpectNoDiagnostics(result); | ||
ExpectSource( | ||
result, | ||
""" | ||
// <auto-generated /> | ||
using Robust.Shared.GameObjects; | ||
public partial class FooComponent | ||
{ | ||
[RobustAutoGenerated] | ||
public sealed class FooComponent_AutoPauseSystem : EntitySystem | ||
{ | ||
public override void Initialize() | ||
{ | ||
SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused); | ||
} | ||
private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args) | ||
{ | ||
component.Foo += args.PausedTime; | ||
Dirty(uid, component); | ||
} | ||
} | ||
} | ||
"""); | ||
} | ||
|
||
[Test] | ||
public void TestExplicitDirty() | ||
{ | ||
var result = RunGenerator(""" | ||
[AutoGenerateComponentPause(Dirty = true)] | ||
public sealed partial class FooComponent : IComponent | ||
{ | ||
[AutoPausedField] | ||
public TimeSpan Foo; | ||
} | ||
"""); | ||
|
||
ExpectNoDiagnostics(result); | ||
ExpectSource( | ||
result, | ||
""" | ||
// <auto-generated /> | ||
using Robust.Shared.GameObjects; | ||
public partial class FooComponent | ||
{ | ||
[RobustAutoGenerated] | ||
public sealed class FooComponent_AutoPauseSystem : EntitySystem | ||
{ | ||
public override void Initialize() | ||
{ | ||
SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused); | ||
} | ||
private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args) | ||
{ | ||
component.Foo += args.PausedTime; | ||
Dirty(uid, component); | ||
} | ||
} | ||
} | ||
"""); | ||
} | ||
|
||
[Test] | ||
public void TestDiagnosticNotIComponent() | ||
{ | ||
var result = RunGenerator(""" | ||
[AutoGenerateComponentPause] | ||
public sealed partial class FooComponent | ||
{ | ||
[AutoPausedField] | ||
public TimeSpan Foo; | ||
} | ||
"""); | ||
|
||
ExpectNoSource(result); | ||
ExpectDiagnostics(result, [ | ||
(Diagnostics.IdComponentPauseNotComponent, new LinePositionSpan(new LinePosition(1, 28), new LinePosition(1, 40))) | ||
]); | ||
} | ||
|
||
[Test] | ||
public void TestDiagnosticNoFields() | ||
{ | ||
var result = RunGenerator(""" | ||
[AutoGenerateComponentPause] | ||
public sealed partial class FooComponent : IComponent | ||
{ | ||
public TimeSpan Foo; | ||
} | ||
"""); | ||
|
||
ExpectNoSource(result); | ||
ExpectDiagnostics(result, [ | ||
(Diagnostics.IdComponentPauseNoFields, new LinePositionSpan(new LinePosition(1, 28), new LinePosition(1, 40))) | ||
]); | ||
} | ||
|
||
[Test] | ||
public void TestDiagnosticNoParentAttribute() | ||
{ | ||
var result = RunGenerator(""" | ||
public sealed partial class FooComponent : IComponent | ||
{ | ||
[AutoPausedField] | ||
public TimeSpan Foo, Fooz; | ||
[AutoPausedField] | ||
public TimeSpan Bar { get; set; } | ||
} | ||
"""); | ||
|
||
ExpectNoSource(result); | ||
ExpectDiagnostics(result, [ | ||
(Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(3, 20), new LinePosition(3, 23))), | ||
(Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(3, 25), new LinePosition(3, 29))), | ||
(Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(6, 20), new LinePosition(6, 23))) | ||
]); | ||
} | ||
|
||
[Test] | ||
public void TestDiagnosticWrongType() | ||
{ | ||
var result = RunGenerator(""" | ||
[AutoGenerateComponentPause] | ||
public sealed partial class FooComponent : IComponent | ||
{ | ||
[AutoPausedField] | ||
public int Foo, Fooz; | ||
[AutoPausedField] | ||
public int Bar { get; set; } | ||
} | ||
"""); | ||
|
||
ExpectNoSource(result); | ||
ExpectDiagnostics(result, [ | ||
(Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(4, 15), new LinePosition(4, 18))), | ||
(Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(4, 20), new LinePosition(4, 24))), | ||
(Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(7, 15), new LinePosition(7, 18))) | ||
]); | ||
} | ||
|
||
private static void ExpectSource(GeneratorRunResult result, string expected) | ||
{ | ||
Assert.That(result.GeneratedSources, Has.Length.EqualTo(1)); | ||
|
||
var source = result.GeneratedSources[0]; | ||
|
||
Assert.That(source.SourceText.ToString(), Is.EqualTo(expected)); | ||
} | ||
|
||
private static void ExpectNoSource(GeneratorRunResult result) | ||
{ | ||
Assert.That(result.GeneratedSources, Is.Empty); | ||
} | ||
|
||
private static void ExpectNoDiagnostics(GeneratorRunResult result) | ||
{ | ||
Assert.That(result.Diagnostics, Is.Empty); | ||
} | ||
|
||
private static void ExpectDiagnostics(GeneratorRunResult result, (string code, LinePositionSpan span)[] diagnostics) | ||
{ | ||
Assert.Multiple(() => | ||
{ | ||
Assert.That(result.Diagnostics, Has.Length.EqualTo(diagnostics.Length)); | ||
foreach (var (code, span) in diagnostics) | ||
{ | ||
Assert.That( | ||
result.Diagnostics.Any(x => x.Id == code && x.Location.GetLineSpan().Span == span), | ||
$"Expected diagnostic with code {code} and location {span}"); | ||
} | ||
}); | ||
} | ||
|
||
private static GeneratorRunResult RunGenerator(string source) | ||
{ | ||
var compilation = (Compilation)CSharpCompilation.Create("compilation", | ||
new[] | ||
{ | ||
CSharpSyntaxTree.ParseText(source, path: "Source.cs"), | ||
CSharpSyntaxTree.ParseText(TypesCode, path: "Types.cs") | ||
}, | ||
new[] { MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location) }, | ||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); | ||
|
||
var generator = new ComponentPauseGenerator(); | ||
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); | ||
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out _); | ||
var result = driver.GetRunResult(); | ||
|
||
return result.Results[0]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.