Click here to Skip to main content
15,881,794 members
Articles / Programming Languages / C#

Roslyn Code Analysis in Easy Samples (Part 2)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
1 Feb 2015CPOL6 min read 24.9K   1.1K   10   5
Present Roslyn's code analysis capabilities in easy samples

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

  1. Generic Classes
  2. Generic Methods
  3. Methods with variable number of arguments
  4. 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";

// start Roslyn workspace
MSBuildWorkspace workspace = MSBuildWorkspace.Create();

// open solution we want to analyze
Solution solutionToAnalyze =
    workspace.OpenSolutionAsync(pathToSolution).Result;

// get the project we want to analyze out
// of the solution
Project sampleProjectToAnalyze =
    solutionToAnalyze.Projects
                        .Where((proj) => proj.Name == projectName)
                        .FirstOrDefault();

// get the project's compilation
// compilation contains all the types of the 
// project and the projects referenced by 
// our project. 
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";  

// getting type out of the compilation
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)
        {
            // insert comma if not first iteration                    
            if (isFirstIteration)
            {
                isFirstIteration = false;
            }
            else
            {
                result += ", ";
            }

            ITypeParameterSymbol typeParameterSymbol =
                typeArg as ITypeParameterSymbol;

            string strToAdd = null;
            if (typeParameterSymbol != null)
            {
                // this is a generic argument
                strToAdd = typeParameterSymbol.Name;
            }
            else
            {
                // this is a generic argument value. 
                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)
{
    // this is a generic argument
    strToAdd = typeParameterSymbol.Name;
}
else
{
    // this is a generic argument value. 
    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 not first constraint prepend with comma
        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 not first constraint prepend with comma
    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) // variable num arguments case
{
    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:

// dealing with attributes
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)
{

    // Get the parameter
    IParameterSymbol parameterSymbol = attributeData.AttributeConstructor
        .Parameters
        .Where((constructorParam) => constructorParam.Name == argName).FirstOrDefault();

    // get the index of the parameter
    int parameterIdx = attributeData.AttributeConstructor.Parameters.IndexOf(parameterSymbol);

    // get the construct argument corresponding to this parameter
    TypedConstant constructorArg = attributeData.ConstructorArguments[parameterIdx];

    // the case of variable number of arguments
    if (constructorArg.Kind == TypedConstantKind.Array)
    {
        List<object> result = new List<object>();

        foreach(TypedConstant typedConst in constructorArg.Values)
        {
            result.Add(typedConst.Value);
        }

        return result;
    }

    // return the value passed to the attribute
    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.

// getting a project by file path:
string filePath = 
    @"..\..\..\SampleToAnalyze\SampleToAnalyze\ClassToAnalyze.cs";

// get absolute path
string absoluteFilePath = Path.GetFullPath(filePath);

// get the DocumentId of the file
DocumentId classToAnalyzeDocId =
    solutionToAnalyze
        .GetDocumentIdsWithFilePath(absoluteFilePath).FirstOrDefault();

// get the project id of the project containing the file
ProjectId idOfProjectThatContainsTheFile = classToAnalyzeDocId.ProjectId;

// get the project itself from the solution
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:

// get the project id of 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:

// get the project itself from the solution
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect AWebPros
United States United States
I am a software architect and a developer with great passion for new engineering solutions and finding and applying design patterns.

I am passionate about learning new ways of building software and sharing my knowledge with others.

I worked with many various languages including C#, Java and C++.

I fell in love with WPF (and later Silverlight) at first sight. After Microsoft killed Silverlight, I was distraught until I found Avalonia - a great multiplatform package for building UI on Windows, Linux, Mac as well as within browsers (using WASM) and for mobile platforms.

I have my Ph.D. from RPI.

here is my linkedin profile

Comments and Discussions

 
QuestionGetWhereStatement is missing "new()" Pin
Stef Heyenrath31-Jul-21 21:21
Stef Heyenrath31-Jul-21 21:21 
QuestionVisual Studio integration Pin
m_kramar6-Mar-16 17:33
m_kramar6-Mar-16 17:33 
AnswerRe: Visual Studio integration Pin
Nick Polyak9-May-16 3:02
mvaNick Polyak9-May-16 3:02 
QuestionFind References Pin
BenjaminWebb20-May-15 5:21
BenjaminWebb20-May-15 5:21 
AnswerRe: Find References Pin
Nick Polyak7-Jun-15 13:09
mvaNick Polyak7-Jun-15 13:09 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.