CSNIPPEX:問答網站的可編譯代碼段的自動合成


CSNIPPEX:問答網站的可編譯代碼段的自動合成

摘要

目前,眾多代碼片段彙集於主流的問答網站(如 StackOverflow)上。但是,這些代碼片段中的大部分都具有類型信息不完整的問題,這使得它們常常無法編譯,進而無法應用於各種軟件工程任務。 基於此問題,本文提出了一種名為 CSnippEx 的技術。該技術通過解析外部依賴關係、生成導入聲明以及修復語法錯誤,將無用代碼片段自動地轉化為可編譯的 Java 源代碼文件。在本文中,作者團隊以 Eclipse 插件的形式對 CSnippEx 進行了實現,並利用 242,175 個包含代碼片段的 StackOverflow 帖子對其性能進行了評估。CSnippEx 成功地將其中的 40,410 個代碼片段轉化成了可編譯的 Java 文件。此外,CSnippEx 還能在幾秒內有效地恢復每個帖子中代碼片段的引用聲明,其修復精度達到了 91.04%。

關鍵詞

人工代碼片段;開發折社交網絡;程序合成;

1 引言

目前,越來越多的軟件開發人員通過社交網絡進行協作,或是分享工作經驗。特別是問答類網站(Q&A),如 StackOverflow, CodeRanch 和 CodeProject,都是開發人員經常光顧的地方。在這些問答網站上,開發人員以問答的形式,提出問題或分享解決方案,並對問題和回答進行評級以達成更好的合作。隨著時間的推移,這些問答網站積累了大量的群體知識。截至 2016 年 1 月,StackOverflow 已經索引了 1100 萬個問題和 1800 萬個答案。

這些群體知識對現代軟件開發實踐大有裨益。其原因在於:問答帖子經常會包含一些高質量的代碼片段。這些代碼片段,或是為編程任務提供解決方案;或是指導 bug 修復、提供 API 的使用範例。問答帖子通過不斷被人們瀏覽、評價、討論和更新不斷優化,為代碼重用和代碼分析提供了寶貴的代碼資源。為了提高工作生產的效率,開發人員經常會重用問答帖中的一些代碼片段;當工作遭遇技術難題時,也常常會轉向這些代碼片段以汲取“編碼靈感”。

然而,人們在這些 Q&A 代碼片段進行重用和分析時,通常面臨一個巨大的困難:這些代碼片段無法直接編譯。對於精確靜態分析來說,大多數 Q&A 帖子(從 StackOverflow 收集的 491,906 篇帖子中的 91.59%)所包含的代碼片段並不具有完整的語義,它們通常不可編譯、不可執行,自然也就無法使用。出現這種情況的原因在於:回答者在編寫代碼片段時一般僅處於說明性目的,他們一般不會考慮這段代碼是否能夠通過編譯。事實上,問答網站確實也很少對提交的代碼片段進行語法檢驗。為了在高層次(設計或指導層面)上傳達問題的解決方案,代碼片段通常十分簡潔,一般不會有具體的實現細節。儘管缺少的實現細節可以人工填補,但由於需要開發者對各種庫都有相當程度的瞭解,這個填補過程一般非常繁瑣。對於通常涉及眾多代碼片段的、基於群體的軟件工程目標來說,這種人工填補顯然是不符合需求的。

若要自動化地解決代碼片段不可編譯的問題,有兩個技術挑戰亟待解決:第一個難點是如何對代碼片段的外部依賴關係進行解析。大多數代碼片段使用簡化名稱引用庫類。在完全類名(如 org.library1.sub.C)限制的情況下,命名(引用)二義性問題就很容易產生;第二個難點是如何將一個帖子中的多個代碼片段自動地分割成適當數量的源文件。簡單的啟發式法則(如總是或從不將帖子中的代碼片段合併到單個源文件中)無法適用於複雜的實際情況。

2 背景與動機

CSNIPPEX:問答網站的可編譯代碼段的自動合成

圖 1:Q&A 代碼片段合成的示例

2.1 問題表述

輸入:CSnippEx 的輸入是一個 Q&A 帖子(一個文本單元)。該帖子應包含一個或多個代碼片段,也就是一個包含若干 Java 源代碼行的集合。然而問答帖子也有可能含有一些用於交流的自然語言,這對代碼片段的提取會產生一些干擾。一般來說,主流問答網站多使用專用的 HTML 標記對代碼片段進行封裝 (如 StackOverflow 使用

<code>標籤),因此提取清晰完整的代碼片段並不是一件難事。

