Roslyn 是微軟為 C# 設計的一套分析器,它具有很強的擴展性。以至於我們只需要編寫很少量的代碼便能夠分析我們的項目文件。
作為 Roslyn 入門篇文章,你將可以通過本文學習如何開始編寫一個 Roslyn 擴展項目,如何開始分析一個解決方案(.sln)中項目(.csproj)的代碼文件(.cs)。
本文是 Roslyn 入門系列之一:
- Roslyn 入門:使用 Visual Studio 的語法可視化(Syntax Visualizer)窗格查看和了解代碼的語法樹
- Roslyn 入門:使用 .NET Core 版本的 Roslyn 編譯並執行跨平台的靜態的源碼
- Roslyn 入門:使用 Roslyn 靜態分析現有項目中的代碼(本文)
如果你希望真實地靜態分析一個實際項目,並且理解這樣的分析過程是如何進行的(而不只是寫個 demo),那么本文的所有內容都將是必要的。
准備工作
為了能夠進行后面關鍵的操作,我們需要先有一個能跑起來的項目。
▲ 在 Visual Studio 新建項目,選擇“控制台程序(.NET Framework)”
在目前({% include date.html date=page.date %}),如果我們需要像本文一樣分析現有的解決方案和項目,那么 .NET Framework 是必須的;如果只是分析單個文件,那么也可以選擇 .NET Core,參見 Roslyn 入門:使用 .NET Core 版本的 Roslyn 編譯並執行跨平台的靜態的源碼。
當然,如果你有一個現成的 .NET Core 項目,可以通過修改 .csproj 文件改成 .NET Framework 的:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<!-- 從 netcoreapp2.0 改成 net471,因為 NuGet 包中的 ValueTuple 與 net47 不兼容,所以只能選擇 net471 或以上 -->
<TargetFramework>net471</TargetFramework>
</PropertyGroup>
</Project>
安裝必要的 NuGet 包
在 NuGet 包管理器中搜索並安裝 Microsoft.CodeAnalysis 包 —— 這是一個包含 Roslyn 所有 API 的各種 NuGet 包的合集。
當然,如果你只是做一些特定的事情,當然不需要安裝這么全的 NuGet 包,像 Roslyn 靜態分析 - 林德熙 的 demo 和 Roslyn 編譯與執行 - 呂毅 中的教程就不需要安裝所有 NuGet 包。
特別注意!!!如果前面你是通過 .NET Core 項目改過來的,那么還需要額外安裝以下三個 NuGet 包,否則運行時會無法打開解決方案和項目。
Microsoft.Build
Microsoft.Build.Tasks.Core
System.Threading.Tasks.Dataflow
打開一個解決方案/項目和其中的文件
現在,我們使用這些代碼打開解決方案。我以 MSTestEnhancer 為例:
// 打開 MSTestEnhancer(https://github.com/dotnet-campus/MSTestEnhancer/) 解決方案文件。
// 注意這里的 MSBuildWorkspace.Create() 會返回 WorkSpace 的實例。
// 雖然 WorkSpace 是跨平台的,但是 MSBuildWorkspace 僅在 Windows 下可用。
var solution = await MSBuildWorkspace.Create().OpenSolutionAsync(
@"D:\Developments\Open\MSTestEnhancer\MSTest.Extensions.sln");
// 從解決方案中選出 MSTest.Extensions 項目。
var project = solution.Projects.First(x => x.Name == "MSTest.Extensions");
// 從 MSTest.Extensions 項目中選出我們要分析的 ContractTestContext.cs 文件。
// 這里只是一個示例,所以我們只分析一個文件。你可以從 Documents 集合中找出這個項目的所有文件進行分析。
var document = project.Documents.First(x =>
x.Name.Equals("ContractTestContext.cs", StringComparison.InvariantCultureIgnoreCase));
分析代碼
我們要分析的代碼大致是這樣的:
// 這里是 using,省略。
// 這里是命名空間,省略。
public class ContractTestContext<T>
{
// 這是代碼的細節,省略。
}
現在,我們開始使用 Roslyn API 找出里面的泛型 T
。
這里,我們必須引入一個概念 —— Syntax Rewriter。
語法重寫——Syntax Rewriter
Roslyn 對 C# 代碼進行分析的一個非常關鍵的 API 是 CSharpSyntaxRewriter
——這是一個專門用來給你繼承的類。CSharpSyntaxRewriter
是訪問者模式中訪問者的一個實現,如果你不了解訪問者模式,推薦閱讀 23種設計模式(9):訪問者模式 - CSDN博客 進行了解,否則我們后面的代碼你將只能跟着我寫,而不能明白其中的含義。
當你閱讀到這里時,我開始假設你已經了解了訪問者模式了。
我們每個人都可能會寫出不同的基於 Roslyn 的分析器,這些分析器通常都會對不同文件的 C# 語法樹進行不同的操作;於是,我們通過重寫 CSharpSyntaxRewriter
可以實現各種各樣不同的操作。在訪問者模式中,由於 C# 的語法在一個 C# 版本發布之后就會確定,其中各種各樣類型的語法對應訪問者模式中的各種不同類型的數據,Roslyn 為我們構建的語法樹對應訪問者模式中需要訪問的龐大的數據結構。由於 Roslyn 的語法樹是非常龐大的,以至於對其進行遍歷也是一個非常復雜的操作;所以 Roslyn 通過訪問者模式為我們封裝了這種復雜的遍歷過程,我們只需要重寫 CSharpSyntaxRewriter
就可以實現對某種特定語法節點的操作。
現在,我們編寫一個用於找出泛型參數 T
的 Syntax Rewriter。
class TypeParameterVisitor : CSharpSyntaxRewriter
{
public override SyntaxNode VisitTypeParameterList(TypeParameterListSyntax node)
{
var lessThanToken = this.VisitToken(node.LessThanToken);
var parameters = this.VisitList(node.Parameters);
var greaterThanToken = this.VisitToken(node.GreaterThanToken);
return node.Update(lessThanToken, parameters, greaterThanToken);
}
}
如果你想了解更多語法節點,推薦另一篇入門文章:Roslyn 入門:使用 Visual Studio 的語法可視化(Syntax Visualizer)窗格查看和了解代碼的語法樹。
訪問泛型參數
現在,我們繼續在之前打開解決方案和項目文件的代碼后面增添代碼:
// 從我們一開始打開的項目文件中獲取語法樹。
var tree = await document.GetSyntaxTreeAsync();
var syntax = tree.GetCompilationUnitRoot();
// 使用我們剛剛重寫 CSharpSyntaxRewriter 的類來訪問語法樹。
var visitor = new TypeParameterVisitor();
var node = visitor.Visit(syntax);
// 得到的 node 是新的語法樹節點,
// 如果我們在 `TypeParameterVisitor` 中修改了語法樹,
// 那么這里就會得到修改后的 node 節點。
// 我們可以通過這個 node 節點做各種后續的操作。
現在,整合以上的三大段代碼,你的項目應該能夠完整地跑起來了。哪三段?1. 打開項目文件;2. TypeParameterVisitor
;3. 訪問泛型參數。其中 1 和 3 寫在一個方法中,2 是一個新類。
分析這個泛型參數
直到現在,我們所寫的任何代碼都還只是為了使使用 Roslyn API 的代碼能夠跑起來,沒有進行任何實質上的分析。接下來,我們會修改 CSharpSyntaxRewriter
以進行真正的分析。不過在此之前,我假設上面的代碼你是能正常跑起來而且沒有錯誤的。(如果不行,就在下面留言吧!留言有郵件通知的,我會在第一時間回復你。)
如果你不了解 Roslyn,強烈建議去 VisitTypeParameterList
重寫方法中打一個斷點觀察 lessThanToken
parameters
greaterThanToken
這幾個實例的含義。lessThanToken
就是 <
,greaterThanToken
就是 >
;而 parameters
是一個泛型參數列表,在這里,是一個 T
。
現在,我們構造一個自己的泛型參數列表試試,名字不是 T
了,而是 TParameter
。
var parameters = new SeparatedSyntaxList<TypeParameterSyntax>();
parameters = parameters.Add(SyntaxFactory.TypeParameter("TParameter"));
特別注意:SeparatedSyntaxList
的 Add
操作不會修改原集合,而是會返回一個新的集合!所以上面 Add
之后的賦值語句不能少!這樣的設計應該是為了避免遍歷語法樹的時候語法樹被修改導致遍歷不可控。
於是,我們的 TypeParameterVisitor
變成了這樣:
class TypeParameterVisitor : CSharpSyntaxRewriter
{
public override SyntaxNode VisitTypeParameterList(TypeParameterListSyntax node)
{
// 構造一個自己的泛型列表,名字改為了 TParameter。
var parameters = new SeparatedSyntaxList<TypeParameterSyntax>();
parameters = parameters.Add(SyntaxFactory.TypeParameter("TParameter"));
// 依然保留之前的更新語法節點的方法。
// 這樣,我們將會在語法樹訪問結束后得到新的語法樹。
var lessThanToken = this.VisitToken(node.LessThanToken);
var greaterThanToken = this.VisitToken(node.GreaterThanToken);
return node.Update(lessThanToken, parameters, greaterThanToken);
}
}
總結
我們總共編寫了兩個關鍵類:
- Program
- Main(用於打開項目和文件,並調用 TypeParameterVisitor 遍歷語法樹)
需要注意,Main 函數只有 C#7.2 及以上才支持async
,如果沒有這么高,需要再編寫一個新函數,然后在 Main 里面調用它。
- Main(用於打開項目和文件,並調用 TypeParameterVisitor 遍歷語法樹)
- TypeParameterVisitor
- VisitTypeParameterList(用於遍歷和修改語法樹中的泛型參數列表)
以上便是分析和修改 Roslyn 語法樹的簡單實例了,我將整個 Program.cs 文件貼在下面,以便整體查看。
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;
namespace Walterlv.Demo.Roslyn
{
class Program
{
static void Main(string[] args)
{
RunAsync().Wait();
}
private static async Task RunAsync()
{
var solution = await MSBuildWorkspace.Create().OpenSolutionAsync(
@"D:\Developments\Open\MSTestEnhancer\MSTest.Extensions.sln");
var project = solution.Projects.First(x => x.Name == "MSTest.Extensions");
var document = project.Documents.First(x =>
x.Name.Equals("ContractTestContext.cs", StringComparison.InvariantCultureIgnoreCase));
var tree = await document.GetSyntaxTreeAsync();
var syntax = tree.GetCompilationUnitRoot();
var visitor = new TypeParameterVisitor();
var node = visitor.Visit(syntax);
var text = node.GetText();
File.WriteAllText(document.FilePath, text.ToString());
}
}
class TypeParameterVisitor : CSharpSyntaxRewriter
{
public override SyntaxNode VisitTypeParameterList(TypeParameterListSyntax node)
{
var syntaxList = new SeparatedSyntaxList<TypeParameterSyntax>();
syntaxList = syntaxList.Add(SyntaxFactory.TypeParameter("TParameter"));
var lessThanToken = this.VisitToken(node.LessThanToken);
var greaterThanToken = this.VisitToken(node.GreaterThanToken);
return node.Update(lessThanToken, syntaxList, greaterThanToken);
}
}
}
參考資料