Revert "Revert "Add analyzer & fixer to detect when proxy methods are not used (ProxyForAttribute)"" (#6439)

Revert "Revert "Add analyzer & fixer to detect when proxy methods are not use…"

This reverts commit 1af32c3129.
This commit is contained in:
Tayrtahn
2026-03-07 11:32:01 -05:00
committed by GitHub
parent e428a341e1
commit fcc2c01d01
10 changed files with 1119 additions and 3 deletions
@@ -0,0 +1,316 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.ProxyForAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
[TestOf(typeof(ProxyForAnalyzer))]
public sealed class ProxyForAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<ProxyForAnalyzer, DefaultVerifier>()
{
TestState =
{
Sources = { code },
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Analyzers.ProxyForAttribute.cs"
);
test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
private const string TestTypeDefs = """
using Robust.Shared.Analyzers;
public sealed class TargetClass
{
public void DoSomething() { }
public bool TryDoSomething<T>(int foo, out T? bar)
{
bar = default;
return true;
}
}
public abstract partial class ProxyClass
{
protected TargetClass TargetClass = new();
}
""";
[Test]
public async Task TestAutoName()
{
const string code = """
using Robust.Shared.Analyzers;
public abstract partial class ProxyClass
{
[ProxyFor(typeof(TargetClass))]
public void DoSomething()
{
TargetClass.DoSomething();
}
}
public sealed class Tester : ProxyClass
{
public void Good()
{
DoSomething();
}
public void Bad()
{
TargetClass.DoSomething();
}
}
""";
await Verifier(code,
// /0/Test0.cs(18,9): warning RA0037: Use the proxy method DoSomething instead of calling TargetClass.DoSomething directly
VerifyCS.Diagnostic(ProxyForAnalyzer.PreferProxyDescriptor).WithSpan(21, 9, 21, 34).WithArguments("DoSomething", "TargetClass.DoSomething")
);
}
[Test]
public async Task TestSetName()
{
const string code = """
using Robust.Shared.Analyzers;
public abstract partial class ProxyClass
{
[ProxyFor(typeof(TargetClass), nameof(TargetClass.DoSomething))]
public void DoIt()
{
TargetClass.DoSomething();
}
}
public sealed class Tester : ProxyClass
{
public void Good()
{
DoIt();
}
public void Bad()
{
TargetClass.DoSomething();
}
}
""";
await Verifier(code,
// /0/Test0.cs(21,9): warning RA0037: Use the proxy method DoIt instead of calling TargetClass.DoSomething directly
VerifyCS.Diagnostic(ProxyForAnalyzer.PreferProxyDescriptor).WithSpan(21, 9, 21, 34).WithArguments("DoIt", "TargetClass.DoSomething")
);
}
[Test]
public async Task TestGeneric()
{
const string code = """
using Robust.Shared.Analyzers;
public abstract partial class ProxyClass
{
[ProxyFor(typeof(TargetClass))]
public bool TryDoSomething<T>(int foo, out T? bar)
{
return TargetClass.TryDoSomething(foo, out bar);
}
}
public sealed class Tester : ProxyClass
{
public void Good()
{
TryDoSomething<string>(5, out var bar);
}
public void Bad()
{
TargetClass.TryDoSomething<string>(5, out var bar);
}
}
""";
await Verifier(code,
// /0/Test0.cs(21,9): warning RA0037: Use the proxy method TryDoSomething instead of calling TargetClass.TryDoSomething directly
VerifyCS.Diagnostic(ProxyForAnalyzer.PreferProxyDescriptor).WithSpan(21, 9, 21, 59).WithArguments("TryDoSomething", "TargetClass.TryDoSomething")
);
}
[Test]
public async Task TestRedundantMethodName()
{
const string code = """
using Robust.Shared.Analyzers;
public abstract partial class ProxyClass
{
[ProxyFor(typeof(TargetClass), nameof(TargetClass.DoSomething))]
public void DoSomething()
{
TargetClass.DoSomething();
}
}
""";
await Verifier(code,
// /0/Test0.cs(5,36): warning RA0038: Set method name matches the proxy method name and can be omitted
VerifyCS.Diagnostic(ProxyForAnalyzer.RedundantMethodNameDescriptor).WithSpan(5, 36, 5, 67)
);
}
[Test]
public async Task TestNoMatchingSetMethodName()
{
const string code = """
using Robust.Shared.Analyzers;
public abstract partial class ProxyClass
{
[ProxyFor(typeof(TargetClass), "SomeOtherName")]
public void DoSomething()
{
TargetClass.DoSomething();
}
}
""";
await Verifier(code,
// /0/Test0.cs(5,15): error RA0039: Unable to find target method TargetClass.SomeOtherName()
VerifyCS.Diagnostic(ProxyForAnalyzer.TargetMethodNotFoundDescriptor).WithSpan(5, 15, 5, 34).WithArguments("TargetClass.SomeOtherName()")
);
}
[Test]
public async Task TestNoMatchingSignature()
{
const string code = """
using Robust.Shared.Analyzers;
public abstract partial class ProxyClass
{
[ProxyFor(typeof(TargetClass))]
public void DoSomething(int foo)
{
TargetClass.DoSomething();
}
}
""";
await Verifier(code,
// /0/Test0.cs(5,15): error RA0039: Unable to find target method TargetClass.DoSomething(int foo)
VerifyCS.Diagnostic(ProxyForAnalyzer.TargetMethodNotFoundDescriptor).WithSpan(5, 15, 5, 34).WithArguments("TargetClass.DoSomething(int foo)")
);
}
[Test]
public async Task TestIgnoreDelegate()
{
const string code = """
using Robust.Shared.Analyzers;
public abstract partial class ProxyClass
{
[ProxyFor(typeof(TargetClass))]
public void DoSomething()
{
TargetClass.DoSomething();
}
public delegate void ThingDoer(TargetClass TargetClass);
public void RunDelegate(ThingDoer doer) { }
}
public sealed class Tester : ProxyClass
{
public void Test()
{
RunDelegate(target =>
{
target.DoSomething();
});
}
}
""";
await Verifier(code, []);
}
[Test]
public async Task TestIgnoreOtherClassMember()
{
const string code = """
using Robust.Shared.Analyzers;
public abstract partial class ProxyClass
{
[ProxyFor(typeof(TargetClass))]
public void DoSomething()
{
TargetClass.DoSomething();
}
}
public sealed class OtherClass
{
public TargetClass Target;
}
public sealed class Tester : ProxyClass
{
public void Test()
{
var other = new OtherClass();
other.Target.DoSomething();
}
}
""";
await Verifier(code, []);
}
[Test]
public async Task TestNoMatchingAutoMethodName()
{
const string code = """
using Robust.Shared.Analyzers;
public abstract partial class ProxyClass
{
[ProxyFor(typeof(TargetClass))]
public void SomeOtherName()
{
TargetClass.DoSomething();
}
}
""";
await Verifier(code,
// /0/Test0.cs(5,15): error RA0039: Unable to find target method TargetClass.SomeOtherName()
VerifyCS.Diagnostic(ProxyForAnalyzer.TargetMethodNotFoundDescriptor).WithSpan(5, 15, 5, 34).WithArguments("TargetClass.SomeOtherName()")
);
}
}
+163
View File
@@ -0,0 +1,163 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.ProxyForAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
[TestOf(typeof(ProxyForFixer))]
public sealed class ProxyForFixerTest
{
private static Task Verifier(string code, string fixedCode, params DiagnosticResult[] expected)
{
var test = new CSharpCodeFixTest<ProxyForAnalyzer, ProxyForFixer, DefaultVerifier>()
{
TestState =
{
Sources = { code },
},
FixedState =
{
Sources = { fixedCode },
}
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Analyzers.ProxyForAttribute.cs"
);
TestHelper.AddEmbeddedSources(
test.FixedState,
"Robust.Shared.Analyzers.ProxyForAttribute.cs"
);
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task TestSubstituteProxy()
{
const string code = """
using Robust.Shared.Analyzers;
public sealed class TargetClass
{
public void DoSomething<T>(T? foo, string bar) { }
}
public abstract class ProxyClass
{
protected TargetClass TargetClass = new();
[ProxyFor(typeof(TargetClass))]
public void DoSomething<T>(T? foo, string bar)
{
TargetClass.DoSomething(foo, bar);
}
}
public sealed class Tester : ProxyClass
{
public void Test()
{
// Comment
TargetClass.DoSomething<int>(5, "bar");
}
}
""";
const string fixedCode = """
using Robust.Shared.Analyzers;
public sealed class TargetClass
{
public void DoSomething<T>(T? foo, string bar) { }
}
public abstract class ProxyClass
{
protected TargetClass TargetClass = new();
[ProxyFor(typeof(TargetClass))]
public void DoSomething<T>(T? foo, string bar)
{
TargetClass.DoSomething(foo, bar);
}
}
public sealed class Tester : ProxyClass
{
public void Test()
{
// Comment
DoSomething<int>(5, "bar");
}
}
""";
await Verifier(code, fixedCode,
// /0/Test0.cs(23,9): warning RA0037: Use the proxy method DoSomething instead of calling TargetClass.DoSomething directly
VerifyCS.Diagnostic(ProxyForAnalyzer.PreferProxyDescriptor).WithSpan(25, 9, 25, 47).WithArguments("DoSomething", "TargetClass.DoSomething")
);
}
[Test]
public async Task TestRemoveRedundantMethodName()
{
const string code = """
using Robust.Shared.Analyzers;
public sealed class TargetClass
{
public void DoSomething(int foo, string bar) { }
}
public abstract class ProxyClass
{
protected TargetClass TargetClass = new();
[ProxyFor(typeof(TargetClass), nameof(TargetClass.DoSomething))]
public void DoSomething(int foo, string bar)
{
TargetClass.DoSomething(foo, bar);
}
}
""";
const string fixedCode = """
using Robust.Shared.Analyzers;
public sealed class TargetClass
{
public void DoSomething(int foo, string bar) { }
}
public abstract class ProxyClass
{
protected TargetClass TargetClass = new();
[ProxyFor(typeof(TargetClass))]
public void DoSomething(int foo, string bar)
{
TargetClass.DoSomething(foo, bar);
}
}
""";
await Verifier(code, fixedCode,
// /0/Test0.cs(12,36): warning RA0038: Set method name matches the proxy method name and can be omitted
VerifyCS.Diagnostic(ProxyForAnalyzer.RedundantMethodNameDescriptor).WithSpan(12, 36, 12, 67)
);
}
}
@@ -14,6 +14,7 @@
<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\Analyzers\PreferOtherTypeAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferOtherTypeAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ProxyForAttribute.cs" LogicalName="Robust.Shared.Analyzers.ProxyForAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ForbidLiteralAttribute.cs" LogicalName="Robust.Shared.Analyzers.ForbidLiteralAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ObsoleteInheritanceAttribute.cs" LogicalName="Robust.Shared.Analyzers.ObsoleteInheritanceAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ValidateMemberAttribute.cs" LogicalName="Robust.Shared.Analyzers.ValidateMemberAttribute.cs" LinkBase="Implementations" />
+307
View File
@@ -0,0 +1,307 @@
#nullable enable
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ProxyForAnalyzer : DiagnosticAnalyzer
{
private const string ProxyForAttributeType = "Robust.Shared.Analyzers.ProxyForAttribute";
public static readonly string ProxyMethodName = "proxy";
public static readonly DiagnosticDescriptor PreferProxyDescriptor = new(
Diagnostics.IdPreferProxy,
"Use the proxy method",
"Use the proxy method {0} instead of calling {1} directly",
"Usage",
DiagnosticSeverity.Warning,
true,
"Use the proxy method."
);
public static readonly DiagnosticDescriptor RedundantMethodNameDescriptor = new(
Diagnostics.IdProxyForRedundantMethodName,
"Method name is redundant",
"Set method name matches the proxy method name and can be omitted",
"Usage",
DiagnosticSeverity.Warning,
true,
"Remove the method name from the attribute."
);
public static readonly DiagnosticDescriptor TargetMethodNotFoundDescriptor = new(
Diagnostics.IdProxyForTargetMethodNotFound,
"Target method not found",
"Unable to find target method {0}",
"Usage",
DiagnosticSeverity.Error,
true,
"Make sure a method exists with the target name and matching signature."
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
[
PreferProxyDescriptor,
RedundantMethodNameDescriptor,
TargetMethodNotFoundDescriptor,
];
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.ReportDiagnostics | GeneratedCodeAnalysisFlags.Analyze);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(static ctx =>
{
if (ctx.Compilation.GetTypeByMetadataName(ProxyForAttributeType) is not { } proxyForAttributeType)
return;
ctx.RegisterSymbolStartAction(symbolContext =>
{
// We only care about classes
if (symbolContext.Symbol is not INamedTypeSymbol typeSymbol || typeSymbol.TypeKind != TypeKind.Class)
return;
// Find information about all marked proxy methods available to this class
if (!TryGetProxyMethods(typeSymbol, proxyForAttributeType, out var proxyMethods))
return;
// No proxy methods are available to this class, so we're done
if (proxyMethods.Length == 0)
return;
// Pass proxy method information to the analyzer state
var state = new AnalyzerState(proxyMethods);
// Analyze each method invocation within the class
symbolContext.RegisterOperationAction(state.AnalyzeInvocation, OperationKind.Invocation);
}, SymbolKind.NamedType);
ctx.RegisterOperationAction(operationContext => AnalyzeAttribute(operationContext, proxyForAttributeType), OperationKind.Attribute);
});
}
/// <summary>
/// Data about a proxy method and its target.
/// </summary>
private record class ProxyMethod(
IMethodSymbol Method,
INamedTypeSymbol TargetType,
string TargetMethod
);
/// <summary>
/// Returns information about all proxy methods available to the specified class.
/// </summary>
private static bool TryGetProxyMethods(INamedTypeSymbol typeSymbol, INamedTypeSymbol proxyForAttribute, [NotNullWhen(true)] out ProxyMethod[]? proxyMethods)
{
proxyMethods = null;
// Don't fault the proxy type for not using its own proxy methods
if (typeSymbol.BaseType is null)
return false;
HashSet<ProxyMethod> proxySet = [];
// Search for methods in each type this inherits from
foreach (var baseType in TypeSymbolHelper.GetBaseTypes(typeSymbol))
{
HashSet<ProxyMethod> classMethods = [];
// Check each member
foreach (var member in baseType.GetMembers())
{
// We only care about methods
if (member is not IMethodSymbol method)
continue;
// Make sure the method is marked as a proxy
if (!AttributeHelper.HasAttribute(method, proxyForAttribute, out var attributeData))
continue;
var targetType = attributeData.ConstructorArguments[0].Value as INamedTypeSymbol;
var targetMethod = attributeData.ConstructorArguments[1].Value as string ?? member.Name;
classMethods.Add(new ProxyMethod(method, targetType!, targetMethod));
}
proxySet.UnionWith(classMethods);
}
if (proxySet.Count == 0)
return false;
proxyMethods = proxySet.ToArray();
return true;
}
private sealed class AnalyzerState(ProxyMethod[] ProxyMethods)
{
public void AnalyzeInvocation(OperationAnalysisContext context)
{
if (context.Operation is not IInvocationOperation operation)
return;
// Make sure the invocation is happening on a member, not a parameter or something else
if (operation.Instance is not IMemberReferenceOperation reference)
return;
// Make sure the member belongs to the proxy class
if (!TypeSymbolHelper.Inherits(context.ContainingSymbol.ContainingType, reference.Member.ContainingType))
return;
// Get the method being invoked
var invokedMethod = operation.TargetMethod;
// Check each method we found
foreach (var (method, targetType, targetMethod) in ProxyMethods)
{
// Make sure the Type specified by the attribute is the one containing the method being invoked
if (!SymbolEqualityComparer.Default.Equals(targetType, invokedMethod.ContainingType))
continue;
// Make sure the method name specified by the attribute is same as the one being invoked
if (targetMethod != invokedMethod.Name)
continue;
// Make sure this method has the same signature as the one being invoked
if (!DoSignaturesMatch(invokedMethod, method))
continue;
var props = new Dictionary<string, string?>
{
{ ProxyMethodName, method.Name }
};
context.ReportDiagnostic(Diagnostic.Create(
PreferProxyDescriptor,
operation.Syntax.GetLocation(),
props.ToImmutableDictionary(),
method.MetadataName,
$"{invokedMethod.ContainingType.Name}.{invokedMethod.Name}"
));
// We should only need to report one violation
break;
}
}
}
/// <summary>
/// Check for incorrect use of the attribute.
/// </summary>
private static void AnalyzeAttribute(OperationAnalysisContext context, INamedTypeSymbol proxyForAttribute)
{
if (context.ContainingSymbol is not IMethodSymbol methodSymbol)
return;
if (context.Operation is not IAttributeOperation operation)
return;
if (operation.Syntax is not AttributeSyntax attributeSyntax)
return;
if (operation.Operation is not IObjectCreationOperation creationOperation)
return;
// Make sure we're looking at the right attribute
if (!SymbolEqualityComparer.Default.Equals(creationOperation.Type, proxyForAttribute))
return;
// Get the target Type specified by the attribute
if ((creationOperation.Arguments[0].Value as ITypeOfOperation)?.TypeOperand is not { } targetType)
return;
// Try to get the set method name from the attribute constructor
var targetMethodName = creationOperation.Arguments[1].Value.ConstantValue.Value as string;
// Check for a redundant set method name
if (targetMethodName == methodSymbol.Name)
{
var location = creationOperation.Arguments[1].Syntax.GetLocation();
context.ReportDiagnostic(Diagnostic.Create(
RedundantMethodNameDescriptor,
location
));
}
// Fall back to the method name
targetMethodName ??= methodSymbol.Name;
// Find all methods belonging to the target Type that have the right name
var members = targetType.GetMembers(targetMethodName).Where(m => m is IMethodSymbol).Cast<IMethodSymbol>();
// Find the location of the argument's node
var targetArgumentLocation = creationOperation.Arguments[0].Syntax.GetLocation();
// Make sure there's a method with the right name and matching signature
var found = false;
foreach (var member in members)
{
if (DoSignaturesMatch(member, methodSymbol))
found = true;
}
if (!found)
{
var methodParams = methodSymbol.Parameters.Length > 0 ? methodSymbol.Parameters.Select(p => p.ToDisplayString()) : [];
var methodSignature = $"{targetType.Name}.{targetMethodName}({string.Join(", ", methodParams)})";
context.ReportDiagnostic(Diagnostic.Create(
TargetMethodNotFoundDescriptor,
targetArgumentLocation,
methodSignature
));
}
}
private static bool DoSignaturesMatch(IMethodSymbol first, IMethodSymbol second)
{
// Make sure the number of type arguments is the same
if (first.TypeArguments.Length != second.TypeArguments.Length)
return false;
// Make sure any type constraints on the methods are the same
for (var i = 0; i < first.TypeParameters.Length; i++)
{
var firstConstraints = first.TypeParameters[i].ConstraintTypes;
var secondConstraints = second.TypeParameters[i].ConstraintTypes;
for (var j = 0; j < firstConstraints.Length; j++)
{
if (!SymbolEqualityComparer.Default.Equals(firstConstraints[j], secondConstraints[j]))
return false;
}
}
// Convert any type arguments in second to use the types of first
if (second.IsGenericMethod)
second = second.Construct(first.TypeArguments, first.TypeArgumentNullableAnnotations);
// Filter out any optional parameters
var firstParams = first.Parameters.Where(p => !p.IsOptional).ToArray();
var secondParams = second.Parameters.Where(p => !p.IsOptional).ToArray();
// A different number of parameters means no match
if (firstParams.Length != secondParams.Length)
return false;
for (var i = 0; i < firstParams.Length; i++)
{
// Check if the parameter type is a generic type symbol (like T, TComp, etc.)
if (firstParams[i].Type is INamedTypeSymbol namedType && namedType.IsGenericType)
{
// If the compared parameter also is a generic type symbol, consider that a match
if (secondParams[i].Type is INamedTypeSymbol namedTypeSecond && namedTypeSecond.IsGenericType)
continue;
// Otherwise, no match
return false;
}
// Make sure the Types match
if (!SymbolEqualityComparer.IncludeNullability.Equals(firstParams[i].Type, secondParams[i].Type))
return false;
}
return true;
}
}
+126
View File
@@ -0,0 +1,126 @@
#nullable enable
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Robust.Roslyn.Shared.Diagnostics;
namespace Robust.Analyzers;
[ExportCodeFixProvider(LanguageNames.CSharp)]
public sealed class ProxyForFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds =>
[
IdPreferProxy,
IdProxyForRedundantMethodName,
];
public override FixAllProvider? GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}
public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
foreach (var diagnostic in context.Diagnostics)
{
switch (diagnostic.Id)
{
case IdPreferProxy:
return RegisterSubstituteProxy(context, diagnostic);
case IdProxyForRedundantMethodName:
return RegisterRemoveRedundantMethodName(context, diagnostic);
}
}
return Task.CompletedTask;
}
private async Task RegisterSubstituteProxy(CodeFixContext context, Diagnostic diagnostic)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
var span = diagnostic.Location.SourceSpan;
var token = root?.FindToken(span.Start).Parent?.AncestorsAndSelf().OfType<InvocationExpressionSyntax>().First();
if (token == null)
return;
if (diagnostic.Properties[ProxyForAnalyzer.ProxyMethodName] is not string methodName)
return;
context.RegisterCodeFix(CodeAction.Create(
"Substitute proxy method",
c => SubstituteProxy(context.Document, token, methodName, c),
"Substitute proxy method"
), diagnostic);
}
private async Task<Document> SubstituteProxy(Document document, InvocationExpressionSyntax token, string methodName, CancellationToken cancellation)
{
var root = (CompilationUnitSyntax?)await document.GetSyntaxRootAsync(cancellation);
var model = await document.GetSemanticModelAsync(cancellation);
if (model == null)
return document;
if (token.Expression is not MemberAccessExpressionSyntax expression)
return document;
// Create a token with the proxy method name
var identifierToken = SyntaxFactory.Identifier(methodName);
// Create a replacement expression using the proxy method
ExpressionSyntax newExpression = expression.Name switch
{
// Copy over any type arguments from the old invocation
GenericNameSyntax old => SyntaxFactory.GenericName(identifierToken, old.TypeArgumentList),
// Handle methods with no type arguments
SimpleNameSyntax => SyntaxFactory.IdentifierName(identifierToken),
_ => throw new InvalidOperationException()
};
// Create a replacement invocation expression
var replacement = token.WithExpression(newExpression).WithTriviaFrom(token);
// Replace the original expression with the new one
root = root!.ReplaceNode(token, replacement);
return document.WithSyntaxRoot(root);
}
private async Task RegisterRemoveRedundantMethodName(CodeFixContext context, Diagnostic diagnostic)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
var span = diagnostic.Location.SourceSpan;
var token = root?.FindToken(span.Start).Parent?.AncestorsAndSelf().OfType<AttributeArgumentSyntax>().First();
if (token == null)
return;
context.RegisterCodeFix(CodeAction.Create(
"Remove method name parameter",
c => RemoveRedundantMethodName(context.Document, token, c),
"Remove method name parameter"
), diagnostic);
}
private async Task<Document> RemoveRedundantMethodName(Document document, AttributeArgumentSyntax token, CancellationToken cancellation)
{
var root = (CompilationUnitSyntax?)await document.GetSyntaxRootAsync(cancellation);
var model = await document.GetSemanticModelAsync(cancellation);
if (model == null)
return document;
// Get the argument list containing the offending argument
if (token.Parent is not AttributeArgumentListSyntax listSyntax)
return document;
// Create a new list with the argument removed
var newListSyntax = listSyntax.WithArguments(listSyntax.Arguments.Remove(token));
// Replace the original argument list with the new one
root = root!.ReplaceNode(listSyntax, newListSyntax);
return document.WithSyntaxRoot(root);
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ public static class AttributeHelper
}
public static bool HasAttribute(
ITypeSymbol symbol,
ISymbol symbol,
ITypeSymbol attribute,
[NotNullWhen(true)] out AttributeData? matchedAttribute)
{
+3
View File
@@ -48,6 +48,9 @@ public static class Diagnostics
public const string IdPrototypeRedundantType = "RA0042";
public const string IdPrototypeEndsWithPrototype = "RA0043";
public const string IdValidateMember = "RA0044";
public const string IdPreferProxy = "RA0045";
public const string IdProxyForRedundantMethodName = "RA0046";
public const string IdProxyForTargetMethodNotFound = "RA0047";
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");
+27
View File
@@ -69,4 +69,31 @@ public static class TypeSymbolHelper
return type;
}
/// <summary>
/// Enumerates all base types of the given <paramref name="type"/>.
/// </summary>
public static IEnumerable<ITypeSymbol> GetBaseTypes(ITypeSymbol type)
{
var baseType = type.BaseType;
while (baseType != null)
{
yield return baseType;
baseType = baseType.BaseType;
}
}
/// <summary>
/// Checks if the given <paramref name="type"/> inherits from <paramref name="other"/>.
/// </summary>
/// <returns>True if <paramref name="type"/> inherits from <paramref name="other"/>, otherwise false.</returns>
public static bool Inherits(ITypeSymbol type, ITypeSymbol other)
{
foreach (var baseType in GetBaseTypes(type))
{
if (SymbolEqualityComparer.Default.Equals(baseType, other))
return true;
}
return false;
}
}
@@ -0,0 +1,26 @@
using System;
namespace Robust.Shared.Analyzers;
/// <summary>
/// Indicates that a method is a proxy method that can and should be used as a shortcut
/// for calling a method in another class. This will cause a compiler warning on any code
/// within the descendants of this class that attempts to call the target method directly
/// instead of using the proxy method.
/// The proxy method must have the same parameters as the target method.
/// </summary>
/// <param name="type"><see cref="System.Type"/> containing the target method.</param>
/// <param name="method">Name of the target method. If null, the name of the proxy method will be used.</param>
[AttributeUsage(AttributeTargets.Method)]
public sealed class ProxyForAttribute(Type type, string? method = null) : Attribute
{
/// <summary>
/// <see cref="System.Type"/> containing the target method.
/// </summary>
public Type Type = type;
/// <summary>
/// Name of the target method. If null, the name of the proxy method will be used.
/// </summary>
public string? Method = method;
}
File diff suppressed because it is too large Load Diff