從Visual Studio裡抓取抽象文法樹(AST)

來源:互聯網
上載者:User

前幾天測試一個代碼產生的軟體,測試目的是將軟體產生的C#或者VB.NET原始碼檔案,和之前的基準C#或者VB.NET原始碼檔案進行對比。如果實際產生的檔案和基準檔案有不一致的地方,就說明,軟體有潛在的編碼失誤(Bug)。

 

當前的方法是將兩個檔案讀入記憶體,一行一行逐字逐字地對比。當然啦,為了避免空格的問題,檔案事先已經將空格都刪除掉了。但是,這種方法的問題是,很多時候,軟體產生的原始碼檔案中,雖然程式碼的放置順序不一樣,但是實現的功能是完全一樣的。舉個例子,在使用Visual Studio中編寫Winform程式時,在InitializeComponent()函數裡面,先產生建立按鈕的代碼,然後再產生文字框的代碼;與先產生建立文字框的代碼,然後再產生按鈕的代碼的效果是完全一樣的。

 

那這樣是否可以嘗試這種實現,將兩個檔案讀入記憶體中,然後將檔案按照程式碼排序後再對比?這樣也不行,因為你不能將調用構造對象的代碼放在使用對象的代碼後面。

 

於是,我們就想是否能夠通過對比實際的CodeDom與基準CodeDom來實現?一般來說,在.NET世界裡,代碼產生的功能都是通過CodeDom技術來實現的。CodeDom通俗點講,就是一個抽象的代碼樹—不依賴任何程式設計語言,可以使用不同的語言產生器遍曆CodeDom來產生不同語言的原始碼檔案—對CodeDom感興趣的讀者可以自己參考MSDN上面的說明。

 

然而這個方案還是被大家否決了,因為前幾個版本的測試過程中,使用的是檔案對比的模式,已經產生了很多基準原始碼檔案了。如果使用CodeDom技術,這就意味著需要為前面幾百個基準源檔案重建對應的基準CodeDom。

 

這個時候我想到使用編譯器來分析兩個原始碼檔案,然後對比結果的抽象文法樹來達到類似CodeDom的功效。我有兩個編譯器可以支援這個方案,一個是csc.exe,另外一個是Visual Studio用來支援即時文法高亮顯示的編譯器。

 

為了支援即時的文法高亮顯示以及智能感應功能,Visual Studio實際上在後台線程運行編譯器進行Just-In-Time 編譯,在需要執行文法高亮、智能感應、代碼重構等功能時,Visual Studio會查詢背景編譯器裡儲存的符號表、抽象文法樹來擷取相關的即時資訊。

 

但是這個編譯器和我們日常工作編譯C#(這裡以C#為例)的編譯器csc.exe不是同一個東西。之所以要另外為Visual Studio單獨實現一個編譯器,因為

1.         在進行即時文法高亮顯示,智能感應等功能時。編譯器不是處理一個完整的原始碼。這跟csc.exe不一樣,因為csc.exe處理的是完整的C#原始碼。

 

2.         另外, csc.exe與支援文法高亮顯示的編譯器對於語法錯誤的態度也是不一樣的,csc.exe可以不容忍任何語法錯誤,即一旦有語法錯誤發生,csc.exe可以拒絕處理後續的語義分析的工作。然而文法高亮編譯器卻不能這樣,畢竟使用它的時候,程式員正在編寫原始碼,有很多尚未完成的地方。即使輸入的源檔案代碼有很多的語法錯誤,文法高亮編譯器也需要能夠繼續執行後續的編譯任務(例如語義分析)。

 

3.         還有文法高亮編譯器還需要可以實現增量編譯的功能,即後續加入的原始碼行合并到以編譯好的代碼中。比如說,在調試過程中,你可以在“立即”視窗裡面定義一個變數,然後可以在同一個運算式裡同時評估這個變數和被調程式已有的變數的計算結果。

 

下面兩個.NET Assembly是Visual Studio用來支援C#即時文法高亮等功能的(實際上,你還需要一個Win32 C++的DLL檔案,但是這個檔案不會被我的程式直接用到):

