初探 Roslyn 編譯器平台 (2): 使用 Roslyn 提供的 Syntax API 以及 Compilations 物件類別

Roslyn 專案的簡介請閱讀前一篇

安裝 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) 的程式碼餵進 CSharpSyntaxTreeParseText 方法中,如此一來便會建立出一個語法樹狀結構,接著就可以走訪這棵樹來分析語法。例如,要拿到 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 來修改或者說重構程式碼

參考資料

 

原始文章發佈於「開發者之魂」部落格