mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-06-09 10:06:34 +02:00
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:
@@ -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()")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ public static class AttributeHelper
|
||||
}
|
||||
|
||||
public static bool HasAttribute(
|
||||
ITypeSymbol symbol,
|
||||
ISymbol symbol,
|
||||
ITypeSymbol attribute,
|
||||
[NotNullWhen(true)] out AttributeData? matchedAttribute)
|
||||
{
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user