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





5.00/5 (4投票s)
用简单示例展示 Roslyn 的代码分析功能
Roslyn 代码分析简单示例(第二部分)
引言
在Roslyn 代码分析简明示例(第一部分)中,我们描述了一些允许进行代码分析的基本 Roslyn 功能。在这第二部分中,我们将展示如何分析更复杂的类和方法,包括:
- 泛型类
- 泛型方法
- 具有可变数量参数的方法
- 其构造函数具有可变数量参数的特性
安装 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; }
从实现中可以看出,ITypeParameterSymbol
的HasReferenceTypeConstraint
属性指定是否存在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(); }
IParameterSymbol
的IsParams
属性指定这是否为可变参数。如果是,则为了获取参数数组中每个参数的类型,我们将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 的代码分析功能扩展到一些更有趣的案例,例如泛型类、泛型方法以及具有可变参数数量的方法。