什麼是 Roslyn?── 用編譯器的視角讀懂、修正、產生 C# 程式碼
· 小村 豪 · .NET, CSharp, Roslyn, Analyzer, SourceGenerator, Compiler, StaticAnalysis, CodeGeneration, 既有資產活用
1. 首先要掌握的事
想要處理 C# 原始碼的場合,其實比想像中更多。舉例來說,有這類工作:
想要禁止使用某個特定的 API
想要機械式地找出過時的寫法
想要收集方法或類別的清單
想要調查整個專案的相依關係
想要在編譯時期生成固定格式的程式碼
想要在建置時對自家函式庫的誤用發出警告
想要安全地進行大規模的替換或移轉
這時候,可能會想單純打開 *.cs 檔案,用字串搜尋或正規表達式來處理。
然而,C# 並不是字串。以下這兩段程式碼,外觀相似,但意義上是完全不同的東西。
Console.WriteLine("Hello");
MyCompany.Logging.Console.WriteLine("Hello");
此外,Console 這個名稱也有可能指向另一個型別。
using Console = MyCompany.Logging.Console;
Console.WriteLine("Hello");
若只看字串,這些看起來全都是 Console.WriteLine。
但從編譯器的角度來看,若不進行名稱解析,就無法判斷它究竟是 System.Console.WriteLine,還是別的型別。
這時候登場的就是 Roslyn。Roslyn 是一個把 C# 與 Visual Basic 編譯器所持有的資訊,以 API 的形式提供給應用程式與工具使用的平台。
簡單來說,使用 Roslyn 之後,就能以下列方式處理 C# 程式碼。
不是當作字串,而是當作語法來讀取
不是看外觀,而是看語意來理解
不是單一檔案,而是以專案或方案的形式來讀取
根據讀取的結果,輸出警告、修正建議與生成程式碼
本文將整理 Roslyn 的整體概觀、Syntax Tree、SemanticModel、Workspace、Analyzer、Source Generator,以及在實務上的使用場合。
另外,本文中出現的程式碼,已整理為一套可建置、可執行的範例(處理 Syntax Tree / SemanticModel 的函式庫、對 DateTime.Now 發出警告的 Analyzer、Source Generator、解析整個方案的示範程式,以及確認誤判與漏判的單元測試),並公開於 GitHub。
roslyn-dotnet-compiler-platform - komurasoft-blog-samples (GitHub)
2. Roslyn 是什麼
Roslyn 正式名稱為 .NET Compiler Platform。它是 C# 與 Visual Basic 的編譯器實作,同時也是一組用來建立程式碼分析工具的 API。
過去,編譯器往往被當作這樣的「黑盒子」來看待。
輸入原始碼
編譯器進行處理
輸出 DLL / EXE
一般來說,開發者無法存取編譯器內部產生的資訊。
但實際上,編譯器並不只是單純把文字轉換成機器碼或 IL,而是在編譯過程中產生了這些資訊。
這個字串是類別宣告
這個識別字是區域變數
這個方法呼叫指向這個型別的這個方法
這個運算式的傳回值型別是 string
這段程式碼含有語法錯誤
這個參考指向組件 A 中的型別
這個 using 實際上並未被使用
Roslyn 讓開發者可以使用這類資訊。因此,Roslyn 不只是單純的編譯器,而是一個用來理解程式碼的平台。
3. 使用 Roslyn 能做什麼
使用 Roslyn,主要能做到以下事情。
C# / VB 的語法解析
型別與方法的語意解析
取得編譯資訊
解析整個專案或方案
建立自訂 Analyzer
建立 Code Fix
建立 Source Generator
建立重構工具
程式碼生成
程式碼轉換
寫得更貼近實務一點,用法大致如下。
使用被禁止的 API 時發出建置警告
列出使用舊版 API 的位置
檢查 async 方法的命名規則
偵測 IDisposable 處理不當的情況
引導正確使用自家框架的方式
在編譯時期生成 DTO 或對應程式碼
從設定檔或屬性生成固定格式的程式碼
輔助從 .NET Framework 移轉到 .NET 的調查工作
Roslyn 之所以重要,是因為它讓「處理 C# 原始碼的程式」可以建立在與編譯器相同的基礎之上。
用正規表達式或自製剖析器來讀取 C#,很快就會遇到極限。舉例來說,以下這些元素並不容易被正確處理。
using alias
擴充方法
partial class
partial method
global using
nullable annotations
泛型型別
多載解析
條件式編譯
前置處理器指示詞
保留註解與空白的改寫
Roslyn 提供了依照 C# 語言規格來處理這些元素的 API。
4. Roslyn 將「語法」與「語意」分開思考
理解 Roslyn 時,首先要區分開來的是這兩個概念。
語法:程式碼是怎麼寫的
語意:這段程式碼指的是什麼
舉例來說,來看這段程式碼。
Console.WriteLine(message);
從語法上來看,它是這樣的結構。
運算式陳述式
呼叫運算式
成員存取運算式
識別字 Console
識別字 WriteLine
引數 message
但光靠這些,還無法知道它的語意。Console 屬於哪個型別、WriteLine 是哪個多載、message 的型別是什麼,光靠語法是無法判斷的。
要理解語意,需要以下這些資訊。
using 的狀態
參考的組件
同一個專案內的型別定義
對其他專案的參考
型別推論
多載解析
語言版本
nullable context
在 Roslyn 中,這個差異也反映在 API 上。
Syntax Tree : 表示程式碼的語法
SemanticModel : 表示語法所代表的語意
Compilation : 表示編譯所需的整體資訊
Workspace : 處理方案、專案、文件
掌握這個區別之後,Roslyn 的整體架構就會清晰許多。
5. 什麼是 Syntax Tree
Syntax Tree 是表示原始碼語法結構的樹狀結構。舉例來說,假設有這段程式碼。
class User
{
public string Name { get; set; }
public void Rename(string name)
{
Name = name;
}
}
從 Roslyn 的角度來看,這段程式碼大致是這樣的結構。
CompilationUnit
ClassDeclaration: User
PropertyDeclaration: Name
MethodDeclaration: Rename
Parameter: name
Block
ExpressionStatement
AssignmentExpression
Syntax Tree 並不是單純把文字依行拆開,而是依類別、方法、屬性、運算式、陳述式、引數、運算子等 C# 的語法元素整理而成的結構。
來看一個簡單的範例。
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
var source = """
class User
{
public string Name { get; set; }
public void Rename(string name)
{
Name = name;
}
}
""";
var tree = CSharpSyntaxTree.ParseText(source);
var root = tree.GetCompilationUnitRoot();
var methods = root
.DescendantNodes()
.OfType<MethodDeclarationSyntax>();
foreach (var method in methods)
{
Console.WriteLine(method.Identifier.Text);
}
這段程式碼會從原始碼中尋找方法宣告,並輸出方法名稱。在這個例子中會取得 Rename。
重點在於,這裡不是用字串搜尋去找 void,而是以 C# 的語法概念來尋找「方法宣告」。
6. Node、Token、Trivia
處理 Syntax Tree 時,常會出現以下這三個詞彙。
SyntaxNode
SyntaxToken
SyntaxTrivia
SyntaxNode
SyntaxNode 是語法上的一個整體單位。舉例來說,包括:
類別宣告
方法宣告
屬性宣告
if 陳述式
for 陳述式
指定運算式
呼叫運算式
Lambda 運算式
在 C# 語法上,還擁有子元素的部分就是 Node。
SyntaxToken
SyntaxToken 是構成語法的最小單位。舉例來說,包括:
class 關鍵字
public 關鍵字
識別字 User
識別字 Rename
{ 或 }
; 或 ,
字串常值
數值常值
Token 是位於語法樹末端的元素。
SyntaxTrivia
SyntaxTrivia 是與一般語意解析沒有直接關係,但為了還原原始碼所必需的資訊。舉例來說,包括:
空白
換行
註解
前置處理器指示詞
正因為有這個 Trivia,Roslyn 才能以高保真度處理包含註解與空白的原始碼。
在進行程式碼格式化、重構、機械式改寫時,Trivia 是非常重要的存在。
如果只是要建立 AST,捨棄註解似乎也無妨。 但在實務上的程式碼轉換中,不破壞註解與換行是很重要的一點。
7. Syntax Tree 是不可變的
Roslyn 的 Syntax Tree 是不可變的。也就是說,並不會直接修改已取得的語法樹,而是會建立一個加上變更後的新語法樹。
舉例來說,即使想更改方法名稱,也不會直接就地修改現有的 MethodDeclarationSyntax。
var newMethod = oldMethod.WithIdentifier(
SyntaxFactory.Identifier("NewName"));
而是像這樣建立一個新的節點。
不可變性帶來了幾項優點。
容易在多執行緒下處理
可以安全地處理 IDE 編輯中的快照
容易產生差異
容易比較變更前後的內容
一開始可能會覺得有點麻煩。但在 IDE、建置、Analyzer、Source Generator 這類多個處理程序同時參考同一份程式碼的世界中,不可變性會帶來很大的優勢。
8. 什麼是 SemanticModel
單靠 Syntax Tree,只能知道程式碼的外觀。要理解語意,需要使用 SemanticModel。
舉例來說,來看這段程式碼。
Console.WriteLine("Hello");
從 Syntax Tree 只能知道有 Console 與 WriteLine 這兩個識別字。但無法知道它究竟指向 System.Console.WriteLine(string?),還是別的型別的方法。
使用 SemanticModel,就能取得「這個語法節點被解析成哪個 Symbol」。
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
var source = """
using System;
class Program
{
static void Main()
{
Console.WriteLine("Hello");
}
}
""";
var tree = CSharpSyntaxTree.ParseText(source);
var compilation = CSharpCompilation.Create(
assemblyName: "Sample",
syntaxTrees: new[] { tree },
references: new[]
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location)
});
var semanticModel = compilation.GetSemanticModel(tree);
var root = tree.GetCompilationUnitRoot();
var invocation = root
.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.First();
var symbolInfo = semanticModel.GetSymbolInfo(invocation);
var method = (IMethodSymbol?)symbolInfo.Symbol;
Console.WriteLine(method?.ContainingType.ToDisplayString());
Console.WriteLine(method?.Name);
透過這種方式,就能取得 Console.WriteLine 實際上被解析成哪個方法。
Roslyn 的優勢,就在於不只依賴語法,還能運用編譯器的名稱解析結果。
9. 什麼是 Symbol
在 Roslyn 中,型別、方法、屬性、欄位、引數、區域變數等,都會被當作 Symbol 來處理。
以下列出具代表性的介面。
INamedTypeSymbol : 類別、結構、介面等
IMethodSymbol : 方法
IPropertySymbol : 屬性
IFieldSymbol : 欄位
IParameterSymbol : 引數
ILocalSymbol : 區域變數
INamespaceSymbol : 命名空間
Symbol 所表示的不是原始碼上的外觀,而是編譯器所解析出的語意。
舉例來說,以下這兩段程式碼,外觀不同。
System.Console.WriteLine("Hello");
using System;
Console.WriteLine("Hello");
但只要兩者都指向同一個 System.Console.WriteLine,在 Roslyn 的語意解析中,就能被視為同一個方法 Symbol。
透過這個特性,就能進行以下這類判斷。
這個呼叫是否真的是自家禁止使用的 API
這個型別是否實作了特定介面
這個方法是否為 async
這個傳回值是否為 nullable
這個屬性(Attribute)是否真的有被附加
這個類別是否繼承自特定的基底類別
這使得分析不再只是單純的字串搜尋,而是能依據編譯器的判斷來進行。
10. 什麼是 Compilation
Compilation 匯總了編譯 C# 或 Visual Basic 程式所需的資訊。
具體來說,它擁有以下這些資訊。
SyntaxTree 的集合
參考的組件
編譯選項
語言版本
已定義的 Symbol
型別與成員的資訊
診斷資訊
如果只是要以語法方式讀取單一檔案,SyntaxTree 就已經足夠。但若要進行型別解析或參考解析,就需要 Compilation。
舉例來說,以下這類需求就屬於這種情況。
想知道這個方法呼叫是哪個型別的方法
想知道這個類別是否實作了 IDisposable
想知道這個屬性(Attribute)實際上是哪種屬性型別
想知道這個運算式的傳回值型別
想取得編譯錯誤或警告
這些光靠語法是無法判斷的,必須連同專案的參考與編譯選項一起考量。
11. 什麼是 Workspace
如果想處理的不是單一檔案,而是整個方案或專案,就要使用 Workspace。
Workspace 處理的是以下這些單位。
Solution
Project
Document
舉例來說,從整個方案讀取所有專案,並解析所有文件的情境。
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.MSBuild;
MSBuildLocator.RegisterDefaults();
using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync("Sample.sln");
foreach (var project in solution.Projects)
{
Console.WriteLine(project.Name);
foreach (var document in project.Documents)
{
var root = await document.GetSyntaxRootAsync();
Console.WriteLine($" {document.Name}: {root?.DescendantNodes().Count()} nodes");
}
}
建立這樣的工具,就能用於調查既有程式碼庫或輔助移轉工作。
舉例來說,可以用於以下用途。
將特定 API 呼叫的清單匯出為 CSV
列出使用舊版命名空間的位置
建立 public API 的清單
調查專案之間的相依關係
調查大型方案中違反程式碼規範的情況
進行機械式的程式碼轉換
Analyzer 是與 IDE 及建置流程一體運作的機制。 另一方面,使用 Workspace 建立的主控台工具,則適合用於調查或一次性的大規模移轉。
這兩者雖然相似,但區分使用場合會比較好。
12. 使用 Roslyn 的代表性方式
Roslyn 的使用方式,大致可分為以下四種。
1. 當作函式庫使用
2. 建立 Analyzer
3. 建立 Code Fix
4. 建立 Source Generator
各自的目的並不相同。
當作函式庫使用
從自製的主控台應用程式或內部工具中呼叫 Roslyn 的 API。
適合的用途大致如下。
既有程式碼庫的調查
批次轉換
指標蒐集
移轉輔助
報表製作
以這種方式使用時,可以在任何想要的時機執行工具。由於不需要在 IDE 輸入過程中即時運作,因此可以容許一定程度較重的處理。
建立 Analyzer
Analyzer 是分析程式碼並輸出警告或錯誤的機制。
舉例來說,可以建立以下這類規則。
不使用 DateTime.Now,改用 DateTimeOffset.UtcNow
async 方法名稱必須以 Async 結尾
不能以錯誤的順序呼叫函式庫的初始化 API
新程式碼中不能使用特定的命名空間
禁止使用 Task.Result / Wait
Analyzer 可以在 Visual Studio 或建置時執行。它讓團隊規範或函式庫的正確用法,不必仰賴審查者的記憶,而是能以機械式的方式偵測出來。
建立 Code Fix
Code Fix 是針對 Analyzer 發現的問題,提出修正建議的機制。
可以想像成 Visual Studio 中,從燈泡圖示套用修正的情境。
舉例來說,假設 Analyzer 偵測到了這段程式碼。
DateTime.Now
Code Fix 可以提出這樣的修正建議。
DateTimeOffset.UtcNow
Code Fix 的優勢,不只在於「發出警告」,還能自動化「安全的修正方式」。
建立 Source Generator
Source Generator 是在編譯時期生成程式碼,並將這段程式碼加入同一次編譯的機制。
舉例來說,有以下這些用途。
從附加了屬性(Attribute)的類別生成固定格式的程式碼
從設定檔生成型別安全的存取器
生成 DTO 的對應程式碼
生成序列化用的程式碼
生成 enum 與字串的轉換程式碼
生成路由或 DI 註冊用的程式碼
有些原本在執行時期用 Reflection 蒐集資訊的處理,可以改為在編譯時期生成程式碼來取代。這有時能降低啟動成本,並改善與 AOT 之間的相容性。
13. Analyzer 可以當作「自動程式碼審查」來使用
Analyzer 在實務上,可以理解成「自動程式碼審查」,這樣比較容易掌握。
在程式碼審查中,有時每次都會出現相同的指摘。
請不要使用這個 API
這個方法名稱不符合規範
這個 catch 是把例外吞掉了
這個 null 檢查是不必要的
這個呼叫在效能上有問題
如果人類每次都要指出這些問題,那麼其中一部分就可以交給 Analyzer 處理。
特別適合用 Analyzer 處理的,是這類規則。
可以明確判斷好壞
例外情況很少
修正方向已經確定
整個團隊都想遵守
在審查中頻繁出現
可以在建置階段擋下來
反過來,也有不適合用 Analyzer 處理的情況。
需要依賴情境才能判斷,難度較高
需要設計上的判斷
例外情況太多
不同人意見不一致
警告太多導致沒有人在看
Analyzer 是強力的工具。正因為強力,導入太多反而會讓開發體驗變差。最好從少數重要的規則開始導入。
14. 從 .NET SDK 內建的 Analyzer 開始
在撰寫自訂 Analyzer 之前,先確認 .NET SDK 內建的 Analyzer 是比較現實的做法。
在 .NET 5 以後的專案中,.NET 的程式碼分析預設是啟用的。
常見的診斷 ID,主要有這兩個系列。
CAxxxx : 程式碼品質、可靠性、效能、安全性等
IDExxxx: 程式碼樣式、IDE 支援等
Analyzer 的嚴重程度可以透過 .editorconfig 調整。
# 範例:將未使用的 using 設為警告
dotnet_diagnostic.IDE0005.severity = warning
# 範例:將 CA2000 設為錯誤
dotnet_diagnostic.CA2000.severity = error
也可以在專案檔中啟用或強化相關設定。
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
導入既有專案時,一開始可能會出現大量警告。這種情況下,不要一口氣把所有項目都設為錯誤,而是分階段推進會比較好。
先掌握整體警告數量
方針上不在新程式碼中增加警告
只把重要規則設為 warning
只把真正想守住的規則設為 error
既有違規則有計畫地逐步減少
可以把自訂 Analyzer,視為在這個基礎之上,補足「自家公司專屬規則」的存在。
15. Analyzer 的最小示例
Analyzer 會尋找特定的語法或 Symbol,並回報 Diagnostic。
舉例來說,來看一個對 DateTime.Now 的使用發出警告的 Analyzer。
在實際的產品程式碼中,需要更嚴謹地處理型別解析與例外情況,但最小示例大致如下。
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class NoDateTimeNowAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
id: "CMP001",
title: "不要直接使用DateTime.Now",
messageFormat: "請不要使用DateTime.Now,並依用途考慮使用DateTimeOffset.UtcNow等替代方案",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
=> ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(
AnalyzeMemberAccess,
SyntaxKind.SimpleMemberAccessExpression);
}
private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
{
var memberAccess = (MemberAccessExpressionSyntax)context.Node;
if (memberAccess.Name.Identifier.Text != "Now")
{
return;
}
var symbol = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
if (symbol is not IPropertySymbol propertySymbol)
{
return;
}
if (propertySymbol.Name == "Now" &&
propertySymbol.ContainingType.ToDisplayString() == "System.DateTime")
{
var diagnostic = Diagnostic.Create(Rule, memberAccess.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}
在這個範例中,重要的是它並不是單純用字串去尋找 DateTime.Now。
這裡使用了 SemanticModel,確認它是否真的指向 System.DateTime.Now。
因此,就能避免誤判以下這類完全不同的東西。
MyCompany.DateTime.Now
Analyzer 中經常採用的流程是:先用語法快速篩選候選對象,再用語意解析確認是否真的符合條件。
用 Syntax 快速尋找候選對象
用 SemanticModel 精確判斷
用 Diagnostic 回報位置與訊息
16. Code Fix 是連「修正方式」都一併提供的機制
Analyzer 用來發現問題,Code Fix 則提出該問題的修正方式。
舉例來說,偵測到 DateTime.Now 時,可以提出這樣的修正建議。
替換成 DateTimeOffset.UtcNow
替換成 IClock.Now 之類的抽象化介面
不過,Code Fix 需要謹慎設計。並不是把 DateTime.Now 一律換成 DateTimeOffset.UtcNow 就萬無一失。想顯示當地時間的情境,與想處理儲存、比較用時間的情境,適合的型別與時區處理方式並不相同。
因此,Code Fix 適合用在滿足以下條件的情境。
修正後的語意很明確
副作用很小
可以用機械式轉換安全地修正
容易讓人確認結果
舉例來說,以下這類修正就很適合搭配 Code Fix。
把舊的 API 名稱替換成新的 API 名稱
補上缺少的 using
依照命名規則變更名稱
新增屬性(Attribute)
移除不需要的引數
另一方面,需要設計判斷的修正,有些情況下只發出警告,而不提供自動修正,會比較妥當。
17. Source Generator 是「編譯時期的程式碼生成」
Source Generator 在編譯時期執行,並將生成的 C# 程式碼加入同一次編譯之中。
大致上的流程如下。
讀取使用者的原始碼
檢查屬性(Attribute)或型別定義
生成所需的 C# 程式碼
將生成的程式碼加入編譯對象
以下是一個簡單的 Source Generator 範例。
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
[Generator]
public sealed class BuildInfoGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(static ctx =>
{
var source = """
namespace Generated;
public static class BuildInfo
{
public static string Tool => "Roslyn Source Generator";
}
""";
ctx.AddSource(
"BuildInfo.g.cs",
SourceText.From(source, Encoding.UTF8));
});
}
}
在參考了這個 Generator 的專案中,即使沒有撰寫對應的原始檔案,也可以使用這個型別。
Console.WriteLine(Generated.BuildInfo.Tool);
Source Generator 並不會改寫使用者的既有程式碼。它能做的,是生成額外的原始碼,並讓這些程式碼參與編譯。
因此,可以這樣理解。
不是用來轉換既有程式碼
而是在讀取既有程式碼後,生成額外的程式碼
如果想要一次性地大規模改寫既有程式碼,應該考慮的不是 Source Generator,而是用 Roslyn 打造的移轉工具或 Code Fix。
18. Source Generator 的參照方式
開發過程中,若要從其他專案參照 Generator 專案,處理方式與一般的函式庫參照不同。
這是因為生成器並不是執行時期會被參照的函式庫,而是在編譯時期以 Analyzer 的形式被載入的元件。
在專案參照中,需要這樣指定。
<ItemGroup>
<ProjectReference Include="..\BuildInfoGenerator\BuildInfoGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
透過設定 ReferenceOutputAssembly="false",可以避免將 Generator 的 DLL 當作一般的參考組件來處理。
若要以 NuGet 套件的形式發佈,也需要以能被當作 Analyzer / Source Generator 載入的方式來配置。
Source Generator 雖然方便,但生命週期與一般函式庫不同。
一般函式庫:在執行時期被應用程式使用
Source Generator:在編譯時期被編譯器使用
意識到這個差異,是很重要的一點。
19. 適合用 Source Generator 的情況
Source Generator 並不是什麼都適合拿來生成。
適合的是以下這類程式碼。
人類撰寫時既枯燥又容易出錯
可以從輸入資訊機械式地決定內容
生成結果易於閱讀
可以減少執行時期的 Reflection
能改善與 AOT 或 trimming 的相容性
能提升型別安全性
舉幾個例子。
JSON 序列化用的中繼資料
DI 註冊用的程式碼
設定值存取器
API 用戶端
enum 轉換程式碼
從 SQL 或 CSV 定義生成型別
INotifyPropertyChanged 的輔助程式碼
不過,如果生成出來的程式碼過於複雜,一旦發生問題就會難以追蹤。
使用 Source Generator 時,最好留意以下幾點。
讓生成的程式碼可以被檢視
讓生成程式碼的名稱保持穩定
讓生成結果具有決定性
讓錯誤時的 Diagnostic 容易理解
避免輸入只有微小變化,卻產生大量差異
生成的程式碼不應該看起來像是魔法。讓未來的維護者能夠讀懂,才是重點所在。
20. 不適合用 Source Generator 的情況
也有一些處理不適合用 Source Generator 來完成。
依賴執行時期狀態的處理
需要存取網路的處理
依賴外部服務目前數值的處理
每次結果都會改變的處理
既有原始碼的改寫
巨大的整個方案解析
因為 Generator 在編譯時期執行,速度緩慢的 Generator 會拖慢建置時間,並讓 IDE 的使用體驗變差。
此外,與環境相依的 Generator,會引發以下這類問題。
在開發者的電腦上能通過,但在 CI 上失敗
在 CI 上能通過,但在其他作業系統上失敗
結果會依快取狀態而改變
因網路故障而導致建置失敗
Source Generator 應盡量設計成純粹的處理。
輸入:原始碼、AdditionalFiles、AnalyzerConfigOptions
輸出:生成的 C# 程式碼、Diagnostic
這個關係越明確,Generator 就會越穩定。
21. 使用 Roslyn 的程式碼調查工具
Roslyn 的用途,不僅限於 Analyzer 或 Source Generator。從自製的主控台工具中使用 Roslyn,在實務上也相當有效。
舉例來說,有以下這些需求。
想列出使用舊版 API 的位置
想計算每個專案中 public class 的數量
想把附加了特定屬性(Attribute)的類別匯出為 CSV
想調查大型方案中相依的命名空間
想在 .NET Framework 移轉前,找出所有依賴 Windows 的 API
在這種情況下,與做成 Analyzer 相比,做成單次執行或定期執行的調查工具,有時反而更容易處理。
舉例來說,以下是列舉方案中 public class 的簡單示範。
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;
MSBuildLocator.RegisterDefaults();
using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(args[0]);
foreach (var project in solution.Projects)
{
foreach (var document in project.Documents)
{
var root = await document.GetSyntaxRootAsync();
if (root is null)
{
continue;
}
var classes = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(c => c.Modifiers.Any(m => m.Text == "public"));
foreach (var cls in classes)
{
Console.WriteLine($"{project.Name},{document.FilePath},{cls.Identifier.Text}");
}
}
}
這個範例只觀察了語法。如果想調查「繼承自特定基底類別的 public class」,就需要使用 SemanticModel 來查看型別的繼承關係。
如果名稱或形狀就足夠,用 Syntax
如果需要型別或參考目標,用 SemanticModel
如果需要處理整個專案,用 Workspace
這是基本的使用區分方式。
22. 與正規表達式的差異
正規表達式雖然方便,但並不適合處理 C# 程式碼的語意。
舉例來說,來看這段程式碼。
// Console.WriteLine("debug");
用正規表達式搜尋 Console.WriteLine,也可能會誤抓到註解中的字串。
也有這樣的字串常值。
var text = "Console.WriteLine";
或者,也可以跨行撰寫。
Console
.WriteLine("Hello");
甚至,也可以使用別名。
using C = System.Console;
C.WriteLine("Hello");
用正規表達式正確處理這些情況相當困難。
使用 Roslyn,就能分別處理註解、字串常值、語法上的方法呼叫,以及實際解析出的方法。
當然,如果只是單純的調查,用 grep 或 ripgrep 有時也已經足夠。但如果要根據結果做出設計判斷或自動修正,使用 Roslyn 會更安全。
只是想粗略搜尋,用字串搜尋就好
想以 C# 的角度正確判斷,就用 Roslyn
23. 用 Roslyn 調查既有資產
從 .NET Framework 移轉到現行 .NET 時,首先需要的就是「掌握現狀」。這時 Roslyn 就能派上用場。
舉例來說,有以下這類調查。
System.Web 相依項目的清單
以 App.config / Web.config 為前提的程式碼清單
Windows Forms / WPF 專屬 API 的使用位置
Remoting / BinaryFormatter 的使用位置
是否存在 COM 參考
P/Invoke 的清單
尚未非同步化的 I/O 處理
舊版加密 API 的使用位置
單純的字串搜尋也能列出候選項目。但使用 Roslyn,可以依據解析為型別或方法的結果來列出清單。
舉例來說,只搜尋 BinaryFormatter 這個字串,也會連註解或文件都一起抓進來。
若用 Roslyn 調查 System.Runtime.Serialization.Formatters.Binary.BinaryFormatter 型別的使用情況,就能列出更精確的候選項目。
在移轉工作中,一開始不需要打造完美的 Analyzer。光是先做成調查用的主控台工具,能輸出這樣的 CSV,就已經有價值。
Project,File,Line,Symbol,Kind
Legacy.Web,Controllers/HomeController.cs,42,System.Web.HttpContext.Current,Property
Legacy.Core,Serialization/OldStore.cs,18,System.Runtime.Serialization.Formatters.Binary.BinaryFormatter,Type
有了這樣的清單,就更容易制定移轉計畫。
24. 對函式庫開發者而言的 Roslyn
Roslyn 不只對應用程式開發者有用,對函式庫開發者也很有幫助。函式庫都有其正確的使用方式。
舉例來說,會有這類規則。
必須先呼叫初始化方法
必須附加特定的屬性(Attribute)
必須呼叫 Dispose
指定特定選項會有風險
不希望在新程式碼中使用已棄用的 API
如果只靠文件來傳達這些規則,使用者有時會漏看。
若將 Analyzer 隨函式庫的 NuGet 套件一起發佈,就能在使用者的程式碼上直接顯示警告。
舉例來說,假設有自家的函式庫 Company.Messaging,就能偵測出這類誤用。
var client = new MessageClient();
client.Send(message); // 在呼叫Configure之前就呼叫了Send
Analyzer 可以發出這樣的警告。
CMP1001: 請在呼叫MessageClient.Send之前先呼叫Configure
再加上 Code Fix,還能提供修正建議或範例程式碼。這能改善函式庫的使用體驗。
把文件上寫的內容,直接傳達到使用者的編輯器上
這個想法,正是 Roslyn 的一大價值。
25. 用 NuGet 發佈 Analyzer 時的思考方式
Analyzer 可以以 NuGet 套件的形式發佈。不過,需要與一般的執行時期函式庫分開考量。因為 Analyzer 並非應用程式執行時期所需要的東西,而是在建置時期或 IDE 上使用的元件。
因此,在設計套件時,需要考慮以下這些要點。
是否要把執行時期函式庫與 Analyzer 放進同一個套件
是否要把 Analyzer 拆成獨立套件
預設是否要發出警告
嚴重程度該如何設定
是否能透過 .editorconfig 控制
是否會突然對既有使用者發出大量警告
如果是自家公司內部使用,即使規則比較嚴格,也比較容易被接受。
若是以公開函式庫的形式發佈,就需要顧慮不要突然破壞使用者的建置流程。
一開始先從 Info 或 Warning 開始,之後再依需求讓使用者自行提升為 Error,通常會比較容易處理。
26. Roslyn 與 IDE 功能
在 Visual Studio 及其他 .NET 開發環境中,Roslyn 的概念與 IDE 功能有著深刻的關聯。
舉例來說,包括以下這些功能。
IntelliSense
Go to Definition
Find All References
Rename
Extract Method
Quick Actions
程式碼樣式警告
偵測未使用的 using
這些功能,光靠單純的字串搜尋無法實現。舉例來說,在 Rename 功能中,不能誤把同名的另一個 Symbol 也一起改掉。
class User
{
public string Name { get; set; }
}
class Product
{
public string Name { get; set; }
}
想更改 User.Name 時,不能連 Product.Name 也一起改掉。這需要不只依賴語法,還要以 Symbol 為單位加以區分。
Roslyn 的 API,正是能將這類 IDE 功能應用到自製工具上的基礎。
27. 效能上的注意事項
Roslyn 很強大,但若寫出繁重的處理,速度自然會變慢。尤其 Analyzer 與 Source Generator,有可能在開發者輸入程式碼時或建置過程中執行。
因此,需要注意以下幾點。
避免不必要的 SemanticModel 取得
先用 Syntax 篩選候選對象,再進行語意解析
避免檔案 I/O
不要進行網路存取
避免繁重的 Reflection
尊重取消要求
留意平行執行
不要把整個方案的解析放進 Analyzer
在 Analyzer 中,應盡量縮小在 Initialize 中註冊的對象範圍。
不良範例如下。
先看過所有 SyntaxNode,再在內部用大量 if 陳述式判斷
較好的方向如下。
只註冊所需的 SyntaxKind
先用名稱或形狀做輕量篩選
只在必要時才用 SemanticModel 做最終確認
Analyzer 有可能常駐在使用者的開發環境中。因此,不只是正確性,輕量化也是一種品質。
28. Diagnostic 的設計
Analyzer 輸出的 Diagnostic,並不是單純發出警告就好。開發者看到時,需要能理解以下這些內容。
問題是什麼
為什麼會有問題
該修正哪個地方
該怎麼修正
是否有例外情況
以下是不好的訊息範例。
CMP001: 這是被禁止的
這樣完全看不出問題出在哪裡。
以下是較好方向的範例。
CMP001: DateTime.Now 會依賴執行環境的當地時間。若用於儲存或比較用的時間,請使用 DateTimeOffset.UtcNow 或時間提供者。
Diagnostic ID 也最好事先設計好。
CMP0001-CMP0999:共用規則
CMP1000-CMP1999:函式庫 A 的規則
CMP2000-CMP2999:移轉輔助規則
如果能準備好對應的文件頁面,在 DiagnosticDescriptor 上設定 HelpLinkUri 也是有效的做法。
警告是與開發者之間的溝通。如果訊息過於草率,規則本身也會漸漸失去信任。
29. 嚴重程度的設計
Analyzer 的嚴重程度需要謹慎決定。具代表性的等級如下。
Hidden / Silent
Info
Suggestion
Warning
Error
在實務上,不要一開始就設為 Error,通常會比較好。尤其在既有程式碼較多的情況下,一開始就設為 Error,會讓導入停滯不前。
現實中,比較容易採用的是這種漸進式導入的方式。
1. 先以 Warning 導入
2. 在 CI 上將警告數量視覺化
3. 不增加新的違規
4. 只將重要規則設為 Error
5. 制定計畫逐步減少既有違規
Analyzer 的目的,不是為了讓開發者感到困擾,而是在不造成過大負擔的情況下,提升程式碼庫的品質。
30. Source Generator 的除錯
Source Generator 由於執行的位置與一般應用程式不同,除錯時會有一些特殊之處。
基本上,可以透過以下方式進行調查。
查看生成出來的原始碼
輸出 Diagnostic
撰寫測試
必要時附加偵錯器
在 SDK 樣式的專案中,使用輸出生成檔案的設定,會更容易確認結果。
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
這樣就能更容易確認生成出來的 .g.cs。
$(BaseIntermediateOutputPath) 通常是指 obj/ 底下的路徑。
如果指定成像 Generated 這種位於專案根目錄下的路徑,由於 SDK 樣式的專案預設會將 **/*.cs 全部納入編譯對象,下一次建置時,已經生成好的 .g.cs 就會被當作一般原始碼再次納入,導致型別或成員重複的錯誤。
如果無論如何都要輸出到專案根目錄下,就必須像 <Compile Remove="Generated/**/*.cs" /> 這樣,明確地將其從編譯對象中排除。
在 Generator 的測試中,經常採用比較輸入程式碼與生成結果的方式。
準備輸入原始碼
執行 Generator
確認生成出的原始碼
確認預期的 Diagnostic
Source Generator 只靠手動確認動作,很快就會壞掉。生成邏輯越複雜,測試就越重要。
31. 使用 Roslyn 的測試
Analyzer 與 Source Generator,都應該透過撰寫測試來逐步完善。尤其 Analyzer,誤判與漏判都會成為問題。
在測試中,需要準備以下這些模式。
應該被偵測到的程式碼
不應該被偵測到的程式碼
使用 using alias 的程式碼
使用完整限定名稱的程式碼
使用相似名稱但不同型別的程式碼
被視為 generated code 的程式碼
啟用 nullable 時的程式碼
舉例來說,若是禁止使用 System.DateTime.Now 的 Analyzer,就需要確認以下這些情況。
// 應該被偵測到
var x = System.DateTime.Now;
// 即使有using,也應該被偵測到
using System;
var x = DateTime.Now;
// 若是不同型別,就不應該被偵測到
namespace MyCompany;
public static class DateTime
{
public static string Now => "now";
}
var x = DateTime.Now;
最後這個例子,正是用字串搜尋很容易搞錯的情況。在 Roslyn Analyzer 中,可以透過 SemanticModel 確認目標 Symbol,來避免這種錯誤。
32. 在 .NET Framework 專案中也能使用嗎
Roslyn 並不是只能用在現行 .NET 上。不過,注意事項會依「使用方式」而有所不同。
當作調查工具使用時
用 .NET 8 或 .NET 10 打造主控台應用程式作為 Roslyn 工具,並讀取、解析 .NET Framework 的方案,是相當可行的選項。
在這種情況下,工具本身可以在現行 .NET 上執行,而解析對象則可以是 .NET Framework 的程式碼。
不過,若要用 MSBuildWorkspace 讀取方案,就需要能建置目標專案的 MSBuild、SDK、參考組件與 NuGet 還原環境。
也就是說,並非光靠 Roslyn 就能讀懂一切,要解析實際的專案結構,還是需要建置環境的配合。
當作 Analyzer 使用時
Analyzer 是被編譯器或 IDE 載入後執行的。
只要目標專案所在的環境,其編譯器能夠載入 Analyzer,即使目標專案是 .NET Framework,也依然能使用。
不過,在舊版 csproj、舊版 Visual Studio、舊版 MSBuild,以及以 packages.config 為前提的架構中,導入與運作有時不像現行 SDK 樣式的專案那麼順暢。
若要導入既有的 .NET Framework 專案,建議先確認以下幾點。
Visual Studio / MSBuild 的版本
是否能使用 PackageReference
CI 上是否能執行相同的 Analyzer
警告是否會顯示在建置紀錄中
.editorconfig 是否有效
當作 Source Generator 使用時
Source Generator 是在編譯時期被編譯器載入的機制。
因此,比起目標專案的執行時期框架,用於建置的編譯器與 SDK 的支援狀況更為重要。
在現行 .NET 的 SDK 樣式專案中比較容易處理,但在舊版 .NET Framework 專案中,需要依專案格式與建置環境多加留意。
對於 .NET Framework 的既有資產,與一開始就導入 Source Generator 相比,先從以 Roslyn 為基礎的調查工具或 Analyzer 開始,通常比較安全。
33. 版本選擇的注意事項
與 Roslyn 相關的 NuGet 套件,包含 Microsoft.CodeAnalysis.* 系列。
具代表性的套件如下。
Microsoft.CodeAnalysis.CSharp
Microsoft.CodeAnalysis.CSharp.Workspaces
Microsoft.CodeAnalysis.Workspaces.MSBuild
Microsoft.CodeAnalysis.Analyzers
Microsoft.CodeAnalysis.CSharp.CodeFix.Testing
Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing
這裡要注意的是,Analyzer 與 Source Generator 會被載入到使用者端的編譯器中。
也就是說,如果開發者手邊或 CI 上的 SDK / Visual Studio 版本過舊,使用了過新 Roslyn API 的 Analyzer / Generator,就可能無法運作。
如果是僅供公司內部使用,能統一 CI 與開發環境,就比較容易採用較新的 API。
另一方面,若是對外發佈的函式庫,就需要考慮到廣泛的使用者環境,謹慎選擇所依賴的 Microsoft.CodeAnalysis 版本。
原則上,可以這樣思考。
公司內部專用:在統一 CI 與開發環境之後,可以使用較新的 API
對外發佈:考量使用者的 SDK / VS 範圍,保守選擇
Generator:盡可能設計成 Incremental Generator
Analyzer:以不破壞 IDE 使用體驗的輕量化為優先
Roslyn 屬於接近編譯器的領域,因此容易受到版本差異的影響。
34. 不要想用 Roslyn 解決所有問題
Roslyn 很強大,但並不是能解決所有問題的工具。舉例來說,以下這些問題,光靠 Roslyn 無法解決。
執行時期會走哪個分支
正式環境的資料會傳入什麼值
透過 Reflection 動態呼叫的方法
DI 容器在執行時期的註冊結果
會依設定檔而改變的處理
外部服務傳回的值
Roslyn 主要是用來處理原始碼與編譯資訊的工具。若想了解執行時期的行為,就需要測試、記錄、追蹤、效能分析、傾印分析等其他手段。
因此,Roslyn 的角色可以這樣理解。
以高精確度處理靜態上可知的內容
如果硬要用 Roslyn 去解決那些只能在動態時期才能知道的問題,就會變成複雜又不精確的機制。
35. 導入的順序
若要在實務上開始使用 Roslyn,建議依照以下順序進行。
1. 整理好既有的 .NET Analyzer 與 .editorconfig
2. 撰寫使用 Syntax Tree 的小型調查工具
3. 嘗試用 SemanticModel 進行型別解析
4. 用 MSBuildWorkspace 讀取方案
5. 建立團隊專屬的小型 Analyzer
6. 必要時加入 Code Fix
7. 對於固定格式程式碼較多的地方,考慮導入 Source Generator
不必一開始就直接跳到 Source Generator。在許多現場中,Analyzer 與調查工具通常比較容易先看到成效。
尤其在既有資產規模較大的情況下,以下流程會比較現實。
用調查工具掌握現狀
把頻繁出現的問題做成 Analyzer
只把能安全修正的部分做成 Code Fix
把重複撰寫的固定格式程式碼做成 Generator
Roslyn 是一個可以分階段使用的工具。
36. 小範例:列出方法呼叫清單
最後,我們用更貼近實務的方式,再看一次 Roslyn 的使用方法。這裡示範的是列出方案中方法呼叫清單的情境。
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;
MSBuildLocator.RegisterDefaults();
using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(args[0]);
foreach (var project in solution.Projects)
{
var compilation = await project.GetCompilationAsync();
if (compilation is null)
{
continue;
}
foreach (var document in project.Documents)
{
var tree = await document.GetSyntaxTreeAsync();
if (tree is null)
{
continue;
}
var root = await tree.GetRootAsync();
var semanticModel = compilation.GetSemanticModel(tree);
var invocations = root
.DescendantNodes()
.OfType<InvocationExpressionSyntax>();
foreach (var invocation in invocations)
{
var symbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
if (symbol is null)
{
continue;
}
var lineSpan = invocation.GetLocation().GetLineSpan();
var line = lineSpan.StartLinePosition.Line + 1;
Console.WriteLine(string.Join(",", new[]
{
project.Name,
document.FilePath ?? document.Name,
line.ToString(),
symbol.ContainingType.ToDisplayString(),
symbol.Name
}));
}
}
}
只要稍微擴充這樣的工具,就能進行以下調查。
只擷取特定方法的呼叫
輸出已棄用 API 的使用位置
輸出各專案的使用頻率
建立移轉對象 API 的清單
能夠以編譯器的視角閱讀原始碼,會讓既有程式碼的調查工作變得輕鬆許多。
37. 用 Roslyn 改寫程式碼時的注意事項
在 Roslyn 中,也可以利用語法樹來改寫程式碼。
舉例來說,可以更改特定的方法名稱、新增屬性(Attribute),或加入 using。
不過,改寫程式碼時應謹慎進行。需要注意的地方大致如下。
確認語意沒有改變
不破壞註解與空白
避免差異過於龐大
統一格式
避免一次進行過多轉換
讓 Git 差異容易審查
由於 Roslyn 的 Syntax Tree 會保留 Trivia,因此可以在保留註解與空白的前提下進行轉換。但如果隨意建立節點,生成出來的程式碼格式可能會變得混亂。
打造改寫工具時,以下方針較為安全。
先只進行偵測
確認轉換前後的差異
從小規模的轉換開始
為轉換工具本身撰寫測試
在 CI 中先從偵測模式開始
在進行大規模機械式轉換時,Roslyn 雖然強大,但最終仍需要人工審查。
38. Roslyn 與 AI 程式碼輔助
近年來,透過 AI 進行程式碼生成或審查輔助已相當普遍,但即便如此,Roslyn 的價值並未因此降低。AI 擅長處理自然語言與周邊語境,而 Roslyn 作為編譯器,擅長處理精確的語法與語意資訊。兩者與其說是競爭關係,更接近互補關係。
舉例來說,可以考慮以下這種分工方式。
用 Roslyn 精確擷取目標位置
用 AI 生成修正方向或說明文字
用 Roslyn 確認修正方案是否能通過編譯
用 Analyzer 防止問題再次發生
與請 AI「把這個程式碼庫中所有舊版 API 都修好」相比,用 Roslyn 精確擷取目標位置,有時反而更安全。
然後,把 AI 用在檢討修正方向或審查輔助上,會更貼近實務。
編譯器能理解的部分,就交給編譯器。 人類與 AI,則專注在更上層的判斷。
這樣的分工非常重要。
39. 實務檢查清單
在使用 Roslyn 之前,建議先確認以下幾點。
目的是調查、警告、修正,還是生成
只靠語法就足夠,還是需要語意解析
單一檔案就夠,還是需要整個專案
是否需要在 IDE 上運作,還是單次工具就好
是否可以接受影響建置時間
是否要在 CI 上執行
既有程式碼是否會產生大量警告
Analyzer 的嚴重程度該如何設定
Code Fix 是否能安全套用
Source Generator 生成的程式碼是否可供檢視
使用者的 SDK / Visual Studio 版本是否一致
如果難以判斷,可以這樣區分。
想調查 -> 使用 Roslyn 的主控台工具
想一律強制遵守 -> Analyzer
修正方式已經確定 -> Code Fix
想生成固定格式程式碼 -> Source Generator
依照這樣的方式區分,就不容易用錯 Roslyn 的使用場合。
40. 結語
Roslyn 把 C# 與 Visual Basic 的編譯器,開放成開發者可以使用的 API。
使用 Roslyn,就能以下列方式處理原始碼,而不只是把它當作單純的字串。
以 Syntax Tree 的形式讀取語法
以 SemanticModel 的形式讀取語意
以 Compilation 的形式處理整個編譯
以 Workspace 的形式處理方案或專案
以 Analyzer 的形式發出警告
以 Code Fix 的形式提出修正建議
以 Source Generator 的形式生成程式碼
在實務上,以下這些場合特別有幫助。
既有程式碼庫的調查
從 .NET Framework 移轉到 .NET 的輔助工作
團隊規範的自動檢查
給函式庫使用者的引導
固定格式程式碼的生成
在 IDE 或 CI 上確保品質
重要的是,不要把 Roslyn 當作「困難的編譯器技術」而過度緊張。
一開始只要用 CSharpSyntaxTree.ParseText 讀取一個檔案,並列出方法名稱就足夠了。之後再逐步擴展到 SemanticModel、Workspace、Analyzer、Source Generator 即可。
如果要用一句話來說明 Roslyn,可以這樣講:
讓 C# 程式碼不只是字串,而能以編譯器所理解的結構來處理。
有了這個視角,程式碼審查、移轉、調查、生成的自動化,都能更進一步地推展開來。
參考資料
- 本文的完整範例程式碼(函式庫、Analyzer / Source Generator、示範程式、單元測試) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/roslyn-dotnet-compiler-platform
- dotnet/roslyn - GitHub
- The .NET Compiler Platform SDK - Microsoft Learn
- Work with syntax - Microsoft Learn
- Work with semantics - Microsoft Learn
- Work with a workspace - Microsoft Learn
- Overview of .NET source code analysis - Microsoft Learn
- Code analysis using .NET compiler platform analyzers - Microsoft Learn
- Get started with Roslyn analyzers - Microsoft Learn
- Introducing C# Source Generators - .NET Blog
- Source Generator Cookbook - GitHub
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
PDB(程式資料庫)是什麼 ── 理解偵錯資訊、符號與 Source Link
整理 PDB(Program Database / 程式資料庫)是什麼、裡面有什麼、沒有什麼,並從實務角度說明 Debug / Release、Portable PDB、Source Link、符號伺服器、傾印分析之間的關係。
TCP 中「Send 單位=Receive 單位」的誤解 ── 把 TCP 視為位元組串流來設計接收邏輯
在 TCP 通訊中,若誤以為呼叫 Send / Write 的單位可以直接對應到接收端的單位,就會發生封包分割、合併、亂碼、協定損壞等問題。本文整理把 TCP 視為位元組串流、在應用層自行進行訊框化(framing)的思考方式,並提供 .NET / C# 的實作範例。
在 C#(CSharp)中執行 PowerShell 並以物件形式接收結果
本文從實務角度整理如何從 C# 啟動 PowerShell,並以 PSObject 而非字串來接收結果,涵蓋 PowerShell SDK、AddCommand、AddParameter、BaseObject、Properties 到錯誤處理。
用 PerfView 與 dotnet-trace 找出「變慢」的原因 ── .NET 效能調查實務入門
當商用應用程式「變慢」「CPU 卡住不動」「偶爾當掉」時,該用哪個工具看什麼?本文整理 PerfView 與 dotnet-trace 的角色分工、CPU 取樣的讀法(inclusive/exclusive)、用 ThreadTime 調查阻塞時間,以及與 EventSou...
Windows 事件記錄・ETW 入門 ── 讓業務應用程式的日誌搭上 OS 標準機制
Windows 業務應用程式的日誌,是不是只靠檔案日誌解決?事件記錄與 ETW,是維運人員與 OS 標準工具能看到的另一個層級的記錄。本文說明三種手段的分工、.NET 的寫入方式、透過 EventSource 進行 ETW 儀器化、收集與調查的實務,以及來源未註冊、日誌肥大...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
既有資產活用 & 遷移支援
在持續活用 COM / ActiveX / OCX 資產、原生程式碼與 32 位元相依的同時,協助規劃階段性的遷移。
常見問題
整理諮詢這個主題時常見的問題。
- Roslyn 是什麼?
- Roslyn 正式名稱為 .NET Compiler Platform,是 C# 與 Visual Basic 的編譯器實作,同時也是一組用來建立程式碼分析工具的 API。過去編譯器一直是個黑盒子,而 Roslyn 讓編譯器內部產生的資訊(例如這個識別字屬於哪個型別、這個呼叫指向哪個方法等)能夠被應用程式與工具存取。透過這些資訊,開發者可以把 C# 程式碼當作語法而非字串來讀取,當作語意而非外觀來理解,並據此輸出警告、修正建議與生成程式碼。
- 使用 Roslyn 可以做什麼?
- 可以進行 C# / VB 的語法解析、型別與方法的語意解析、整個專案或方案的解析,也可以建立自訂的 Analyzer、Code Fix、Source Generator,還能進行程式碼的生成與轉換。從實務角度來說,例如:在使用被禁止的 API 時發出建置警告、列出使用舊版 API 的位置、在編譯時期生成 DTO 對應程式碼、輔助從 .NET Framework 移轉到 .NET 的調查工作等。使用方式大致可分為四類:當作函式庫使用、Analyzer、Code Fix、Source Generator。
- 與正規表達式或 grep 搜尋程式碼有什麼不同?
- 正規表達式很難正確處理註解中的字串、字串常值、跨行的呼叫,以及使用 using alias 的別名呼叫。使用 Roslyn,則可以分別處理註解、字串常值、語法上的方法呼叫,以及實際解析出的方法。如果只是想粗略搜尋,字串搜尋通常已經足夠;但如果要根據結果做出設計判斷或自動修正,依據編譯器名稱解析結果進行判斷的 Roslyn 會更安全。
- 該從哪裡開始學習 Roslyn?
- 不必一開始就直接學 Source Generator。建議的順序是:先整理好現有 .NET SDK 內建的 Analyzer 與 .editorconfig,接著寫一個用 CSharpSyntaxTree.ParseText 讀取單一檔案並列出方法名稱的小型調查工具,再從這裡延伸到用 SemanticModel 做型別解析、用 MSBuildWorkspace 讀取方案,最後再擴展到團隊專屬的小型 Analyzer。在許多現場中,Analyzer 與調查工具通常比 Source Generator 更快看到成效。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。