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 bcc0264bca4..10d7d549912 100644 --- a/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj +++ b/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj @@ -11,6 +11,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 98471b626d1..1d078348165 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 94c0c84aaa4..6d27bdfa30a 100644 --- a/Robust.Roslyn.Shared/Diagnostics.cs +++ b/Robust.Roslyn.Shared/Diagnostics.cs @@ -33,6 +33,7 @@ public static class Diagnostics public const string IdDataFieldRedundantTag = "RA0027"; public const string IdMustCallBase = "RA0028"; public const string IdDataFieldNoVVReadWrite = "RA0029"; + public const string IdUseNonGenericVariant = "RA0030"; 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 9b2ff11cbd1..fa9b6bc8730 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -439,6 +439,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);