Analyzing Dictionaries with String Keys that are Created Using ToDictionary
In the past we have covered working with types in your analyzer. We even built an analyzer that looked at generic types to work with dictionaries that have string
keys to ensure they specified a string comparer. Since we already have that built, let's modify the existing analyzer to handle the ToDictionary
call on an IEnumerable.
Let's start with the existing code for the analyzer:
public override void Initialize(AnalysisContext context)
{
context.RegisterCompilationStartAction(compilationContext =>
{
var dictionaryTokenType = compilationContext.Compilation.GetTypeByMetadataName("System.Collections.Generic.Dictionary`2");
var equalityComparerInterfaceType = compilationContext.Compilation.GetTypeByMetadataName("System.Collections.Generic.IEqualityComparer`1");
if (dictionaryTokenType != null)
{
compilationContext.RegisterSyntaxNodeAction(symbolContext =>
{
var creationNode = (ObjectCreationExpressionSyntax)symbolContext.Node;
var variableTypeInfo = symbolContext.SemanticModel.GetTypeInfo(symbolContext.Node).ConvertedType as INamedTypeSymbol;
if (variableTypeInfo == null)
return;
if (!variableTypeInfo.OriginalDefinition.Equals(dictionaryTokenType))
return;
// We only care about dictionaries who use a string as the key
if (variableTypeInfo.TypeArguments.First().SpecialType != SpecialType.System_String)
return;
var arguments = creationNode.ArgumentList?.Arguments;
if (arguments == null || arguments.Value.Count == 0)
{
symbolContext.ReportDiagnostic(Diagnostic.Create(Rule, symbolContext.Node.GetLocation()));
return;
}
bool hasEqualityComparer = false;
foreach (var argument in arguments)
{
var argumentType = symbolContext.SemanticModel.GetTypeInfo(argument.Expression);
if (argumentType.ConvertedType == null)
return;
if (argumentType.ConvertedType.OriginalDefinition.Equals(equalityComparerInterfaceType))
{
hasEqualityComparer = true;
break;
}
}
if (!hasEqualityComparer)
{
symbolContext.ReportDiagnostic(Diagnostic.Create(Rule, symbolContext.Node.GetLocation()));
}
}, SyntaxKind.ObjectCreationExpression);
}
});
}
In order to modify this code, we need to do a few things. First, we need to change from using ObjectCreatoinExpressoin
to InvocationExpression
. Simply modify the syntax kind on the RegisterSyntaxNodeAction
and update the cast to use an InvocationExpressionSyntax
(Note, I also changed the variable name for clarity):
compilationContext.RegisterSyntaxNodeAction(symbolContext =>
{
var invocationNode = (InvocationExpressionSyntax)symbolContext.Node;
/* rest of the analyzer */
}, SyntaxKind.InvocationExpression);
With that done, you can now run the code and it will catch all ToDictionary
calls that return a Dictionary<string, X>
, unfortunately, it will also catch any other method calls that happen to return a dictionary with a string key. To fix this, we need to check the method invocation to make sure it is a ToDictionary
call. To do this, we can access the expression from the invocationNode
and check the identifier text to see if is ToDictionary
:
var expression = invocationNode.Expression as MemberAccessExpressionSyntax;
var name = expression?.Name.Identifier.Text;
if (name == null || !name.Equals("ToDictionary"))
{
return;
}
That's it. The full code for the analyzer is:
public override void Initialize(AnalysisContext context)
{
context.RegisterCompilationStartAction(compilationContext =>
{
var dictionaryTokenType = compilationContext.Compilation.GetTypeByMetadataName("System.Collections.Generic.Dictionary`2");
var equalityComparerInterfaceType = compilationContext.Compilation.GetTypeByMetadataName("System.Collections.Generic.IEqualityComparer`1");
if (dictionaryTokenType != null)
{
compilationContext.RegisterSyntaxNodeAction(symbolContext =>
{
var invocationNode = (InvocationExpressionSyntax)symbolContext.Node;
var variableTypeInfo = symbolContext.SemanticModel.GetTypeInfo(symbolContext.Node).ConvertedType as INamedTypeSymbol;
if (variableTypeInfo == null)
return;
if (!variableTypeInfo.OriginalDefinition.Equals(dictionaryTokenType))
return;
// We only care about dictionaries who use a string as the key
if (variableTypeInfo.TypeArguments.First().SpecialType != SpecialType.System_String)
return;
var expression = invocationNode.Expression as MemberAccessExpressionSyntax;
var name = expression?.Name.Identifier.Text;
if (name == null || !name.Equals("ToDictionary"))
{
return;
}
var arguments = invocationNode.ArgumentList?.Arguments;
if (arguments == null || arguments.Value.Count == 0)
{
symbolContext.ReportDiagnostic(Diagnostic.Create(Rule, symbolContext.Node.GetLocation()));
return;
}
bool hasEqualityComparer = false;
foreach (var argument in arguments)
{
var argumentType = symbolContext.SemanticModel.GetTypeInfo(argument.Expression);
if (argumentType.ConvertedType == null)
return;
if (argumentType.ConvertedType.OriginalDefinition.Equals(equalityComparerInterfaceType))
{
hasEqualityComparer = true;
break;
}
}
if (!hasEqualityComparer)
{
symbolContext.ReportDiagnostic(Diagnostic.Create(Rule, symbolContext.Node.GetLocation()));
}
}, SyntaxKind.InvocationExpression);
}
});
}
That is all you need to update to get a functioning analyzer that will handle the ToDictionary
calls on an IEnumerable to ensure that you have dictionaries that explicitly define their string comparers. Obviously, this analyzer isn't perfect as it technically catches any method called ToDictionary
regardless if the method is the IEnumerable
ToDictionary
or some other ToDictionary
. I'll leave that fix as an exercise for the reader :).