Working with types in a Roslyn analyzer

 
 
  • Gérald Barré

This post is part of the series 'Roslyn Analyzers'. Be sure to check out the rest of the blog posts of the series!

When creating a Roslyn analyzer, you almost always have to work with types. For instance, you may want to find an argument of a specific type or check if a method is declared on a specific type. For instance, your analyzer may need to answer the following question: What is the type of the sample in var sample = new Test();? Or does the Test class has a constructor with a CancellationToken parameter? To answer these questions, you need to understand how to work with types with Roslyn. That's what we'll see in this post!

#How to get a Compilation instance?

The compilation object represents the assembly you are compiling. This gives you access to the code of the assembly and all the assemblies it references. This means you'll have access to all the types accessible by your code. The compilation class also exposes some methods to find some types as we'll see later. But first, you need to get an instance of the compilation from an analyzer. Here're a few possible ways depending on what your analyzer does:

C#
public class SampleAnalyzer : DiagnosticAnalyzer
{
    public override void Initialize(AnalysisContext context)
    {
        context.RegisterCompilationStartAction((CompilationStartAnalysisContext ctx) =>
        {
            Compilation compilation = ctx.Compilation;
            ...
        });

        context.RegisterOperationAction((OperationAnalysisContext ctx) =>
        {
            Compilation compilation = ctx.Compilation;
            ...
        }, OperationKind.MethodBodyOperation);

        context.RegisterSymbolAction((SymbolAnalysisContext ctx) =>
        {
            Compilation compilation = ctx.Compilation;
            ...
        }, SymbolKind.Method);

        context.RegisterSyntaxNodeAction((SyntaxNodeAnalysisContext ctx) =>
        {
            Compilation compilation = ctx.Compilation;
            ...
        }, SyntaxKind.MethodDeclaration);
    }
}

#Finding well-known types

Most common types such as string, object, int or DateTime are available using the method Compilation.GetSpecialType:

C#
INamedTypeSymbol stringType = compilation.GetSpecialType(SpecialType.System_String);

#Finding other types

You don't always want to use well-known types, so you'll need to use Compilation.GetTypeByMetadataName. This method takes the full name of the type you are looking for as a parameter:

C#
INamedTypeSymbol consoleType = compilation.GetTypeByMetadataName("System.Console");

#Finding generic types

Working with generic types, such as Nullable<int>, is a little more complicated. First, you need to get the type Nullable<T>, and then construct the specific type.

C#
// Get the type Nullable<T>
// `1 because Nullable<T> has 1 generic parameter
INamedTypeSymbol nullableT = compilation.GetTypeByMetadataName("System.Nullable`1");

// Construct Nullable<int> from Nullable<T>
var nullableInt = nullableT.Construct(compilation.GetSpecialType(SpecialType.System_Int32));

// Get the type Dictionary<TKey, TValue>
// `2 because Dictionary<TKey, TValue> has 2 generic parameters
INamedTypeSymbol dictionary = compilation.GetTypeByMetadataName("System.Collections.Generic.Dictionary`2");

// Construct Dictionary<string, int?>
var dictionaryStringInt32 = nullableT.Construct(
      compilation.GetSpecialType(SpecialType.System_String),
      nullableInt);

You can also get the generic type from a constructed type using OriginalDefinition:

C#
// Nullable<int> => Nullable<T>
var nullableT = nullableInt.OriginalDefinition;

#Get the type of a SyntaxNode (variable, parameter, …)

When working with the syntax tree, you may need to get the type of a variable or any other syntax node. You can use the semantic model to get the information you want.

C#
var type = context.SemanticModel.GetTypeInfo(node).Type;
var convertedType = context.SemanticModel.GetTypeInfo(node).ConvertedType; // type after an implicit conversion

For instance, here's the difference between Type and ConvertedType

C#
int? a = 10; // SyntaxNode = 10, Type = int, ConvertedType = int?
int? b = (int?)10; // SyntaxNode = (int?)10, Type = int?, ConvertedType = int?
string c = ""; // SyntaxNode = "", Type = string, ConvertedType = string

If the syntax node is a type declaration (class, struct, interface or enum), you have to use GetDeclaredSymbol to get the declared type:

