In my last post we discussed how to work with types in your analyzers. We even covered the basics of getting a generic type. In this post, we will go a little deeper and look at how you can analyze the generic type parameters used in the creation of a generic type. We will even delve a little into construction parameters, so we can write an analyzer that verifies you specify an IEqualityComparer when you use a string as the key to your dictionary.

To start, as a refresher from last time, in order to access types from the Roslyn compiler we need to register a compilation start action and get the metadata type by name.

context.RegisterCompilationStartAction(compilationContext =>
{
    var dictionaryTokenType = compilationContext.Compilation.GetTypeByMetadataName("System.Collections.Generic.Dictionary`2");

To get the dictionary type, we are using the name of the generic class plus the arity to determine the metadata name.

Once we have done that, we can then register a SyntaxNodeAction for all ObjectCreationExpressions. We will start by verifying the type is a dictionary type.

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;
...

Now we are at a point where we can start looking at the generic type parameters to determine if this is a dictionary we care about. Since I am looking for a string as the key, I can use the SpecialType.System_String to check against a type. To access the generic type parameters, you examine the TypeArguments on the INamedTypeSymbol.

if (variableTypeInfo.TypeArguments.First().SpecialType != SpecialType.System_String)
    return;

Since we know we are dealing with a dictionary and it uses the key as the first parameter, we simply check the first type argument to see if it is a string. Now that we have the type determination out of the way we can now start looking at the constructor arguments to determine if an equality comparer was passed in. First, in our compilation start action we need to capture the IEqualityComparer type:

var equalityComparerInterfaceType = compilationContext.Compilation.GetTypeByMetadataName("System.Collections.Generic.IEqualityComparer`1");

Again, this is a generic with an arity of 1, so we can use that information to generate the correct metadata name. Next we can access the ArgumentList off of the ObjectCreatoinExpressionSyntax node to check to see if any of the arguments match that type. The first check to perform is to simply check if there are any constructor parameters sent:

var arguments = creationNode.ArgumentList?.Arguments;
if (arguments==null || arguments.Value.Count == 0)
{
    symbolContext.ReportDiagnostic(Diagnostic.Create(Rule, symbolContext.Node.GetLocation()));
    return;
}

Next, we can loop over the arguments to see if any of them are of the type IEqualityComparer`1. If there is no equality comparer, then we can raise a diagnostic:

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()));
}

Now that we have all of that wired up, our analyzer will properly report a diagnostic on a dictionary with a string key that does not specify the equality comparer when the dictionary is constructed.

Given the following sample code, you can see when the diagnostic would be triggered and when it would not:

// Will trigger diagnostic
var foo = new Dictionary<string, string>();
var foo1 = new Dictionary<string, string>(6);
var foo2 = new Dictionary<string, string>(foo);

// Will not trigger diagnostic
var bar1 = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase);
var bar2 = new Dictionary<string, string>(6, StringComparer.CurrentCultureIgnoreCase);
var bar3 = new Dictionary<string, string>(bar2, StringComparer.CurrentCultureIgnoreCase);

The full text of the analyzer is available on github if you want to test it out for yourself.

If you have ever used a string as a key to a dictionary and have been bitten by case sensitivity issues, this type of rule could have helped you prevent those types of bugs before they ever happened. Clearly, there is no magic going on here and it is very easy to wire up an analyzer that can make your life easier in just a few short minutes. At some point in the future, we can revisit this analyzer and see how we could handle the ToDictionary extension method to ensure that any dictionary with a string key is done correctly in our applications.