65.9K
CodeProject 正在变化。 阅读更多。
Home

Roslyn 代码分析简单示例(第二部分)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2015年2月1日

CPOL

6分钟阅读

viewsIcon

26211

downloadIcon

1102

用简单示例展示 Roslyn 的代码分析功能

Roslyn 代码分析简单示例(第二部分)

引言

Roslyn 代码分析简明示例(第一部分)中,我们描述了一些允许进行代码分析的基本 Roslyn 功能。在这第二部分中,我们将展示如何分析更复杂的类和方法,包括:

  1. 泛型类
  2. 泛型方法
  3. 具有可变数量参数的方法
  4. 其构造函数具有可变数量参数的特性

 

安装 Roslyn Dll

所有示例都是使用 VS2015 预览版构建的。为了节省时间,我决定暂时不切换到 VS2015 CTP。一旦 VS2015 完整版发布,我将上传该版本的示例。我假设在 VS2015 CTP 中安装 Roslyn 相关 dll 与在预览版中完全相同。

在 VS 2015 预览版中,您需要使用包管理器安装 Roslyn dll,如Roslyn中所述。

代码位置和描述

就像在Roslyn 代码分析简明示例(第一部分)中一样,代码由两个解决方案组成——解决方案SampleToAnalyze.sln包含正在分析的代码,而解决方案RoslynAnalysis.sln包含基于 Roslyn 的代码分析。

SampleToAnalyze

让我们先看看SampleToAnalyze

它有一个非常简单的接口MyInterface,只包含一个方法——MyMethod

public interface MyInterface
{
    int MyMethod();
}  

ClassToAnalyze是分析的主要对象。它是一个带有泛型参数的类,其中一些参数被指定实现MyInterface(这是该项目需要此接口的唯一目的)。

[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)
    {

    }
}  

如您所见,该类包含事件MySimpleEvent和两个方法MySimpleMethod(...)以及MyVarArgMethod(...)

该事件具有使用泛型参数的Action<T1, T2, int>

方法MySimpleMethod<T4, T5>(...)包含泛型参数,并附带相应的where子句中的一些限制。

方法MyVarArgMethod(...)是一个可变参数方法,其参数ints带有params修饰符。

ClassToAnalyze<T1, T2, T3>还有一个类属性AttrToAnalyze。另请注意,该Attribute的构造函数具有可变数量的参数;

public AttrToAnalyzeAttribute(int intProp, params string[] stringProps)
{
    IntProp = intProp;
    StringProps = stringProps.ToList();
}  

RoslynAnalysis

现在让我们看看包含我们在这里要讨论的核心功能的RoslynAnalysis解决方案。

我们以与第一部分完全相同的方式获取要分析项目的 Roslyn 编译。

  
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;  

现在,我们要从编译中提取类ClassToAnalyze的类型。请注意,由于该类是泛型的,我们在类名末尾的反引号后指定泛型属性的数量。

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

运行以上代码将打印

Full class name:
ClassToAnalyze<T1, T2, T3>

为了返回包含在<...>括号内的泛型参数的正确类名字符串,已修改了扩展方法GetFullTypeString()。该方法位于 Extensions.cs 文件中。

public static string GetFullTypeString(this INamedTypeSymbol type)
{
    string result = 
        type.Name + 
        type.GetTypeArgsStr((symbol) => ((INamedTypeSymbol)symbol).TypeArguments);

    return result;
}  

如您所见,它调用了另一个扩展方法——GetTypeArgsStr(...),以创建与泛型参数对应的字符串<T1, T2, T3>

这是GetTypeArgsStr(...)方法的代码。

static string GetTypeArgsStr
(
    this ISymbol symbol, 
    Func> typeArgGetter
)
{
    IEnumerable 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;
}  

这段代码比显示类的泛型类型参数所需的更通用。它也可以应用于除INamedTypeSymbol以外的其他对象,这些对象具有包含泛型类型信息的TypeArguments属性——例如,我在下面将相同的方法应用于从IMethodSymbol对象中提取泛型类型信息。这就是为什么此方法的第一个参数是ISymbol,第二个参数是用于从第一个参数中提取TypeArguments信息的委托。

请注意,对于类,我们使用(symbol) => ((INamedTypeSymbol)symbol).TypeArguments作为委托,而对于方法,它将是(symbol) => ((IMethodSymbol)symbol).TypeArguments

该方法还可以处理类型实例声明或方法调用,其中一些类型参数可能是具体的,例如ClassToAnalyze<T1, T2, int>——请注意,此类型声明中的最后一个参数是具体的——int。这就是为什么该方法包含以下if子句的原因:

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;
}

泛型参数在TypeArguments中以ITypeParameterSymbol对象的形式出现,而泛型类型的具体实现将以INamedTypeSymbol的形式出现。

这是我们打印泛型类型约束的方式

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

以下是扩展方法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;
}  

从实现中可以看出,ITypeParameterSymbolHasReferenceTypeConstraint属性指定是否存在class约束,HasValueTypeConstraint指定是否存在struct约束,HasConstructorConstraint指定是否存在new()约束。

其他约束,例如派生自类或实现接口,在ConstraintTypes集合中指定(参见循环)。