輸出:CSnippEx 的輸出是一個或多個編譯單元。編譯單元(C-Unit)是指一個合法的 Java 源代碼文件,它可以被標準編譯器接受(能夠正常通過編譯)。在編譯單元中,若要進行 Java 類型的引用,該引用聲明需要在源代碼中對應體現 (參見圖 1)。每個編譯單元及其包含的所有聲明類型都隸屬於同一個命名空間(或包)。編譯單元可以通過兩種方式在代碼中引用 Java 類型:簡化名稱 (如 List),完整名稱(如 java.util.List)。隸屬於同一個包的類型彼此之間可以通過簡化名稱相互引用。若要引用在其他包中定義的外部類型(即外部類型,如第三方庫),則需要導入聲明來保證 Java 編譯器能夠正確解析該引用。同時,Java 允許兩種(非靜態)導入聲明:1) 單類型引入,通過指定完整名稱的形式向編譯單元中引入一個類。2) 按需類型引入,即將一個公共包下的所有類引入編譯單元(如,import java.util.∗;)。此外,編譯單元自身也需要有一個名稱。按照 java 命名規則,該名稱需要與編譯單元聲明的公共類或接口類的名稱對應相同。作者將“融合一個問答帖子中所有編譯單元”後得到編譯單元集合稱為編譯組(Compilation Group)。每個編譯組都需要與一個構建路徑(Build Path)相關聯。該構建路徑包含編譯單元引用的所有外部類型的聲明(通常以 JAR 檔案的形式存儲)。對於 Java 這樣的靜態類型語言,所有構建路徑必須完整正確,以保證每個引用類型的聲明在編譯時都正確可用的。

擬解決的問題:本文主要研究如何自動地將一個問答帖子轉化為一個不會產生任何編譯錯誤的編譯組。

2.2 基於 StackOverflow 的問題分析

為了更好地理解要研究的問題,同時代碼片段的不完整程度進行量化,作者調查分析了 StackOverflow 中的 Java 代碼片段。該項調查是為了確定有多少問答帖子通過簡單處理就可以轉化為可用編譯組。為了獲得調查結果,作者首先構建了一項基線合成技術。調查和技術的相關描述如下:

數據集:作者團隊對最新的 StackOverflow 數據進行了分析,收集了 907 226 個 Java 相關問題以及 1,660,039 個回答帖,並選取其中的高質量(被提問者採納或得分至少為 1)的代碼片段。調查範圍最終集中於 1,103,464 個高質量回答。

代碼包裝:規範的編譯單元不允許懸空(dangling)語句或是懸空方法聲明,語句與方法必須包含在相應類或接口聲明中。為了將帖子中的每個代碼片段轉換成規範的編譯單元,基線合成技術將生成一個合成公共類,用於嵌入懸空方法聲明和懸空語句。在嵌入語句時,懸空語句首先被嵌入到一個主方法(main 方法)中,之後再嵌入到合成類。

基線合成:作者將每篇帖子中的每個代碼片段都視為獨立的編譯單元。基線合成技術根據在各個代碼段中出現過的外部引用,為每個編譯單元添加所有相應的導入聲明。依照 Java 語言規範,基線合成技術首先使用代碼段中存在的頂級公共類型(類或接口)名稱來命名編譯單元。如果不存在這樣的頂級公共類型,基線合成技術將為編譯單元隨機命名。

所有編譯單元最終會形成一個編譯組。基線合成技術會將編譯組中的所有編譯單元放置在同一個包下,以保證它們之間可以通過簡化名稱相互引用。每個編譯組的構建路徑初始時包含 Java JDK 1.7 和 Android-20 SDK。如果編譯單元包含未添加的導入聲明,基線合成技術將自動查詢 Maven 中央存儲庫,下載並在構建路徑中配置必要的 jar 庫。基於向後兼容特性,當 Maven 庫中有多個包版本時,基線合成技術將選擇下載最新的版本。

2.3 IDE 快速修復工具的侷限性

現代集成開發環境(IDE)(例如 Eclipse,IntelliJ IDEA 等)提供了成熟、完善的快速修正(Quick Fix)工具。這些工具可向使用者推薦代碼片段,協助開發者進行代碼補全和代碼更正。然而,這些快速修正工具並不能有效地恢復解決代碼段中丟失類型信息的問題。 本文以圖 1 為例,對快速修正工具存在的侷限性進行說明。

圖 1 中:Input 一欄展示了一個問答帖示例。該帖子中包含一個缺失了關於類的類型聲明的片段(即,第一個代碼片段中的 A,B 和 C),以及一個缺失了變量類型聲明的片段(即,第二個代碼片段中的 b);Output 一欄則羅列了定義 input 生成的可編譯 Java 源代碼。假設 A,B,C 三個相關類是由某些外部源定義的(如在庫 JAR 中定義的),在缺失依賴的情況下,即是將這些代碼片段包裝在合成類中,並對方法聲明進行補充,這些代碼依舊會因為無法解析而無法編譯,並且產生 cannot.resolve 錯誤。主要異常如下:

1)無法找到符號類 A,B 和 C:如果構建路徑中沒有配置適當的 JAR 庫,快速修正工具就無法向用戶推薦正確的導入聲明。 圖 2 展示了 Eclipse 快速修正工具是如何對圖 1 中的代碼片段提出修復建議的:快速修復工具提示使用無法解析的類名創建一個空類、接口或枚舉類型。雖然這一舉措可以解決編譯錯誤,但本質上這只是在模擬聲明完整性。這種修復方式最終會損害合成代碼的實用性。此外,即使構建路徑中包含正確的 JAR 庫,由於類名的名稱的二義性問題,面對同一簡化名稱,通常會有許多類的完整名稱成為導入候選。快速修正工具根本無法幫助用戶確定具體要生成哪一個類。

