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






4.98/5 (24投票s)
提供简单示例,描述 Roslyn 代码分析功能
引言
在 Implementing Adapter Pattern and Imitating Multiple Inheritance in C# using Roslyn based VS Extension Wrapper Generator 这篇文章中,我描述了如何构建一个基于 Roslyn 的 VS 2015 预览版扩展,用于生成类成员包装器。
然而,在那篇文章及后续的两篇文章中,我并没有详细介绍 VS 扩展是如何构建的,而是主要讨论了它的使用方式。
在这里,我想更详细地回顾一下我在构建这个 Visual Studio 扩展过程中学到的知识,特别是分享我使用 Roslyn 的经验。希望这将鼓励其他人编写自己的基于 Roslyn 的扩展,并最终能够提升 C# 语言的能力。
起初,我只想简单地描述我的 NP.WrapperGenerator.vsix 代码,但后来我决定,如果我用一系列简单的示例来展示 Roslyn 的功能,每个示例都强调某个 Roslyn 特性,这样读者就可以将其作为 Roslyn 代码分析教程来阅读,这样会更有用。
虽然 Roslyn 的代码分析功能非常出色,但从我的角度来看,Roslyn 的代码生成过程过于冗长且难以调试。因此,在处理 NP.WrapperGenerator.vsix 扩展时,我采用了更简单、更可靠的 CodeDOM 代码生成功能。不过,CodeDOM 已经存在一段时间了,也有一些文章详细介绍了它,这里我只关注 Roslyn 的代码分析部分。
VS 2015 扩展本身的构建方式在上述文章中有详细描述。另一方面,使用简单的控制台项目可以更快地使用 Roslyn。否则,每次启动扩展项目都需要等待整个 VS Studio 启动。因此,我所有的示例都构建为简单的控制台项目。
要运行这些示例,您必须安装 VS 2015 预览版。
在本文中,我们重点关注如何使用 Roslyn 编译来获取有关简单命名空间、属性、事件、方法和特性的信息。
在本文的第二部分,我计划讨论更复杂的情况 - 特别是
- 构造函数具有可变数量参数的特性。
- 具有可变数量参数(
params
数组)的方法。 - 泛型类和方法。
Roslyn 的潜力无限
在 VS 2015 之前,对于编写影响和扩展 C# 或 VB 语言的 VS 扩展的开发人员来说,主要问题在于缺乏一种可靠的方式来获取有关被扩展代码的信息。
当然,一些大公司可以构建自己的框架来维护完整的解决方案信息 - 例如 ReSharper 产品,但个人开发者却无能为力。例如,我多年前就有构建适配器和使用包装器生成实现多重继承的想法,但直到 Roslyn 被集成到 VS 2015 中,我才得以实现它。
VS 2015 的 C# 和 VB 编译器都基于 Roslyn。VS 2015 也是第一个可以使用 Roslyn 来构建 VS 扩展的 Visual Studio 版本。VS 2015 为应用了 VS 扩展的解决方案维护 Roslyn 工作区,使开发人员能够获取有关被扩展项目和文件的任何所需信息。
现在,微软基本上已经公开了其编译器“黑盒子”,希望各种开发人员能够利用它来创建自己的 C# 和 VB 语言扩展,其中一些将被开发社区和微软本身采纳。本文的主要目的是展示 Roslyn 的代码分析能力,并引起开发人员的兴趣。
安装 Roslyn Dlls
在 VS 2015 预览版中,您需要使用包管理器安装 Roslyn dll,具体方法请参阅 Roslyn。
您可以打开 NuGet 包管理器控制台,然后输入 Install-Package Microsoft.CodeAnalysis -Pre
您还需要运行 Install-Package Microsoft.Composition 来安装 MEF 2。
Roslyn 代码分析 API 概述
Roslyn 代码分析 API 允许在不实际加载编译后的代码的情况下获取有关该代码的完整信息。我认为它在很大程度上类似于 System.Reflection
库,不同之处在于它不需要被检查的代码加载到您的 .NET 解决方案中。这使得它对于编写编译器和 VS 扩展非常有用。
示例
示例包含两个解决方案 - SampleToAnalyze
和 SimpleRoslynAnalysis
。
SampleToAnalyze
包含要分析的代码。SimpleRoslynAnalysis
展示了使用 Roslyn 进行代码分析的示例。
SampleToAnalyze
包含由 SimpleRoslynAnalysis
项目分析的 SimpleClassToAnalyze
类。这是该类的代码
namespace SampleToAnalyze.SubNamespace { [SimpleAttr(5, "Hello World")] public class SimpleClassToAnalyze { public int MySimpleProperty { get; set; } public event Action<object> MySimpleEvent; public int MySimpleMethod(string str, out bool flag, int i = 5) { flag = true; return 5; } } }
如您所见,它包含一个属性 MySimpleProperty
、一个事件 MySimpleEvent
和一个方法 MySimpleMethod
。还有一个类特性 - SimpleAttr
。此特性定义在同一个项目中
// set this to be a class-only attribute [AttributeUsage(AttributeTargets.Class)] public class SimpleAttrAttribute : Attribute { public int IntProp { get; protected set; } public string StringProp { get; protected set; } public SimpleAttrAttribute(int intProp, string stringProp) { IntProp = intProp; StringProp = stringProp; } }
SimpleRoslynAnalysis
是一个控制台项目。其主 Program
类包含了所有示例。它利用静态 Extension
类中的扩展方法。
这是我们用来获取 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;
我们通过使用 GetTypeByMetadataName
方法,传入类的完整名称(包括命名空间),从编译对象中提取有关我们 SampleClassToAnalyze
的信息。
string classFullName = "SampleToAnalyze.SubNamespace.SimpleClassToAnalyze"; // getting type out of the compilation INamedTypeSymbol simpleClassToAnalyze = sampleToAnalyzeCompilation.GetTypeByMetadataName(classFullName);
INamedTypeSymbol
包含有关该类型的全部编译信息(类似于常规 Reflection
代码检查中的 System.Type
)。
第一行代码展示了如何获取 INamedTypeSymbol
(或任何 ISymbol
)的完整 C# namespace
(或者任何 ISymbol
)。
string fullNamespacePath = simpleClassToAnalyze.GetFullNamespace();
变量 fullNamespacePath
将包含“SampleToAnalyze.SubNamespace”。请查看 Extensions.GetFullNamespace(...)
扩展方法。
public static string GetFullNamespace(this ISymbol symbol) { if ((symbol.ContainingNamespace == null) || (string.IsNullOrEmpty(symbol.ContainingNamespace.Name))) { return null; } // get the rest of the full namespace string string restOfResult = symbol.ContainingNamespace.GetFullNamespace(); string result = symbol.ContainingNamespace.Name; if (restOfResult != null) // if restOfResult is not null, append it after a period result = restOfResult + '.' + result; return result; }
如您所见,这是一个递归方法,它递归地使用 ContainingNamespace
属性来创建最终的命名空间字符串。
为了获取类成员,我们使用 GetMembers(...)
方法。
下面是如何获取属性
IPropertySymbol propertySymbol = simpleClassToAnalyze.GetMembers("MySimpleProperty").FirstOrDefault() as IPropertySymbol;
从 IPropertySymbol
中,我们可以获取属性类型:propertySymbol.Type
和属性名称:propertySymbol.Name
。
IEventSymbol
包含有关事件的信息
IEventSymbol eventSymbol = simpleClassToAnalyze.GetMembers("MySimpleEvent").FirstOrDefault() as IEventSymbol;
请注意,MySimpleEvent
定义为 Action<object>
- 一个带有泛型参数的类型。
如果打印 eventSymbol.Type.Name
,只会显示“Action”。为了重构整个泛型类型,我们使用了 GetFullTypeString(...)
递归扩展方法。
public static string GetFullTypeString(this INamedTypeSymbol type) { string result = type.Name; if (type.TypeArguments.Count() > 0) { result += "<"; bool isFirstIteration = true; foreach(INamedTypeSymbol typeArg in type.TypeArguments) { if (isFirstIteration) { isFirstIteration = false; } else { result += ", "; } result += typeArg.GetFullTypeString(); } result += ">"; } return result; }
泛型类型参数位于 INamedTypeSymbol
对象的 TypeArguments
属性中。
GetFullTypeString(...)
方法通过为每个类型参数递归调用自身,并将它们放在用逗号分隔的 <...>
括号中,来组装最终的字符串。
为了获取有关 MySimpleMethod
方法的信息,我们采用了类似的方法
IMethodSymbol methodSymbol = simpleClassToAnalyze.GetMembers("MySimpleMethod").FirstOrDefault() as IMethodSymbol;
GetMethodSignuture(...)
扩展方法展示了如何从 IMethodSymbol
对象重构 MySimpleMethod
方法的签名。
public static string GetMethodSignature(this IMethodSymbol methodSymbol) { string result = methodSymbol.DeclaredAccessibility.ConvertAccessabilityToString(); if (methodSymbol.IsAsync) result += " async"; if (methodSymbol.IsAbstract) result += " abstract"; if (methodSymbol.IsVirtual) { result += " virtual"; } if (methodSymbol.IsStatic) { result += " static"; } if (methodSymbol.IsOverride) { result += " override"; } if (methodSymbol.ReturnsVoid) { result += " void"; } else { result += " " + (methodSymbol.ReturnType as INamedTypeSymbol).GetFullTypeString(); } result += " " + methodSymbol.Name + "("; bool isFirstParameter = true; foreach(IParameterSymbol parameter in methodSymbol.Parameters) { if (isFirstParameter) { isFirstParameter = false; } else { result += ", "; } if (parameter.RefKind == RefKind.Out) { result += "out "; } else if (parameter.RefKind == RefKind.Ref) { result += "ref "; } string parameterTypeString = (parameter.Type as INamedTypeSymbol).GetFullTypeString(); result += parameterTypeString; result += " " + parameter.Name; if (parameter.HasExplicitDefaultValue) { result += " = " + parameter.ExplicitDefaultValue.ToString(); } } result += ")"; return result; }
函数的封装级别(或任何其他类成员)由 Microsoft.CodeAnalysis.Accessibility
枚举的 DeclaredAccessibility
属性确定。我创建了一个实用方法 ConvertAccessabilityToString(...)
来将此枚举转换为字符串。
public static string ConvertAccessabilityToString(this Accessibility accessability) { switch (accessability) { case Accessibility.Internal: return "internal"; case Accessibility.Private: return "private"; case Accessibility.Protected: return "protected"; case Accessibility.Public: return "public"; case Accessibility.ProtectedAndInternal: return "protected internal"; default: return "private"; } }
正如您从 GetMethodSignature(...)
方法的实现中看到的,IMethodSymbol
具有各种标志 - IsAsync
、IsAbstract
、IsVirtual
、IsStatic
、IsOverride
,这些标志指定了函数是否是异步的、抽象的、虚拟的、静态的,或者重写了超类中声明的函数。
ReturnsVoid
布尔属性在函数为 void
时为 true
。如果此属性为 false
,则方法的返回类型由 methodSymbol.ReturnType
指定(可以并且应该转换为 INamedTypeSymbol
)。
方法的参数由 methodSymbol.Parameters
数组描述,该数组由 IParameterSymbol
对象组成。
每个 IParameterSymbol
包含参数的类型(作为 parameterSymbol.Type
)和参数名称(作为 parameterSymbol.Name
)。它们还包含 RefKind
枚举的 RefKind
属性,该属性指定参数是否为 out
或 ref
,或者都不是。
IParameterSymbol
的 HasExplicityDefaultValue
指定参数是否具有默认值 - (在我们的例子中,最后一个参数 i
的默认值为 5
)。该值本身作为 C# object
包含在 IParameterSymbol.ExplicitDefaultValue
属性中。
最后,我们将演示如何从类定义中提取 Attribute
信息 - 尽管此处未展示,但从方法定义中提取 Attribute
信息的方式完全相同。
如上所示,我们在 SampleToAnalize
项目中使用了 SimpleAttrAttribute
。这是该特性的代码
// set this to be a class-only attribute [AttributeUsage(AttributeTargets.Class)] public class SimpleAttrAttribute : Attribute { public int IntProp { get; protected set; } public string StringProp { get; protected set; } public SimpleAttrAttribute(int intProp, string stringProp) { IntProp = intProp; StringProp = stringProp; } }
请注意,该特性的属性的 setter 是受保护的,因此设置它们的唯一方法是通过构造函数。这是特意为之,以简化使用 Roslyn 解析特性的信息 - 现在我们只需要弄清楚构造函数参数对应于哪个特性属性。
我们使用 GetAttributes()
方法来提取类的特性信息。
AttributeData attrData = simpleClassToAnalyze.GetAttributes().FirstOrDefault();
如您所见,特性信息是以 Microsoft.CodeAnalysis.AttributeData
对象的形式出现的。
我创建了 GetAttributeConstructorValueByParameterName(...)
扩展方法来从构造函数中提取特性的属性值 - 这里展示了我们如何在 Program.Main
中使用它来获取 IntProp
和 StringProp
的值。
object intProperty = attrData.GetAttributeConstructorValueByParameterName("intProp"); Console.WriteLine(); Console.WriteLine("Attribute's IntProp = " + intProperty); object stringProperty = attrData.GetAttributeConstructorValueByParameterName("stringProp"); Console.WriteLine(); Console.WriteLine("Attribute's StringProp = " + stringProperty);
现在,让我们看看 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]; // return the value passed to the attribute return constructorArg.Value; }
代码注释基本解释了其中的内容。我们通过使用 attributeData.AttributeConstructor.Parameters
集合来检查构造函数参数的索引。然后,使用该索引获取相应的构造函数参数。
这是 Program.Main
方法的实现。
static void Main(string[] args) { 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; string classFullName = "SampleToAnalyze.SubNamespace.SimpleClassToAnalyze"; // getting type out of the compilation INamedTypeSymbol simpleClassToAnalyze = sampleToAnalyzeCompilation.GetTypeByMetadataName(classFullName); string fullNamespacePath = simpleClassToAnalyze.GetFullNamespace(); Console.WriteLine("Full Namespace:"); Console.WriteLine(fullNamespacePath); Console.WriteLine(); IPropertySymbol propertySymbol = simpleClassToAnalyze.GetMembers("MySimpleProperty").FirstOrDefault() as IPropertySymbol; INamedTypeSymbol propertyType = propertySymbol.Type as INamedTypeSymbol; IEventSymbol eventSymbol = simpleClassToAnalyze.GetMembers("MySimpleEvent").FirstOrDefault() as IEventSymbol; Console.WriteLine("Event Name:"); Console.WriteLine(eventSymbol.Name); INamedTypeSymbol eventType = eventSymbol.Type as INamedTypeSymbol; Console.WriteLine(); Console.WriteLine("Event Type:"); Console.WriteLine(eventType.GetFullTypeString()); IMethodSymbol methodSymbol = simpleClassToAnalyze.GetMembers("MySimpleMethod").FirstOrDefault() as IMethodSymbol; string methodDeclarationString = methodSymbol.GetMethodSignature(); Console.WriteLine(); Console.WriteLine("Method Signature:"); Console.WriteLine(methodDeclarationString); // dealing with attributes AttributeData attrData = simpleClassToAnalyze.GetAttributes().FirstOrDefault(); object intProperty = attrData.GetAttributeConstructorValueByParameterName("intProp"); Console.WriteLine(); Console.WriteLine("Attribute's IntProp = " + intProperty); object stringProperty = attrData.GetAttributeConstructorValueByParameterName("stringProp"); Console.WriteLine(); Console.WriteLine("Attribute's StringProp = " + stringProperty); }
运行时,它在控制台上显示如下内容:
Full Namespace: SampleToAnalyze.SubNamespace Event Name: MySimpleEvent Event Type: Action<Object> Method Signature: public Int32 MySimpleMethod(String str, out Boolean flag, Int32 i = 5) Attribute's IntProp = 5 Attribute's StringProp = Hello World
结论
我们展示了一些基本的 Roslyn 功能,允许在不加载相应 dll 的情况下分析 .NET 代码。在第二部分,我们计划讨论更复杂的情况,包括泛型类和函数、具有可变数量参数的方法以及我们在构建 NP.WrappgerGenerator.vsix 扩展时使用的其他有趣功能。