Roslyn是C#和Visual Basic编译器的开源实现,具有用于构建代码分析工具的API表面。Roslyn还提供可供IDE使用的语言服务,例如重构、代码修复或编辑并继续。
Roslyn 分析器允许您使用 Roslyn 中的数据来检查代码以检测问题。分析器可以直接在编辑器中添加错误、警告或波浪线。
首先创建一个Analyzer with Code Fix项目命名为MyRoslyn。框架我选择的4.7.2版本。
该解决方案包含4个项目:
首先我们打开MyRoslynAnalyzer代码。查看其中的每一行意思。
/// <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项目,添加了一些简单的代码。
// 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的节点的可以忽略了。代码就这样写:
// 获取当前的节点
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.
对应的代码如下:
// 无法将表达式转换成成员、方法、属性的去掉
// 一般找都是从右往左去找
if (!(node.Expression is MemberAccessExpressionSyntax addAccess)) return;
// 判断方法名是否为Add
if (addAccess.Name.Identifier.Text != "Add") return;
然后我们以此内推Empty也是这样。
// 获取上一个的成员、方法、属性
if (!(addAccess.Expression is MemberAccessExpressionSyntax emptyAccess)) return;
// 判断是不是Empty,不是就直接返回
if (emptyAccess.Name.Identifier.Text != "Empty") return;
然后到解析ImmutableArray有变化了。
// 判断是不是GenericNameSyntax类型的
if (!(emptyAccess.Expression is GenericNameSyntax ImmutableArrayAccess)) return;
// 判断是不是是否有一个泛型的类型
if (ImmutableArrayAccess.TypeArgumentList.Arguments.Count != 1) return;
// 判断是否是ImmutableArray
if (ImmutableArrayAccess.Identifier.Text != "ImmutableArray") return;
然后我贴上完整的CreationAnalyzer代码。
/// <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提供的。