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
This commit is contained in:
Pieter-Jan Briers
2024-02-20 10:15:32 +01:00
committed by GitHub
parent ef0bc1a2e4
commit ae6cebbfbb
30 changed files with 1318 additions and 61 deletions

View File

@@ -51,11 +51,14 @@
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="System.Numerics.Vectors" Version="4.5.0" />
<PackageVersion Include="System.Memory" Version="4.5.5" />
<PackageVersion Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.22621.5" />
<PackageVersion Include="TerraFX.Interop.Xlib" Version="6.4.0" />
<PackageVersion Include="VorbisPizza" Version="1.3.0" />
<PackageVersion Include="YamlDotNet" Version="13.7.1" />
<PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="PolySharp" Version="1.14.1" />
</ItemGroup>
</Project>

View File

@@ -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;

View File

@@ -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];
}
}

View File

@@ -1,4 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<SkipRobustAnalyzer>true</SkipRobustAnalyzer>
</PropertyGroup>
<Import Project="..\MSBuild\Robust.Properties.targets"/>
<Import Project="..\MSBuild\Robust.Engine.props"/>
@@ -23,5 +27,6 @@
<ItemGroup>
<ProjectReference Include="..\Robust.Analyzers\Robust.Analyzers.csproj"/>
<ProjectReference Include="..\Robust.Serialization.Generator\Robust.Serialization.Generator.csproj" Aliases="SerializationGenerator" />
</ItemGroup>
</Project>

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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
{

View File

@@ -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,

View File

@@ -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;

View File

@@ -1,17 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>10</LangVersion>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
</ItemGroup>
<ItemGroup>
<!-- Needed for NotNullableFlagAnalyzer. -->
<Compile Include="..\Robust.Shared\Analyzers\NotNullableFlagAttribute.cs" />
@@ -28,4 +16,10 @@
<Compile Include="..\Robust.Shared\Analyzers\PreferGenericVariantAttribute.cs" />
</ItemGroup>
<Import Project="../Robust.Roslyn.Shared/Robust.Roslyn.Shared.props" />
<PropertyGroup>
<Nullable>disable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -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
{

View File

@@ -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;

View File

@@ -1,19 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Compile Link="XamlX\filename" Include="../XamlX/src/XamlX/**/*.cs" />
<Compile Remove="../XamlX/src/XamlX/**/SreTypeSystem.cs" />
<Compile Remove="../XamlX/src/XamlX/obj/**" />
<Compile Include="..\Robust.Client\UserInterface\ControlPropertyAccess.cs" />
</ItemGroup>
<Import Project="../Robust.Roslyn.Shared/Robust.Roslyn.Shared.props" />
<PropertyGroup>
<!-- XamlX doesn't do NRTs. -->
<Nullable>disable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -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;
}
}

View File

@@ -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.");

View File

