Roslyn Code Analysis in Easy Samples (Part 2)
Introduction
In Roslyn Code Analysis in Easy Samples (Part 1) we described some basic Roslyn features that allow code analysis. In this second part we are showing how to analyze more complex classes and methods, including
- Generic Classes
- Generic Methods
- Methods with variable number of arguments
- Attributes whose constructors have variable number of arguments
Installing Roslyn Dlls
All the samples are built using VS2015 preview. I decided against switching to VS2015 CTP for now in order to save time. Once the VS2015 full version is released, I'll upload the samples for that version. I assume that installing roslyn related dlls is exactly the same in VS2015 CTP as it is in Preview version.
In VS 2015 Preview you need to use the package manager to install your Roslyn dlls as described at Roslyn.
Code Location and Description
Just like in Roslyn Code Analysis in Easy Samples (Part 1), the code consists of two solutions - solution SampleToAnalyze.sln
contains the code that is being analized, while solution RoslynAnalysis.sln
contains that Roslyn based code analysis.
SampleToAnalyze
Let us first take a peek at SampleToAnalyze
.
It has a very simple interface MyInterface
containing only one method - MyMethod
:
public interface MyInterface
{
int MyMethod();
}
ClassToAnalyze
is the main object of analysis. It is a class with generic arguments some of which are specified to implement MyInterface
(this is the only purpose why this interface is needed in the project):
[AttrToAnalize(5, "Str1", "Str2", "Str3")]
public class ClassToAnalyze<T1, T2, T3>
where T1 : class, MyInterface, new()
where T2 : MyInterface
{
public event Action<T1, T2, int> MySimpleEvent;
public int MySimpleMethod<T4, T5>(string str, out bool flag, int i = 5)
where T4 : class, MyInterface, new()
where T5 : MyInterface, new()
{
flag = true;
return 5;
}
public void MyVarArgMethod(string str, params int[] ints)
{
}
}
As you can see, the class contains event MySimpleEvent
and two methods MySimpleMethod(...)
and MyVarArgMethod(...)
.
The event has a Action<T1, T2, int>
that uses generic arguments.
Method MySimpleMethod<T4, T5>(...)
contains generic arguments with some restrictions given the corresponding where
clauses.
Method MyVarArgMethod(...)
is a variable argument method that has params
specifier for its argument ints
.
Class ClassToAnalyze<T1, T2, T3>
also has a class attribute AttrToAnalyze
. Note also that that Attribute
's constructor has variable number of arguments;
public AttrToAnalyzeAttribute(int intProp, params string[] stringProps)
{
IntProp = intProp;
StringProps = stringProps.ToList();
}
RoslynAnalysis
Now let us look at RoslynAnalysis
solution that contains the core of the functionality we want to discuss here.
We get the Roslyn compilation of the project we want to analyse in exactly the same way as we did in part 1:
const string pathToSolution = @"..\..\..\SampleToAnalyze\SampleToAnalyze.sln";
const string projectName = "SampleToAnalyze";
MSBuildWorkspace workspace = MSBuildWorkspace.Create();
Solution solutionToAnalyze =
workspace.OpenSolutionAsync(pathToSolution).Result;
Project sampleProjectToAnalyze =
solutionToAnalyze.Projects
.Where((proj) => proj.Name == projectName)
.FirstOrDefault();
Compilation sampleToAnalyzeCompilation =
sampleProjectToAnalyze.GetCompilationAsync().Result;
Now, we want to pull the type of the class ClassToAnalyze
from the compilation. Note that since the class is generic, we specify the number of generic attributes after a reverse apostrophe at the end of the class name:
string classFullName = "SampleToAnalyze.ClassToAnalyze`3";
INamedTypeSymbol simpleClassToAnalyze =
sampleToAnalyzeCompilation.GetTypeByMetadataName(classFullName);
string fullClassName = simpleClassToAnalyze.GetFullTypeString();
Console.WriteLine("Full class name:");
Console.WriteLine(fullClassName);
Running the above code will print:
Full class name:
ClassToAnalyze<T1, T2, T3>
In order to return correct class name string that contains the generic arguments within <...>
braces, the extension method GetFullTypeString()
has been modified. The method is located within Extensions.cs file:
public static string GetFullTypeString(this INamedTypeSymbol type)
{
string result =
type.Name +
type.GetTypeArgsStr((symbol) => ((INamedTypeSymbol)symbol).TypeArguments);
return result;
}
As you can see it calls another extension method - GetTypeArgsStr(...)
in order to create the string <T1, T2, T3>
corresponding to the generic arguments.
Here is the code for GetTypeArgsStr(...)
method:
static string GetTypeArgsStr
(
this ISymbol symbol,
Func<isymbol, itypesymbol="">> typeArgGetter
)
{
IEnumerable<itypesymbol> typeArgs = typeArgGetter(symbol);
string result = "";
if (typeArgs.Count() > 0)
{
result += "<";
bool isFirstIteration = true;
foreach (ITypeSymbol typeArg in typeArgs)
{
if (isFirstIteration)
{
isFirstIteration = false;
}
else
{
result += ", ";
}
ITypeParameterSymbol typeParameterSymbol =
typeArg as ITypeParameterSymbol;
string strToAdd = null;
if (typeParameterSymbol != null)
{
strToAdd = typeParameterSymbol.Name;
}
else
{
INamedTypeSymbol namedTypeSymbol =
typeArg as INamedTypeSymbol;
strToAdd = namedTypeSymbol.GetFullTypeString();
}
result += strToAdd;
}
result += ">";
}
return result;
}
</itypesymbol></isymbol,>
The code is made more generic that needed for displaying the classes' generic type arguments. It can also be applied to objects other than INamedTypeSymbol
that have TypeArguments
property containing generic type info - e.g. I apply the same method below to extract generic type information from IMethodSymbol
object. This is why the first argument of this method is made ISymbol
and the second argument is a delegate that extracts TypeArguments
information from the first argument.
Note that for class we use (symbol) => ((INamedTypeSymbol)symbol).TypeArguments
as the delegate, while for a method it will (symbol) => ((IMethodSymbol)symbol).TypeArguments
.
The method can also handle type instance declarations or method calls where some of the type arguments might be concrete e.g. ClassToAnalyze<T1, T2, int>
- notice that the last argument in this type declaration is concrete - int
. This is why the method contains the following if
clause:
ITypeParameterSymbol typeParameterSymbol =
typeArg as ITypeParameterSymbol;
string strToAdd = null;
if (typeParameterSymbol != null)
{
strToAdd = typeParameterSymbol.Name;
}
else
{
INamedTypeSymbol namedTypeSymbol =
typeArg as INamedTypeSymbol;
strToAdd = namedTypeSymbol.GetFullTypeString();
}
result += strToAdd;
}
Generic arguments appear in TypeArguments
as ITypeParameterSymbol
objects, while the concrete realization of generic type would appear as INamedTypeSymbol
.
Here is how we print generic type constraints:
Console.WriteLine();
Console.WriteLine("Class Where Statements:");
foreach (var typeParameter in simpleClassToAnalyze.TypeArguments)
{
ITypeParameterSymbol typeParameterSymbol =
typeParameter as ITypeParameterSymbol;
if (typeParameterSymbol != null)
{
string whereStatement = typeParameterSymbol.GetWhereStatement();
if (whereStatement != null)
{
Console.WriteLine(whereStatement);
}
}
}
Here is the implementation of the extension method GetWhereStatement
public static string GetWhereStatement(this ITypeParameterSymbol typeParameterSymbol)
{
string result = "where " + typeParameterSymbol.Name + " : ";
string constraints = "";
bool isFirstConstraint = true;
if (typeParameterSymbol.HasReferenceTypeConstraint)
{
constraints += "class";
isFirstConstraint = false;
}
if (typeParameterSymbol.HasValueTypeConstraint)
{
constraints += "struct";
isFirstConstraint = false;
}
foreach(INamedTypeSymbol contstraintType in typeParameterSymbol.ConstraintTypes)
{
if (!isFirstConstraint)
{
constraints += ", ";
}
else
{
isFirstConstraint = false;
}
constraints += contstraintType.GetFullTypeString();
}
if (string.IsNullOrEmpty(constraints))
return null;
result += constraints;
return result;
}
As you can see from the implmentation - the ITypeParameterSymbol
's HasReferenceTypeConstraint
property specifies whether or not class
constraint is present, HasValueTypeConstraint
specifies whether or not struct
constraint is present and HasConstructorConstraint
specifies whether or not new()
constraint is present.
The other constraints, like being derived from classes or implmenting interfaces are specified in ConstraintTypes
collection (see the loop)
foreach(INamedTypeSymbol contstraintType in typeParameterSymbol.ConstraintTypes)
{
if (!isFirstConstraint)
{
constraints += ", ";
}
else
{
isFirstConstraint = false;
}
constraints += contstraintType.GetFullTypeString();
}
ConstraintTypes
is a collection of INamedTypeSymbol
objects providing the class and interfaces that the generic argument must be derived from.
Here is what is being printed as the constraints of the generic arguments of the class:
where T1 : class, MyInterface, new()
where T2 : MyInterface
Now we want to get the information about MySimpleEvent
and print its type:
IEventSymbol eventSymbol =
simpleClassToAnalyze.GetMembers("MySimpleEvent").FirstOrDefault()
as IEventSymbol;
string eventTypeStr = (eventSymbol.Type as INamedTypeSymbol).GetFullTypeString();
Console.WriteLine("The event type is:");
Console.WriteLine(eventTypeStr);
And here is what we get:
The event type is:
Action<T1, T2, Int32>
Note that we are using the same GetFullTypeString()
function which helps to resolve generic type arguments and their concrete realizations as was explained above: Note that the last argument of the Action
is concrete type Int32
(or int
).
Now we are going to print the signature of the method by using GetMethodSignature()
extension:
IMethodSymbol methodWithGenericTypeArgsSymbol =
simpleClassToAnalyze.GetMembers("MySimpleMethod").FirstOrDefault()
as IMethodSymbol;
string genericMethodSignature = methodWithGenericTypeArgsSymbol.GetMethodSignature();
Console.WriteLine("Generic Method Signature:");
Console.WriteLine(genericMethodSignature);
GetMethodSignature
extension method has been improved in comparison to the one described in part 1 of the article, to show the generic type arguments:
...
result +=
" " +
methodSymbol.Name +
methodSymbol.GetTypeArgsStr((symbol) => ((IMethodSymbol)symbol).TypeArguments);
...
The printed result is:
Generic Method Signature:
public Int32 MySimpleMethod<T4, T5>(String str, out Boolean flag, Int32 i = 5)
Now we are going to print the generic type argument constraints for this method:
Console.WriteLine();
Console.WriteLine("Generic Method's Where Statements:");
foreach (var typeParameter in methodWithGenericTypeArgsSymbol.TypeArguments)
{
ITypeParameterSymbol typeParameterSymbol =
typeParameter as ITypeParameterSymbol;
if (typeParameterSymbol != null)
{
string whereStatement = typeParameterSymbol.GetWhereStatement();
if (whereStatement != null)
{
Console.WriteLine(whereStatement);
}
}
}
For this, we are using the same GetWhereStatement()
extension function that we used for the class and that was described above.
Here is what we get printed:
Generic Method's Where Statements:
where T4 : class, MyInterface, new()
where T5 : MyInterface, new()
We have also modified GetMethodSignature()
method to handle correctly the case of functions variable number of argument:
IMethodSymbol varArgsMethodSymbol =
simpleClassToAnalyze.GetMembers("MyVarArgMethod").FirstOrDefault()
as IMethodSymbol;
string varArgsMethodSignature = varArgsMethodSymbol.GetMethodSignature();
Console.WriteLine();
Console.WriteLine("Var Args Method Signature:");
Console.WriteLine(varArgsMethodSignature);
The above code prints:
Var Args Method Signature:
public void MyVarArgMethod(String str, params Int32[] ints)
The part of the GetMethodSignature(...)
extension method responsible for detecting var arg condition is located with the argument loop:
string parameterTypeString = null;
if (parameter.IsParams)
{
result += "params ";
INamedTypeSymbol elementType =
(parameter.Type as IArrayTypeSymbol).ElementType as INamedTypeSymbol;
result += elementType.GetFullTypeString() + "[]";
}
else
{
parameterTypeString =
(parameter.Type as INamedTypeSymbol).GetFullTypeString();
}
IsParams
property of IParameterSymbol
specifies if this is a var arg parameter. If it is, then in order to get the type of the each parameter within the parameter array we convert the parameter.Type
to IArrayTypeSymbol
and check its ElementType
property:
INamedTypeSymbol elementType =
(parameter.Type as IArrayTypeSymbol).ElementType as INamedTypeSymbol;
Now we are going to discuss how to process an attribute whose constructor has a variable number of arguments:
AttributeData attrData =
simpleClassToAnalyze.GetAttributes().FirstOrDefault();
object intProperty = attrData.GetAttributeConstructorValueByParameterName("intProp");
Console.WriteLine();
Console.WriteLine("Attribute's IntProp = " + intProperty);
IEnumerable<object> stringProperties =
attrData.GetAttributeConstructorValueByParameterName("stringProps") as IEnumerable<object>;
Console.WriteLine();
Console.WriteLine("String properties");
foreach (object str in stringProperties)
{
Console.WriteLine(str);
}
The above code results in the following print out:
Attribute's IntProp = 5
String properties
Str1
Str2
Str3
In order to get the attribute constructor parameter values (whether single value or an array). we employ GetAttributeConstructorValueByParameterName(...)
extension method:
public static object
GetAttributeConstructorValueByParameterName(this AttributeData attributeData, string argName)
{
IParameterSymbol parameterSymbol = attributeData.AttributeConstructor
.Parameters
.Where((constructorParam) => constructorParam.Name == argName).FirstOrDefault();
int parameterIdx = attributeData.AttributeConstructor.Parameters.IndexOf(parameterSymbol);
TypedConstant constructorArg = attributeData.ConstructorArguments[parameterIdx];
if (constructorArg.Kind == TypedConstantKind.Array)
{
List<object> result = new List<object>();
foreach(TypedConstant typedConst in constructorArg.Values)
{
result.Add(typedConst.Value);
}
return result;
}
return constructorArg.Value;
}
As you can see from the method's implementation, the condition when constructor argument corresponds to a param
array, can be detected by checking that its Kind
property is set to TypeContantKind.Array
. In that case we are using constructorArg.Values
property instead of constructorArg.Value
. The Values
property is an array of TypeConstant
objects each of which contains Value
propety that has corresponding constructor value. So, when our constructor argument is of type TypeConstantKind.Array
, we return an array of objects corresponding to the argument values.
Finally, I want to show how to find a project to which a certain file belongs within the solution. I used this trick before in Implementing Adapter Pattern and Imitating Multiple Inheritance in C# using Roslyn based VS Extension Wrapper Generator article.
string filePath =
@"..\..\..\SampleToAnalyze\SampleToAnalyze\ClassToAnalyze.cs";
string absoluteFilePath = Path.GetFullPath(filePath);
DocumentId classToAnalyzeDocId =
solutionToAnalyze
.GetDocumentIdsWithFilePath(absoluteFilePath).FirstOrDefault();
ProjectId idOfProjectThatContainsTheFile = classToAnalyzeDocId.ProjectId;
Project projectThatContainsTheFile = solutionToAnalyze.GetProject(idOfProjectThatContainsTheFile);
Console.WriteLine();
Console.WriteLine("Name of the Project containing file ClassToAnalyze.cs:");
Console.WriteLine(projectThatContainsTheFile.Name);
As shown in the code above - first we get the absolute path to the file using System.IO.Path.GetFullPath(...)
method - for some reason relative paths do not work.
Then we use GetDocumentIdsWithFilePath(...)
Roslyn method defined on the Roslyn Solution
object in order to pull the document id of the corresponding file.
DocumentId
object of a file also contains ProjectId
property for the project containing the file:
ProjectId idOfProjectThatContainsTheFile = classToAnalyzeDocId.ProjectId;
Once you know the project id, you can pull the project out of Roslyn solution by using Roslyn's GetProject(...)
method:
Project projectThatContainsTheFile = solutionToAnalyze.GetProject(idOfProjectThatContainsTheFile);
Conclusion
In this article we expanded investigation of Roslyn's code analysis capabilities to some more interesting cases or generic classes, generic methods and methods with variable number of parameters.