Files
RobustToolbox/Robust.Analyzers/ProxyForAnalyzer.cs
T
TayrtahnandGitHub fcc2c01d01 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.
2026-03-07 11:32:01 -05:00

308 lines
12 KiB
C#

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