給找 Bug 的工具(larastan)找 Bug

RightCapital發表於2020-06-02

相關背景

PHPStan 是近期比較熱門的一個 PHP 程式碼靜態分析工具,可以在不執行程式碼的情況下找出程式碼中潛在的 bug。

然而 Laravel 框架使用了太多“魔法“,Laravel 專案程式碼通常會大量使用容器、動態方法呼叫等等,使 PHPStan 無法推匯出相關變數的型別。

為了解決這個問題,Nuno Maduro 開源了 larastan 。這個針對 Laravel 專案的 PHPStan 擴充套件,載入了 larastan 擴充套件的 PHPStan 能夠識別絕大多數的 Laravel 特性,包括容器、Facade、甚至會通過掃描專案的 migrations 檔案來推匯出資料庫模型的各個欄位型別。

發現問題

在 RightCapital,技術團隊每週都會把專案依賴升級到最新版本。在最近一次升級後,larastan 升級到了 0.5.5,我們發現 larastan 針對布林型別的資料庫欄位的推導失效了,所有布林型別的資料庫欄位全部被推導成了 mixed,導致我們引入的另外一個 PHPStan 擴充套件 phpstan-strict-rules 爆出了大量錯誤,所以就有了本次 larastan 的 bug 追蹤之旅。

初步分析

PHPStan 的擴充套件通常都會提供一個 .neon 格式的配置檔案,larastan 也不例外,如圖:

larastan 提供的配置檔案

其中 services 段就是告訴 PHPStan 這個擴充套件會由哪些類來擴充套件功能,所以我們分析問題的第一個步驟就是看看 larastan 用了哪些類來擴充套件。

根據 services 段裡各個類的名字,我們很容易就鎖定了與模型欄位推導相關的類 NunoMaduro\Larastan\Properties\ModelPropertyExtension(論命名的重要性),簡單瀏覽這個類的程式碼之後可以確定這個就是我們要找的地方,有 bug 的程式碼極有可能就在這個檔案裡。

在找到疑似有問題的檔案之後,通常可以通過檢視這個檔案的歷史變更記錄來追蹤可能出問題的程式碼行,但這種方式比較適合於熟悉整個專案架構的人,對於我這個第一次看 larastan 原始碼的人來說可能需要花費較長時間去了解 PHPStan 的擴充套件機制以及 larastan 的程式碼架構,從時間成本來看不太合適,所以我選擇了 var_dump 除錯大法。

var_dump 的正確姿勢

既然選擇了 var_dump 大法,我們需要決定在哪個檔案的哪一行 dump 哪一個變數,合理的 dump 事半功倍,不合理的 dump 只會讓自己迷暈在深不可測的呼叫棧中。

首先分析一下 ModelPropertyExtension 這個類的結構,可以發現只有 hasProperty()getProperty() 兩個公共方法。hasProperty 會掃描專案的 migrations 檔案來構建各個表各個欄位的資料庫型別,並判斷引數中傳入的模型所對應的表是否存在對應的欄位,順帶根據資料庫欄位型別、模型的 Casts 屬性分析了這個欄位的讀(readableType)、寫(writableType)型別並儲存下來;而 getProperty 就更簡單了,直接返回了 hasProperty 推匯出的欄位型別。

所以我們的第一個 dump 打在 getProperty 的型別推導之後,看看 larastan 推匯出來的型別是不是真的是 mixed 而不是 boolean,從方法名可以知道負責推導的方法是 getReadableAndWritableTypes(再次論命名的重要性):

由於整個專案程式碼量非常多,這裡的 dump 會被呼叫非常多次,為了減少干擾,我們專門新建一個類來給 PHPStan 分析:

app/LarastanDebug.php

<?php

namespace App;

class LarastanDebug
{
 public function checkBooleanProperty(): void
 { $user = new \App\User();
 if ($user->blocked) {
 // do something } }}

其中 $user->blocked 欄位在在 migrations 中是 boolean 型別,在 \App\User 類中也 cast 成了 boolean

app/User.php

.
.
.
 protected $casts = [ 'blocked' => 'boolean', ];.
.
.

現在用 PHPStan 來分析一下這個 LarastanDebug 這個類:

./vendor/bin/phpstan analyze app/LarastanDebug.php

可以看到輸出:

string(5) "mixed"
string(5) "mixed"
string(8) "App\User"
string(7) "blocked"

看來真的是 larastan 的鍋,需要看一下負責推導的 getReadableAndWritableTypes 方法裡到底做了什麼事:

可以看到 $readableType 變數被初始化成 mixed,然後根據 $column->readableType 的值去做對應的調整。

所以我們的第二個 dump 就要看看 $column->readableType 的值到底是什麼:

再次執行 PHPStan 分析命令,會發現這次沒有任何的 dump 輸出了,這是因為 PHPStan 在 0.12 之後的版本會快取分析結果,如果檔案內容沒有發生變化會直接使用上一次的分析結果。

我們直接在 LarastanDebug 類最後面加一個空行並儲存,再次執行分析命令,輸出:

string(7) "boolean"
string(5) "mixed"
string(5) "mixed"
string(8) "App\User"
string(7) "blocked"

可以看到 $column->readableType 的值是 boolean,而 getReadableAndWritableTypes 方法裡的 switch 只有 bool 沒有 boolean,導致其跳過了整個 switch,所以返回了初始值 mixed

Bug 修復

這個 bug 的修復也十分簡單,直接在 getReadableAndWritableTypes 方法裡的 switch 裡新增一行 case 'boolean': 即可:

刪掉 LarastanDebug 類最後一個空行之後再次執行分析命令,輸出:

string(7) "boolean"
string(3) "boolean"
string(8) "boolean"
string(8) "App\User"
string(7) "blocked"

符合預期。

最終提交了 PR 給 larastan 倉庫,目前已經合併。


請關注我們的微信公眾號「rightcapital」

本作品採用《CC 協議》,轉載必須註明作者和本文連結

歡迎關注我們的微信公眾號「RightCapital」

相關文章