前幾天測試一個代碼生成的軟體,測試目的是将軟體生成的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實際上在背景線程運作編譯器進行實時編譯,在需要執行文法高亮、智能感應、代碼重構等功能時,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, refobject 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格式的):
本文轉自 donjuan 部落格園部落格,原文連結: http://www.cnblogs.com/killmyday/archive/2010/05/22/1741601.html ,如需轉載請自行聯系原作者