CSNIPPEX:問答網站的可編譯代碼段的自動合成

圖 2:IDE(如 Eclipse)的快速修復建議。

2)標識符 b 不能解析為變量: 快速修正工具可以自動生成缺少的變量聲明,如圖 2 中的第 21 行。但由於不瞭解變量 b 的類型,工具最終自動創建了泛型類型 java.lang.Object。這顯然是不滿足用戶想要的。

3)方法 m1(Object)未定義: 要修復這個錯誤,正確的做法是將兩個代碼段合併在同一個類中。然而快速修正工具並不支持這類修改,這意味著開發人員只能自行構造正確的編譯單元。針對這個問題,快速修正工具的建議是創建方法 m1,這也是在模擬聲明完整性。

2.4 技術挑戰

自動化解決上述編譯錯誤是具有一定的挑戰性:

首先,儘管創建一個合成類並向其中添加方法聲明很容易實現,但推斷問答帖中多個代碼段是否應該合併卻很困難。始終將每個代碼片段獨立成單個編譯單元可能會導致編譯錯誤,就如圖 2 中的第 22 行所示。而若是總將所有的代碼片段集合在一起,則可能會導致 already.defined 錯誤。

其次,通常情況下很難自動識別問答帖子中的類型引用具體指向哪些庫。這是因為大多數代碼片段都使用簡化名稱引用庫類。簡化名稱提供的信息通常不足以讓工具鏈接到正確的完整名稱(鏈接到正確的類)。如何在缺少完整名稱的情況下確定正確的外部依賴關係,是問答代碼片段合成面臨的主要技術挑戰。

3 CSNIPPEX 技術概覽

CSNIPPEX:問答網站的可編譯代碼段的自動合成

圖 3:CSnippEx 概覽

設計和構建 CSnippEx 框架的最終目的是為了能夠利用問答代碼片段合成可編譯的 Java 源代碼文件。 圖 3 對 CSnippEx 框架的整體技術流程進行了概述。首先,CSnippEx 通過將 2.2 節中介紹的基線合成技術應用於給定的 Q&A 帖子,來獲取若干編譯組;然後,在給定的時間內,迭代地對編譯組進行優化。該迭代過程將持續到編譯組可以成功通過編譯為止。總地來說,CSnippEx 採用了一種反饋制導的方法:利用 Java 編譯器的反饋(編譯錯誤消息),指導代碼片段的合成。每一輪迭代,CSnippEx 都會根據前一次迭代返回的編譯錯誤來對當前編譯組進行優化,並在優化完成之後嘗試編譯執行新版本的編譯組。如果依然有新的編譯錯誤產生,並且沒有達到規定時限的情況下,迭代過程將繼續。而這些錯誤信息將作為反饋材料應用於下一輪迭代。

CSnippEx 包含三個功能組件,他們分別用於處理編譯單元推斷、依賴關係恢復記憶以及支撐 Eclipse 的快速修復機制。

  • 編譯單元推斷: 該組件通過合併同一帖子中多個代碼片段的方式解決類型聲明缺失的問題。編譯單元推斷組件將自動地把這些代碼段劃分為多個編譯單元,同時根據 Java 編譯器的反饋信息決定是否對其中某些代碼片段(編譯單元)進行合併。
  • 依賴解析: 該組件主要解決的外部依賴缺失的問題。該組件會自動地識別對應的外部庫、下載 JAR 歸檔文件、並最終生成正確的導入聲明。該組件利用集群假設和反饋制導思想協作完成技術目標:前者可幫助工具確定最合適的(最優的)導入聲明簇;而後者則可通過排除引起編譯錯誤的導入聲明來對解決方案進行進一步優化。
  • 代碼修正: 該組件主要解決由於錯誤語法、錯誤拼寫和缺少變量聲明導致的編譯錯誤。 該組件主要依據 Eclipse 快速修正工具的給出修復建議,自動執行代碼補全、修復編譯錯誤。

請注意,這三個組件的執行順序非常重要: 在前兩個組件工作完成之前,對生成的編譯組(單元)應用快速修復只能做到模擬聲明完整性,並不能獲得真正可用的編譯組。總的來說,編譯單元推斷和依賴關係解析是本研究工作的核心部分,它們將為代碼修正組件預備好所需的工作環境。

致謝

國家重點研發計劃課題:基於協同編程現場的智能實時質量提升方法與技術(2018YFB1003901)和國家自然科學基金項目:基於可理解信息融合的人機協同移動應用測試研究(61802171)

該文章由南京大學軟件學院 2020 級碩士生錢瑞祥轉述

"/<code>


分享到:


相關文章: