代码改变世界

扩展iQuery使其支持多种编程语言(三) – 兼编译器的语义分析简介

2012-09-24 16:20  知平软件  阅读(...)  评论(...编辑  收藏

iQuery是一个开源的自动化测试框架项目,有兴趣的朋友可以在这里下载:https://github.com/vowei/iQuery/downloads

源码位置:https://github.com/vowei/iQuery

相关的使用文档,请参看:

  1. 开源类库iQuery Android版使用说明 
  2. 类jQuery selector的控件查询iQuery开源类库介绍 
  3. 开源手机自动化测试框架iQuery入门教程(一)
  4. 开源手机自动化测试框架iQuery入门教程(二) 
  5. 开源手机自动化测试框架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的错误处理。

 

本文由知平软件 施懿民编写,请关注我们的微博