Skip to content

Commit

Permalink
Add PreferNonGenericVariantFor attribute and analyzer (#5190)
Browse files Browse the repository at this point in the history
Co-authored-by: Pieter-Jan Briers <[email protected]>
  • Loading branch information
Tayrtahn and PJB3005 authored Jul 10, 2024
1 parent 8e50924 commit c3d8080
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 0 deletions.
71 changes: 71 additions & 0 deletions Robust.Analyzers.Tests/PreferNonGenericVariantForTest.cs
Original file line number Diff line number Diff line change
@@ -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<Robust.Analyzers.PreferNonGenericVariantForAnalyzer>;

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<PreferNonGenericVariantForAnalyzer, NUnitVerifier>()
{
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<T>() { }
}

public class Test
{
public void DoBad()
{
Foo.DoFoo<Bar>();
}

public void DoGood()
{
Foo.DoFoo<Okay>();
}
}
""";

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")
);
}
}
1 change: 1 addition & 0 deletions Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<EmbeddedResource Include="..\Robust.Shared\Analyzers\AccessAttribute.cs" LogicalName="Robust.Shared.Analyzers.AccessAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\AccessPermissions.cs" LogicalName="Robust.Shared.Analyzers.AccessPermissions.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\MustCallBaseAttribute.cs" LogicalName="Robust.Shared.IoC.MustCallBaseAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferNonGenericVariantForAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferNonGenericVariantForAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
</ItemGroup>

Expand Down
65 changes: 65 additions & 0 deletions Robust.Analyzers/PreferNonGenericVariantForAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDescriptor> 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<ITypeSymbol> 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));
}
}
}
}
5 changes: 5 additions & 0 deletions Robust.Analyzers/Robust.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
<Compile Include="..\Robust.Shared\Analyzers\PreferGenericVariantAttribute.cs" LinkBase="Implementations" />
</ItemGroup>

<ItemGroup>
<!-- Needed for PreferNonGenericVariantAnalyzer. -->
<Compile Include="..\Robust.Shared\Analyzers\PreferNonGenericVariantForAttribute.cs" LinkBase="Implementations" />
</ItemGroup>

<ItemGroup>
<!-- Needed for DataDefinitionAnalyzer. -->
<Compile Include="..\Robust.Shared\Serialization\Manager\Definition\DataDefinitionUtility.cs" LinkBase="Implementations" />
Expand Down
1 change: 1 addition & 0 deletions Robust.Roslyn.Shared/Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
18 changes: 18 additions & 0 deletions Robust.Shared/Analyzers/PreferNonGenericVariantForAttribute.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions Robust.Shared/GameObjects/EntitySystem.Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ protected T Comp<T>(EntityUid uid) where T : IComponent

/// <inheritdoc cref="IEntityManager.TryGetComponent&lt;T&gt;(EntityUid, out T)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[PreferNonGenericVariantFor(typeof(TransformComponent), typeof(MetaDataComponent))]
protected bool TryComp<T>(EntityUid uid, [NotNullWhen(true)] out T? comp) where T : IComponent
{
return EntityManager.TryGetComponent(uid, out comp);
Expand Down

0 comments on commit c3d8080

Please sign in to comment.