在流程中實作靜態分析,而不是用它來找出錯誤

大量關於靜態分析的資料越來越吸引我的注意,促使我寫這篇文章。 首先,這個 PVS-工作室博客,借助對其工具在開源專案中發現的錯誤的審查,在 Habré 上積極宣傳自己。 最近PVS-studio實施 Java支援,當然還有 IntelliJ IDEA 的開發人員,其內建分析器可能是當今最先進的 Java 分析器, 無法離開.

閱讀這類評論時,您會感覺我們正在談論一種靈丹妙藥:按下按鈕,它就出現在您眼前 - 一系列缺陷。 似乎隨著分析儀的改進,越來越多的錯誤會自動被發現,而這些機器人掃描的產品也會變得越來越好,而無需我們付出任何努力。

但沒有靈丹妙藥。 我想談談在「這是我們的機器人可以找到的東西」之類的帖子中通常不會談論的內容:分析器不能做什麼,它們在軟體交付過程中的真正作用和位置是什麼,以及如何正確實現它們。

在流程中實作靜態分析,而不是用它來找出錯誤
棘輪(來源: 維基百科).

靜態分析儀永遠無法做到的事情

從實用的角度來看,什麼是原始碼分析? 我們提供一些原始程式碼作為輸入和輸出,在短時間內(比運行測試短得多)我們獲得了有關係統的一些資訊。 基本的、數學上無法克服的限制是,我們透過這種方式只能獲得相當狹窄的一類資訊。

無法使用靜態分析解決的問題的最著名的例子是 關機問題:這是一個定理,證明不可能開發出一種通用演算法來從程式的原始程式碼中確定程式是否會在有限時間內循環或終止。 這個定理的擴展是 賴斯定理,它指出對於可計算函數的任何非平凡屬性,確定任意程式是否評估具有此類屬性的函數是演算法上難以解決的問題。 例如,不可能編寫一個分析器來從任何原始程式碼確定正在分析的程式是否是計算整數平方等演算法的實作。

因此,靜態分析儀的功能具有難以克服的限制。 靜態分析器永遠無法在所有情況下檢測到諸如允許 null 值的語言中“空指針異常”的出現,或者在所有情況下都無法確定“空指針異常”的出現。動態類型語言中未找到屬性” 。 最先進的靜態分析器所能做的就是突出顯示特殊情況,在原始程式碼的所有可能問題中,毫不誇張地說,特殊情況的數量只是九牛一毛。

靜態分析不是為了發現錯誤

綜上所述,結論如下:靜態分析並不是減少程式缺陷數量的手段。 我敢說:當第一次應用於你的專案時,它會在程式碼中找到「有趣」的地方,但是,很可能,它不會發現任何影響你的程式品質的缺陷。

分析器自動發現的缺陷範例令人印象深刻,但我們不應該忘記這些範例是透過掃描大量大型程式碼庫發現的。 根據同樣的原理,有機會在大量帳戶上嘗試多個簡單密碼的駭客最終會找到那些具有簡單密碼的帳戶。

這是否意味著不應使用靜態分析? 當然不是! 出於完全相同的原因,值得檢查每個新密碼以確保其包含在「簡單」密碼的停止清單中。

靜態分析不僅僅是發現錯誤

事實上,透過分析實際解決的問題要廣泛得多。 畢竟,一般來說,靜態分析是在原始程式碼發布之前對其進行的任何驗證。 您可以執行以下操作:

  • 從最廣泛的意義上檢查編碼風格。 這包括檢查格式、尋找空/額外括號的使用、設定方法的行數/圈複雜度等指標的閾值——任何可能妨礙程式碼可讀性和可維護性的事情。 在Java中,這樣的工具是Checkstyle,在Python中是flake8。 此類程式通常稱為“linter”。
  • 不僅可以分析可執行程式碼。 JSON、YAML、XML、.properties 等資源檔案可以(而且應該!)自動檢查有效性。 畢竟,在自動 Pull 請求驗證的早期階段發現 JSON 結構由於一些不成對的引號而被破壞比在測試執行或運行時更好? 可以使用適當的工具:例如 YAMLlint, JSONLint.
  • 編譯(或稱動態程式語言的解析)也是靜態分析的一種。 一般來說,編譯器能夠產生警告,表示原始碼品質有問題,不應被忽視。
  • 有時編譯不僅僅是編譯可執行程式碼。 例如,如果您有以下格式的文檔 ASCII醫生,然後在將其轉換為 HTML/PDF 時,AsciiDoctor 處理程序(Maven插件)可以發出警告,例如有關損壞的內部連結的警告。 這是不接受帶有文件變更的 Pull 請求的一個很好的理由。
  • 拼字檢查也是靜態分析的一種。 公用事業 阿斯佩爾 不僅可以檢查文件中的拼寫,還可以檢查各種程式語言(包括 C/C++、Java 和 Python)的程式原始碼(註解和文字)中的拼寫。 使用者介面或文件中的拼字錯誤也是一個缺陷!
  • 配置測試(關於它們是什麼 - 請參閱。 и 報告)雖然在單元測試運行時(例如 pytest)中執行,但實際上也是一種靜態分析,因為它們在執行期間不執行原始程式碼。

