扩展iQuery使其支持多种编程语言(三) – 兼编译器的语义分析简介
2012-09-24 16:20 知平软件 阅读(1408) 评论(0) 编辑 收藏 举报iQuery是一个开源的自动化测试框架项目,有兴趣的朋友可以在这里下载:https://github.com/vowei/iQuery/downloads
源码位置:https://github.com/vowei/iQuery
相关的使用文档,请参看:
- 开源类库iQuery Android版使用说明
- 类jQuery selector的控件查询iQuery开源类库介绍
- 开源手机自动化测试框架iQuery入门教程(一)
- 开源手机自动化测试框架iQuery入门教程(二)
- 开源手机自动化测试框架iQuery入门教程(三)
在上一篇文章中,简单介绍了iQuery解释器的语义分析部分。
ANTLR使用的LL(*)的语法解析技术,它在从语法文件生成编译器时,会将每一个语法元素生成为一个函数,为了在语法元素之间传递数据,ANTLR支持函数调用和参数传递的概念,比如(摘自:https://github.com/vowei/iQuery/blob/master/java/iquery/iquery-core/src/main/java/cc/iqa/iquery/iQuery.g):
query [List<ITreeNode> candidates] returns [List<ITreeNode> survival] : selectors[$candidates] NEWLINE* EOF ; selectors [List<ITreeNode> candidates] returns [List<ITreeNode> survival]
在上面的语法里,“query [List<ITreeNode> candidates]”就是在antlr里声明参数的方式,因为antlr默认是生成Java源码,所以参数声明的方式也是遵循Java语法的。“returns [List<ITreeNode> survival]”指明了生成的query函数的返回值,返回值的类型“List<ITreeNode>”和保存返回值的局部变量名“survival”。而第2行里,“selectors[$candidates]”则是语法推演,在推演过程中,antlr使用类似函数调用的概念,允许上级语法元素传递参数给下级语法,而“selector”的函数形式可以参看第5行。
在语法文件里填充好函数声明和函数之间的调用关系后,可以使用下面这个命令生成解释器的源码:
java -cp antlr-3.3-complete.jar org.antlr.Tool iQuery.g
其中antlr-3.3-complete.jar可以从antlr的官网上下载。需要注意的是,当前写作时的最新版antlr-3.4,对生成JavaScript语言的解释器有Bug,因此建议使用3.3版本。
代码生成后,上例中的语法就会被翻译成类似下面的代码:
// 下面一行对应代码: // query [List<ITreeNode> candidates] public final List<ITreeNode> query(List<ITreeNode> candidates) throws RecognitionException { ... ... // 下面代码对应:selectors[$candidates] selectors1=selectors(candidates); ... ... // 对应代码:returns [List<ITreeNode> survival] return survival; } public final iQueryParser.selectors_return selectors(List<ITreeNode> candidates) throws RecognitionException { ... ... }
相应的,如果是要生成JavaScript版本的解释器 – 可用在iOS上,只需要在语法文件的顶部,加上一个选项指示(参考代码 - https://github.com/vowei/iQuery/blob/master/iOS/lib/iQuery.g):
options { language=JavaScript; }
对应的参数声明和参数传递使用JavaScript语法填充:
prog [candidates] returns [survival] : p=selectors[$candidates] NEWLINE* EOF ; selectors [candidates] returns [survival]
对比Java版和JavaScript版的语法,可以看到语法之间传递数据的方式在antlr里是固定的 - 参看“selectors[$candidates]”。
定义好参数列表和函数之间的调用关系之后,所需要做的就是填充自定义的过滤代码,根据不同的语法元素所代表的语义来过滤候选控件集合(candidates)。
例如,下面的代码中就是实现“:first-child”的语义,从候选控件集合中过滤出第一个子控件并返回:
| ':' FIRST_CHILD { List<ITreeNode> nodes = new ArrayList<ITreeNode>(); for ( int i = 0; i < $candidates.size(); ++i ) { ITreeNode node = $candidates.get(i); if ( node.getChildren().size() > 0 ) { nodes.add(node.getChildren().get(0)); } } $survival = nodes; }
上面的代码有几个地方需要留意,首先自定义的代码是用大括号“{”括起来的,参见第2-12行,antlr直接将里面的代码插入到生成的编译器代码的指定位置。另外,不需要在代码里显式使用“return”跳出函数,而是给预先定义的返回值变量赋值 - “ $survival = nodes;”。例如上面的代码最终会生成:
switch ( input.LA(2) ) { ... ... case FIRST_CHILD: { alt9=7; } break; ... ... case 7 : // cc/iqa/iquery/iQuery.g:674:7: ':' FIRST_CHILD { match(input,37,FOLLOW_37_in_selector_expression884); match(input,FIRST_CHILD,FOLLOW_FIRST_CHILD_in_selector_expression886); List<ITreeNode> nodes = new ArrayList<ITreeNode>(); for ( int i = 0; i < candidates.size(); ++i ) { ITreeNode node = candidates.get(i); if ( node.getChildren().size() > 0 ) { nodes.add(node.getChildren().get(0)); } } survival = nodes; } break; case 8 :
由于antlr为每一个语法元素生成一个函数,因此可以使用一些小的编程技巧,比如为了实现“>>”和“>”这样的子孙节点操作符,这些操作符后面可能还会跟随有过滤条件,例如“>> :first”这个查询语句的意思就是,在当前候选控件集合里,取第一个控件的所有子孙节点,并返回子孙节点集合里的第一个元素。而iQuery.g这个语法是无状态的,需要在操作符“>>”和过滤条件“:first”之间传递子孙节点集合,因此在源码里是这样写的:
| DESCENDANT c=selector[descendants($candidates, -1)] { $survival = $c.survival; }
注意上例中“selector[descendants($candidates, -1)]”,在调用“selector”这个语法元素之前,调用了函数“descendants”,目的就是获取当前“candidates”控件集合里的子孙控件之后再传递给“selector”语法。而变量“c”则是antlr的语法糖,用来保留“selector”语法元素过滤后返回的控件集合。
好了,本文对iQuery的语义分析的介绍就讲到这里,下一篇文章讲解iQuery的错误处理。