PHP工具箱:PHPStan —— PHP 靜態程式碼分析工具

Summer__發表於2018-11-14

image

PHPStan:無需寫測試就能找到程式碼中的 Bug

每當我看到開發人員從 Java 或 C# 等編譯語言切換到 PHP 這樣的解釋語言時解放了生產力後感到很高興。除了這些常規的執行模型(發起、處理請求和結束請求)和更短的反饋環(無需等待編譯器)外,還有一個能解決開發人員日常問題的開源框架生態系統,因此,PHP 是目前來說 web 開發中最流行的語言

但它有一個缺點。

你會在什麼時候發現錯誤?

編譯型語言需要在程式執行之前瞭解每個變數的型別,每個方法的返回型別。這就是為什麼編譯器需要確保程式是沒有錯誤的,並且會在原始碼中向你指出這些型別的錯誤,比如呼叫了未定義的方法或者是向某個函式傳遞了錯誤數量的引數。在把應用程式部署到生產環境前,編譯器算是第一道防線。

然而 PHP 就不會這樣了。如果程式出錯,會執行到錯誤的程式碼的時候崩潰。在測試 PHP 應用時,不管是自動化測試還是手動測試,開發人員都會花費大量時間去查一些其它編譯型語言不會犯的錯從而減少測試實際業務邏輯的時間。

我想改變這一點。

歡迎來到 PHPStan 的世界

現階段 PHP 實踐所產生的程式碼庫中,我們可以確定大部分資料的型別,並且轉換為靜態型別的語言,儘管還保留著一些動態語言的特性。人們把現在的 PHP 程式碼庫變得跟其他語言一樣更加有趣。物件導向,依賴注入以及設計模式的使用已經變得非常普遍。

這讓我想到了 PHP 的 靜態分析工具,它將替代其他語言的編譯器角色。我花了很多時間研究它,並且已經使用它的各種開發版本來檢查我們的程式碼庫超過一年。

它就是 PHPStan, 開源且免費


它目前校驗什麼?

  • 有關類中涉及的,物件例項, 錯誤/異常捕獲,型別約束以及其他語言結構的存在性。 PHP 照舊不會檢查這些, 但是會展現其中未被使用的程式碼。
  • 被呼叫的方法和函式的存在性和可訪問性。同樣也會檢查他們的引數個數。
  • 方法是否返回了它宣告的返回值型別。
  • 被訪問成員變數的存在性和可見性。它也可指出是否將一個其他的型別的值賦給了既定型別的成員變數。
  • sprintf/printf 函式基於格式化字串所應接收的引數個數。
  • 分支和迴圈範圍中的變數的存在性。
  • 無用的形式指定。例如 (string) 'foo' ,以及不同型別變數間的嚴格比較 (=== 和 !==),因為他們的結果總為 false。

這個清單的內容隨著每次釋出都在遞增。但成就 PHPStan 也不會只仰賴此一技之微。

PHPStan 迅疾如飛...

它設法一次性檢查整個程式碼庫。 它無需多次遍歷程式碼。 只需瀏覽您想要分析的程式碼,例如 你寫的程式碼。它無需解析和分析第三方依賴項。 相反,它使用反射來找出有關你程式碼庫中引用的他人程式碼的有用資訊。

PHPStan 能在一分鐘裡檢查我們的程式碼庫 (6000 個檔案, 600k LOCs) 。它可在一秒內完成自查。

...可擴充套件性

即便當前正在使用靜態型別,開發者也可以合法的使用 PHP 的動態語法特性,例如 **get, **set 和 __call 這些魔術方法。它們可以在執行時去定義新屬性和方法。通常,靜態分析都會爆出屬性和方法未定義,但是有一種機制可以告訴引擎如何建立新的屬性和方法。

它得益於對允許使用者擴充套件的原生 PHP 反射的自定義抽象。更多細節可檢視 README 中類反射擴充套件章節。

某些方法返回的型別取決於它的引數。它可以取決於你傳遞給它的類名,也可能返回與傳遞的物件相同的類的物件。這就是 動態返回型別擴充套件 的用途。

壓軸語: 如果你想自己出一個 PHPStan 的新的檢查項, 你可以自力更生。可以提出基於特定框架的規則,例如檢查 DQL查詢中引用的實體和欄位是否存在,或者你選擇的 MVC 框架中生成的連結是否和現存的控制器有關。

選擇規範級別

我使用過其他工具,並將之整合進現有的程式碼庫中,這種體驗真是往事不堪回首。他們爆出成千上萬的錯誤讓你沒法使用。

取而代之,我回顧如何整合 PHPStan 到剛進入開發階段的程式碼庫中。 首個版本的功能不是很強大,這時並未發現多少錯誤。但從整合的角度來看,它還是非常不錯的 --- 有空時,我就為它增加新規則,我修復了它在版本庫中找到的錯誤,並將新程式碼合併到主分支。我們會使用新版本幾周用來發現其找到的錯誤,並不斷重複這件事。這種逐級增加的規範性的做法在實踐中看來大有裨益,所以我使用 PHPStan 的現有功能來模擬它。

預設情況下,PHPStan 只檢查它確定的程式碼---常量,例項化,呼叫$ this的方法,靜態呼叫的方法,函式和各種語言結構中的現有類。 通過增加級別(從預設值0到當前值4),您還可以增加它對程式碼所做的假設數量以及它檢查的規則數量。

如果內建級別無法滿足你的要求,你同樣也可以自定義規則。


少寫單元測試! (披沙揀金)

可能這個建議你聞所未聞。即便是非常細碎的程式碼,開發者也不得不編寫單元測試,因為這方面犯錯的機率都是均等的,例如簡單的拼寫錯誤或者忘記將結果賦值給變數。為那些經常出現在控制器或者門臉中的轉發程式碼編寫單元測試是很不划算的事。

單元測試也有其成本。它們同樣也是程式碼,難逃編寫和維護的窠臼。最理想的做法就是在持續整合伺服器上,每次更改時都執行 PHPStan,從而在無需單元測試的情況下防止此類錯誤的產生。實現100%的程式碼覆蓋率真的很難,並且非常昂貴,但你可以靜態分析100%的程式碼。

至於單元測試的重點應當集中在靜態分析程式碼難以察覺的,容易出錯的地方。包括:複雜的資料過濾,迴圈,條件判斷,乘除法包含舍入的計算等。

站在巨人的肩膀上

如果不是 Nikita Popov 建立了 PHP Parser。就不會有 PHPStan 的出現。


PHP 在 2016 年開始廣泛使用 包管理, 單元測試 和 編碼標準 的工具。然而到現在也沒有一個廣泛使用的工具,可以在不執行程式碼的情況下檢查程式碼中的錯誤。所以我建立了一個易於使用,快速,可擴充套件的版本,既不會對您的程式碼有嚴格的要求,你還會從這些檢查中受益。檢視 GitHub 倉庫 ,瞭解如何將其整合到您的專案中!

更多文章:laravel-china.org/c/translati…

相關文章