Roslyn 入門:使用 Roslyn 靜態分析現有項目中的代碼


版權聲明:本文為博主原創文章,遵循 CC 4.0 by-sa 版權協議,轉載請附上原文出處鏈接和本聲明。
本文鏈接: https://blog.csdn.net/WPwalter/article/details/79616402

Roslyn 是微軟為 C# 設計的一套分析器,它具有很強的擴展性。以至於我們只需要編寫很少量的代碼便能夠分析我們的項目文件。

作為 Roslyn 入門篇文章,你將可以通過本文學習如何開始編寫一個 Roslyn 擴展項目,如何開始分析一個解決方案(.sln)中項目(.csproj)的代碼文件(.cs)。


本文是 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>
現在,我們有了一個可以開始寫代碼的 Program.cs 文件,接下來就可以正式開始入門了。

安裝必要的 NuGet 包

在 NuGet 包管理器中搜索並安裝 Microsoft.CodeAnalysis 包 —— 這是一個包含 Roslyn 所有 API 的各種 NuGet 包的合集。

Microsoft.CodeAnalysis

當然,如果你只是做一些特定的事情,當然不需要安裝這么全的 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);
    }
}
  其實這段代碼就是 CSharpSyntaxRewriter 基類中的代碼,我把它貼出來可以幫助我們理解它。 你也依然需要將他放入到我們的項目中 ,因為我們接下來的代碼就開始要使用它了。

如果你想了解更多語法節點,推薦另一篇入門文章: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 節點做各種后續的操作。
如果我們使用 node 的方式是修改代碼,那么可以使用 var text = node.GetText(); 來得到新的語法樹生成的代碼,使用這段文本替換之前的文本可以達到修改代碼的目的。不過,這不是本文的重點,本文的重點依然在入門。

現在,整合以上的三大段代碼,你的項目應該能夠完整地跑起來了。哪三段?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"));
 

特別注意:SeparatedSyntaxListAdd 操作不會修改原集合,而是會返回一個新的集合!所以上面 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 里面調用它。
  • 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);
        }
    }
}
 

參考資料

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM