程式碼品質

有許多工具與方法可以協助開發者寫出高品質程式碼。本講會介紹:

加碼主題還會談到正規表示式。它是跨領域的實用工具,不只可用在程式碼品質(例如只執行符合特定樣式的測試),也常用於 IDE(例如搜尋與取代)。

這些工具有些是語言專屬(例如 Python 的 Ruff linter/formatter),有些則支援多語言(例如 Prettier)。不過核心觀念幾乎通用——任何程式語言都能找到格式化工具、linter、測試函式庫等。

格式化(Formatting)

程式碼自動格式化工具會自動整理表層語法。這樣你能把注意力放在真正困難的問題上,讓工具處理瑣碎細節,例如字串使用 ' 或 " 的一致性、二元運算子前後空白(x + y 而非 x+y)、import 排序、以及過長行等。格式化工具的一大好處是:能統一整個程式碼庫的風格,降低團隊協作摩擦。

有些工具(如 Prettier)可高度客製化;建議把設定檔納入專案的版本控制。另外有些工具(如 Black 與 gofmt)幾乎不提供可調參數,用意是減少無謂爭論。

你可以把格式化工具和 IDE 整合,讓程式碼在輸入或儲存時自動格式化。你也可以在專案加入 EditorConfig 檔案,向 IDE 傳達專案層級設定(例如各檔案類型的縮排大小)。

Lint(靜態檢查)

Linter 會做靜態分析(不執行程式就分析程式碼),找出反模式與潛在問題。這類工具比格式化器更深入,不只看表面語法;不同 linter 的分析深度也不一樣。

Linter 通常內建一組 rules,並提供可在專案層級調整的預設組合。有些規則可能出現誤報(false positives),因此可在檔案或單行層級停用。

好的 linter 會有清楚文件,說明每條規則在抓什麼、為何不好、以及更佳替代寫法。例如可以看 Ruff 的 SIM102 規則,它會抓出 Python 中不必要的巢狀 if。

有些 linter 不只會標記問題,還能自動修正部分問題。

除了語言專屬 linter,另一個常用工具是 semgrep。它是「語意版 grep」,在 AST 層級運作(不像 grep 僅是字元層級),支援多種語言。你可以用 semgrep 輕鬆為專案寫客製規則。例如想禁止 Python 中危險的 subprocess.Popen(..., shell=True),可用:

semgrep -l python -e "subprocess.Popen(..., shell=True, ...)"

測試(Testing)

軟體測試是提升「程式正確性信心」的標準方法。你先寫功能程式碼,再寫測試程式碼去驗證行為;若結果不符預期,測試就會報錯。

你可以在不同粒度撰寫測試:單一函式的 unit tests、模組/服務互動的 integration tests、端到端情境的 functional tests。你也可以採用 test-driven development(先寫測試再寫實作)。當發現 bug 時,可以補 regression tests,避免未來同類問題回歸。你也可以做 property-based tests(最早由 Haskell 的 QuickCheck 推廣,Python 的 Hypothesis 等函式庫也有實作)。適合哪種方法取決於專案,實務上多半會混合使用。

如果程式依賴外部資源(例如資料庫或 Web API),測試時通常以 mock 取代真實依賴會更穩定,也更容易除錯。

程式碼覆蓋率(Code coverage)

Code coverage 是衡量測試覆蓋程度的指標。它會記錄測試執行時哪些程式行被跑過,幫你檢查是否覆蓋到不同程式路徑。覆蓋率工具通常可提供逐行結果,協助補強測試。Codecov 等服務也提供網頁介面,追蹤專案歷史覆蓋率。

和其他指標一樣,覆蓋率並不完美;不要只追數字,重點仍是寫出高品質測試。

Pre-commit hooks

Git 的 pre-commit hooks(搭配 pre-commit 框架更方便)會在每次 commit 前自動執行你指定的檢查。專案常用它在每次提交前自動跑 formatter、linter,有時也會跑測試,確保提交內容符合專案風格且不含特定問題。

持續整合(Continuous integration)

持續整合(CI)服務(如 GitHub Actions)可在每次 push、每個 pull request 或排程時間自動執行腳本。開發者常用 CI 跑程式碼品質工具,包含 formatter、linter、測試。對編譯型語言可驗證是否可編譯;對靜態型別語言可驗證型別檢查。每次 push 跑 CI 可及早發現主分支新引入的錯誤;在 PR 上跑 CI 可攔截貢獻內容問題;排程跑 CI 則可提早發現外部依賴變動造成的影響(例如某套件誤發了雖標示 semver 相容但其實破壞相容性的版本)。

由於 CI 腳本是在開發者機器之外執行,你可以更容易安排長時間作業。例如跑跨作業系統、跨語言版本的 matrix 測試,確認軟體在各環境都能正常運作。

一般來說,CI 不會直接改你的程式碼,而是用「只檢查(check-only)」模式而非「自動修復(fix)」模式執行工具。例如格式不符時,formatter 會報錯而不會幫你改檔。

專案常在 README 放上狀態徽章,顯示 CI 結果與覆蓋率等資訊。以下是 Missing Semester 目前的建置狀態。

Build Status Links Status

我們的連結檢查器使用 proof-html GitHub Action,常因第三方網站問題而失敗。即便如此,它仍幫我們抓出並修正許多失效連結(有些是拼字錯誤,多數則是網站搬動內容卻未設轉址,或網站直接消失)。

學 CI、formatter、linter、測試函式庫的最佳方式之一是看範例。到 GitHub 找高品質開源專案——越接近你專案的語言、領域、規模越好——研究它們的 pyproject.toml、.github/workflows/、DEVELOPMENT.md 等相關檔案。

持續部署(Continuous deployment)

持續部署會利用 CI 基礎設施直接 部署 變更。例如 Missing Semester 專案使用持續部署到 GitHub Pages:每次 git push 更新講義後,網站就會自動建置與部署。你也可以在 CI 產出其他產物(artifacts),例如應用程式執行檔或服務用 Docker 映像。

指令執行器(Command runners)

像 just 這類 command runner 可簡化專案內指令執行。當你逐步建立品質流程時,不會希望團隊死背像 uv run ruff check --fix 這種長指令。透過 command runner 可改成 just lint,也可提供 just format、just typecheck 等一致入口。

有些語言的專案/套件管理器已內建類似功能,因此你不一定要用語言無關工具(如 just)。例如 Node.js 的 npm 在 package.json 的 scripts 區塊,或 Python 的 Hatch 在 pyproject.toml 的 tool.hatch.envs.*.scripts 區塊,都能做到。

正規表示式(Regular expressions)

Regular expressions(常簡稱 regex)是一種用來表示字串集合的語言。Regex 樣式常用於各種情境下的樣式匹配,例如命令列工具與 IDE。像 ag 支援全專案 regex 搜尋(例如 ag "import .* as .*" 可找出 Python 中被重新命名的 import),go test 也支援 -run [regexp] 來挑選測試子集合。此外,多數程式語言都有內建或第三方 regex 函式庫,可用於匹配、驗證與解析。

為了建立直覺,下面列出幾個 regex 範例。本講使用 Python regex syntax。Regex 有很多方言,尤其在進階功能上會有差異。你可用 regex101 這類線上工具開發與除錯。

Regex 語法

完整語法可參考官方文件(或其他線上資源)。以下是一些基本構件:

捕獲群組與參照

若使用 regex 群組 (...),可參照匹配到的子區段做擷取或搜尋取代。例如要從 YYYY-MM-DD 日期中擷取月份,可用:

>>> import re
>>> re.match(r"\d{4}-(\d{2})-\d{2}", "2026-01-14").group(1)
'01'

在文字編輯器中,你也可在取代樣式裡引用捕獲群組。不同 IDE 語法可能不同。例如 VS Code 可用 $1、$2,Vim 可用 \1、\2 參照群組。

限制

Regular languages 雖然強大但有限制;有些字串集合無法用標準 regex 表達(例如不可能寫出匹配 {a^n b^n | n ≥ 0} 的正規表示式,也就是同數量 a 後接同數量 b;更實務地說,像 HTML 這類語言不是 regular language)。現代 regex 引擎雖支援 lookahead、backreference 等擴充,實用性很高,但表達能力依然有限。面對更複雜語言時,你可能需要更完整的 parser(例如 pyparsing,屬於 PEG parser)。

如何學 regex

我們建議先掌握基礎(也就是本講內容),之後按需查語法,而不是試圖一次背完整個 regex 語言。

對話式 AI 工具也很適合協助產生 regex。你可以試著對喜歡的 LLM 輸入以下提示:

Write a Python-style regex pattern that matches the requested path from log lines from Nginx. Here is an example log line:

169.254.1.1 - - [09/Jan/2026:21:28:51 +0000] "GET /feed.xml HTTP/2.0" 200 2995 "-" "python-requests/2.32.3"

練習

  1. 在你正在開發的專案中設定 formatter、linter 與 pre-commit hooks。若錯誤很多:格式問題可先靠 autoformatting。對 linter 問題,試著用 AI agent 修正全部錯誤。請確保 AI agent 能實際執行 linter 並看到結果,才能迭代修到乾淨。最後務必人工檢查,確認 AI 沒把程式改壞。
  2. 選一個你熟悉語言的測試函式庫,為手邊專案寫一個單元測試。執行 coverage 工具,產出 HTML 覆蓋率報告並觀察:你看得出哪些行被覆蓋嗎?一開始覆蓋率通常很低。先手動補一些測試來提升,再試著用 AI agent 協助提高覆蓋率;請確保 agent 能跑含 coverage 的測試並產出逐行報告,才能知道該補哪裡。AI 生成的測試真的有品質嗎?
  3. 為你的專案設定每次 push 都會執行的 CI。讓 CI 跑格式化、lint 與測試。接著故意引入錯誤(例如 linter 違規),確認 CI 能正確攔截。
  4. 嘗試自己寫一個regex 樣式,並用 grep 這個命令列工具 在你的程式碼中找 subprocess.Popen(..., shell=True)。接著想辦法「打破」你的 regex。當 grep 失手時,semgrep 是否仍能抓到危險寫法?
  5. 在 IDE 或文字編輯器中練習 regex 搜尋與取代:把這份講義中的 - Markdown 項目符號 改成 *。注意:不能直接把檔案所有 - 都取代,因為很多 - 不是項目符號。
  6. 寫一個 regex,從 {"name": "Alyssa P. Hacker", "college": "MIT"} 這種 JSON 結構中擷取 name(此例為 Alyssa P. Hacker)。提示:第一版你可能會錯抓成 Alyssa P. Hacker", "college": "MIT;可閱讀 Python regex 文件中的 greedy quantifier 說明來修正。
    1. 讓 regex 在 name 內含 " 時也能正常運作(JSON 可用 \" 跳脫雙引號)。
    2. 實務上我們不建議用 regex 解決複雜解析問題。請改用你使用語言的 JSON parser 完成這題:寫一個命令列程式,從 stdin 讀入上述 JSON 結構,在 stdout 輸出 name。這題通常只要幾行程式;以 Python 來說,扣掉 import json,一行就能完成。

編輯此頁面。

本內容採用 CC BY-NC-SA 授權。