C#
StructDeclarationSyntax node;
INamedTypeSymbol symbol = context.SemanticModel.GetDeclaredSymbol(node);

#Checking if a type is an array

C#
var type = context.SemanticModel.GetTypeInfo(node).Type;
var isArray = type.TypeKind == TypeKind.Array;
if (type is IArrayTypeSymbol arrayTypeSymbol)
{
    var elementType = arrayTypeSymbol.ElementType;
}

#Checking the nullable annotation of a type

If the compilation uses Nullable Reference Types, you can use NullableAnnotation to know the present nullability state of the expression.

C#
var type = context.SemanticModel.GetTypeInfo(node).Type;
var annotation = type.NullableAnnotation;

// The expression is annotated (does have a ?).
_ = annotation == NullableAnnotation.Annotated;

// The expression is not annotated (does not have a ?).
_ = annotation == NullableAnnotation.NotAnnotated;

To better understand, let's look at the following example:

C#
void A(string? value) // value: NullableAnnotation.Annotated
{
    if (value != null) // value: NullableAnnotation.Annotated
    {
        _ = value; // value: NullableAnnotation.NotAnnotated
    }

    _ = value; // value: NullableAnnotation.Annotated
}

#Searching a type in all available assemblies

The method GetTypeByMetadataName returns null if a type is defined in 2 different assemblies. So, if you want to get all types that match a full name, you have to look at all assemblies and call GetTypeByMetadataName per assembly.

C#
public static IEnumerable<INamedTypeSymbol> GetTypesByMetadataName(this Compilation compilation, string typeMetadataName)
{
    return compilation.References
        .Select(compilation.GetAssemblyOrModuleSymbol)
        .OfType<IAssemblySymbol>()
        .Select(assemblySymbol => assemblySymbol.GetTypeByMetadataName(typeMetadataName))
        .Where(t => t != null);
}

#Comparing types

If you want to compare with a special type, you can compare the SpecialType property with the desired type:

C#
var areEquals = variableTypeInfo.SpecialType == SpecialType.System_String;

If you want to compare types that are not defined in SpecialType, you have to find the type using one of the ways described here before, for instance, compilation.GetTypeByMetadataName. Then, you must use the Equals method to compare the instances of ITypeSymbol.

C#
var type1 = compilation.GetTypeByMetadataName("System.Threading.Tasks.Task");
var type2 = context.SemanticModel.GetTypeInfo(syntaxNode).Type;

var areEqual = SymbolEqualityComparer.Default.Equals(type1, type2);
var areEqualWithNullability = SymbolEqualityComparer.IncludeNullability.Equals(type1, type2);

#Checking a type implements an interface

You can test if a symbol such as a class, a struct, or an interface implements a specific interface:

C#
private static bool Implements(INamedTypeSymbol symbol, ITypeSymbol type)
{
    return symbol.AllInterfaces.Any(i => type.Equals(i));
}

#Checking a type inherits from a base class

You can test if a symbol such as a class inherits from a specific class:

C#
private static bool InheritsFrom(INamedTypeSymbol symbol, ITypeSymbol type)
{
    var baseType = symbol.BaseType;
    while (baseType != null)
    {
        if (type.Equals(baseType))
            return true;

        baseType = baseType.BaseType;
    }

    return false;
}

#Registering your analyzer only if a specific type exists

Often, your analyzer doesn't need to run if a specific type or method is not available. You can use context.RegisterCompilationStartAction to search for the type when the compilation is ready. If what you are looking for is available, you can then use compilationContext.RegisterXXX() to register the analyzer on a kind of nodes or operations. This way you optimize your analyzer won't do too much useless work.

C#
public override void Initialize(AnalysisContext context)
{
    context.RegisterCompilationStartAction(compilationContext =>
    {
        // Search Meziantou.SampleType
        var typeSymbol = compilationContext.Compilation.GetTypeByMetadataName("Meziantou.SampleType");
        if (typeSymbol == null)
            return;

        // register the analyzer on Method symbol
        compilationContext.RegisterSymbolAction(Analyze, SymbolKind.Method);
    });
}

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub