1 Java程序文件中函数起始行和终止行在程序文件位置中的判定__抽象语法树方法

应用需求:

实现对BigCloneBench中函数体的克隆检测,必须标注出起始行号和终止行号。

问题:

给定一个Java文件,从中提取出每个函数的起始行和终止行。

难点:

这个问题的难点在于,对于Java的解析器而言,其在形成抽象语法树的过程中,已经对源码文件进行了划分,然后,形成了对函数的抽象语法树。但是这部分操作是不开源的,因此我们无法操作。我们只能在已经形成的抽象语法树上进行操作,读取函数的起始行和终止行。

技术手段:

 Eclipse中的Eclipse JDT提供了一组访问和操作Java源代码的API,Eclipse AST是其中一个重要组成部分,它提供了AST、ASTParser、ASTNode、ASTVisitor等类,通过这些类可以获取、创建、访问和修改抽象语法树。

实验与观察:

示例函数:

主体程序代码:

        CompilationUnit cu = extractCompilationUnit(sourceFilePath, javaVersion);
        
        //Method visitor
        MethodVisitor methodVisitor = new MethodVisitor();
        cu.accept(methodVisitor);
        List<MethodDeclaration> methods = methodVisitor.getMethods();
        for(MethodDeclaration method : methods){
            
            int methodStartLineNumber=cu.getLineNumber(method.getStartPosition());
            System.out.println("methodCode:");
            System.out.println(method.toString());
            System.out.println(methodStartLineNumber);

            //Visit the method node and extract all ASTNodes
            nodes = ASTNodeVisitor.visitMethod(method);
            int j=0;
            for (ASTNode node : nodes) {
                System.out.println("子节点"+(++j));
                System.out.println("所在起始行:"+cu.getLineNumber(node.getStartPosition()));//计算起始行
                System.out.println("所在终止行:"+cu.getLineNumber(node.getStartPosition()+node.getLength()-1));//计算终止行
                System.out.println("子节点类型:"+ASTNode.nodeClassForType(node.getNodeType()));
                System.out.println("子节点内容:");
                System.out.println(node.toString());                
            }
}

其中,cu是使用ASTParser类对Java文件进行解析以后得到的CompilationUnit类的编译单元。MethodVisitor继承ASTVisitor类,是对抽象语法树的每个MethodDeclaration类节点进行存储,构建methods列表,每个元素对应一个函数的抽象语法树的顶层节点。ASTNodeVisitor的visitMethod方法则对method对应抽象语法树的每个节点进行遍历,将节点存储到nodes列表中。

部分输出结果是:

methodCode:
/** 
 * Creates an instance of  {@link Antlr4ErrorLog}.
 * @param log The Maven log
 */
public Antlr4ErrorLog(Tool tool,BuildContext buildContext,Log log){
  this.tool=tool;
  this.buildContext=buildContext;
  this.log=log;
}
52

可以看到:示例函数的起始行52是javadoc对应起始行的位置,并不是public起始行的位置。这是因为一个method的抽象语法树单元是包括javadoc单元和block单元的,其规则为:

* <pre>
 * MethodDeclaration:
 *    [ Javadoc ] { ExtendedModifier } [ <b>&lt;</b> TypeParameter { <b>,</b> TypeParameter } <b>&gt;</b> ] ( Type | <b>void</b> )
 *        Identifier <b>(</b>
 *            [ ReceiverParameter <b>,</b> ] [ FormalParameter { <b>,</b> FormalParameter } ]
 *        <b>)</b> { Dimension }
 *        [ <b>throws</b> Type { <b>,</b> Type } ]
 *        ( Block | <b>;</b> )
 * ConstructorDeclaration:
 *    [ Javadoc ] { ExtendedModifier } [ <b>&lt;</b> TypeParameter { <b>,</b> TypeParameter } <b>&gt;</b> ]
 *        Identifier <b>(</b>
 *            [ ReceiverParameter <b>,</b> ] [ FormalParameter { <b>,</b> FormalParameter } ]
 *        <b>)</b> { Dimension }
 *        [ <b>throws</b> Type { <b>,</b> Type } ]
 *        ( Block | <b>;</b> )
 * </pre>
可以看到Block是最后的一个元素。

我们做三种进一步的小实验:

实验一:移动大括号{,观察函数主体位置

假如我们将{从public所在行打到下一行去,即58行,我们再观察一下:

输出结果:

我们会看到,block的所在行从57行变成了58行。

实验二:添加人工注释,观察节点内容变化

输出结果:

之前终止行是61行,现在是第62行。然后public还有block等的起始行都是58。虽然第57行注释没有被解析为抽象语法树的内容,但是,整个函数的行数还是包括人工注释的行数!

所以,如何精确计算函数的起始位置和终止位置?不能简简单单的去除javadoc的行数!

结论:人工注释不被解析,只有javadoc格式才会被解析成为抽象语法树的一部分。但是,尽管没有被解析进去,但是人工注释的那一行被包含在内。你会发现函数的总行数增加了。

实验三:在大括号}后添加注释

最终方案:

我们不能简单通过去除javadoc行数的方式得到函数真正函数体和声明的起始行和终止行。

我们只有首先判断是否有javadoc节点,如果没有,接下来的第一个节点的行号就是起始行。然后,函数的终止行很好计算。

于是,就有了方法。

 不再展开。

 

posted @ 2019-01-04 13:31  秦皇汉武  阅读(1508)  评论(0编辑  收藏  举报