示範環境
Visual Studio 2017
.NET Compiler Platform SDK
簡介
今天,Visual Basic和C#編譯器是黑盒子:輸入文本然後輸出位元組,編譯管道的中間階段沒有透明性。使用.NET編譯器平台(以前稱為“Roslyn”),工具和開發人員可以利用編譯器使用的完全相同的資料結構和算法來分析和了解代碼。 本篇文章,我們将會慢慢熟悉文法API,通過文法API來檢視解析器,文法樹,用于推理和構造它們的實用程式。
了解文法樹
Trivia,Token和Node形成了一個完全代表Visual Basic或C#代碼片段中所有内容的樹
SyntaxTree
它的執行個體表示整個解析樹。SyntaxTree是一個抽象類,具有特定于語言的派生類。要解析特定語言的文法,您需要使用CSharpSyntaxTree(或VisualBasicSyntaxTree)類上的解析方法。
SyntaxNode
它的執行個體表示的文法結構如聲明,語句,子句和表達式。
SyntaxToken
它代表一個單獨的關鍵字,識别符,操作員或标點符号
SyntaxTrivia
它表示文法上無關緊要的資訊,例如令牌之間的空白,預處理指令和注釋。
下圖示例:SyntaxNode: 藍色 | SyntaxToken: 綠色 | SyntaxTrivia: 紅色
周遊文法樹
- 建立項目“CodeAnalysisDemo”
- 引入Nuget
Microsoft.CodeAnalysis.CSharp
Microsoft.CodeAnalysis.CSharp.Workspaces
- 命名空間:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
- 準備要分析的代碼
using System;
namespace UsingCollectorCS
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World");
}
}
class Student
{
public string Name { get; set; }
}
}
- 核心代碼
/// <summary>
///解析文法樹
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
public SyntaxNode GetRoot(string code)
{
var tree = CSharpSyntaxTree.ParseText(code);
//SyntaxTree的根root
var root = (CompilationUnitSyntax)tree.GetRoot();
//member
var firstmember = root.Members[0];
//命名空間Namespace
var helloWorldDeclaration = (NamespaceDeclarationSyntax)firstmember;
//類 class
var programDeclaration = (ClassDeclarationSyntax)helloWorldDeclaration.Members[0];
//方法 Method
var mainDeclaration = (MethodDeclarationSyntax)programDeclaration.Members[0];
//參數 Parameter
var argsParameter = mainDeclaration.ParameterList.Parameters[0];
//查詢方法,查詢方法名稱為Main的第一個參數。
var firstParameters = from methodDeclaration in root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
where methodDeclaration.Identifier.ValueText == "Main"
select methodDeclaration.ParameterList.Parameters.First();
var argsParameter2 = firstParameters.Single();
return root;
}
- 入口Main方法
var code = @"using System;
namespace UsingCollectorCS
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(""Hello World"");
}
}
class Student
{
public string Name { get; set; }
}
}";
var tree = new AnalysisDemo().GetRoot(code);
- Debug調試

經過對比可知以下部分
利用CSharpSyntaxTree.ParseText(code)擷取文法樹SyntaxTree
利用(CompilationUnitSyntax)tree.GetRoot()擷取文法樹的跟節點
利用 (NamespaceDeclarationSyntax)root.Members[0]可擷取命名空間
利用 (ClassDeclarationSyntax)helloWorldDeclaration.Members[0]可擷取類
利用 (MethodDeclarationSyntax)programDeclaration.Members[0]可擷取方法
利用linq查詢,可從**root.DescendantNodes()**節點内查詢方法/參數等成員。
SyntaxWalkers
通常,您需要在文法樹中查找特定類型的所有節點,例如,檔案中的每個屬性聲明。
通過擴充CSharpSyntaxWalker類并重寫VisitPropertyDeclaration方法,您可以在不事先知道其結構的情況下處理文法樹中的每個屬性聲明。
CSharpSyntaxWalker是一種特殊的SyntaxVisitor,它以遞歸方式通路節點及其每個子節點。
我們先來示範CSharpSyntaxWalker的兩個虛virtual方法VisitUsingDirective 和VisitPropertyDeclaration
- 核心代碼如下:
/// <summary>
/// 收集器
/// </summary>
public class UsingCollector : CSharpSyntaxWalker
{
public readonly Dictionary<string, List<string>> models = new Dictionary<string, List<string>>();
public readonly List<UsingDirectiveSyntax> Usings = new List<UsingDirectiveSyntax>();
public override void VisitUsingDirective(UsingDirectiveSyntax node) {
if (node.Name.ToString() != "System" &&
!node.Name.ToString().StartsWith("System."))
{
this.Usings.Add(node);
}
}
public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node)
{
var classnode = node.Parent as ClassDeclarationSyntax;
if (!models.ContainsKey(classnode.Identifier.ValueText))
{
models.Add(classnode.Identifier.ValueText, new List<string>());
}
models[classnode.Identifier.ValueText].Add(node.Identifier.ValueText);
}
}
/// <summary>
/// 示範CSharpSyntaxWalker
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
public UsingCollector GetCollector(string code)
{
var tree = CSharpSyntaxTree.ParseText(code);
var root = (CompilationUnitSyntax)tree.GetRoot();
var collector = new UsingCollector();
collector.Visit(root);
return collector;
}
- Main調用入口:
var code2 =
@"using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace TopLevel
{
using Microsoft;
using System.ComponentModel;
namespace Child1
{
using Microsoft.Win32;
using System.Runtime.InteropServices;
class Foo {
public string FChildA{get;set;}
public string FChildB{get;set;}
}
}
namespace Child2
{
using System.CodeDom;
using Microsoft.CSharp;
class Bar {
public string BChildA{get;set;}
public string BChildB{get;set;}
}
}
}";
var collector = new AnalysisDemo().GetCollector(code2);
foreach (var directive in collector.Usings)
{
Console.WriteLine($"Name:{directive.Name}");
}
Console.WriteLine($"models:{JsonConvert.SerializeObject(collector.models)}");
- 執行結果
我們可以得出結論
VisitUsingDirective 主要用于擷取Using命名空間
VisitPropertyDeclaration主要用于擷取屬性。
總結
本篇文章主要講了
- 文法樹SyntaxTree,以及SyntaxNode,SyntaxToken,SyntaxTrivia。
- 通過重寫CSharpSyntaxWalker的虛方法,可以實作自定義擷取。
- 附上官方截取的部分流程圖
Roslyn編譯管道功能區
API圖層
Roslyn由兩個主要的API層組成 - 編譯器API和工作區API。
源碼
CsharpFanDemo
參考連結
Roslyn-Overview
Getting Started C# Syntax Analysis
從零開始學習 dotnet 編譯過程和 Roslyn 源碼分析
手把手教你寫 Roslyn 修改編譯