@@ -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
/// <summary>
/// Extensions for <see cref="EquatableArray{T}"/>.
/// </summary>
public static class EquatableArray
{
/// <summary>
/// Creates an <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.
/// </summary>
/// <typeparam name="T">The type of items in the input array.</typeparam>
/// <param name="array">The input <see cref="ImmutableArray{T}"/> instance.</param>
/// <returns>An <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.</returns>
public static EquatableArray<T> AsEquatableArray<T>(this ImmutableArray<T> array)
where T : IEquatable<T>
{
return new(array);
}
}
/// <summary>
/// An immutable, equatable array. This is equivalent to <see cref="ImmutableArray{T}"/> but with value equality support.
/// </summary>
/// <typeparam name="T">The type of values in the array.</typeparam>
public readonly struct EquatableArray<T> : IEquatable<EquatableArray<T>>, IEnumerable<T>
where T : IEquatable<T>
{
/// <summary>
/// The underlying <typeparamref name="T"/> array.
/// </summary>
private readonly T[]? array;
/// <summary>
/// Creates a new <see cref="EquatableArray{T}"/> instance.
/// </summary>
/// <param name="array">The input <see cref="ImmutableArray{T}"/> to wrap.</param>
public EquatableArray(ImmutableArray<T> array)
{
this.array = Unsafe.As<ImmutableArray<T>, T[]?>(ref array);
}
/// <summary>
/// Gets a reference to an item at a specified position within the array.
/// </summary>
/// <param name="index">The index of the item to retrieve a reference to.</param>
/// <returns>A reference to an item at a specified position within the array.</returns>
public ref readonly T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref AsImmutableArray().ItemRef(index);
}
/// <summary>
/// Gets a value indicating whether the current array is empty.
/// </summary>
public bool IsEmpty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => AsImmutableArray().IsEmpty;
}
/// <sinheritdoc/>
public bool Equals(EquatableArray<T> array)
{
return AsSpan().SequenceEqual(array.AsSpan());
}
/// <sinheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is EquatableArray<T> array && Equals(this, array);
}
/// <sinheritdoc/>
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();
}
/// <summary>
/// Gets an <see cref="ImmutableArray{T}"/> instance from the current <see cref="EquatableArray{T}"/>.
/// </summary>
/// <returns>The <see cref="ImmutableArray{T}"/> from the current <see cref="EquatableArray{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ImmutableArray<T> AsImmutableArray()
{
return Unsafe.As<T[]?, ImmutableArray<T>>(ref Unsafe.AsRef(in this.array));
}
/// <summary>
/// Creates an <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.
/// </summary>
/// <param name="array">The input <see cref="ImmutableArray{T}"/> instance.</param>
/// <returns>An <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.</returns>
public static EquatableArray<T> FromImmutableArray(ImmutableArray<T> array)
{
return new(array);
}
/// <summary>
/// Returns a <see cref="ReadOnlySpan{T}"/> wrapping the current items.
/// </summary>
/// <returns>A <see cref="ReadOnlySpan{T}"/> wrapping the current items.</returns>
public ReadOnlySpan<T> AsSpan()
{
return AsImmutableArray().AsSpan();
}
/// <summary>
/// Copies the contents of this <see cref="EquatableArray{T}"/> instance to a mutable array.
/// </summary>
/// <returns>The newly instantiated array.</returns>
public T[] ToArray()
{
return AsImmutableArray().ToArray();
}
/// <summary>
/// Gets an <see cref="ImmutableArray{T}.Enumerator"/> value to traverse items in the current array.
/// </summary>
/// <returns>An <see cref="ImmutableArray{T}.Enumerator"/> value to traverse items in the current array.</returns>
public ImmutableArray<T>.Enumerator GetEnumerator()
{
return AsImmutableArray().GetEnumerator();
}
/// <sinheritdoc/>
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return ((IEnumerable<T>)AsImmutableArray()).GetEnumerator();
}
/// <sinheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)AsImmutableArray()).GetEnumerator();
}
/// <summary>
/// Implicitly converts an <see cref="ImmutableArray{T}"/> to <see cref="EquatableArray{T}"/>.
/// </summary>
/// <returns>An <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.</returns>
public static implicit operator EquatableArray<T>(ImmutableArray<T> array)
{
return FromImmutableArray(array);
}
/// <summary>
/// Implicitly converts an <see cref="EquatableArray{T}"/> to <see cref="ImmutableArray{T}"/>.
/// </summary>
/// <returns>An <see cref="ImmutableArray{T}"/> instance from a given <see cref="EquatableArray{T}"/>.</returns>
public static implicit operator ImmutableArray<T>(EquatableArray<T> array)
{
return array.AsImmutableArray();
}
/// <summary>
/// Checks whether two <see cref="EquatableArray{T}"/> values are the same.
/// </summary>
/// <param name="left">The first <see cref="EquatableArray{T}"/> value.</param>
/// <param name="right">The second <see cref="EquatableArray{T}"/> value.</param>
/// <returns>Whether <paramref name="left"/> and <paramref name="right"/> are equal.</returns>
public static bool operator ==(EquatableArray<T> left, EquatableArray<T> right)
{
return left.Equals(right);
}
/// <summary>
/// Checks whether two <see cref="EquatableArray{T}"/> values are not the same.
/// </summary>
/// <param name="left">The first <see cref="EquatableArray{T}"/> value.</param>
/// <param name="right">The second <see cref="EquatableArray{T}"/> value.</param>
/// <returns>Whether <paramref name="left"/> and <paramref name="right"/> are not equal.</returns>
public static bool operator !=(EquatableArray<T> left, EquatableArray<T> right)
{
return !left.Equals(right);
}
}

View File

