From 8a1e584e808c2e351636f574a629de6b0b4559a7 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Fri, 31 May 2024 16:45:29 -0400 Subject: [PATCH] Add PreferNonGenericVariantFor attribute and analyzer --- .../PreferNonGenericVariantForTest.cs | 71 +++++++++++++++++++ .../Robust.Analyzers.Tests.csproj | 1 + .../PreferNonGenericVariantForAnalyzer.cs | 65 +++++++++++++++++ Robust.Analyzers/Robust.Analyzers.csproj | 5 ++ Robust.Roslyn.Shared/Diagnostics.cs | 1 + .../PreferNonGenericVariantForAttribute.cs | 18 +++++ .../GameObjects/EntitySystem.Proxy.cs | 1 + 7 files changed, 162 insertions(+) create mode 100644 Robust.Analyzers.Tests/PreferNonGenericVariantForTest.cs create mode 100644 Robust.Analyzers/PreferNonGenericVariantForAnalyzer.cs create mode 100644 Robust.Shared/Analyzers/PreferNonGenericVariantForAttribute.cs diff --git a/Robust.Analyzers.Tests/PreferNonGenericVariantForTest.cs b/Robust.Analyzers.Tests/PreferNonGenericVariantForTest.cs new file mode 100644 index 00000000000..2d8e4d5c53a --- /dev/null +++ b/Robust.Analyzers.Tests/PreferNonGenericVariantForTest.cs @@ -0,0 +1,71 @@ +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using NUnit.Framework; +using VerifyCS = + Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier; + +namespace Robust.Analyzers.Tests; + +[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] +[TestFixture] +public sealed class PreferNonGenericVariantForTest +{ + private static Task Verifier(string code, params DiagnosticResult[] expected) + { + var test = new CSharpAnalyzerTest() + { + TestState = + { + Sources = { code }, + }, + }; + + TestHelper.AddEmbeddedSources( + test.TestState, + "Robust.Shared.Analyzers.PreferNonGenericVariantForAttribute.cs" + ); + + // ExpectedDiagnostics cannot be set, so we need to AddRange here... + test.TestState.ExpectedDiagnostics.AddRange(expected); + + return test.RunAsync(); + } + + [Test] + public async Task Test() + { + const string code = """ + using Robust.Shared.Analyzers; + + public class Bar { }; + public class Baz { }; + public class Okay { }; + + public static class Foo + { + [PreferNonGenericVariantFor(typeof(Bar), typeof(Baz))] + public static void DoFoo() { } + } + + public class Test + { + public void DoBad() + { + Foo.DoFoo(); + } + + public void DoGood() + { + Foo.DoFoo(); + } + } + """; + + await Verifier(code, + // /0/Test0.cs(17,9): warning RA0029: Use the non-generic variant of this method for type Bar + VerifyCS.Diagnostic().WithSpan(17, 9, 17, 25).WithArguments("Bar") + ); + } +} diff --git a/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj b/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj index 513bdebfbfa..c47a873b663 100644 --- a/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj +++ b/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/Robust.Analyzers/PreferNonGenericVariantForAnalyzer.cs b/Robust.Analyzers/PreferNonGenericVariantForAnalyzer.cs new file mode 100644 index 00000000000..37c65afe10c --- /dev/null +++ b/Robust.Analyzers/PreferNonGenericVariantForAnalyzer.cs @@ -0,0 +1,65 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Robust.Roslyn.Shared; + +namespace Robust.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PreferNonGenericVariantForAnalyzer : DiagnosticAnalyzer +{ + private const string AttributeType = "Robust.Shared.Analyzers.PreferNonGenericVariantForAttribute"; + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + UseNonGenericVariantDescriptor + ); + + private static readonly DiagnosticDescriptor UseNonGenericVariantDescriptor = new( + Diagnostics.IdUseNonGenericVariant, + "Consider using the non-generic variant of this method", + "Use the non-generic variant of this method for type {0}", + "Usage", + DiagnosticSeverity.Warning, + true, + "Use the generic variant of this method."); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.ReportDiagnostics | GeneratedCodeAnalysisFlags.Analyze); + context.EnableConcurrentExecution(); + context.RegisterOperationAction(CheckForNonGenericVariant, OperationKind.Invocation); + } + + private void CheckForNonGenericVariant(OperationAnalysisContext obj) + { + if (obj.Operation is not IInvocationOperation invocationOperation) return; + + var preferNonGenericAttribute = obj.Compilation.GetTypeByMetadataName(AttributeType); + + HashSet forTypes = []; + foreach (var attribute in invocationOperation.TargetMethod.GetAttributes()) + { + if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, preferNonGenericAttribute)) + continue; + + foreach (var type in attribute.ConstructorArguments[0].Values) + forTypes.Add((ITypeSymbol)type.Value); + + break; + } + + if (forTypes == null) + return; + + foreach (var typeArg in invocationOperation.TargetMethod.TypeArguments) + { + if (forTypes.Contains(typeArg)) + { + obj.ReportDiagnostic( + Diagnostic.Create(UseNonGenericVariantDescriptor, + invocationOperation.Syntax.GetLocation(), typeArg.Name)); + } + } + } +} diff --git a/Robust.Analyzers/Robust.Analyzers.csproj b/Robust.Analyzers/Robust.Analyzers.csproj index e2990ffbd23..f9fc3f409d9 100644 --- a/Robust.Analyzers/Robust.Analyzers.csproj +++ b/Robust.Analyzers/Robust.Analyzers.csproj @@ -16,6 +16,11 @@ + + + + + diff --git a/Robust.Roslyn.Shared/Diagnostics.cs b/Robust.Roslyn.Shared/Diagnostics.cs index 03b20162cf5..dbc89994e31 100644 --- a/Robust.Roslyn.Shared/Diagnostics.cs +++ b/Robust.Roslyn.Shared/Diagnostics.cs @@ -31,6 +31,7 @@ public static class Diagnostics public const string IdDependencyFieldAssigned = "RA0025"; public const string IdUncachedRegex = "RA0026"; public const string IdDataFieldRedundantTag = "RA0027"; + public const string IdUseNonGenericVariant = "RA0029"; public static SuppressionDescriptor MeansImplicitAssignment => new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned."); diff --git a/Robust.Shared/Analyzers/PreferNonGenericVariantForAttribute.cs b/Robust.Shared/Analyzers/PreferNonGenericVariantForAttribute.cs new file mode 100644 index 00000000000..5309334ecb6 --- /dev/null +++ b/Robust.Shared/Analyzers/PreferNonGenericVariantForAttribute.cs @@ -0,0 +1,18 @@ +using System; + +#if ROBUST_ANALYZERS_IMPL +namespace Robust.Shared.Analyzers.Implementation; +#else +namespace Robust.Shared.Analyzers; +#endif + +[AttributeUsage(AttributeTargets.Method)] +public sealed class PreferNonGenericVariantForAttribute : Attribute +{ + public readonly Type[] ForTypes; + + public PreferNonGenericVariantForAttribute(params Type[] forTypes) + { + ForTypes = forTypes; + } +} diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index c70bb05a362..c21cb54d6b8 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -431,6 +431,7 @@ protected EntityStringRepresentation ToPrettyString(NetEntity netEntity) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] + [PreferNonGenericVariantFor(typeof(TransformComponent), typeof(MetaDataComponent))] protected bool TryComp(EntityUid uid, [NotNullWhen(true)] out T? comp) where T : IComponent { return EntityManager.TryGetComponent(uid, out comp);