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><</b> TypeParameter { <b>,</b> TypeParameter } <b>></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><</b> TypeParameter { <b>,</b> TypeParameter } <b>></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节点,如果没有,接下来的第一个节点的行号就是起始行。然后,函数的终止行很好计算。
于是,就有了方法。
不再展开。