Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
</PropertyGroup>
<ItemGroup>
<!-- Analyzers -->
<PackageVersion Include="ErrorProne.NET.CoreAnalyzers" Version="0.6.1-beta.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0" />
<PackageVersion Include="ErrorProne.NET.CoreAnalyzers" Version="0.8.0-beta.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="4.13.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.13.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.13.0" />
<PackageVersion Include="ILRepack.Lib.MSBuild.Task" Version="2.0.43" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.7.115" />
<PackageVersion Include="RuntimeContracts" Version="0.5.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,76 @@ void Foo() {
await Verify.VerifyAsync(test);
}

[Test]
public async Task NoWarn_On_Different_Instance()
{
var test = @"
public class Node
{
public void Foo() { Parent?.Foo();}
public Node Parent { get; set; }
}
";
await Verify.VerifyAsync(test);
}

[Test]
public async Task NoWarn_With_Ref_Parameter_When_Touched()
{
var test = @"
public class Node
{
public void Foo(ref int x)
{
Bar(ref x);
Foo(ref x);
}

private void Bar(ref int x)
{
x++;
}
}
";
await Verify.VerifyAsync(test);
}

[Test]
public async Task NoWarn_With_Ref_Parameter_When_Changed()
{
var test = @"
public class Node
{
public void Foo(ref int x)
{
x = 42;
Foo(ref x);
}

private void Bar(ref int x)
{
x++;
}
}
";
await Verify.VerifyAsync(test);
}

[Test]
public async Task Warn_With_Ref_Parameter_When_Not_Touched()
{
var test = @"
public class Node
{
public void Foo(ref int x)
{
[|Foo(ref x)|];
}
}
";
await Verify.VerifyAsync(test);
}

[Test]
public async Task WarnsOnConditionalRecursiveCall()
{
Expand Down
104 changes: 103 additions & 1 deletion src/ErrorProne.NET.CoreAnalyzers/RecursiveCallAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
Expand All @@ -24,6 +25,10 @@ private static void AnalyzeMethodBody(OperationAnalysisContext context)
{
var method = (IMethodSymbol)context.ContainingSymbol;
var methodBody = (IMethodBodyOperation)context.Operation;

// Find all ref parameters that have been "touched" (modified or passed to other methods)
var touchedRefParameters = GetTouchedRefParameters(methodBody, method);

foreach (var invocation in methodBody.Descendants().OfType<IInvocationOperation>())
{
// Check if all parameters are passed as-is
Expand All @@ -32,13 +37,22 @@ private static void AnalyzeMethodBody(OperationAnalysisContext context)
// Checking that the method is the same.
SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.OriginalDefinition, method.OriginalDefinition) &&

// Check if the method is being called on the same instance
// For instance methods, we need to check if the receiver is 'this' or implicit (null)
// For static methods, there's no instance to check
IsCalledOnSameInstance(invocation, method) &&

// Checking if the parameters are passed as is.
// It is possible to have a false positive here if the parameters are mutable.
// But it is a very rare case, so we will ignore it for now.
invocation.Arguments.Zip(method.Parameters, (arg, param) =>
arg.Value is IParameterReferenceOperation paramRef &&
SymbolEqualityComparer.Default.Equals(paramRef.Parameter, param)
).All(b => b))
).All(b => b) &&

// For ref parameters, check if they were touched before this call
// If any ref parameter was touched, don't warn
!HasTouchedRefParameterBeforeCall(invocation, method, touchedRefParameters))
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.EPC30,
Expand All @@ -47,5 +61,93 @@ arg.Value is IParameterReferenceOperation paramRef &&
}
}
}

private static bool HasTouchedRefParameterBeforeCall(IInvocationOperation recursiveCall, IMethodSymbol method, HashSet<IParameterSymbol> touchedRefParameters)
{
// Check if any ref parameter in the recursive call was touched
for (int i = 0; i < recursiveCall.Arguments.Length; i++)
{
var arg = recursiveCall.Arguments[i];
var param = method.Parameters[i];

// If this is a ref parameter and it's passed as-is, check if it was touched
if (param.RefKind == RefKind.Ref &&
arg.Value is IParameterReferenceOperation paramRef &&
SymbolEqualityComparer.Default.Equals(paramRef.Parameter, param) &&
touchedRefParameters.Contains(param))
{
return true;
}
}

return false;
}

private static HashSet<IParameterSymbol> GetTouchedRefParameters(IMethodBodyOperation methodBody, IMethodSymbol method)
{
var touchedParams = new HashSet<IParameterSymbol>(SymbolEqualityComparer.Default);

// Look for assignments to ref parameters
foreach (var assignment in methodBody.Descendants().OfType<IAssignmentOperation>())
{
if (assignment.Target is IParameterReferenceOperation paramRef &&
paramRef.Parameter.RefKind == RefKind.Ref)
{
touchedParams.Add(paramRef.Parameter);
}
}

// Look for ref parameters being passed to other methods
foreach (var invocation in methodBody.Descendants().OfType<IInvocationOperation>())
{
// Skip the method itself to avoid checking recursive calls
if (SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.OriginalDefinition, method.OriginalDefinition))
{
continue;
}

foreach (var arg in invocation.Arguments)
{
// Check if a ref parameter is being passed as ref/out to another method
if (arg.ArgumentKind == ArgumentKind.Explicit &&
(arg.Parameter?.RefKind == RefKind.Ref || arg.Parameter?.RefKind == RefKind.Out) &&
arg.Value is IParameterReferenceOperation paramRef &&
paramRef.Parameter.RefKind == RefKind.Ref)
{
touchedParams.Add(paramRef.Parameter);
}
}
}

return touchedParams;
}

private static bool IsCalledOnSameInstance(IInvocationOperation invocation, IMethodSymbol containingMethod)
{
// For static methods, there's no instance to check, so any call to the same static method is recursive
if (containingMethod.IsStatic)
{
return true;
}

// For instance methods, check if the receiver is 'this' (implicit or explicit)
var receiver = invocation.Instance;

// If receiver is null, it means it's an implicit 'this' call (e.g., just Foo() instead of this.Foo())
if (receiver == null)
{
return true;
}

// If receiver is an explicit 'this' reference
if (receiver is IInstanceReferenceOperation instanceRef &&
instanceRef.ReferenceKind == InstanceReferenceKind.ContainingTypeInstance)
{
return true;
}

// Any other receiver (like Parent?.Foo()) means it's called on a different instance
return false;
}
}
}