@@ -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
/// <summary>
/// A polyfill type that mirrors some methods from <see cref="HashCode"/> on .NET 6.
/// </summary>
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;
/// <summary>
/// Initializes the default seed.
/// </summary>
/// <returns>A random seed.</returns>
private static unsafe uint GenerateGlobalSeed()
{
byte[] bytes = new byte[4];
using (RandomNumberGenerator generator = RandomNumberGenerator.Create())
{
generator.GetBytes(bytes);
}
return BitConverter.ToUInt32(bytes, 0);
}
/// <summary>
/// Adds a single value to the current hash.
/// </summary>
/// <typeparam name="T">The type of the value to add into the hash code.</typeparam>
/// <param name="value">The value to add into the hash code.</param>
public void Add<T>(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);
}
}
/// <summary>
/// Gets the resulting hashcode from the current instance.
/// </summary>
/// <returns>The resulting hashcode from the current instance.</returns>
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;
}
/// <inheritdoc/>
[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();
/// <inheritdoc/>
[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();
/// <summary>
/// Rotates the specified value left by the specified number of bits.
/// Similar in behavior to the x86 instruction ROL.
/// </summary>
/// <param name="value">The value to rotate.</param>
/// <param name="offset">The number of bits to rotate by.
/// Any value outside the range [0..31] is treated as congruent mod 32.</param>
/// <returns>The rotated value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint RotateLeft(uint value, int offset)
{
return (value << offset) | (value >> (32 - offset));
}
}

View File

@@ -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
/// <summary>
/// All the information to make a partial type alternative for a type.
/// </summary>
public sealed record PartialTypeInfo(
string? Namespace,
string Name,
string DisplayName,
EquatableArray<string> TypeParameterNames,
bool IsValid,
Location SyntaxLocation,
Accessibility Accessibility,
TypeKind Kind,
bool IsRecord,
bool IsAbstract)
{
public static PartialTypeInfo FromSymbol(INamedTypeSymbol symbol, TypeDeclarationSyntax syntax)
{
var typeParameters = ImmutableArray<string>.Empty;
if (symbol.TypeParameters.Length > 0)
{
var builder = ImmutableArray.CreateBuilder<string>(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
}
}

View File

@@ -0,0 +1,38 @@
<Project>
<!--
I wanted to make a Robust.Roslyn.Shared library project,
but doing that causes various random library load failures in practice.
Instead, you'll get this vomit. Enjoy.
-->
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>enable</ImplicitUsings>
<PolySharpIncludeGeneratedTypes>System.Index;System.Diagnostics.CodeAnalysis.NotNullWhenAttribute;System.Runtime.CompilerServices.IsExternalInit;System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute</PolySharpIncludeGeneratedTypes>
<NoWarn>RS2008</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Common" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
<PackageReference Include="PolySharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Robust.Roslyn.Shared\**\*.cs">
<Link>Robust.Roslyn.Shared\%(RecursiveDir)%(Filename)%(Extension)</Link>
</Compile>
<Compile Remove="..\Robust.Roslyn.Shared\obj\**\*.cs" />
</ItemGroup>
</Project>

View File

@@ -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;
}
}

View File

@@ -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;
/// <summary>
/// Automatically generates implementations for handling timer unpausing.
/// </summary>
[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<FieldInfo>();
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<FieldInfo>.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("""
// <auto-generated />
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<string>(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<FieldInfo> 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);
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"Generators": {
"commandName": "DebugRoslynComponent",
"targetProject": "../../Content.Shared/Content.Shared.csproj"
}
}
}

View File

@@ -1,16 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>11</LangVersion>
<Nullable>enable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Common" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
</ItemGroup>
<Import Project="../Robust.Roslyn.Shared/Robust.Roslyn.Shared.props" />
</Project>

View File

@@ -32,7 +32,7 @@ namespace Robust.Shared.CompNetworkGenerator
private const string GlobalHashSetName = "global::System.Collections.Generic.HashSet<T>";
private const string GlobalListName = "global::System.Collections.Generic.List<T>";
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 =>

View File

@@ -1,13 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>9</LangVersion>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<Import Project="../Robust.Roslyn.Shared/Robust.Roslyn.Shared.props" />
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,29 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Robust.Shared.Analyzers;
/// <summary>
/// Indicate that a <see cref="Component"/> should automatically handle unpausing of timer fields.
/// </summary>
/// <remarks>
/// When this attribute is set on a <see cref="Component"/>, an <see cref="EntitySystem"/> will automatically be
/// generated that increments any fields tagged with <see cref="AutoPausedFieldAttribute"/> when the entity is unpaused
/// (<see cref="EntityUnpausedEvent"/>).
/// </remarks>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
[BaseTypeRequired(typeof(IComponent))]
public sealed class AutoGenerateComponentPauseAttribute : Attribute
{
public bool Dirty = false;
}
/// <summary>
/// Mark a field or property to automatically handle unpausing with <see cref="AutoGenerateComponentPauseAttribute"/>.
/// </summary>
/// <remarks>
/// The type of the field or prototype must be <see cref="TimeSpan"/> (potentially nullable).
/// </remarks>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class AutoPausedFieldAttribute : Attribute;