Analyzing the Order of Method Calls
Recently there was a question on StackOverflow that piqued my interest. The question was basically asking how an analyzer could ensure that one method on an instance was called prior to calling another method on an instance.
An example of this would be if you wanted to ensure you called the HasValue
method on a System.Nullable
before you called the Value
property. I provided an answer to the question, but let's dig a little deeper here.
Since we want to look at System.Nullable
, we'll first need to get that type to check it against variable types when doing our analysis. To do this, we will register an action to run on compilation start, as I demonstrated in the Working with Types in Your Analyzer post. We'll also register a syntax node action to analyze all MethodDeclaration
nodes.
context.RegisterCompilationStartAction((compilationStartContext) =>
{
var nullableType = compilationStartContext.Compilation.GetTypeByMetadataName("System.Nullable`1");
compilationStartContext.RegisterSyntaxNodeAction((analysisContext) =>
{
// ...
}, SyntaxKind.MethodDeclaration);
});
Next, we will capture all MemberAccessExpressionSyntax
nodes from the method, which will give us all nodes where an object is accessed (i.e. method calls, property calls, field accesses, ...).
var invocations =
analysisContext.Node.DescendantNodes().OfType<MemberAccessExpressionSyntax>();
We'll also create a HashSet<string>
to keep track of all the HasValue
calls for a given variable.
var hasValueCalls = new HashSet<string>();
Now we can iterate over the invocations to keep track of the HasValue
and Value
calls. To start, we check if the Expression on the invocation is an
IdentifierNameSyntax`:
foreach (var invocation in invocations)
{
var e = invocation.Expression as IdentifierNameSyntax;
if (e == null)
continue;
//...
}
Next we will use the Semantic Model to get the type information from the expression. To do this we simply, call the GetTypeInfo
method on the SemanticModel:
var typeInfo = analysisContext.SemanticModel.GetTypeInfo(e).Type as INamedTypeSymbol;
We now have the type info, so we will check that it is a System.Nullable
type. Since all base nullable types (i.e. int?
, bool?
, ...) are constructed from the System.Nullable
type, we can use the nullableType
we captured earlier to verify the type.
if (typeInfo?.ConstructedFrom == null)
continue;
if (!typeInfo.ConstructedFrom.Equals(nullableType))
continue;
At this point, we know that the variable is a System.Nullable
, so we can now check if the invocation is a HasValue
or a Value
call. If it is a HasValue
call, we will add that variable to our HashSet, so we know that HasValue
has been called on this variable.
string variableName = e.Identifier.Text;
if (invocation.Name.ToString() == "HasValue")
{
hasValueCalls.Add(variableName);
}
Finally, we can check the Value
calls and see if there was a previous HasValue
call for the variable name. If not, we can raise a diagnostic.
if (invocation.Name.ToString() == "Value")
{
if (!hasValueCalls.Contains(variableName))
{
analysisContext.ReportDiagnostic(Diagnostic.Create(Rule, e.GetLocation()));
}
}
So now, if we try some code against this analyzer, we will see the following code raise a diagnostic:
int? x = null;
var n = x.Value;
While the following code will not raise a diagnostic:
int? x = null;
if (x.HasValue)
{
var n = x.Value;
}
In the end, the full diagnostic is:
public override void Initialize(AnalysisContext context)
{
context.RegisterCompilationStartAction((compilationStartContext) =>
{
var nullableType = compilationStartContext.Compilation.GetTypeByMetadataName("System.Nullable`1");
compilationStartContext.RegisterSyntaxNodeAction((analysisContext) =>
{
var invocations =
analysisContext.Node.DescendantNodes().OfType<MemberAccessExpressionSyntax>();
var hasValueCalls = new HashSet<string>();
foreach (var invocation in invocations)
{
var e = invocation.Expression as IdentifierNameSyntax;
if (e == null)
continue;
var typeInfo = analysisContext.SemanticModel.GetTypeInfo(e).Type as INamedTypeSymbol;
if (typeInfo?.ConstructedFrom == null)
continue;
if (!typeInfo.ConstructedFrom.Equals(nullableType))
continue;
string variableName = e.Identifier.Text;
if (invocation.Name.ToString() == "HasValue")
{
hasValueCalls.Add(variableName);
}
if (invocation.Name.ToString() == "Value")
{
if (!hasValueCalls.Contains(variableName))
{
analysisContext.ReportDiagnostic(Diagnostic.Create(Rule, e.GetLocation()));
}
}
}
}, SyntaxKind.MethodDeclaration);
});
}
Getting the order of method calls can be a bit tricky, but once you work out the logic and the syntax nodes that you need to process, the work to be done becomes clear. If you have a story of an analyzer you wrote that does something similar, share it in the comments below.