1. 程式人生 > >1 Java程式檔案中函式起始行和終止行在程式檔案位置中的判定__抽象語法樹方法

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節點,如果沒有,接下來的第一個節點的行號就是起始行。然後,函式的終止行很好計算。

於是,就有了方法。

 不再展開。