正如您所看到的,在此列表中搜尋錯誤起著最不重要的作用,其他所有內容都可以透過使用免費的開源工具來獲得。

您應該在專案中使用下列哪種類型的靜態分析? 當然是越多越好! 最主要的是正確實施它,這將進一步討論。

輸送管道作為多層過濾器,靜態分析作為第一級

持續整合的經典比喻是一條管道,變更通過該管道流動,​​從原始碼變更到交付再到生產。 此管道中的標準階段順序如下所示:

  1. 靜態分析
  2. 彙編
  3. 單元測試
  4. 整合測試
  5. 使用者介面測試
  6. 人工檢查

在管道第 N 階段拒絕的更改不會傳輸到階段 N+1。

為什麼要這樣而不是其他方式? 在管道的測試部分,測試人員將認識到眾所周知的測試金字塔。

在流程中實作靜態分析,而不是用它來找出錯誤
測試金字塔。 來源: 文章 馬丁·福勒。

金字塔底部的測試更容易編寫,執行速度更快,不易失敗。 因此,它們應該有更多,它們應該覆蓋更多的程式碼並首先執行。 在金字塔的頂部,情況恰恰相反,因此整合和 UI 測試的數量應減少到必要的最低限度。 這條鏈中的人是最昂貴、最慢、最不可靠的資源,因此他位於最後,只有在前面的階段沒有發現任何缺陷的情況下才執行工作。 然而,在與測試不直接相關的部分中,可以使用相同的原理來建立管道!

我想以多層水過濾系統的形式進行類比。 髒水(有缺陷的變化)被供應到輸入端;在輸出端,我們必須收到乾淨的水,其中所有不需要的污染物都已被消除。

在流程中實作靜態分析,而不是用它來找出錯誤
多級過濾器。 來源: 維基共享資源

如您所知,清潔過濾器的設計使得每個後續級聯都可以過濾掉越來越細小的污染物。 同時,較粗的純化級聯具有更高的通量和更低的成本。 在我們的類比中,這意味著輸入質量門更快,啟動起來更省力,並且本身在操作上更樸實 - 這就是它們的構建順序。 正如我們現在所理解的,靜態分析的作用是過濾器級聯最開始處的「泥漿」網格的作用,它只能清除最嚴重的缺陷。

靜態分析本身並不能提高最終產品的質量,就像「泥漿過濾器」不能使水可供飲用一樣。 然而,與管道中的其他元素結合起來,它的重要性是顯而易見的。 儘管在多級過濾器中,輸出級可能能夠捕捉輸入級所做的一切,但很明顯,如果嘗試僅使用精細淨化級而不使用輸入級,將會產生什麼後果。

「泥漿陷阱」的目的是為了避免隨後的級聯捕獲非常嚴重的缺陷。 例如,至少,進行程式碼審查的人員不應因格式不正確的程式碼和違反既定編碼標準(例如額外的括號或嵌套太深的分支)而分心。 像 NPE 這樣的錯誤應該透過單元測試來捕獲,但是如果分析器在測試之前就向我們表明錯誤肯定會發生,那麼這將顯著加快其修復速度。

我相信現在很清楚為什麼靜態分析如果偶爾使用並不能提高產品的質量,而應該經常使用來過濾掉具有嚴重缺陷的更改。 使用靜態分析儀是否會提高產品品質的問題大致相當於問:“如果從髒池塘中取出的水通過漏勺,其飲用品質會得到改善嗎?”

實施到遺留項目中

一個重要的實際問題:如何將靜態分析作為「品質門」融入持續整合過程中? 在自動測試的情況下,一切都是顯而易見的:有一組測試,其中任何一個測試失敗就足以有理由相信組件沒有通過品質門。 嘗試根據靜態分析的結果以相同的方式安裝門會失敗:遺留程式碼中存在太多分析警告,您不想完全忽略它們,但也不可能停止交付產品只是因為它包含分析器警告。

首次使用時,分析儀會對任何項目產生大量警告,其中絕大多數與產品的正常運作無關。 一次性糾正所有這些評論是不可能的,而且很多都是沒有必要的。 畢竟,即使在引入靜態分析之前,我們也知道我們的產品作為一個整體是有效的!

因此,許多人僅限於偶爾使用靜態分析,或僅在資訊模式下使用靜態分析(僅在組裝期間發布分析報告)。 這相當於沒有任何分析,因為如果我們已經有很多警告,那麼在更改程式碼時出現另一個警告(無論多麼嚴重)就不會被注意到。

引入質量門的以下方法是已知的:

  • 設定警告總數的限製或警告數量除以程式碼行數。 這效果很差,因為這樣的門可以自由地允許帶有新缺陷的更改通過,只要不超過它們的限制。
  • 在某個時刻修復程式碼中所有舊警告被忽略的問題,並在出現新警告時拒絕建置。 此功能由 PVS-studio 和一些線上資源(例如 Codacy)提供。 我沒有機會在PVS-studio工作,就我在Codacy的經驗而言,他們的主要問題是確定什麼是“舊”錯誤和什麼是“新”錯誤是一個相當複雜的演算法,並不總是有效正確,尤其是在文件被大量修改或重命名的情況下。 根據我的經驗,Codacy 可能會忽略拉取請求中的新警告,同時由於與給定 PR 的程式碼變更無關的警告而不會傳遞拉取請求。
  • 我認為最有效的解決方案是書中描述的 持續交付 「棘輪法」。 基本概念是,靜態分析警告的數量是每個版本的屬性,並且僅允許不增加警告總數的變更。

棘輪

它的工作原理是這樣的:

  1. 在初始階段,在元資料中記錄分析器發現的程式碼中警告的發布數量。 因此,當您建置上游時,您的儲存庫管理員不僅會寫入“release 7.0.2”,還會寫入“release 7.0.2 contains 100500 checkstyle warnings”。 如果您使用高階儲存庫管理員(例如 Artifactory),則儲存有關您的版本的此類元資料很容易。
  2. 現在,每個拉取請求在建置時都會將產生的警告數量與目前版本中可用的警告數量進行比較。 如果 PR 導致這個數字增加,那麼程式碼就沒有通過靜態分析的品質關。 如果警告數量減少或沒有變化,則警告通過。
  3. 在下一個版本中,重新計算的警告數量將再次記錄在版本元資料中。

如此一點一點地、穩定地(就像棘輪工作時一樣),警告的數量將趨於零。 當然,系統可以透過引入新的警告但糾正其他人的警告來欺騙。 這是正常的,因為在很長的距離上它會給出結果:警告通常不是單獨糾正,而是立即以某種類型的組糾正,並且所有容易消除的警告都會很快消除。

該圖顯示了這種「棘輪」裝置運作六個月後,Checkstyle 警告的總數 我們的開源專案之一。 警告數量減少了一個數量級,這是自然發生的,與產品開發同時進行!

在流程中實作靜態分析,而不是用它來找出錯誤

我使用此方法的修改版本,分別計算專案模組和分析工具的警告,產生包含構建元資料的 YAML 文件,如下所示:

celesta-sql:
  checkstyle: 434
  spotbugs: 45
celesta-core:
  checkstyle: 206
  spotbugs: 13
celesta-maven-plugin:
  checkstyle: 19
  spotbugs: 0
celesta-unit:
  checkstyle: 0
  spotbugs: 0

在任何先進的 CI 系統中,任何靜態分析工具都可以實現棘輪,而不需要依賴插件和第三方工具。 每個分析器都會以易於分析的簡單文字或 XML 格式產生自己的報表。 剩下的就是在 CI 腳本中編寫必要的邏輯。 您可以看到這是如何在我們基於 Jenkins 和 Artifactory 的開源專案中實現的 這裡這裡。 這兩個範例都依賴函式庫 棘輪庫: 方法 countWarnings() 以通常的方式計算 Checkstyle 和 Spotbugs 產生的檔案中的 xml 標籤,以及 compareWarningMaps() 實現相同的棘輪,當任何類別中的警告數量增加時拋出錯誤。

「棘輪」的一個有趣的實作是可以使用 aspell 來分析註解、文字文字和文件的拼字。 如您所知,在檢查拼字時,並非所有標準字典未知的單字都是錯誤的;它們可以添加到使用者字典中。 如果您將自訂字典作為專案原始程式碼的一部分,則可以透過以下方式製定拼字品質閘:使用標準和自訂字典執行 aspell 不應該 發現沒有拼字錯誤。

關於修復分析器版本的重要性

總之,需要注意的一點是,無論您如何在交付管道中實施分析,分析器的版本都必須是固定的。 如果您允許分析器自發性更新,那麼在組裝下一個拉取請求時,可能會「彈出」新的缺陷,這些缺陷與程式碼變更無關,但與新分析器能夠發現更多缺陷有關 -這將破壞您接受拉取請求的過程。 升級分析儀應該是一個有意識的行為。 然而,每個組裝部件的版本的剛性固定通常是必要的要求,也是單獨討論的主題。

發現

  • 靜態分析不會為您發現錯誤,也不會因為單一應用程式而提高產品品質。 只有在交付過程中不斷使用它才能對品質產生正面的影響。
  • 尋找錯誤根本不是分析的主要任務;絕大多數有用的功能都可以在開源工具中找到。
  • 在交付管道的第一階段根據靜態分析的結果實施品質關,對遺留程式碼使用「棘輪」。

引用

  1. 持續交付
  2. A. Kudryavtsev:程式分析:如何了解自己是個優秀的程式設計師 報告不同的程式碼分析方法(不僅僅是靜態!)

來源: www.habr.com

添加評論