.net Roslyn的基本使用 电脑版发表于:2024/9/28 21:30 ![](https://img.tnblog.net/arcimg/hb/12fd3b511cec4b60a83c422f92c4ed80.png) >#.net Roslyn的基本使用 [TOC] Roslyn简介 ------------ tn2>Roslyn是C#和Visual Basic编译器的开源实现,具有用于构建代码分析工具的API表面。 Roslyn还提供可供IDE使用的语言服务,例如重构、代码修复或编辑并继续。 Roslyn分析器 ------------ tn2>Roslyn 分析器允许您使用 Roslyn 中的数据来检查代码以检测问题。分析器可以直接在编辑器中添加错误、警告或波浪线。 简单实践 ------------ tn2>首先创建一个`Analyzer with Code Fix`项目命名为`MyRoslyn`。 框架我选择的`4.7.2`版本。 ![](https://img.tnblog.net/arcimg/hb/eaed7797530f4dbcb295671bea2ea6e1.png) ![](https://img.tnblog.net/arcimg/hb/f0966abf3a4b4fb6b7b4ad373b819357.png) tn2>该解决方案包含4个项目: ![](https://img.tnblog.net/arcimg/hb/03438276858948d3a9f6b04ca7eefc66.png) | 项目名 | 描述 | | ------------ | ------------ | | `MyRoslyn` | 负责定义代码分析器(Analyzer)。 | | `MyRoslyn.CodeFixes` | 用于修复由分析器(Analyzer)检测到的问题。 | | `MyRoslyn.Package` | 用于打包你的分析器和代码修复项目为一个 NuGet 包。 | | `MyRoslyn.Test` | 编写测试代码来验证你的分析器和代码修复器是否正常工作。 | | `MyRoslyn.Vsix` | 将你的分析器和代码修复器集成到 Visual Studio 中,让它们能够自动在 IDE 中运行和生效。 | tn2>首先我们打开`MyRoslynAnalyzer`代码。 查看其中的每一行意思。 ```csharp /// <summary> /// 这是一个 C# 的诊断分析器 /// </summary> [DiagnosticAnalyzer(LanguageNames.CSharp)] public class MyRoslynAnalyzer : DiagnosticAnalyzer { // 诊断 ID,用来标识分析器,类似身份证号 public const string DiagnosticId = "MyRoslyn"; // LocalizableResourceString 这样可以支持多语言。 // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Localizing%20Analyzers.md for more on localization // 分析器的标题 private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AnalyzerTitle), Resources.ResourceManager, typeof(Resources)); // 分析器发现问题时显示具体的提示内容 private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources)); // 对问题的详细描述,解释为什么这是个问题 private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AnalyzerDescription), Resources.ResourceManager, typeof(Resources)); // 这个问题的分类,这里是命名问题 private const string Category = "Naming"; // 定义诊断规则,包括诊断ID、标题、消息格式、分类、严重性等 private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( DiagnosticId, // 诊断ID Title, // 标题 MessageFormat, // 提示消息 Category, // 分类 DiagnosticSeverity.Warning, // 严重性,这里是警告 isEnabledByDefault: true, // 默认启用 description: Description); // 问题描述 // 这里是分析器支持的所有规则列表,这个分析器目前只有一个规则 public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } // 返回诊断规则 } // 分析器的初始化方法,主要是注册具体的分析动作 public override void Initialize(AnalysisContext context) { // 这两行代码告诉分析器不要分析自动生成的代码,并启用并发执行 context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); // 注册一个分析符号的动作,更多信息参考链接 // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Analyzer%20Actions%20Semantics.md for more information context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType); } // 这是实际的分析逻辑 private static void AnalyzeSymbol(SymbolAnalysisContext context) { // 获取当前正在被分析的符号,这里是一个命名类型(例如类或接口) var namedTypeSymbol = (INamedTypeSymbol)context.Symbol; // 找出名称中包含小写字母的命名类型 if (namedTypeSymbol.Name.ToCharArray().Any(char.IsLower)) { // 如果找到了,生成一个诊断信息(也就是“警告”) var diagnostic = Diagnostic.Create(Rule, namedTypeSymbol.Locations[0], namedTypeSymbol.Name); // 报告诊断信息 context.ReportDiagnostic(diagnostic); } } } ``` ```csharp // 使用 ExportCodeFixProvider 特性声明这是一个代码修复提供器,并且是共享的 [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MyRoslynCodeFixProvider)), Shared] public class MyRoslynCodeFixProvider : CodeFixProvider { // 指定这个代码修复器能够修复哪些诊断 ID,此处只修复与 MyRoslynAnalyzer.DiagnosticId 相关的问题 public sealed override ImmutableArray<string> FixableDiagnosticIds { get { return ImmutableArray.Create(MyRoslynAnalyzer.DiagnosticId); } } // 提供 “Fix All” 功能,允许用户一次性修复所有类似的问题 public sealed override FixAllProvider GetFixAllProvider() { // 使用 WellKnownFixAllProviders.BatchFixer,它可以批量修复多个问题 return WellKnownFixAllProviders.BatchFixer; } /// <summary> /// 当发现诊断问题时,注册代码修复操作 /// </summary> /// <param name="context"></param> /// <returns></returns> public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); // 获取语法树的根节点 var diagnostic = context.Diagnostics.First(); // 获取当前诊断问题 var diagnosticSpan = diagnostic.Location.SourceSpan; // 在语法树中找到对应的问题类型声明(比如类的声明) var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<TypeDeclarationSyntax>().First(); // 注册一个修复操作,当用户点击修复时执行 MakeUppercaseAsync 方法 context.RegisterCodeFix( CodeAction.Create( title: CodeFixResources.CodeFixTitle, // 修复的标题 createChangedSolution: c => MakeUppercaseAsync(context.Document, declaration, c),// 生成新的解决方案 equivalenceKey: nameof(CodeFixResources.CodeFixTitle)),// 区分修复操作的键 diagnostic); } /// <summary> /// 这是实际执行修复的逻辑,将类名转换为大写 /// </summary> /// <param name="document"></param> /// <param name="typeDecl"></param> /// <param name="cancellationToken"></param> /// <returns></returns> private async Task<Solution> MakeUppercaseAsync(Document document, TypeDeclarationSyntax typeDecl, CancellationToken cancellationToken) { // 获取类的标识符,即类名 var identifierToken = typeDecl.Identifier; // 将类名转换为全大写 var newName = identifierToken.Text.ToUpperInvariant(); // 获取语义模型,用来理解代码中的符号和上下文 var semanticModel = await document.GetSemanticModelAsync(cancellationToken); // 获取类的符号信息 var typeSymbol = semanticModel.GetDeclaredSymbol(typeDecl, cancellationToken); // 获取原始的解决方案(Solution),包含项目的所有代码和引用 var originalSolution = document.Project.Solution; // 获取重命名操作的设置 var optionSet = originalSolution.Workspace.Options; // 调用 Renamer.RenameSymbolAsync 将类名以及所有引用的地方改为大写 var newSolution = await Renamer.RenameSymbolAsync(document.Project.Solution, typeSymbol, newName, optionSet, cancellationToken).ConfigureAwait(false); // 返回包含更新后类名的解决方案 return newSolution; } } ``` tn2>当然我们也可以自定义一个`CreationAnalyzer`的分析器。 我想对当有写到`ImmutableArray<int>.Empty.Add(1)`代码时就对其中做一些警告的提示处理。 这里我打开了另外的一个窗口,然后创建一个`TempProject1`项目,添加了一些简单的代码。 ![](https://img.tnblog.net/arcimg/hb/029db9aa2dd64b959764a69b0d08394c.png) ```csharp // See https://aka.ms/new-console-template for more information using System.Collections.Immutable; Console.WriteLine("Hello, World!"); int count = 0; var array = ImmutableArray.Create(1,2,3); var array2 = array.Add(4); var array3 = ImmutableArray<int>.Empty.Add(1); ``` tn2>然后我们打开`Syntax Visualizer`窗口。 ![](https://img.tnblog.net/arcimg/hb/60a62e2c50f0405693e36c37657e13e2.png) tn2>分析我们我们选中的`ImmutableArray<int>.Empty.Add(1)`这一行。 ![](https://img.tnblog.net/arcimg/hb/daa2ec8593274e06875cfb296130b434.png) tn2>通过分析我们会发现,表达式树解析是从右往左解析的,举例: `Add(1)`-->`Empty`-->`ImmutableArray<int>`-->`ImmutableArray` 所以我们要锁定这一行的代码的话,首先我们会判断它有一个`ArgumentList`参数是大于0的,所以`ArgumentList`不大于0的节点的可以忽略了。 代码就这样写: ```csharp // 获取当前的节点 var node = (InvocationExpressionSyntax)context.Node; // 我们肯定会根据 ImmutableArray<int>.Empty.Add(1); 找到这个特点 // 我们看到了ArgumentList是有(1)值的,所以小于一个参数的跳过 if (node.ArgumentList.Arguments.Count != 1) return; ``` tn2>然后通过该节点的`Expression`获取到Add方法,如果我们没有Add方法的节点就可以忽略了。 但是怎么知道这个`Expression`的类型内,很简单:只需要选中`ImmutableArray<int>.Empty.Add`,它就显示出它的类型为`MemberAccessExpressionSyntax`. ![](https://img.tnblog.net/arcimg/hb/b03944129ebb4478b32a675cb59478f3.png) tn2>对应的代码如下: ```csharp // 无法将表达式转换成成员、方法、属性的去掉 // 一般找都是从右往左去找 if (!(node.Expression is MemberAccessExpressionSyntax addAccess)) return; // 判断方法名是否为Add if (addAccess.Name.Identifier.Text != "Add") return; ``` tn2>然后我们以此内推`Empty`也是这样。 ```csharp // 获取上一个的成员、方法、属性 if (!(addAccess.Expression is MemberAccessExpressionSyntax emptyAccess)) return; // 判断是不是Empty,不是就直接返回 if (emptyAccess.Name.Identifier.Text != "Empty") return; ``` tn2>然后到解析`ImmutableArray<int>`有变化了。 ![](https://img.tnblog.net/arcimg/hb/48150c1b333a4dcc8715577eec002a7b.png) ```csharp // 判断是不是GenericNameSyntax类型的 if (!(emptyAccess.Expression is GenericNameSyntax ImmutableArrayAccess)) return; // 判断是不是是否有一个泛型的类型 if (ImmutableArrayAccess.TypeArgumentList.Arguments.Count != 1) return; // 判断是否是ImmutableArray if (ImmutableArrayAccess.Identifier.Text != "ImmutableArray") return; ``` tn2>然后我贴上完整的`CreationAnalyzer`代码。 ```csharp /// <summary> /// 这是一个 C# 的诊断分析器 /// </summary> [DiagnosticAnalyzer(LanguageNames.CSharp)] public class CreationAnalyzer : DiagnosticAnalyzer { /// <summary> /// 定义诊断规则,包括诊断ID、标题、消息格式、分类、严重性等 /// </summary> private static DiagnosticDescriptor descriptor = new DiagnosticDescriptor( "BadWayOfCreatingImmutableArray", "Bad Way Of Creating Immutable Array", "Bad Way Of Creating Immutable Array", "Immutable arrays", DiagnosticSeverity.Warning, isEnabledByDefault: true ) ; public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor); public override void Initialize(AnalysisContext context) { context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression); } private void Analyze(SyntaxNodeAnalysisContext context) { // 获取当前的节点 var node = (InvocationExpressionSyntax)context.Node; // 我们肯定会根据 ImmutableArray<int>.Empty.Add(1); 找到这个特点 // 我们看到了ArgumentList是有(1)值的,所以小于一个参数的跳过 if (node.ArgumentList.Arguments.Count != 1) return; // 无法将表达式转换成成员、方法、属性的去掉 // 一般找都是从右往左去找 if (!(node.Expression is MemberAccessExpressionSyntax addAccess)) return; // 判断方法名是否胃Add if (addAccess.Name.Identifier.Text != "Add") return; // 获取上一个的成员、方法、属性 if (!(addAccess.Expression is MemberAccessExpressionSyntax emptyAccess)) return; // 判断是不是Empty,不是就直接返回 if (emptyAccess.Name.Identifier.Text != "Empty") return; // 判断是不是GenericNameSyntax类型的 if (!(emptyAccess.Expression is GenericNameSyntax ImmutableArrayAccess)) return; // 判断是不是是否有一个泛型的类型 if (ImmutableArrayAccess.TypeArgumentList.Arguments.Count != 1) return; // 判断是否是ImmutableArray if (ImmutableArrayAccess.Identifier.Text != "ImmutableArray") return; // 创建提示的消息 context.ReportDiagnostic(Diagnostic.Create(descriptor, node.GetLocation())); } } ``` ### 项目启动测试 tn2>设置`MyRoslyn.Vsix`为项目启动项。 ![](https://img.tnblog.net/arcimg/hb/ce07764986c74114bc9f557ce7e39082.png) tn2>然后按`F5`运行。 打开我们的`TempProject1`项目。 ![](https://img.tnblog.net/arcimg/hb/74c696e60e784709bb23909fb6adc1e8.png) ![](https://img.tnblog.net/arcimg/hb/8da0e265fe084d52890f1c966e172d7c.png) tn2>我们可以看到我们创建的提示消息显示出来了。 除此之外还有它的不能以小写的类名创建,并且还给出命名的提示代码。 ![](https://img.tnblog.net/arcimg/hb/7661f33fd5564c0693827fc629377ba0.png) tn2>当然修复大小写命名的代码是`MyRoslynCodeFixProvider`提供的。 ![](https://img.tnblog.net/arcimg/hb/9cdbb12552964c7094a8fbfd97eb3023.png) ![](https://img.tnblog.net/arcimg/hb/1eb06bf6adf449a49da560b2f3a40083.png)