[ Source Generator ] 使用 SyntaxReceiver 快速篩選關注的 Syntax 資訊

Source Generator 是微軟於 .NET 5 所推出的新功能,

它允許我們從原始碼編譯的結果中取得所需的 meta 資訊,

進而根據這些資訊去組出額外的程式碼,並加至最後的編譯結果中。

而當原始碼數量過於龐大時,將篩選 Syntax 的邏輯寫在 Generator 內就會稍顯雜亂。

這時可以使用 SyntaxReceiver 幫助我們快速篩選所需的 Syntax 資訊!

前言 & 作業環境

前面提到,SyntaxRecevier 可以幫助我們快速篩選所需的 Syntax 資訊,

在實作開始前,請先檢查是否以滿足所需的作業環境

  • Visual Studio 2019 v16.8.0 ↑
  • .NET 5 ( SDK 5.0.100 )

本文適合對 Source Generator 有初步理解的朋友閱讀

第一次接觸的朋友建議可先閱讀 Introducing C# Source Generators 。

實作 SyntaxReceiver

整體作法不算太難,

首先請先實作 ISyntaxRecevier 介面,

public class MySyntaxReceiver : ISyntaxReceiver
{
    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        throw new NotImplementedException();
    }
}

這個介面只有一個方法 OnVisitSyntaxNode

你可以當作 Generator 會把原始碼編譯過的每個 SyntaxNode 丟進來檢查,

若符合想要的條件時,我們可以透過物件屬性 ( Property ) 將它保留下來。

 

舉例來說,如果想要快速的篩選出類別名稱為 MyClass 的 Syntax 資訊,

我們可以先檢查 SyntaxNode 是否為類別宣告用的 ClassDeclarationSyntax

若符合,則透過 Identifier.ValueText 比對類別名稱,

程式碼如下:

public class MySyntaxReceiver : ISyntaxReceiver
{
    public ClassDeclarationSyntax MyClassSyntax { get; set; }

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        if (syntaxNode is ClassDeclarationSyntax classSyntax &&
            classSyntax.Identifier.ValueText == "MyClass")
        {
            MyClassSyntax = classSyntax;
        }
    }
}

 

完成後回到 Generator 的 Initialize 方法,

透過 GeneratorInitializationContext 註冊 MySyntaxRecevier

public void Initialize(GeneratorInitializationContext context)
{
    context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
}

 

最後於 Generator 執行階段的 Execute 方法,

再從 GeneratorExecutionContext 身上將 SyntaxReceiver 取出,

做個簡單的轉型後就可以取回 MySyntaxReceiver 身上的屬性資訊了!

public void Execute(GeneratorExecutionContext context)
{
    if (context.SyntaxReceiver is MySyntaxReceiver mySyntaxReceiver)
    {
        ClassDeclarationSyntax classSyntax = mySyntaxReceiver.MyClassSyntax;
    }
}

 

探討 - 可否實作多個 SyntaxRecevier?

在好奇之下我嘗試了實作多個 SyntaxReceiver

先說結果:「不行,會噴例外。」

這邊我額外註冊了另一個 OtherSyntaxRecevier,程式碼如下。

public void Initialize(GeneratorInitializationContext context)
{
    //手動呼叫偵錯器
    Debugger.Launch();

    context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
    context.RegisterForSyntaxNotifications(() => new OtherSyntaxReceiver());
}

使用偵錯模式就會得到例外 ( 如下圖 )。

從註冊的 RegisterForSyntaxNotifications 方法追了一下,

發現內部會先檢查 InfoBuilder 本身是否已持有 SyntaxReceiverCreator

如果是的話就拋出例外,程式碼如下。

public void RegisterForSyntaxNotifications(SyntaxReceiverCreator receiverCreator)
{
    CheckIsEmpty(InfoBuilder.SyntaxReceiverCreator);
    InfoBuilder.SyntaxReceiverCreator = receiverCreator;
}

private static void CheckIsEmpty<T>(T x)
{
    if (x is object)
    {
        throw new InvalidOperationException(string.Format(CodeAnalysisResources.Single_type_per_generator_0, typeof(T).Name));
    }
}

而 SyntaxReceiverCreator 本身其實是一個回傳 ISyntaxReceiver 的委派,

public delegate ISyntaxReceiver SyntaxReceiverCreator();

 

結語

Source Generator  雖伴隨著 .NET 5 的發布而釋出,

其「無法修改原始碼」的特性讓人又愛又恨,

目前整體來說用起來仍稱不上「順手」。

但官方在 .NET 5 的釋出文件中提到

We expect to make more use of source generators within the .NET product in .NET 6.0 and beyond.

讓我們期待一下它在 .NET 6 會有怎樣的表現吧!

最後附上完整實作程式碼連結 ( GitHub ) 

 

參考

Roslyn - source-generator.cookbook ( GitHub)