foreach(INamedTypeSymbol contstraintType in typeParameterSymbol.ConstraintTypes)
{
    // if not first constraint prepend with comma
    if (!isFirstConstraint)
    {
        constraints += ", ";
    }
    else
    {
        isFirstConstraint = false;
    }

    constraints += contstraintType.GetFullTypeString();
}  

ConstraintTypes是一个INamedTypeSymbol对象集合,提供泛型参数必须派生自的类和接口。

以下是作为类泛型参数约束而打印的内容

where T1 : class, MyInterface, new()
where T2 : MyInterface  

现在我们想获取MySimpleEvent的信息并打印其类型

IEventSymbol eventSymbol =
    simpleClassToAnalyze.GetMembers("MySimpleEvent").FirstOrDefault()
    as IEventSymbol;

string eventTypeStr = (eventSymbol.Type as INamedTypeSymbol).GetFullTypeString();

Console.WriteLine("The event type is:");
Console.WriteLine(eventTypeStr);  

这是我们得到的结果

The event type is:
Action<T1, T2, Int32>  

请注意,我们使用的是相同的GetFullTypeString()函数,该函数有助于解析泛型类型参数及其具体实现,如上所述:请注意,Action的最后一个参数是具体类型Int32(或int)。

现在我们将使用GetMethodSignature()扩展来打印方法的签名。

IMethodSymbol methodWithGenericTypeArgsSymbol =
    simpleClassToAnalyze.GetMembers("MySimpleMethod").FirstOrDefault()
    as IMethodSymbol;

string genericMethodSignature = methodWithGenericTypeArgsSymbol.GetMethodSignature();
Console.WriteLine("Generic Method Signature:");
Console.WriteLine(genericMethodSignature);  

与文章第一部分中描述的GetMethodSignature扩展方法相比,该方法已得到改进,以显示泛型类型参数。

...

result += 
    " " + 
    methodSymbol.Name + 
    methodSymbol.GetTypeArgsStr((symbol) => ((IMethodSymbol)symbol).TypeArguments);
... 

打印结果为

 
Generic Method Signature:
public Int32 MySimpleMethod<T4, T5>(String str, out Boolean flag, Int32 i = 5) 

现在我们将打印此方法的泛型类型参数约束。

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

为此,我们使用与类相同的、前面描述过的GetWhereStatement()扩展函数。

这是我们得到的结果

Generic Method's Where Statements:
where T4 : class, MyInterface, new()
where T5 : MyInterface, new() 

我们还修改了GetMethodSignature()方法,以正确处理函数变量参数数量的情况。

IMethodSymbol varArgsMethodSymbol =
    simpleClassToAnalyze.GetMembers("MyVarArgMethod").FirstOrDefault()
    as IMethodSymbol;

string varArgsMethodSignature = varArgsMethodSymbol.GetMethodSignature();
Console.WriteLine();
Console.WriteLine("Var Args Method Signature:");
Console.WriteLine(varArgsMethodSignature);  

上述代码打印出

Var Args Method Signature:
public void MyVarArgMethod(String str, params Int32[] ints)  

GetMethodSignature(...)扩展方法中负责检测可变参数条件的部分位于参数循环中。

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();
}

IParameterSymbolIsParams属性指定这是否为可变参数。如果是,则为了获取参数数组中每个参数的类型,我们将parameter.Type转换为IArrayTypeSymbol并检查其ElementType属性。

INamedTypeSymbol elementType = 
        (parameter.Type as IArrayTypeSymbol).ElementType as INamedTypeSymbol;  

现在我们将讨论如何处理其构造函数具有可变数量参数的属性。

// 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);
}  

以上代码产生以下输出

Attribute's IntProp = 5

String properties
Str1
Str2
Str3  

为了获取属性构造函数参数值(无论是单个值还是数组),我们使用GetAttributeConstructorValueByParameterName(...)扩展方法。

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;
}  

从方法的实现可以看出,当构造函数参数对应于`param`数组时,可以通过检查其`Kind`属性是否设置为`TypeContantKind.Array`来检测。在这种情况下,我们使用`constructorArg.Values`属性而不是`constructorArg.Value`。`Values`属性是`TypeConstant`对象的数组,每个对象都包含`Value`属性,该属性具有相应的构造函数值。因此,当我们的构造函数参数类型为`TypeConstantKind.Array`时,我们返回一个与参数值对应的对象数组。

最后,我想展示如何在解决方案中查找某个文件所属的项目。我以前在使用基于 Roslyn 的 VS 扩展包装器生成器在 C# 中实现适配器模式和模拟多重继承一文中使用了这个技巧。

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

如上述代码所示,我们首先使用System.IO.Path.GetFullPath(...)方法获取文件的绝对路径——由于某些原因,相对路径不起作用。

然后,我们使用在 Roslyn Solution对象上定义的 Roslyn 方法GetDocumentIdsWithFilePath(...)来拉取相应文件的文档 ID。

文件的DocumentId对象还包含包含该文件的项目的ProjectId属性。

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

一旦您知道项目 ID,就可以使用 Roslyn 的GetProject(...)方法从 Roslyn 解决方案中提取项目。

// get the project itself from the solution
Project projectThatContainsTheFile = solutionToAnalyze.GetProject(idOfProjectThatContainsTheFile);  

结论

在本文中,我们将 Roslyn 的代码分析功能扩展到一些更有趣的案例,例如泛型类、泛型方法以及具有可变参数数量的方法。

© . All rights reserved.