From ae6cebbfbb4cf97cf41649053679933c542b1fb6 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Tue, 20 Feb 2024 10:15:32 +0100 Subject: [PATCH] Source gen reorganizations + component unpause generator. (#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 --- Directory.Packages.props | 3 + Robust.Analyzers.Tests/Aliases.cs | 8 + .../ComponentPauseGeneratorTest.cs | 340 ++++++++++++++++++ .../Robust.Analyzers.Tests.csproj | 5 + Robust.Analyzers/AccessAnalyzer.cs | 1 + Robust.Analyzers/ByRefEventAnalyzer.cs | 7 +- Robust.Analyzers/DataDefinitionAnalyzer.cs | 9 +- Robust.Analyzers/DataDefinitionFixer.cs | 2 +- Robust.Analyzers/ExplicitInterfaceAnalyzer.cs | 1 + Robust.Analyzers/ExplicitVirtualAnalyzer.cs | 1 + .../MeansImplicitAssigmentSuppressor.cs | 1 + Robust.Analyzers/NotNullableFlagAnalyzer.cs | 7 +- .../PreferGenericVariantAnalyzer.cs | 1 + Robust.Analyzers/Robust.Analyzers.csproj | 18 +- Robust.Analyzers/SerializableAnalyzer.cs | 1 + Robust.Analyzers/TaskResultAnalyzer.cs | 1 + .../Robust.Client.NameGenerator.csproj | 18 +- Robust.Roslyn.Shared/AttributeHelper.cs | 46 +++ .../Diagnostics.cs | 6 +- .../Helpers/EquatableArray{T}.cs | 201 +++++++++++ Robust.Roslyn.Shared/Helpers/HashCode.cs | 190 ++++++++++ Robust.Roslyn.Shared/PartialTypeHelper.cs | 121 +++++++ .../Robust.Roslyn.Shared.props | 38 ++ Robust.Roslyn.Shared/TypeSymbolHelper.cs | 28 ++ .../ComponentPauseGenerator.cs | 252 +++++++++++++ .../Properties/launchSettings.json | 9 + .../Robust.Serialization.Generator.csproj | 13 +- .../ComponentNetworkGenerator.cs | 12 +- .../Robust.Shared.CompNetworkGenerator.csproj | 10 +- .../ComponentPauseGeneratorAttributes.cs | 29 ++ 30 files changed, 1318 insertions(+), 61 deletions(-) create mode 100644 Robust.Analyzers.Tests/Aliases.cs create mode 100644 Robust.Analyzers.Tests/ComponentPauseGeneratorTest.cs create mode 100644 Robust.Roslyn.Shared/AttributeHelper.cs rename {Robust.Analyzers => Robust.Roslyn.Shared}/Diagnostics.cs (82%) create mode 100644 Robust.Roslyn.Shared/Helpers/EquatableArray{T}.cs create mode 100644 Robust.Roslyn.Shared/Helpers/HashCode.cs create mode 100644 Robust.Roslyn.Shared/PartialTypeHelper.cs create mode 100644 Robust.Roslyn.Shared/Robust.Roslyn.Shared.props create mode 100644 Robust.Roslyn.Shared/TypeSymbolHelper.cs create mode 100644 Robust.Serialization.Generator/ComponentPauseGenerator.cs create mode 100644 Robust.Serialization.Generator/Properties/launchSettings.json create mode 100644 Robust.Shared/Analyzers/ComponentPauseGeneratorAttributes.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 1e6b2698c..7d6451d1c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -51,11 +51,14 @@ + + + diff --git a/Robust.Analyzers.Tests/Aliases.cs b/Robust.Analyzers.Tests/Aliases.cs new file mode 100644 index 000000000..238d418ec --- /dev/null +++ b/Robust.Analyzers.Tests/Aliases.cs @@ -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; diff --git a/Robust.Analyzers.Tests/ComponentPauseGeneratorTest.cs b/Robust.Analyzers.Tests/ComponentPauseGeneratorTest.cs new file mode 100644 index 000000000..79a5c70c7 --- /dev/null +++ b/Robust.Analyzers.Tests/ComponentPauseGeneratorTest.cs @@ -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, + """ + // + + using Robust.Shared.GameObjects; + + public partial class FooComponent + { + [RobustAutoGenerated] + public sealed class FooComponent_AutoPauseSystem : EntitySystem + { + public override void Initialize() + { + SubscribeLocalEvent(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, + """ + // + + using Robust.Shared.GameObjects; + + public partial class FooComponent + { + [RobustAutoGenerated] + public sealed class FooComponent_AutoPauseSystem : EntitySystem + { + public override void Initialize() + { + SubscribeLocalEvent(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, + """ + // + + using Robust.Shared.GameObjects; + + public partial class FooComponent + { + [RobustAutoGenerated] + public sealed class FooComponent_AutoPauseSystem : EntitySystem + { + public override void Initialize() + { + SubscribeLocalEvent(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, + """ + // + + using Robust.Shared.GameObjects; + + public partial class FooComponent + { + [RobustAutoGenerated] + public sealed class FooComponent_AutoPauseSystem : EntitySystem + { + public override void Initialize() + { + SubscribeLocalEvent(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]; + } +} diff --git a/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj b/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj index 431fe38fa..954204dcb 100644 --- a/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj +++ b/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj @@ -1,4 +1,8 @@ + + true + + @@ -23,5 +27,6 @@ + diff --git a/Robust.Analyzers/AccessAnalyzer.cs b/Robust.Analyzers/AccessAnalyzer.cs index adb5880af..3068d806d 100644 --- a/Robust.Analyzers/AccessAnalyzer.cs +++ b/Robust.Analyzers/AccessAnalyzer.cs @@ -5,6 +5,7 @@ using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; +using Robust.Roslyn.Shared; using Robust.Shared.Analyzers.Implementation; namespace Robust.Analyzers diff --git a/Robust.Analyzers/ByRefEventAnalyzer.cs b/Robust.Analyzers/ByRefEventAnalyzer.cs index 0609c84e1..62236bc12 100644 --- a/Robust.Analyzers/ByRefEventAnalyzer.cs +++ b/Robust.Analyzers/ByRefEventAnalyzer.cs @@ -4,6 +4,7 @@ using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; +using Robust.Roslyn.Shared; using static Microsoft.CodeAnalysis.SymbolEqualityComparer; namespace Robust.Analyzers; @@ -16,7 +17,7 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor ByRefEventSubscribedByValueRule = new( Diagnostics.IdByRefEventSubscribedByValue, "By-ref event subscribed to by value", - "Tried to subscribe to a by-ref event '{0}' by value.", + "Tried to subscribe to a by-ref event '{0}' by value", "Usage", DiagnosticSeverity.Error, true, @@ -26,7 +27,7 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor ByRefEventRaisedByValueRule = new( Diagnostics.IdByRefEventRaisedByValue, "By-ref event raised by value", - "Tried to raise a by-ref event '{0}' by value.", + "Tried to raise a by-ref event '{0}' by value", "Usage", DiagnosticSeverity.Error, true, @@ -36,7 +37,7 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor ByValueEventRaisedByRefRule = new( Diagnostics.IdValueEventRaisedByRef, "Value event raised by-ref", - "Tried to raise a value event '{0}' by-ref.", + "Tried to raise a value event '{0}' by-ref", "Usage", DiagnosticSeverity.Error, true, diff --git a/Robust.Analyzers/DataDefinitionAnalyzer.cs b/Robust.Analyzers/DataDefinitionAnalyzer.cs index 5953ee2c7..14910c10a 100644 --- a/Robust.Analyzers/DataDefinitionAnalyzer.cs +++ b/Robust.Analyzers/DataDefinitionAnalyzer.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; +using Robust.Roslyn.Shared; namespace Robust.Analyzers; @@ -18,7 +19,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor DataDefinitionPartialRule = new( Diagnostics.IdDataDefinitionPartial, "Type must be partial", - "Type {0} is a DataDefinition but is not partial.", + "Type {0} is a DataDefinition but is not partial", "Usage", DiagnosticSeverity.Error, true, @@ -28,7 +29,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new( Diagnostics.IdNestedDataDefinitionPartial, "Type must be partial", - "Type {0} contains nested data definition {1} but is not partial.", + "Type {0} contains nested data definition {1} but is not partial", "Usage", DiagnosticSeverity.Error, true, @@ -38,7 +39,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor DataFieldWritableRule = new( Diagnostics.IdDataFieldWritable, "Data field must not be readonly", - "Data field {0} in data definition {1} is readonly.", + "Data field {0} in data definition {1} is readonly", "Usage", DiagnosticSeverity.Error, true, @@ -48,7 +49,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new( Diagnostics.IdDataFieldPropertyWritable, "Data field property must have a setter", - "Data field property {0} in data definition {1} does not have a setter.", + "Data field property {0} in data definition {1} does not have a setter", "Usage", DiagnosticSeverity.Error, true, diff --git a/Robust.Analyzers/DataDefinitionFixer.cs b/Robust.Analyzers/DataDefinitionFixer.cs index f30ca830d..80ba22a96 100644 --- a/Robust.Analyzers/DataDefinitionFixer.cs +++ b/Robust.Analyzers/DataDefinitionFixer.cs @@ -9,7 +9,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxKind; -using static Robust.Analyzers.Diagnostics; +using static Robust.Roslyn.Shared.Diagnostics; namespace Robust.Analyzers; diff --git a/Robust.Analyzers/ExplicitInterfaceAnalyzer.cs b/Robust.Analyzers/ExplicitInterfaceAnalyzer.cs index 020d5bd56..a689e0711 100644 --- a/Robust.Analyzers/ExplicitInterfaceAnalyzer.cs +++ b/Robust.Analyzers/ExplicitInterfaceAnalyzer.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; +using Robust.Roslyn.Shared; using Document = Microsoft.CodeAnalysis.Document; namespace Robust.Analyzers diff --git a/Robust.Analyzers/ExplicitVirtualAnalyzer.cs b/Robust.Analyzers/ExplicitVirtualAnalyzer.cs index 74a34e00c..1b4a2a986 100644 --- a/Robust.Analyzers/ExplicitVirtualAnalyzer.cs +++ b/Robust.Analyzers/ExplicitVirtualAnalyzer.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; +using Robust.Roslyn.Shared; namespace Robust.Analyzers; diff --git a/Robust.Analyzers/MeansImplicitAssigmentSuppressor.cs b/Robust.Analyzers/MeansImplicitAssigmentSuppressor.cs index c4a2039f8..cadee94d4 100644 --- a/Robust.Analyzers/MeansImplicitAssigmentSuppressor.cs +++ b/Robust.Analyzers/MeansImplicitAssigmentSuppressor.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using Robust.Roslyn.Shared; namespace Robust.Analyzers { diff --git a/Robust.Analyzers/NotNullableFlagAnalyzer.cs b/Robust.Analyzers/NotNullableFlagAnalyzer.cs index 648767140..f50e8d15c 100644 --- a/Robust.Analyzers/NotNullableFlagAnalyzer.cs +++ b/Robust.Analyzers/NotNullableFlagAnalyzer.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; +using Robust.Roslyn.Shared; namespace Robust.Analyzers; @@ -31,7 +32,7 @@ public sealed class NotNullableFlagAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor InvalidNotNullableImplementationRule = new ( Diagnostics.IdInvalidNotNullableFlagImplementation, - "Invalid NotNullable flag implementation.", + "Invalid NotNullable flag implementation", "NotNullable flag is either not typed as bool, or does not have a default value equaling false", "Usage", DiagnosticSeverity.Error, @@ -41,7 +42,7 @@ public sealed class NotNullableFlagAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor InvalidNotNullableTypeRule = new ( Diagnostics.IdInvalidNotNullableFlagType, "Failed to resolve type parameter", - "Failed to resolve type parameter \"{0}\".", + "Failed to resolve type parameter \"{0}\"", "Usage", DiagnosticSeverity.Error, true, @@ -49,7 +50,7 @@ public sealed class NotNullableFlagAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor NotNullableFlagValueTypeRule = new ( Diagnostics.IdNotNullableFlagValueType, - "NotNullable flag not supported for value types.", + "NotNullable flag not supported for value types", "Value types as generic arguments are not supported for NotNullable flags", "Usage", DiagnosticSeverity.Error, diff --git a/Robust.Analyzers/PreferGenericVariantAnalyzer.cs b/Robust.Analyzers/PreferGenericVariantAnalyzer.cs index 9edf020e4..57e0c5ef1 100644 --- a/Robust.Analyzers/PreferGenericVariantAnalyzer.cs +++ b/Robust.Analyzers/PreferGenericVariantAnalyzer.cs @@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; +using Robust.Roslyn.Shared; namespace Robust.Analyzers; diff --git a/Robust.Analyzers/Robust.Analyzers.csproj b/Robust.Analyzers/Robust.Analyzers.csproj index bcba84910..c115cd001 100644 --- a/Robust.Analyzers/Robust.Analyzers.csproj +++ b/Robust.Analyzers/Robust.Analyzers.csproj @@ -1,17 +1,5 @@ - - netstandard2.0 - 10 - true - - - - - - - - @@ -28,4 +16,10 @@ + + + + disable + + diff --git a/Robust.Analyzers/SerializableAnalyzer.cs b/Robust.Analyzers/SerializableAnalyzer.cs index 96ae34404..c9423879e 100644 --- a/Robust.Analyzers/SerializableAnalyzer.cs +++ b/Robust.Analyzers/SerializableAnalyzer.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; +using Robust.Roslyn.Shared; namespace Robust.Analyzers { diff --git a/Robust.Analyzers/TaskResultAnalyzer.cs b/Robust.Analyzers/TaskResultAnalyzer.cs index 9eeaddf9e..96f16375b 100644 --- a/Robust.Analyzers/TaskResultAnalyzer.cs +++ b/Robust.Analyzers/TaskResultAnalyzer.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; +using Robust.Roslyn.Shared; namespace Robust.Analyzers; diff --git a/Robust.Client.NameGenerator/Robust.Client.NameGenerator.csproj b/Robust.Client.NameGenerator/Robust.Client.NameGenerator.csproj index 51ff69c86..f7fa741d8 100644 --- a/Robust.Client.NameGenerator/Robust.Client.NameGenerator.csproj +++ b/Robust.Client.NameGenerator/Robust.Client.NameGenerator.csproj @@ -1,19 +1,17 @@ - - netstandard2.0 - true - - - - - - - + + + + + + disable + + diff --git a/Robust.Roslyn.Shared/AttributeHelper.cs b/Robust.Roslyn.Shared/AttributeHelper.cs new file mode 100644 index 000000000..7a8174b1f --- /dev/null +++ b/Robust.Roslyn.Shared/AttributeHelper.cs @@ -0,0 +1,46 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Robust.Roslyn.Shared; + +#nullable enable + +public static class AttributeHelper +{ + public static bool HasAttribute(ISymbol symbol, string attributeMetadataName, [NotNullWhen(true)] out AttributeData? matchedAttribute) + { + foreach (var attribute in symbol.GetAttributes()) + { + if (attribute.AttributeClass == null) + continue; + + if (TypeSymbolHelper.ShittyTypeMatch(attribute.AttributeClass, attributeMetadataName)) + { + matchedAttribute = attribute; + return true; + } + } + + matchedAttribute = null; + return false; + } + + public static bool GetNamedArgumentBool(AttributeData data, string name, bool defaultValue) + { + foreach (var kv in data.NamedArguments) + { + if (kv.Key != name) + continue; + + if (kv.Value.Kind != TypedConstantKind.Primitive) + continue; + + if (kv.Value.Value is not bool value) + continue; + + return value; + } + + return defaultValue; + } +} diff --git a/Robust.Analyzers/Diagnostics.cs b/Robust.Roslyn.Shared/Diagnostics.cs similarity index 82% rename from Robust.Analyzers/Diagnostics.cs rename to Robust.Roslyn.Shared/Diagnostics.cs index 38873dbc1..b829a2f4c 100644 --- a/Robust.Analyzers/Diagnostics.cs +++ b/Robust.Roslyn.Shared/Diagnostics.cs @@ -1,6 +1,6 @@ using Microsoft.CodeAnalysis; -namespace Robust.Analyzers; +namespace Robust.Roslyn.Shared; public static class Diagnostics { @@ -24,6 +24,10 @@ public static class Diagnostics public const string IdNestedDataDefinitionPartial = "RA0018"; public const string IdDataFieldWritable = "RA0019"; public const string IdDataFieldPropertyWritable = "RA0020"; + public const string IdComponentPauseNotComponent = "RA0021"; + public const string IdComponentPauseNoFields = "RA0022"; + public const string IdComponentPauseNoParentAttribute = "RA0023"; + public const string IdComponentPauseWrongTypeAttribute = "RA0024"; public static SuppressionDescriptor MeansImplicitAssignment => new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned."); diff --git a/Robust.Roslyn.Shared/Helpers/EquatableArray{T}.cs b/Robust.Roslyn.Shared/Helpers/EquatableArray{T}.cs new file mode 100644 index 000000000..f141f2744 --- /dev/null +++ b/Robust.Roslyn.Shared/Helpers/EquatableArray{T}.cs @@ -0,0 +1,201 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// Taken from https://github.com/CommunityToolkit/dotnet/blob/ecd1711b740f4f88d2bb943ce292ae4fc90df1bc/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/EquatableArray%7BT%7D.cs + +using System.Collections; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Robust.Roslyn.Shared.Helpers; + +#nullable enable + +/// +/// Extensions for . +/// +public static class EquatableArray +{ + /// + /// Creates an instance from a given . + /// + /// The type of items in the input array. + /// The input instance. + /// An instance from a given . + public static EquatableArray AsEquatableArray(this ImmutableArray array) + where T : IEquatable + { + return new(array); + } +} + +/// +/// An immutable, equatable array. This is equivalent to but with value equality support. +/// +/// The type of values in the array. +public readonly struct EquatableArray : IEquatable>, IEnumerable + where T : IEquatable +{ + /// + /// The underlying array. + /// + private readonly T[]? array; + + /// + /// Creates a new instance. + /// + /// The input to wrap. + public EquatableArray(ImmutableArray array) + { + this.array = Unsafe.As, T[]?>(ref array); + } + + /// + /// Gets a reference to an item at a specified position within the array. + /// + /// The index of the item to retrieve a reference to. + /// A reference to an item at a specified position within the array. + public ref readonly T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref AsImmutableArray().ItemRef(index); + } + + /// + /// Gets a value indicating whether the current array is empty. + /// + public bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsImmutableArray().IsEmpty; + } + + /// + public bool Equals(EquatableArray array) + { + return AsSpan().SequenceEqual(array.AsSpan()); + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is EquatableArray array && Equals(this, array); + } + + /// + public override int GetHashCode() + { + if (this.array is not T[] array) + { + return 0; + } + + HashCode hashCode = default; + + foreach (T item in array) + { + hashCode.Add(item); + } + + return hashCode.ToHashCode(); + } + + /// + /// Gets an instance from the current . + /// + /// The from the current . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ImmutableArray AsImmutableArray() + { + return Unsafe.As>(ref Unsafe.AsRef(in this.array)); + } + + /// + /// Creates an instance from a given . + /// + /// The input instance. + /// An instance from a given . + public static EquatableArray FromImmutableArray(ImmutableArray array) + { + return new(array); + } + + /// + /// Returns a wrapping the current items. + /// + /// A wrapping the current items. + public ReadOnlySpan AsSpan() + { + return AsImmutableArray().AsSpan(); + } + + /// + /// Copies the contents of this instance to a mutable array. + /// + /// The newly instantiated array. + public T[] ToArray() + { + return AsImmutableArray().ToArray(); + } + + /// + /// Gets an value to traverse items in the current array. + /// + /// An value to traverse items in the current array. + public ImmutableArray.Enumerator GetEnumerator() + { + return AsImmutableArray().GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)AsImmutableArray()).GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)AsImmutableArray()).GetEnumerator(); + } + + /// + /// Implicitly converts an to . + /// + /// An instance from a given . + public static implicit operator EquatableArray(ImmutableArray array) + { + return FromImmutableArray(array); + } + + /// + /// Implicitly converts an to . + /// + /// An instance from a given . + public static implicit operator ImmutableArray(EquatableArray array) + { + return array.AsImmutableArray(); + } + + /// + /// Checks whether two values are the same. + /// + /// The first value. + /// The second value. + /// Whether and are equal. + public static bool operator ==(EquatableArray left, EquatableArray right) + { + return left.Equals(right); + } + + /// + /// Checks whether two values are not the same. + /// + /// The first value. + /// The second value. + /// Whether and are not equal. + public static bool operator !=(EquatableArray left, EquatableArray right) + { + return !left.Equals(right); + } +} diff --git a/Robust.Roslyn.Shared/Helpers/HashCode.cs b/Robust.Roslyn.Shared/Helpers/HashCode.cs new file mode 100644 index 000000000..38445d2d8 --- /dev/null +++ b/Robust.Roslyn.Shared/Helpers/HashCode.cs @@ -0,0 +1,190 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// Taken from https://raw.githubusercontent.com/CommunityToolkit/dotnet/ecd1711b740f4f88d2bb943ce292ae4fc90df1bc/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/HashCode.cs + +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +#pragma warning disable CS0809 + +namespace System; + +#nullable enable + +/// +/// A polyfill type that mirrors some methods from on .NET 6. +/// +public struct HashCode +{ + private const uint Prime1 = 2654435761U; + private const uint Prime2 = 2246822519U; + private const uint Prime3 = 3266489917U; + private const uint Prime4 = 668265263U; + private const uint Prime5 = 374761393U; + + private static readonly uint seed = GenerateGlobalSeed(); + + private uint v1, v2, v3, v4; + private uint queue1, queue2, queue3; + private uint length; + + /// + /// Initializes the default seed. + /// + /// A random seed. + private static unsafe uint GenerateGlobalSeed() + { + byte[] bytes = new byte[4]; + + using (RandomNumberGenerator generator = RandomNumberGenerator.Create()) + { + generator.GetBytes(bytes); + } + + return BitConverter.ToUInt32(bytes, 0); + } + + /// + /// Adds a single value to the current hash. + /// + /// The type of the value to add into the hash code. + /// The value to add into the hash code. + public void Add(T value) + { + Add(value?.GetHashCode() ?? 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) + { + v1 = seed + Prime1 + Prime2; + v2 = seed + Prime2; + v3 = seed; + v4 = seed - Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Round(uint hash, uint input) + { + return RotateLeft(hash + input * Prime2, 13) * Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint QueueRound(uint hash, uint queuedValue) + { + return RotateLeft(hash + queuedValue * Prime3, 17) * Prime4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixState(uint v1, uint v2, uint v3, uint v4) + { + return RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixEmptyState() + { + return seed + Prime5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixFinal(uint hash) + { + hash ^= hash >> 15; + hash *= Prime2; + hash ^= hash >> 13; + hash *= Prime3; + hash ^= hash >> 16; + + return hash; + } + + private void Add(int value) + { + uint val = (uint)value; + uint previousLength = this.length++; + uint position = previousLength % 4; + + if (position == 0) + { + this.queue1 = val; + } + else if (position == 1) + { + this.queue2 = val; + } + else if (position == 2) + { + this.queue3 = val; + } + else + { + if (previousLength == 3) + { + Initialize(out this.v1, out this.v2, out this.v3, out this.v4); + } + + this.v1 = Round(this.v1, this.queue1); + this.v2 = Round(this.v2, this.queue2); + this.v3 = Round(this.v3, this.queue3); + this.v4 = Round(this.v4, val); + } + } + + /// + /// Gets the resulting hashcode from the current instance. + /// + /// The resulting hashcode from the current instance. + public int ToHashCode() + { + uint length = this.length; + uint position = length % 4; + uint hash = length < 4 ? MixEmptyState() : MixState(this.v1, this.v2, this.v3, this.v4); + + hash += length * 4; + + if (position > 0) + { + hash = QueueRound(hash, this.queue1); + + if (position > 1) + { + hash = QueueRound(hash, this.queue2); + + if (position > 2) + { + hash = QueueRound(hash, this.queue3); + } + } + } + + hash = MixFinal(hash); + + return (int)hash; + } + + /// + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => throw new NotSupportedException(); + + /// + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) => throw new NotSupportedException(); + + /// + /// Rotates the specified value left by the specified number of bits. + /// Similar in behavior to the x86 instruction ROL. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..31] is treated as congruent mod 32. + /// The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RotateLeft(uint value, int offset) + { + return (value << offset) | (value >> (32 - offset)); + } +} diff --git a/Robust.Roslyn.Shared/PartialTypeHelper.cs b/Robust.Roslyn.Shared/PartialTypeHelper.cs new file mode 100644 index 000000000..2044b0985 --- /dev/null +++ b/Robust.Roslyn.Shared/PartialTypeHelper.cs @@ -0,0 +1,121 @@ +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Robust.Roslyn.Shared.Helpers; + +namespace Robust.Roslyn.Shared; + +#nullable enable + +/// +/// All the information to make a partial type alternative for a type. +/// +public sealed record PartialTypeInfo( + string? Namespace, + string Name, + string DisplayName, + EquatableArray TypeParameterNames, + bool IsValid, + Location SyntaxLocation, + Accessibility Accessibility, + TypeKind Kind, + bool IsRecord, + bool IsAbstract) +{ + public static PartialTypeInfo FromSymbol(INamedTypeSymbol symbol, TypeDeclarationSyntax syntax) + { + var typeParameters = ImmutableArray.Empty; + if (symbol.TypeParameters.Length > 0) + { + var builder = ImmutableArray.CreateBuilder(symbol.TypeParameters.Length); + foreach (var typeParameter in symbol.TypeParameters) + { + builder.Add(typeParameter.Name); + } + + typeParameters = builder.MoveToImmutable(); + } + + return new PartialTypeInfo( + symbol.ContainingNamespace.IsGlobalNamespace ? null : symbol.ContainingNamespace.ToDisplayString(), + symbol.Name, + symbol.ToDisplayString(), + typeParameters, + syntax.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword)), + syntax.Keyword.GetLocation(), + symbol.DeclaredAccessibility, + symbol.TypeKind, + symbol.IsRecord, + symbol.IsAbstract); + } + + public bool CheckPartialDiagnostic(SourceProductionContext context, DiagnosticDescriptor diagnostic) + { + if (!IsValid) + { + context.ReportDiagnostic(Diagnostic.Create(diagnostic, SyntaxLocation, DisplayName)); + return true; + } + + return false; + } + + public string GetGeneratedFileName() + { + var name = Namespace == null ? Name : $"{Namespace}.{Name}"; + if (TypeParameterNames.AsImmutableArray().Length > 0) + name += $"`{TypeParameterNames.AsImmutableArray().Length}"; + + name += ".g.cs"; + + return name; + } + + public void WriteHeader(StringBuilder builder) + { + if (Namespace != null) + builder.AppendLine($"namespace {Namespace};\n"); + + // TODO: Nested classes + + var access = Accessibility switch + { + Accessibility.Private => "private", + Accessibility.ProtectedAndInternal => "private protected", + Accessibility.ProtectedOrInternal => "protected internal", + Accessibility.Protected => "protected", + Accessibility.Internal => "internal", + _ => "public" + }; + + string keyword; + if (Kind == TypeKind.Interface) + { + keyword = "interface"; + } + else + { + if (IsRecord) + { + keyword = Kind == TypeKind.Struct ? "record struct" : "record"; + } + else + { + keyword = Kind == TypeKind.Struct ? "struct" : "class"; + } + } + + builder.Append($"{access} {(IsAbstract ? "abstract " : "")}partial {keyword} {Name}"); + if (TypeParameterNames.AsSpan().Length > 0) + { + builder.Append($"<{string.Join(", ", TypeParameterNames.AsImmutableArray())}>"); + } + } + + public void WriteFooter(StringBuilder builder) + { + // TODO: Nested classes + } +} diff --git a/Robust.Roslyn.Shared/Robust.Roslyn.Shared.props b/Robust.Roslyn.Shared/Robust.Roslyn.Shared.props new file mode 100644 index 000000000..aefce3e02 --- /dev/null +++ b/Robust.Roslyn.Shared/Robust.Roslyn.Shared.props @@ -0,0 +1,38 @@ + + + + netstandard2.0 + 12 + enable + true + true + true + enable + System.Index;System.Diagnostics.CodeAnalysis.NotNullWhenAttribute;System.Runtime.CompilerServices.IsExternalInit;System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute + RS2008 + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + Robust.Roslyn.Shared\%(RecursiveDir)%(Filename)%(Extension) + + + + diff --git a/Robust.Roslyn.Shared/TypeSymbolHelper.cs b/Robust.Roslyn.Shared/TypeSymbolHelper.cs new file mode 100644 index 000000000..cb969139c --- /dev/null +++ b/Robust.Roslyn.Shared/TypeSymbolHelper.cs @@ -0,0 +1,28 @@ +using Microsoft.CodeAnalysis; + +namespace Robust.Roslyn.Shared; + +#nullable enable + +public static class TypeSymbolHelper +{ + public static bool ShittyTypeMatch(INamedTypeSymbol type, string attributeMetadataName) + { + // Doing it like this only allocates when the type actually matches, which is good enough for me right now. + if (!attributeMetadataName.EndsWith(type.Name)) + return false; + + return type.ToDisplayString() == attributeMetadataName; + } + + public static bool ImplementsInterface(INamedTypeSymbol type, string interfaceTypeName) + { + foreach (var interfaceType in type.AllInterfaces) + { + if (ShittyTypeMatch(interfaceType, interfaceTypeName)) + return true; + } + + return false; + } +} diff --git a/Robust.Serialization.Generator/ComponentPauseGenerator.cs b/Robust.Serialization.Generator/ComponentPauseGenerator.cs new file mode 100644 index 000000000..a1ee87c1f --- /dev/null +++ b/Robust.Serialization.Generator/ComponentPauseGenerator.cs @@ -0,0 +1,252 @@ +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Robust.Roslyn.Shared; +using Robust.Roslyn.Shared.Helpers; + +namespace Robust.Serialization.Generator; + +/// +/// Automatically generates implementations for handling timer unpausing. +/// +[Generator(LanguageNames.CSharp)] +public sealed class ComponentPauseGenerator : IIncrementalGenerator +{ + private const string AutoGenerateComponentPauseAttributeName = "Robust.Shared.Analyzers.AutoGenerateComponentPauseAttribute"; + private const string AutoPausedFieldAttributeName = "Robust.Shared.Analyzers.AutoPausedFieldAttribute"; + private const string AutoNetworkFieldAttributeName = "Robust.Shared.Analyzers.AutoNetworkedFieldAttribute"; + // ReSharper disable once InconsistentNaming + private const string IComponentTypeName = "Robust.Shared.GameObjects.IComponent"; + + private static readonly DiagnosticDescriptor NotComponentDiagnostic = new( + Diagnostics.IdComponentPauseNotComponent, + "Class must be an IComponent to use AutoGenerateComponentPause", + "Class '{0}' must implement IComponent to be used with [AutoGenerateComponentPause]", + "Usage", + DiagnosticSeverity.Error, + true); + + private static readonly DiagnosticDescriptor NoFieldsDiagnostic = new( + Diagnostics.IdComponentPauseNoFields, + "AutoGenerateComponentPause has no fields", + "Class '{0}' has [AutoGenerateComponentPause] but has no fields or properties with [AutoPausedField]", + "Usage", + DiagnosticSeverity.Warning, + true); + + private static readonly DiagnosticDescriptor NoParentAttributeDiagnostic = new( + Diagnostics.IdComponentPauseNoParentAttribute, + "AutoPausedField on type of field without AutoGenerateComponentPause", + "Field '{0}' has [AutoPausedField] but its containing type does not have [AutoGenerateComponentPause]", + "Usage", + DiagnosticSeverity.Error, + true); + + private static readonly DiagnosticDescriptor WrongTypeAttributeDiagnostic = new( + Diagnostics.IdComponentPauseWrongTypeAttribute, + "AutoPausedField has wrong type", + "Field '{0}' has [AutoPausedField] but is not of type TimeSpan", + "Usage", + DiagnosticSeverity.Error, + true); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var componentInfos = context.SyntaxProvider.ForAttributeWithMetadataName( + AutoGenerateComponentPauseAttributeName, + (syntaxNode, _) => syntaxNode is TypeDeclarationSyntax, + (syntaxContext, _) => + { + var symbol = (INamedTypeSymbol)syntaxContext.TargetSymbol; + + var typeDeclarationSyntax = (TypeDeclarationSyntax) syntaxContext.TargetNode; + var partialTypeInfo = PartialTypeInfo.FromSymbol( + symbol, + typeDeclarationSyntax); + + var dirty = AttributeHelper.GetNamedArgumentBool(syntaxContext.Attributes[0], "Dirty", false); + + var fieldBuilder = ImmutableArray.CreateBuilder(); + foreach (var member in symbol.GetMembers()) + { + if (!AttributeHelper.HasAttribute(member, AutoPausedFieldAttributeName, out var _)) + continue; + + var type = member switch + { + IPropertySymbol property => property.Type, + IFieldSymbol field => field.Type, + _ => null + }; + + if (type is not INamedTypeSymbol namedType) + continue; + + var invalid = false; + var nullable = false; + if (namedType.Name != "TimeSpan") + { + if (namedType is { Name: "Nullable", TypeArguments: [{Name: "TimeSpan"}] }) + { + nullable = true; + } + else + { + invalid = true; + } + } + + // If any pause field has [AutoNetworkedField], automatically mark it to dirty on unpause. + if (AttributeHelper.HasAttribute(member, AutoNetworkFieldAttributeName, out var _)) + dirty = true; + + fieldBuilder.Add(new FieldInfo(member.Name, nullable, invalid, member.Locations[0])); + } + + return new ComponentInfo( + partialTypeInfo, + EquatableArray.FromImmutableArray(fieldBuilder.ToImmutable()), + dirty, + !TypeSymbolHelper.ImplementsInterface(symbol, IComponentTypeName), + typeDeclarationSyntax.Identifier.GetLocation()); + }); + + context.RegisterImplementationSourceOutput(componentInfos, static (productionContext, info) => + { + if (info.NotComponent) + { + productionContext.ReportDiagnostic(Diagnostic.Create( + NotComponentDiagnostic, + info.Location, + info.PartialTypeInfo.Name)); + return; + } + + // Component always have to be partial anyways due to the serialization generator. + // So I can't be arsed to define a diagnostic for this. + if (!info.PartialTypeInfo.IsValid) + return; + + if (info.Fields.AsImmutableArray().Length == 0) + { + productionContext.ReportDiagnostic(Diagnostic.Create( + NoFieldsDiagnostic, + info.Location, + info.PartialTypeInfo.Name)); + return; + } + + var builder = new StringBuilder(); + + builder.AppendLine(""" + // + + using Robust.Shared.GameObjects; + + """); + + info.PartialTypeInfo.WriteHeader(builder); + + builder.AppendLine(); + builder.AppendLine("{"); + + builder.AppendLine($$""" + [RobustAutoGenerated] + public sealed class {{info.PartialTypeInfo.Name}}_AutoPauseSystem : EntitySystem + { + public override void Initialize() + { + SubscribeLocalEvent<{{info.PartialTypeInfo.Name}}, EntityUnpausedEvent>(OnEntityUnpaused); + } + + private void OnEntityUnpaused(EntityUid uid, {{info.PartialTypeInfo.Name}} component, ref EntityUnpausedEvent args) + { + """); + + var anyValidField = false; + foreach (var field in info.Fields) + { + if (field.Invalid) + { + productionContext.ReportDiagnostic(Diagnostic.Create(WrongTypeAttributeDiagnostic, field.Location)); + continue; + } + + if (field.Nullable) + { + builder.AppendLine($""" + if (component.{field.Name}.HasValue) + component.{field.Name} = component.{field.Name}.Value + args.PausedTime; + """); + } + else + { + builder.AppendLine($" component.{field.Name} += args.PausedTime;"); + } + + anyValidField = true; + } + + if (!anyValidField) + return; + + if (info.Dirty) + builder.AppendLine(" Dirty(uid, component);"); + + builder.AppendLine(""" + } + } + """); + + builder.AppendLine("}"); + + info.PartialTypeInfo.WriteFooter(builder); + + productionContext.AddSource(info.PartialTypeInfo.GetGeneratedFileName(), builder.ToString()); + }); + + // Code to report diagnostic for fields that have it but don't have the attribute on the parent. + var allFields = context.SyntaxProvider.ForAttributeWithMetadataName( + AutoPausedFieldAttributeName, + (syntaxNode, _) => syntaxNode is VariableDeclaratorSyntax or PropertyDeclarationSyntax, + (syntaxContext, _) => + { + var errorTarget = syntaxContext.TargetNode is PropertyDeclarationSyntax prop + ? prop.Identifier.GetLocation() + : syntaxContext.TargetNode.GetLocation(); + return new AllFieldInfo( + syntaxContext.TargetSymbol.Name, + syntaxContext.TargetSymbol.ContainingType.ToDisplayString(), + errorTarget); + }); + + var allComponentsTogether = componentInfos.Collect(); + var allFieldsTogether = allFields.Collect(); + var componentFieldJoin = allFieldsTogether.Combine(allComponentsTogether); + + context.RegisterImplementationSourceOutput(componentFieldJoin, (productionContext, info) => + { + var componentsByName = new HashSet(info.Right.Select(x => x.PartialTypeInfo.DisplayName)); + foreach (var field in info.Left) + { + if (!componentsByName.Contains(field.ParentDisplayName)) + { + productionContext.ReportDiagnostic( + Diagnostic.Create(NoParentAttributeDiagnostic, field.Location, field.Name)); + } + } + }); + } + + public sealed record ComponentInfo( + PartialTypeInfo PartialTypeInfo, + EquatableArray Fields, + bool Dirty, + bool NotComponent, + Location Location); + + public sealed record FieldInfo(string Name, bool Nullable, bool Invalid, Location Location); + + public sealed record AllFieldInfo(string Name, string ParentDisplayName, Location Location); +} diff --git a/Robust.Serialization.Generator/Properties/launchSettings.json b/Robust.Serialization.Generator/Properties/launchSettings.json new file mode 100644 index 000000000..9e8e5b146 --- /dev/null +++ b/Robust.Serialization.Generator/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Generators": { + "commandName": "DebugRoslynComponent", + "targetProject": "../../Content.Shared/Content.Shared.csproj" + } + } +} diff --git a/Robust.Serialization.Generator/Robust.Serialization.Generator.csproj b/Robust.Serialization.Generator/Robust.Serialization.Generator.csproj index 8b7a626e1..0ecebf949 100644 --- a/Robust.Serialization.Generator/Robust.Serialization.Generator.csproj +++ b/Robust.Serialization.Generator/Robust.Serialization.Generator.csproj @@ -1,16 +1,5 @@  - - netstandard2.0 - 11 - enable - true - - - - - - - + diff --git a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs index cd1fe3952..b94c9e7d7 100644 --- a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs +++ b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs @@ -32,7 +32,7 @@ namespace Robust.Shared.CompNetworkGenerator private const string GlobalHashSetName = "global::System.Collections.Generic.HashSet"; private const string GlobalListName = "global::System.Collections.Generic.List"; - private static string GenerateSource(in GeneratorExecutionContext context, INamedTypeSymbol classSymbol, CSharpCompilation comp, bool raiseAfterAutoHandle) + private static string? GenerateSource(in GeneratorExecutionContext context, INamedTypeSymbol classSymbol, CSharpCompilation comp, bool raiseAfterAutoHandle) { var nameSpace = classSymbol.ContainingNamespace.ToDisplayString(); var componentName = classSymbol.Name; @@ -280,10 +280,10 @@ public partial class {componentName} { var attr = type.Attribute; var raiseEv = false; - if (attr.ConstructorArguments.Length == 1 && attr.ConstructorArguments[0].Value != null) + if (attr.ConstructorArguments is [{Value: bool raise}]) { // Get the afterautohandle bool, which is first constructor arg - raiseEv = (bool) attr.ConstructorArguments[0].Value; + raiseEv = raise; } var source = GenerateSource(context, type.Type, comp, raiseEv); @@ -325,11 +325,11 @@ public partial class {componentName} attr.AttributeClass != null && attr.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)); + if (typeSymbol == null) + continue; + if (relevantAttribute == null) { - if (typeSymbol == null) - continue; - foreach (var mem in typeSymbol.GetMembers()) { var attribute = mem.GetAttributes().FirstOrDefault(a => diff --git a/Robust.Shared.CompNetworkGenerator/Robust.Shared.CompNetworkGenerator.csproj b/Robust.Shared.CompNetworkGenerator/Robust.Shared.CompNetworkGenerator.csproj index c12253963..076e7a492 100644 --- a/Robust.Shared.CompNetworkGenerator/Robust.Shared.CompNetworkGenerator.csproj +++ b/Robust.Shared.CompNetworkGenerator/Robust.Shared.CompNetworkGenerator.csproj @@ -1,13 +1,5 @@ - - netstandard2.0 - 9 - true - + - - - - diff --git a/Robust.Shared/Analyzers/ComponentPauseGeneratorAttributes.cs b/Robust.Shared/Analyzers/ComponentPauseGeneratorAttributes.cs new file mode 100644 index 000000000..55ddfc5f2 --- /dev/null +++ b/Robust.Shared/Analyzers/ComponentPauseGeneratorAttributes.cs @@ -0,0 +1,29 @@ +using System; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; + +namespace Robust.Shared.Analyzers; + +/// +/// Indicate that a should automatically handle unpausing of timer fields. +/// +/// +/// When this attribute is set on a , an will automatically be +/// generated that increments any fields tagged with when the entity is unpaused +/// (). +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +[BaseTypeRequired(typeof(IComponent))] +public sealed class AutoGenerateComponentPauseAttribute : Attribute +{ + public bool Dirty = false; +} + +/// +/// Mark a field or property to automatically handle unpausing with . +/// +/// +/// The type of the field or prototype must be (potentially nullable). +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public sealed class AutoPausedFieldAttribute : Attribute;