前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >.NET Roslyn快速上手指南

.NET Roslyn快速上手指南

作者头像
郑子铭
发布2024-12-20 17:01:56
发布2024-12-20 17:01:56
14000
代码可运行
举报
运行总次数:0
代码可运行

Roslyn简介

Roslyn是C#和Visual Basic编译器的开源实现,具有用于构建代码分析工具的API表面。Roslyn还提供可供IDE使用的语言服务,例如重构、代码修复或编辑并继续。

Roslyn分析器

Roslyn 分析器允许您使用 Roslyn 中的数据来检查代码以检测问题。分析器可以直接在编辑器中添加错误、警告或波浪线。

简单实践

首先创建一个Analyzer with Code Fix项目命名为MyRoslyn。框架我选择的4.7.2版本。

该解决方案包含4个项目:

首先我们打开MyRoslynAnalyzer代码。查看其中的每一行意思。

代码语言:javascript
代码运行次数:0
复制
/// <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);
        }
    }
}
// 使用 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;
    }
}

当然我们也可以自定义一个CreationAnalyzer的分析器。我想对当有写到ImmutableArray.Empty.Add(1)代码时就对其中做一些警告的提示处理。这里我打开了另外的一个窗口,然后创建一个TempProject1项目,添加了一些简单的代码。

代码语言:javascript
代码运行次数:0
复制
// 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);

然后我们打开Syntax Visualizer窗口。

分析我们我们选中的ImmutableArray.Empty.Add(1)这一行。

通过分析我们会发现,表达式树解析是从右往左解析的,举例:Add(1)—>Empty—>ImmutableArray—>ImmutableArray 所以我们要锁定这一行的代码的话,首先我们会判断它有一个ArgumentList参数是大于0的,所以ArgumentList不大于0的节点的可以忽略了。代码就这样写:

代码语言:javascript
代码运行次数:0
复制
// 获取当前的节点
var node = (InvocationExpressionSyntax)context.Node;
// 我们肯定会根据 ImmutableArray<int>.Empty.Add(1); 找到这个特点
// 我们看到了ArgumentList是有(1)值的,所以小于一个参数的跳过
if (node.ArgumentList.Arguments.Count != 1) return;

然后通过该节点的Expression获取到Add方法,如果我们没有Add方法的节点就可以忽略了。但是怎么知道这个Expression的类型内,很简单:只需要选中ImmutableArray.Empty.Add,它就显示出它的类型为MemberAccessExpressionSyntax.

对应的代码如下:

代码语言:javascript
代码运行次数:0
复制
// 无法将表达式转换成成员、方法、属性的去掉
// 一般找都是从右往左去找
if (!(node.Expression is MemberAccessExpressionSyntax addAccess)) return;
// 判断方法名是否为Add
if (addAccess.Name.Identifier.Text != "Add") return;

然后我们以此内推Empty也是这样。

代码语言:javascript
代码运行次数:0
复制
// 获取上一个的成员、方法、属性
if (!(addAccess.Expression is MemberAccessExpressionSyntax emptyAccess)) return;
// 判断是不是Empty,不是就直接返回
if (emptyAccess.Name.Identifier.Text != "Empty") return;

然后到解析ImmutableArray有变化了。

代码语言:javascript
代码运行次数:0
复制
// 判断是不是GenericNameSyntax类型的
if (!(emptyAccess.Expression is GenericNameSyntax ImmutableArrayAccess)) return;
// 判断是不是是否有一个泛型的类型
if (ImmutableArrayAccess.TypeArgumentList.Arguments.Count != 1) return;
// 判断是否是ImmutableArray
if (ImmutableArrayAccess.Identifier.Text != "ImmutableArray") return;

然后我贴上完整的CreationAnalyzer代码。

代码语言:javascript
代码运行次数:0
复制
/// <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()));
    }
}

项目启动测试

设置MyRoslyn.Vsix为项目启动项。

然后按F5运行。打开我们的TempProject1项目。

我们可以看到我们创建的提示消息显示出来了。除此之外还有它的不能以小写的类名创建,并且还给出命名的提示代码。

当然修复大小写命名的代码是MyRoslynCodeFixProvider提供的。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-12-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Roslyn简介
  • Roslyn分析器
  • 简单实践
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档