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 have 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 are 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 nullableOfT = compilation.GetTypeByMetadataName("System.Nullable`1");

// Construct Nullable<int> from Nullable<T>
var nullableInt = nullableOfT.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 = dictionary.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 nullableOfT = nullableInt.OriginalDefinition;

#Get a type from a documentation comment ID

Documentation Comment IDs are useful to get a reference to a constructed type. In the previous section, I used GetTypeByMetadataName and Construct to get a Nullable<int> symbol

C#
var nullableOfT = compilation.GetTypeByMetadataName("System.Nullable`1");
var nullableOfInt32 = nullableOfT.Construct(compilation.GetSpecialType(SpecialType.System_Int32));

You can use a Documentation Comment ID to get the same type:

C#
var nullableOfInt32 = DocumentationCommentId.GetFirstSymbolForReferenceId("System.Nullable{System.Int32}", compilation);

If you don't know how to construct the XML Comment ID, you can use JetBrains Rider:

#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);

#Get the accessible types by metadata name

When multiple assemblies define the same type, GetTypeByMetadataName returns null. In this case, you may want to get the only accessible type. Here's an extension method to get the type by metadata name:

C#
static class CompilationExtensions
{
    public static INamedTypeSymbol? GetBestTypeByMetadataName(this Compilation compilation, string fullyQualifiedMetadataName)
    {
        INamedTypeSymbol? type = null;

        foreach (var currentType in compilation.GetTypesByMetadataName(fullyQualifiedMetadataName))
        {
            if (ReferenceEquals(currentType.ContainingAssembly, compilation.Assembly))
            {
                Debug.Assert(type is null);
                return currentType;
            }

            switch (currentType.GetResultantVisibility())
            {
                case SymbolVisibility.Public:
                case SymbolVisibility.Internal when currentType.ContainingAssembly.GivesAccessTo(compilation.Assembly):
                    break;

                default:
                    continue;
            }

            if (type is object)
            {
                // Multiple visible types with the same metadata name are present
                return null;
            }

            type = currentType;
        }

        return type;
    }

    // https://github.com/dotnet/roslyn/blob/d2ff1d83e8fde6165531ad83f0e5b1ae95908289/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ISymbolExtensions.cs#L28-L73
    private static SymbolVisibility GetResultantVisibility(this ISymbol symbol)
    {
        // Start by assuming it's visible.
        var visibility = SymbolVisibility.Public;
        switch (symbol.Kind)
        {
            case SymbolKind.Alias:
                // Aliases are uber private.  They're only visible in the same file that they
                // were declared in.
                return SymbolVisibility.Private;
            case SymbolKind.Parameter:
                // Parameters are only as visible as their containing symbol
                return GetResultantVisibility(symbol.ContainingSymbol);
            case SymbolKind.TypeParameter:
                // Type Parameters are private.
                return SymbolVisibility.Private;
        }
        while (symbol is not null && symbol.Kind != SymbolKind.Namespace)
        {
            switch (symbol.DeclaredAccessibility)
            {
                // If we see anything private, then the symbol is private.
                case Accessibility.NotApplicable:
                case Accessibility.Private:
                    return SymbolVisibility.Private;
                // If we see anything internal, then knock it down from public to
                // internal.
                case Accessibility.Internal:
                case Accessibility.ProtectedAndInternal:
                    visibility = SymbolVisibility.Internal;
                    break;
                    // For anything else (Public, Protected, ProtectedOrInternal), the
                    // symbol stays at the level we've gotten so far.
            }
            symbol = symbol.ContainingSymbol;
        }
        return visibility;
    }

    private enum SymbolVisibility
    {
        Public,
        Internal,
        Private,
    }
}

You can then get the symbol using the following code:

C#
var type = compilation.GetBestTypeByMetadataName("System.Diagnostics.CodeAnalysis.MaybeNullAttribute");

#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 for 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 (SymbolEqualityComparer.Default.Equals(type, baseType))
            return true;

        baseType = baseType.BaseType;
    }

    return false;
}

public static bool IsOrInheritFrom(this ITypeSymbol symbol, ITypeSymbol expectedType)
{
    return SymbolEqualityComparer.Default.Equals(symbol, expectedType) || symbol.InheritsFrom(expectedType);
}

#Checking if a type is decorated with an attribute

C#
public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeType, bool inherits = true)
{
    if (attributeType.IsSealed)
    {
        inherits = false;
    }

    foreach (var attribute in symbol.GetAttributes())
    {
        if (attribute.AttributeClass is null)
            continue;

        if (inherits)
        {
            if (attribute.AttributeClass.IsOrInheritFrom(attributeType))
                return attribute;
        }
        else
        {
            if (SymbolEqualityComparer.Default.Equals(attributeType, attribute.AttributeClass))
                return attribute;
        }
    }

    return null;
}

public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attributeType, bool inherits = true)
{
    return GetAttribute(symbol, attributeType, inherits) is not null;
}

#Getting underlying types for Nullable<T>

C#
[return: NotNullIfNotNull(nameof(typeSymbol))]
public static ITypeSymbol? GetUnderlyingNullableTypeOrSelf(this ITypeSymbol? typeSymbol)
{
    if (typeSymbol is INamedTypeSymbol namedTypeSymbol)
    {
        if (namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T && namedTypeSymbol.TypeArguments.Length == 1)
        {
            return namedTypeSymbol.TypeArguments[0];
        }
    }

    return null;
}

#Checking if a type is visible outside the assembly

C#
public static bool IsVisibleOutsideOfAssembly(this ISymbol symbol)
{
    if (symbol.DeclaredAccessibility != Accessibility.Public &&
        symbol.DeclaredAccessibility != Accessibility.Protected &&
        symbol.DeclaredAccessibility != Accessibility.ProtectedOrInternal)
    {
        return false;
    }

    if (symbol.ContainingType is null)
        return true;

    return IsVisibleOutsideOfAssembly(symbol.ContainingType);
}

#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 node or operation. This way you optimize your analyzer and won't do too much useless work.

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

        // register the analyzer on Method symbol
        context.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