初探 Roslyn 編譯器平台 (2): 使用 Roslyn 提供的 Syntax API 以及 Compilations 物件類別
安裝 Roslyn SDK (Preview)
在前一篇文章中我們介紹了在既有的 Visual Studio 2013(或是已經內建整合支援的 Visual Studio "14")裡安裝 Roslyn End User (Preview) 的 Visual Studio 插件,讓一些分析程式碼的工作交給 Roslyn 來處理。而在這篇文章要介紹的是,如何使用 Roslyn 所提供的 APIs 來做到這些程式碼分析的工作。要使用這些 APIs,除了可以下載安裝 Roslyn SDK (Preview) 的 Visual Studio 插件,就可直接建立含有 Roslyn 相關套件的專案範本:
也可以在專案中,透過 NuGet 套件管理系統來安裝 Roslyn NuGet 套件:
Install-Package Microsoft.CodeAnalysis -Pre
一樣可以安裝相關套件參考,並在程式中使用這些功能:
Syntax API
編譯器要對程式碼進行編譯,當然要先確定使用者寫的程式的語法(syntax),除了判斷語法是否正確之外,也要瞭解它的結構——哪些是變數、保留字、字串或數值等等。在 Roslyn 中提供的 Syntax API 就提供了解析(parse)語法、建立語法樹狀結構(syntax tree)以及一些相關的功能。一個典型的範例像是這樣:
SyntaxTree tree = CSharpSyntaxTree.ParseText(
@"using System;
using System.Collections;
using System.Linq;
using System.Text;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(""Hello, World!"");
}
}
}");
var root = (CompilationUnitSyntax)tree.GetRoot();
您可以把一段完整的 C# (或 VB.net) 的程式碼餵進 CSharpSyntaxTree
的 ParseText
方法中,如此一來便會建立出一個語法樹狀結構,接著就可以走訪這棵樹來分析語法。例如,要拿到 Main 函式的引數列,就要像這樣來走訪語法樹,argsParameter
就是引數列的第一個引數資料。
var firstMember = root.Members[0];
var helloWorldDeclaration = (NamespaceDeclarationSyntax)firstMember;
var programDeclaration = (ClassDeclarationSyntax)helloWorldDeclaration.Members[0];
var mainDeclaration = (MethodDeclarationSyntax)programDeclaration.Members[0];
var argsParameter = mainDeclaration.ParameterList.Parameters[0];
總之 Syntax API 提供的 API 主要就是用來解析語法、並且建立語法樹狀結構,有了這棟樹,要怎麼搜尋或分析(相較於還要寫個 parser 而言...)就容易多了。
Compilation Class
Syntax API 只能建立語法樹,但光有這樣的樹狀結構還是無法瞭解這段程式碼到底在做什麼,每一個 syntax node 到底代表的是什麼東西,這時候就能使用 Compilation 物件類別來把語法樹編譯一下,這下你就可以得到這個程式碼的所有符號(symbol),所謂的符號指的就是像資料型態(type)、命名空間(namespace)還有變數的名稱及其代表的表示式等等,而把符號及表示式對應起來的動作就稱為繫結(Binding)。
而下面這段程式碼就是把上述 Syntax API 的範例做編譯,並且找出第一個引入的命名空間符號 systemSymbol
,比起只用 Syntax API 建立的樹狀結構,雖然也可以在語法樹上拿到 syntax node,但 syntax node 是無法知道它是一個命名空間,更別提去分析這個命名空間的成員結構等等。
var compilation = CSharpCompilation.Create("HelloWorld")
.AddReferences(new MetadataFileReference(typeof(object).Assembly.Location))
.AddSyntaxTrees(tree);
var model = compilation.GetSemanticModel(tree);
var nameInfo = model.GetSymbolInfo(root.Usings[0].Name);
var systemSymbol = (INamespaceSymbol)nameInfo.Symbol;
而在 IDE 中要做語法提示(IntelliSense)這個功能,就可以用符號的資訊來處理。像下面這段程式碼就是取得 "Hello, world"
這個符號後,從它的資料型態(string)來列出可以呼叫的方法,這就是一個 IntelliSense 的雛型了:
var helloWorldString = root.DescendantNodes()
.OfType()
.First();
var literalInfo = model.GetTypeInfo(helloWorldString);
var stringTypeSymbol = (INamedTypeSymbol)literalInfo.Type;
var methods = (from method in stringTypeSymbol.GetMembers().OfType()
where method.ReturnType.Equals(stringTypeSymbol) &&
method.DeclaredAccessibility == Accessibility.Public
select method.Name).Distinct();
foreach (var name in methods)
{
Console.WriteLine(name);
}
它會印出所有字串資料可以呼叫的方法。
接下來
這篇文章介紹 Syntax API
以及 Compilation
物件類別,主要都還是在分析程式碼,在下一篇文章中將會介紹如何運用這些 APIs 來修改或者說重構程式碼。
參考資料
- Getting Started - Syntax Analysis (CSharp).pdf or Word docx
- Getting Started - Semantic Analysis (CSharp).pdf or Word docx