Skip to content

Commit 0d364ec

Browse files
authored
GH-150 - proper handling of nested calls when analyzing callinfo (#151)
1 parent d242a71 commit 0d364ec

24 files changed

+2849
-22
lines changed

src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoCallFinder.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,23 @@
77

88
namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers
99
{
10-
internal class CallInfoCallFinder : ICallInfoFinder<InvocationExpressionSyntax, ElementAccessExpressionSyntax>
10+
internal class CallInfoCallFinder : AbstractCallInfoFinder<InvocationExpressionSyntax, ElementAccessExpressionSyntax>
1111
{
1212
public static CallInfoCallFinder Instance { get; } = new CallInfoCallFinder();
1313

1414
private CallInfoCallFinder()
1515
{
1616
}
1717

18-
public CallInfoContext<InvocationExpressionSyntax, ElementAccessExpressionSyntax> GetCallInfoContext(SemanticModel semanticModel, SyntaxNode syntaxNode)
18+
protected override CallInfoContext<InvocationExpressionSyntax, ElementAccessExpressionSyntax> GetCallInfoContextInternal(SemanticModel semanticModel, SyntaxNode syntaxNode)
1919
{
2020
var visitor = new CallInfoVisitor(semanticModel);
2121
visitor.Visit(syntaxNode);
2222

23-
return new CallInfoContext<InvocationExpressionSyntax, ElementAccessExpressionSyntax>(visitor.ArgAtInvocations, visitor.ArgInvocations, visitor.DirectIndexerAccesses);
23+
return new CallInfoContext<InvocationExpressionSyntax, ElementAccessExpressionSyntax>(
24+
argAtInvocations: visitor.ArgAtInvocations,
25+
argInvocations: visitor.ArgInvocations,
26+
indexerAccesses: visitor.DirectIndexerAccesses);
2427
}
2528

2629
private class CallInfoVisitor : CSharpSyntaxWalker
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.Operations;
5+
6+
namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers
7+
{
8+
internal abstract class AbstractCallInfoFinder<TInvocationExpressionSyntax, TIndexerSyntax> : ICallInfoFinder<TInvocationExpressionSyntax, TIndexerSyntax>
9+
where TInvocationExpressionSyntax : SyntaxNode where TIndexerSyntax : SyntaxNode
10+
{
11+
public CallInfoContext<TInvocationExpressionSyntax, TIndexerSyntax> GetCallInfoContext(
12+
SemanticModel semanticModel, SyntaxNode syntaxNode)
13+
{
14+
var callInfoParameterSymbol = GetCallInfoParameterSymbol(semanticModel, syntaxNode);
15+
if (callInfoParameterSymbol == null)
16+
{
17+
return CallInfoContext<TInvocationExpressionSyntax, TIndexerSyntax>.Empty;
18+
}
19+
20+
var callContext = GetCallInfoContextInternal(semanticModel, syntaxNode);
21+
22+
return CreateFilteredCallInfoContext(semanticModel, callContext, callInfoParameterSymbol);
23+
}
24+
25+
protected abstract CallInfoContext<TInvocationExpressionSyntax, TIndexerSyntax> GetCallInfoContextInternal(SemanticModel semanticModel, SyntaxNode syntaxNode);
26+
27+
private static CallInfoContext<TInvocationExpressionSyntax, TIndexerSyntax> CreateFilteredCallInfoContext(
28+
SemanticModel semanticModel,
29+
CallInfoContext<TInvocationExpressionSyntax, TIndexerSyntax> callContext,
30+
IParameterSymbol callInfoParameterSymbol)
31+
{
32+
return new CallInfoContext<TInvocationExpressionSyntax, TIndexerSyntax>(
33+
argAtInvocations: GetMatchingNodes(semanticModel, callContext.ArgAtInvocations, callInfoParameterSymbol),
34+
argInvocations: GetMatchingNodes(semanticModel, callContext.ArgInvocations, callInfoParameterSymbol),
35+
indexerAccesses: GetMatchingNodes(semanticModel, callContext.IndexerAccesses, callInfoParameterSymbol));
36+
}
37+
38+
private static IReadOnlyList<T> GetMatchingNodes<T>(
39+
SemanticModel semanticModel,
40+
IReadOnlyList<T> nodes,
41+
IParameterSymbol parameterSymbol) where T : SyntaxNode
42+
{
43+
return nodes.Where(node => HasMatchingParameterReference(semanticModel, node, parameterSymbol)).ToList();
44+
}
45+
46+
private static bool HasMatchingParameterReference(
47+
SemanticModel semanticModel,
48+
SyntaxNode syntaxNode,
49+
IParameterSymbol callInfoParameterSymbol)
50+
{
51+
var parameterReferenceOperation = FindMatchingParameterReference(semanticModel, syntaxNode);
52+
53+
return parameterReferenceOperation != null &&
54+
parameterReferenceOperation.Parameter.Equals(callInfoParameterSymbol);
55+
}
56+
57+
private static IParameterReferenceOperation FindMatchingParameterReference(SemanticModel semanticModel, SyntaxNode syntaxNode)
58+
{
59+
var operation = semanticModel.GetOperation(syntaxNode);
60+
return FindMatchingParameterReference(operation);
61+
}
62+
63+
private static IParameterReferenceOperation FindMatchingParameterReference(IOperation operation)
64+
{
65+
IParameterReferenceOperation parameterReferenceOperation = null;
66+
switch (operation)
67+
{
68+
case IInvocationOperation invocationOperation:
69+
parameterReferenceOperation = invocationOperation.Instance as IParameterReferenceOperation;
70+
break;
71+
case IPropertyReferenceOperation propertyReferenceOperation:
72+
parameterReferenceOperation = propertyReferenceOperation.Instance as IParameterReferenceOperation;
73+
break;
74+
}
75+
76+
if (parameterReferenceOperation != null)
77+
{
78+
return parameterReferenceOperation;
79+
}
80+
81+
foreach (var innerOperation in operation?.Children ?? Enumerable.Empty<IOperation>())
82+
{
83+
parameterReferenceOperation = FindMatchingParameterReference(innerOperation);
84+
if (parameterReferenceOperation != null)
85+
{
86+
return parameterReferenceOperation;
87+
}
88+
}
89+
90+
return null;
91+
}
92+
93+
private static IParameterSymbol GetCallInfoParameterSymbol(SemanticModel semanticModel, SyntaxNode syntaxNode)
94+
{
95+
if (semanticModel.GetSymbolInfo(syntaxNode).Symbol is IMethodSymbol methodSymbol && methodSymbol.MethodKind != MethodKind.Constructor)
96+
{
97+
return methodSymbol.Parameters.FirstOrDefault();
98+
}
99+
100+
return null;
101+
}
102+
}
103+
}

src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/CallInfoContext.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
1+
using System;
12
using System.Collections.Generic;
3+
using Microsoft.CodeAnalysis;
24

35
namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers
46
{
57
internal class CallInfoContext<TInvocationExpressionSyntax, TIndexerSyntax>
8+
where TInvocationExpressionSyntax : SyntaxNode where TIndexerSyntax : SyntaxNode
69
{
7-
public List<TIndexerSyntax> IndexerAccesses { get; }
10+
public IReadOnlyList<TIndexerSyntax> IndexerAccesses { get; }
811

9-
public List<TInvocationExpressionSyntax> ArgAtInvocations { get; }
12+
public IReadOnlyList<TInvocationExpressionSyntax> ArgAtInvocations { get; }
1013

11-
public List<TInvocationExpressionSyntax> ArgInvocations { get; }
14+
public IReadOnlyList<TInvocationExpressionSyntax> ArgInvocations { get; }
15+
16+
public static CallInfoContext<TInvocationExpressionSyntax, TIndexerSyntax> Empty { get; } =
17+
new CallInfoContext<TInvocationExpressionSyntax, TIndexerSyntax>(
18+
Array.Empty<TInvocationExpressionSyntax>(),
19+
Array.Empty<TInvocationExpressionSyntax>(),
20+
Array.Empty<TIndexerSyntax>());
1221

1322
public CallInfoContext(
14-
List<TInvocationExpressionSyntax> argAtInvocations,
15-
List<TInvocationExpressionSyntax> argInvocations,
16-
List<TIndexerSyntax> indexerAccesses)
23+
IReadOnlyList<TInvocationExpressionSyntax> argAtInvocations,
24+
IReadOnlyList<TInvocationExpressionSyntax> argInvocations,
25+
IReadOnlyList<TIndexerSyntax> indexerAccesses)
1726
{
1827
IndexerAccesses = indexerAccesses;
1928
ArgAtInvocations = argAtInvocations;

src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/ICallInfoFinder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers
44
{
55
internal interface ICallInfoFinder<TInvocationExpressionSyntax, TIndexerSyntax>
6+
where TInvocationExpressionSyntax : SyntaxNode where TIndexerSyntax : SyntaxNode
67
{
78
CallInfoContext<TInvocationExpressionSyntax, TIndexerSyntax> GetCallInfoContext(SemanticModel semanticModel, SyntaxNode syntaxNode);
89
}

src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoCallFinder.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,23 @@
77

88
namespace NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers
99
{
10-
internal class CallInfoCallFinder : ICallInfoFinder<InvocationExpressionSyntax, InvocationExpressionSyntax>
10+
internal class CallInfoCallFinder : AbstractCallInfoFinder<InvocationExpressionSyntax, InvocationExpressionSyntax>
1111
{
1212
public static CallInfoCallFinder Instance { get; } = new CallInfoCallFinder();
1313

1414
private CallInfoCallFinder()
1515
{
1616
}
1717

18-
public CallInfoContext<InvocationExpressionSyntax, InvocationExpressionSyntax> GetCallInfoContext(SemanticModel semanticModel, SyntaxNode syntaxNode)
18+
protected override CallInfoContext<InvocationExpressionSyntax, InvocationExpressionSyntax> GetCallInfoContextInternal(SemanticModel semanticModel, SyntaxNode syntaxNode)
1919
{
2020
var visitor = new CallInfoVisitor(semanticModel);
2121
visitor.Visit(syntaxNode);
2222

23-
return new CallInfoContext<InvocationExpressionSyntax, InvocationExpressionSyntax>(visitor.ArgAtInvocations, visitor.ArgInvocations, visitor.DirectIndexerAccesses);
23+
return new CallInfoContext<InvocationExpressionSyntax, InvocationExpressionSyntax>(
24+
argAtInvocations: visitor.ArgAtInvocations,
25+
argInvocations: visitor.ArgInvocations,
26+
indexerAccesses: visitor.DirectIndexerAccesses);
2427
}
2528

2629
private class CallInfoVisitor : VisualBasicSyntaxWalker

0 commit comments

Comments
 (0)