本文介紹前一段時間開發的BDD語言iQA的編寫以及設計過程,概要介紹詞法分析、語法分析以及分析語法樹生成代碼的過程,由於iQA語言只是一個簡單的代碼生成工具,所以里面並沒有使用到任何的語義分析的過程。
iQA是開源的,其源碼位置在:https://github.com/vowei/iqa
要編譯它,請從antlr的官網下載最新版本,放在src文件夾的lib目錄里,然后按照READM.md文件逐步編譯即可。
關於antlr的詞法、語法分析過程我在前面的文章里已經寫過很多了,請讀者參閱文章:
編譯器的詞法分析簡介:http://www.cnblogs.com/vowei/archive/2012/08/27/2658375.html
編譯器的語法分析簡介:http://www.cnblogs.com/vowei/archive/2012/09/03/2668316.html
編譯器的語義分析簡介:http://www.cnblogs.com/vowei/archive/2012/09/24/2700243.html
編譯器的語法錯誤處理簡介:http://www.cnblogs.com/vowei/archive/2012/09/28/2707451.html
對於iQA來說,詞法分析方面還需要有亮點要說,與純解析內存字符串的iquery不同,iQA需要讀取源文件,而iQA本身是支持中文等國際化語言的,因此需要考慮編碼的問題,特別是Unicode文件里的BOM字符(http://en.wikipedia.org/wiki/Byte_order_mark) - 簡單來說,就是在Unicode文件里,會有一個特殊的字節表示文件的字節順序,有的文件里會有這個字節,而有的文件卻不一定有它,因此為了解決這個問題,在詞法文件iQALexer.g里,我添加了一個符號BOM:
BOM: '\uFEFF' { _seeBom = true; }
;
在語法文件iQAParser.g里,通過指定BOM是一個可選符號來適應這個問題。
prog
: BOM? feature+
;
另外,iQA支持類似python的縮進語法,因此在詞法和語法文件里,針對縮進的空格都做了特殊處理,詳情請參考:http://www.cnblogs.com/killmyday/archive/2012/08/19/2646719.html
最后,為了支持中文等unicode變量以及關鍵字,詞法文件iQALexer.g里通過定義ID_START符號來實現這種支持。
fragment ID_START : '_' | 'A'.. 'Z' | 'a' .. 'z' ... | '\u02C6' .. '\u02D1'
在iQAParser.g里將語法解析完畢后,其實可以直接在iQAParser.g里直接使用println的方式執行代碼生成工作,但這樣一來就限制我只能生成一種編程語言,為了實現生成多種編程語言的功能,在iQAParser.g里實際上是生成一個語法樹,如:
feature
: FEATURE_DEF feature_content? -> ^(FEATURE FEATURE_DEF feature_content?)
;
就是在語法樹里添加類似下圖的節點:

而iQATree.g就是解析這個語法樹,使用StringTemplate來生成代碼,如下面就是解析前面feature節點的代碼:
feature
: ^(FEATURE f=FEATURE_DEF c=feature_content?)
-> class(name = {removeKeyword($f.getText(), "功能")},
methods = {$c.scenarios})
;
antlr是通過StringTemplate來生成代碼的,如上面的代碼使用了class這個StringTemplate生成代碼,可以通過替換class的實現方式來生成不同語言的代碼。
class(name, methods) ::= <<
// <name>就是class這個StringTemplate的參數,在生成代碼時,使用從語法樹傳入的值替換它
public class <name> extends iQATestBase {
// 此處省略代碼 … …
public <name>() throws Exception {
super("cc.iqa.studio.demo.MainActivity", "cc.iqa.studio.demo");
}
// 此處省略代碼 … …
//
// methods也是傳入StringTemplate的參數,是一個數組;
// 在生成代碼時,由於iQATree.g傳入的是$c.scenarios
// 而$c.scenarios的值是針對iQATree.g的scenario節點生成的代碼。
//
<methods; separator = "\n">
// 此處省略代碼 … …
}
>>
這樣一來,可以通過替換StringTemplate的方式來生成不同語言的代碼,例如執行命令
java -cp lib/antlr-3.4-complete.jar:. iQATest ../iqa.test/res/testParseStepBasic.txt cc/iqa/iQAMobileJUnit.stg
就可以將下面的iQA源碼:
功能: 具有縮進編寫方式的功能
場景: 這是一個縮進后的場景
* 這是一個步驟
* 打算不用"*"字符來識別步驟了
生成下面的junit格式代碼:
package cc.iqa.studio.demo.test;
import java.util.*;
import com.jayway.android.robotium.solo.*;
import cc.iqa.runtime.android.*;
import cc.iqa.library.*;
import cc.iqa.core.*;
import com.google.gson.*;
public class 具有縮進編寫方式的功能 extends iQATestBase {
private Solo _solo;
private ControlNameResolver _resolver;
public 具有縮進編寫方式的功能() throws Exception {
super("cc.iqa.studio.demo.MainActivity", "cc.iqa.studio.demo");
}
public void setUp() throws Exception
{
ControlNameMap map = new ControlNameMap();
this._resolver = map.getResolver();
AutomationContext context = new AutomationContext();
this._solo = new Solo(this.getInstrumentation(), this.getActivity());
context.put("solo", this._solo);
this.getContainer().addComponent(context);
}
public void tearDown() throws Exception
{
this._solo.finishOpenedActivities();
this.OnScenarioEnd();
}
public void test這是一個縮進后的場景() throws Exception
{
AutomationContext context = this.getContainer().getComponent(AutomationContext.class);
Hashtable<String, Object> resolver = null;
Hashtable<String, Object> variables = new Hashtable<String, Object>();
this.S("這是一個步驟");
this.S("打算不用\"*\"字符來識別步驟了");
}
public class ControlNameMap {
private ControlNameResolver _resolver;
public ControlNameMap() throws Exception
{
Gson gson = new Gson();
String json = "";
this._resolver = gson.fromJson(json, ControlNameResolver.class);
}
ControlNameResolver getResolver()
{
return this._resolver;
}
}
}
而如果換一個StringTemplate實現,如執行命令:
java -cp lib/antlr-3.4-complete.jar:. iQATest ../iqa.test/res/testParseStepBasic.txt cc/iqa/iQAMobileApple.stg
則會生成下面的代碼:
#import "lib.js"
var testSuite = function() {
var map = { /* need add control map here */ };
var testRunner = new TestRunner(map);
this.test這是一個縮進后的場景 = function() {
var scenarioInfo = {
"title": 這是一個縮進后的場景
};
testRunner.ScenarioSetup(scenarioInfo);
testRunner.Step("這是一個步驟");
testRunner.Step("打算不用\"*\"字符來識別步驟了");
testRunner.ScenarioCleanup();
}
this.test縮進后的第二個場景 = function() {
var scenarioInfo = {
"title": 縮進后的第二個場景
};
testRunner.ScenarioSetup(scenarioInfo);
testRunner.ScenarioCleanup();
}
}
