此文翻譯自Facebook部落格,地址:code.fb.com/developer-t…
譯者:LeoEatle
寫在前面
Facebook的這個Getafix確實能做到自動修復bug,不過目前來看能修復的bug非常有限,在文中詳細介紹了null pointer這種bug的解決方案,但在現實中很多bug是跟業務相關的,計算機幾乎不能理解。
所以在譯者看來,目前這個工具只能算作一個加強版的Lint工具,並且還要依賴大量的程式碼庫提交作為機器學習的原料,才能夠做到修復一些經常出現的常規bug。文中也提到了Facebook內部的多種程式碼檢查工具,這其中能夠獲取到的大量程式碼提交資料,是一般公司根本獲取不到的。機器學習也就無從談起。
不過能夠將機器學習用於自動修復bug,的確是一個創新的嘗試,希望之後這類工具能改進得越來越實用,甚至大家都能為之貢獻修復程式碼的案例讓它學習,最後成為通用的自動修復工具。
- 目前Facebook已經創造了一個叫做
Getafix
的工具,它可以自動找到bug的解決方案並且提供給工程師讓他們去改,這能大大提高工程師的工作效率和程式碼質量。 Getafix
在同類工具中是第一個達到Facebook這樣規模的,並且它已應用於生產環境,它為億萬使用者的app不斷改進穩定性和效能.Getafix
增強了Sapfix
的能力,Sapfix
是一個用於尋找bug的測試工具。同樣,Getafix
也能為靜態工具Infer
提供解決方案。- 因為
Getafix
會去學習工程師之前的程式碼,所以他提供的bug fix方案易讀性非常強。 Getafix
相比之前的自動糾錯工具,最大的提升點在於它能夠從過去提交的程式碼中尋找到一種修bug的模式,它用到了一種強大的聚類演算法(譯者注:hierarchical clustering,一種機器學習演算法),並且,它還會分析出bug行數的上下文,來給出一個最恰當的解決方案。
對於一個已經成熟的專案來說,程式碼庫都無比複雜而且經常要更新。為了能夠創造一個自動修bug的工具,我們可以讓它去學習之前的程式碼提交,它就能從中學到一些套路併為新bug提供最佳的解決方案。
這個工具就是Getafix
,它已經被應用到Facebook的生產環境,並且正在被應用於有億萬使用者的app。它通過配合其他兩個Facebook內部的測試工具來運作,不過理論上這個技術可以用於任何原始碼。目前在Facebook,Infer
作為靜態分析工具,可以先找到bug的位置,例如在Android和Java中常見的null point錯誤,另外還有個自動測試系統,叫做SapFix
,之前已經有介紹過,也可以發現不少bug。這篇文章會專注於Getafux
如何自動修bug,不會對如何找bug做更多的闡述。
Getafix
的目的是為了讓計算機去處理那些常規、固定的bug。當然依然還存在一些需要工程師親自解決的複雜bug。這個工具分析數以千計的人類工程師提交的程式碼,以及這些程式碼的各種語境,從而發現一些隱藏的bug邏輯,修復之前的自動修復工具修不了的bug。
Getafix
同樣能夠縮小修bug所做的程式碼改動的範圍,這樣它就能快速創造一個補丁,而不需要去通過遍歷暴力破解。這種高效的實踐才得以讓它能夠用於生產環境,同時,因為Getafix
會從過去的程式碼中自動學習,所以它提交的程式碼改動對於人類來說都是簡單易懂的。
對於Infer
找到的null dereference bug,Getafix可以做到自動修復,同時,他也能通過對比新舊版本程式碼來解決一些程式碼質量問題。
Getafix 和普通自動修復工具的不同點
目前業界中的自動修復工具主要被用來解決基本的問題,並且它們的修復方案都十分簡單直觀。比如,某個分析工具可能只會警告一些"dead exception",開發者可能會忘了在new Exception(...)
前面新增throw
。這些都可以通過lint規則解決,並不需要知道程式碼的上下文。
Getafix顯然提供了一個更通用的能力,它能通過分析程式碼上下文來提出解決方案。下面這個例子中,Getafix提供了一個PR來解決Infer在22行發現的bug
一個簡單的bug report,包括了Getafix生成的PR
注意這個修復不僅僅依賴於ctx
,也同樣需要關注這個函式的返回型別。不像簡單的lint修復,這種修復是Infer這種工具無法獨自完成的。
下面這個圖展示了另外一個Getafix修復bug的例子。儘管這些bug都一樣(都屬於null method call),每種修復方式卻不一樣。注意這些修復方式跟平時開發者所做的修復幾乎沒什麼兩樣。
技術細節
Getafix的工具鏈由下圖所示,在這個章節,我們會介紹下面三種主要元件的主要功能和所遇到的挑戰。
Tree differencer在程式碼樹的層次上進行比較。
首先一個抽象語法樹比較器會比較兩個版本的程式碼。它會檢測一些經常出現的改bug的模式,比如在if
語句前新增@Nullable
或者import
的註明,或者在一個return
語句前面新增條件判斷。下面這個例子中,如果dog
是null
提前return
、public
改為private
、以及程式碼的刪除都會被視為一個有效修改(concrete edits),這類修改都會被標註出來,
語法樹比較器中一個難點是高效並準確地區分好前後兩個樹,這樣才能正確找到我們要找的有效修改。
一種全新的尋找bugfix的模式
Getafix通過使用層次聚類(hierarchical clustering)技術,加上anti-unification——一種用來概括不同表示式的方法(譯者注:可以訪問wikipedia檢視更多關於這個方法的介紹,它就能夠創造一個包含了所有樹對比資料以及所隱含的修復模式集合。有了這個集合,我們就能抽象出可能會出現的“漏洞”。
下面的這個動圖表現了分析出來的層次聚類解構樹狀圖(和之前舉的例子一致)。每一行都展現了一次修改,“修改前”的是紫色,“修改後”的是藍色,並且還包括了一些其他資料。每個垂直的黑色條表示了層級,最頂部的黑色條包含了所有修改模式。次層級的被包含在更小的黑條中。Anti-unification把“如果dog是null,提前返回”這樣的修改和之前的一個修改結合起來,他們唯一的區別是之前的修改是dog.drink(water)
。這樣的結果是產生了一個新的修改模式。圖中的h0
,代表了一個修改模式“漏洞”。
接下來我們就可以用這樣的修改模式解決相同結構的問題。當我們繼續分析整個語法樹的時候,更多這樣的修改模式會被找出來。比如它可以把這種修改和cat
相關的結合起來,解決動圖中更上一層的問題。
這種層級匹配確確實實地幫助Getafix發現了不少可複用的程式碼改動。下面這張圖展示了一個包含了Infer報告的2288次對於null指標的修復。我們所要尋找的bugfix模式,就隱含在這張圖表內。
其實用anti-unification去尋找可複用模型之前就有人嘗試過,但是有幾個關鍵的改進使得Getafix能夠為新bug提供有效的解決方案。
其中一個改進是我們把程式碼改動的上下文作為學習的重要依據。比如在前面的例子中,我們發現有兩個修改都是在dog.drink(...)
前面加上了if (dog == null) return;
,儘管dog.drink(..);
沒有發生任何改變,這句程式碼依然被包含在了要前後對比的程式碼中,在更高層級的改動中,dog.drink(...)
被合入了一個抽象層h0.h1()
,後面我們會介紹一個更詳細的例子。
一個傳統的貪婪聚類演算法是沒有辦法像這樣去學習上下文的。因為貪婪聚類演算法只會維護每一個聚類單獨的資訊,沒有包括未改動的程式碼。比如,如果我們在do(list.get())
前面加上了if (list != null) return
,這類改動和前面的dog.drink()
放到了一起,貪婪演算法不知道要在什麼地方加上return。而Getafix的演算法就會保留這些上下文,從而找到修復方案。
除了上下文,我們還會將Infer的程式碼報告與這些修改結合在一起。這樣我們就能夠從相關的bug report中學習如何修復bug。Infer在報告中的"erroVar"會變成h0
。這樣我們就能夠把程式碼中具體的變數名替換成h0
,從而表示一種具體修改模式。
Getafix如何建立補丁的
最後一步是把bug修復好。顯然有很多種修復bug的方式。所以難點在於我們如何去選擇一種最合適的方式去修一個bug。下面這個例子解釋了一個我們是怎麼解決這個難題的。
例子1 假設我們現在已經發現了前面找的這種修復:h0.h1();
-> if (h0 == null) return; h0.h1();
Getafix會通過下面步驟建立一個補丁
- 在
mListView.clearListeners();
前面找到子語法樹 - 例項化“漏洞”
h0
和h1
- 把找到的子語法樹替換成例項化的程式碼
注意這裡面的mListView.clearListeners();
,如果沒有這種未修改程式碼,有可能會變成<nothing> → if (h0 == null) return;
,這可能會導致程式碼被加到mListView.clearListeners();
後面,甚至是mListView = null;
後面。
這種插入的模式其實也同樣會出現在高層,比如h0.h1()
。下個例子會介紹Getafix如何處理可能插入多個位置的情況。
例子2: 假設現在是這種模式:h0.h1() → h0!=null && h0.h1()
顯然,這種情況也可以使用if
條件語句和return
表示式解決,所以我們當然也可以用這種方式去替換原來的程式碼。但是這樣會使得像mListView.clearListeners();
也會被匹配到,Getafix的分級策略會根據之前的資料推薦更顯著的修改方案,比如對比例子1的這種修改和if (h0.h1()) { ... } → if (h0!=null && h0.h1()) { ... }
這種修改,前者只會用於語句中而不是表示式中,那麼前者獲勝,因為它的描述更為具體,在分級策略中得分更高。
效果測試
Getafix在Facebook中被用於為Infer找到的空指標錯誤自動提交修復,也同樣被用於解決一些比較明顯的其他bug。
在一次測驗中,我們對比為了解決空指標問題Getafix提交的fix和人類工程師提交的fix,這其中包含了大概200個提交併且每個提交改動不超過5行,結果發現,大概25%的Getafix的提交和人類的提交完全一致。
另一個測驗是關於Instagram的程式碼庫的,包含了大概2000個null method呼叫問題。Getafix可以嘗試修復大概60%的bug。其中90%的修改都通過了自動測試。總體來說,Getafix成功地修復了1077(大概53%)個null method呼叫問題。
除了這種測試工具發現的bug,我們也將它應用到了之前code review中發現的bug中。結果是我們解決了幾百個return not nullable以及field not nullable的bug。並且這些bug的解決率前者從56%提高到了62%,後者從51%提高到了59%。
Getafix也同樣可以用於解決SapFix發現的crash問題,過去的幾個月中,Getafix已經提供了超過一半的修復方案(全部測試通過)。從整個歷史上來說,Getafix提供給SapFix的修復通過測試的成功率已經達到了80%。
Getafix的影響力
Getafix已經幫助我們達到了讓計算機處理常規bug的目的。但我們依然不斷地改進測試和驗證工具,我們希望能有一天Getafix可以解決更大型的問題。
我們也注意到不能只讓Getafix處理Infer找到的那些bug,其實它也可以處理那些人工發現的bug,這能大大提高解決code review中發現的問題的效率。也就是說,一個曾經在程式碼庫中多次出現的錯誤,可以未來的提交中自動修復,並不需要一個人去手動提交。
Getafix是我們基於靜態分析工具以及大型程式碼庫創造出來的智慧工具。這種工具對於改進軟體開發週期、提高開發效率很有幫助。將來,我們在開發Getafix中獲得的經驗也一定能幫助我們創造更好的同類工具。
《IVWEB 技術週刊》 震撼上線了,關注公眾號:IVWEB社群,每週定時推送優質文章。