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 need to work with types. You may want to find an argument of a specific type, or check if a method is declared on a specific type. For example, your analyzer may need to answer questions like: What is the type of sample in var sample = new Test();? Does the Test class have a constructor with a CancellationToken parameter? This post covers how to work with types in Roslyn.
#How to get a Compilation instance?
The compilation object represents the assembly being compiled. It gives you access to the code of the assembly and all referenced assemblies, making all accessible types available. The Compilation class also exposes methods for finding specific types, as covered later. First, you need to obtain a Compilation instance from your analyzer. Here are a few ways to do so, 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
For types not covered by SpecialType, use Compilation.GetTypeByMetadataName, which takes the fully qualified type name as a parameter:
C#
INamedTypeSymbol consoleType = compilation.GetTypeByMetadataName("System.Console");
#Finding generic types
Working with generic types such as Nullable<int> requires an extra step: first get the open generic type Nullable<T>, then construct the specific closed 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 offer an alternative way to reference constructed types. For example, instead of using GetTypeByMetadataName and Construct to obtain 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. Use the semantic model to retrieve this information:
C#
var type = context.SemanticModel.GetTypeInfo(node).Type;
var convertedType = context.SemanticModel.GetTypeInfo(node).ConvertedType; // type after an implicit conversion
For example, here is 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), 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 that case, you may want to resolve the most accessible type. Here is an extension method to do that:
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 determine the nullability state of an 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 GetTypeByMetadataName method returns null if a type is defined in more than one assembly. To retrieve all types matching a fully qualified name, you need to iterate over all assemblies and call GetTypeByMetadataName on each one.
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;
For types not available in SpecialType, find the type using one of the approaches described earlier, such as compilation.GetTypeByMetadataName, then use the Equals method to compare ITypeSymbol instances.
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 does not need to run if a specific type or method is unavailable. Use context.RegisterCompilationStartAction to look up the type when the compilation is ready. If the type is found, register the analyzer for the relevant node or operation kind using compilationContext.RegisterXXX(). This avoids unnecessary work during analysis.
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!