1.         Microsoft.VisualStudio.CSharp.Services.Language.dll

2.         Microsoft.VisualStudio.CSharp.Services.Language.Interop.dll

 

這兩個檔案只有安裝了Visual Studio才會有,你既可以在Visual Studio的安裝資料夾裡,也可以在GAC裡面找到它們。

 

因為這兩個DLL不是Visual Studio公開的API,所以它們和Visual Studio綁定的很緊密,即你只能在Visual Studio裡使用它們,不能在其他程式中使用—除非你把Visual Studio SDK裡由Visual Studio提供的晦澀的介面都實現了。

 

因此我的程式也就只好以Visual Studio的外掛程式(Add in)的形式實現,在Visual Studio裡(我用的Visual Studio 2010)建立一個新的Visual Studio Add-Ins工程,將上面兩個DLL檔案引用進來。在Exec函數裡面實現對應的邏輯就好了,下面是相關代碼:

 

public void Exec(string commandName, vsCommandExecOption executeOption, ref object varIn, ref object varOut, ref bool handled)

{

    handled = false;

    if(executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)

    {

        if(commandName == "MyAddin1.Connect.MyAddin1")

        {

            // 實現自訂的邏輯

            TestCSharpCompiler();

            handled = true;

            return;

        }

    }

}

 

private void TestCSharpCompiler()

{

    // 擷取當前Visual Studio的解決方案,如果Visual Studio還沒有任何方案

    // 就是預設的空解決方案

    var solution = (Solution2)_applicationObject.Solution;

    // 建立一個新的“C# 命令列程式(C# Console Application)”工程

    var csTemplatePath = solution.GetProjectTemplate("ConsoleApplication.zip", "CSharp");

    // 工程名(Test Project)以及儲存工程的檔案夾路徑(d:\temp\test)

    solution.AddFromTemplate(csTemplatePath, @"d:\temp\test", "Test Project", false);

    var project = solution.Projects.Item(1);

    // 將已有的檔案(d:\temp\test.cs)添加到新建立的工程中

    project.ProjectItems.AddFromFileCopy(@"d:\temp\test.cs");

    // 啟用編譯器

    var host = new IDECompilerHost();

    var compiler = host.CreateCompiler(project);

    SourceFile source = null;

 

    // 工程裡一般都有很多檔案,找到感興趣的源檔案

    // 因為那個檔案的抽象文法樹是我要的東西

    foreach (var file in compiler.SourceFiles)

    {

        if (string.Compare(file.Key.Value, @"d:\temp\test\test.cs",

                           StringComparison.InvariantCultureIgnoreCase) == 0)

        {

            source = file.Value;

            break;

        }

    }

 

    // 擷取文法樹的根節點,一般就是源檔案最外層的命名空間

    var tree = source.GetParseTree();

    IDECompilation compilation = (IDECompilation)compiler.GetCompilation();

    // 在文法樹裡擷取第一個命名空間的節點

    compilation.CompileTypeOrNamespace(tree.RootNode);

    var node = tree.RootNode as NamespaceDeclarationNode;

    // 擷取命名空間節點裡面的類定義、或者子命名空間、或者其它

    // 可以定義在命名空間裡面的元素的節點

    foreach (var child in node.NamespaceMemberDeclarations.Root.Children)

    {

        if (child is BinaryExpressionNode)

        {

            var bnode = child as BinaryExpressionNode;

            var left = bnode.Left as ClassDeclarationNode;

            var right = bnode.Right as ClassDeclarationNode;

 

            Trace.WriteLine(left.Identifier.Name.Text);

            Trace.WriteLine(right.Identifier.Name.Text);

        }

        else

        {

            Trace.WriteLine(child.AsName().Name.Text);

        }

    }

}

 

上面的代碼只是做示範用的,裡面解析的原始碼(test.cs)已經包含到下面的完整工程的源檔案裡了(工程檔案是Visual Studio 2010格式的): 

/Files/killmyday/MyAddinForAST.zip

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.