From f55d10ee07e3ce8b0cbf5a9e05cb824d65f2fd5e Mon Sep 17 00:00:00 2001 From: MH Hung Date: Tue, 9 Sep 2025 22:52:34 +0800 Subject: [PATCH] feat(linter): implement Phase 1 linter automation and docs - Add .clang-tidy (analyzer + selected bugprone) and .clang-format (LLVM, 4-space, 100 cols) - Enhance scripts/run-linter.sh to use compile_commands.json when available - Add scripts/setup-hooks.sh pre-commit (format enforcement; advisory tidy) - Update azure-pipelines.yml to export compile_commands and run clang-tidy -p build - Fill docs/linter-setup.md and docs/coding-standards.md for Phase 1 - Add minimal tests in tests/test_main.cpp to ensure CI executes - Rewrite README with Phase 1 workflow --- .clang-format | 11 ++++ .clang-tidy | 31 +++++++++ README.md | 138 +++++++++++++++++++-------------------- azure-pipelines.yml | 38 ++++++++--- docs/coding-standards.md | 22 +++++++ docs/linter-setup.md | 39 +++++++++++ scripts/run-linter.sh | 18 +++-- scripts/setup-hooks.sh | 59 +++++++++++++++++ tests/test_main.cpp | 21 ++++++ 9 files changed, 296 insertions(+), 81 deletions(-) create mode 100644 .clang-format create mode 100644 .clang-tidy diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..20e7956 --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +ColumnLimit: 100 +AllowShortFunctionsOnASingleLine: Empty +BreakBeforeBraces: Attach +SortIncludes: CaseSensitive +SpacesInAngles: Never +PointerAlignment: Right +DerivePointerAlignment: false diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..9a378e1 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,31 @@ +Checks: > + -*, + clang-analyzer-*, + bugprone-argument-comment, + bugprone-assert-side-effect, + bugprone-bad-signal-to-kill-thread, + bugprone-branch-clone, + bugprone-copy-constructor-init, + bugprone-dangling-handle, + bugprone-exception-escape, + bugprone-integer-division, + bugprone-macro-parentheses, + bugprone-misplaced-operator-in-strlen-in-alloc, + bugprone-multiple-new-delete-leaks, + bugprone-sizeof-expression, + bugprone-suspicious-missing-comma, + bugprone-throw-keyword-missing, + bugprone-unhandled-self-assignment, + bugprone-unused-return-value, + clang-analyzer-security.*, + clang-analyzer-cplusplus.*, + clang-analyzer-core.* +HeaderFilterRegex: '(src|tests)/' +AnalyzeTemporaryDtors: false +FormatStyle: none +WarningsAsErrors: '' + +# Phase 1: advisory only (warnings do not fail CI) +CheckOptions: + - key: bugprone-assert-side-effect.AssertMacros + value: 'assert' diff --git a/README.md b/README.md index 43b66ab..e1c51a8 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,94 @@ # C++ Linter Template -這是一個展示如何在 C++ 專案中整合 linter 工具的模板專案。 +這是一個符合最佳實務、可漸進式導入的 C++ linter 自動化模板。現為 Phase 1:格式嚴格、靜態分析建議不擋流程。 -## 目的 -- 展示 clang-tidy 在舊 C++ 代碼中的應用 -- 提供漸進式導入 linter 的範例 -- Azure DevOps CI/CD 整合範例 +## 目標 +- 對舊 C++ 專案導入 `clang-tidy` 與 `clang-format` +- 提供「先格式、後強化」的漸進策略 +- 內建 Azure DevOps CI/CD 串接與 PR 差異分析 -## 快速開始 +## 快速開始(Phase 1) -### 1. 本地開發環境設定 +### 1) 安裝工具 ```bash -# 安裝相依套件 (Ubuntu/Debian) +# Ubuntu/Debian sudo apt install clang-tidy clang-format cmake build-essential -# 安裝相依套件 (macOS) +# macOS brew install llvm cmake - -# 克隆專案 -git clone -cd cpp-linter-template ``` -### 2. 執行 Linter +### 2) 產出 compile_commands.json(建議) ```bash -# 使用提供的腳本 +mkdir -p build && cd build +cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +cd - +``` + +### 3) 執行 Linter +```bash +# 使用腳本(會自動偵測 build/compile_commands.json) ./scripts/run-linter.sh - -# 或手動執行 -clang-tidy src/*.cpp src/*.h tests/*.cpp --config-file=.clang-tidy ``` -### 3. 建構專案 +### 4) 安裝 Git hooks(建議) ```bash -mkdir build && cd build -cmake .. -make +./scripts/setup-hooks.sh +``` +- pre-commit 會自動 clang-format 已 staged 的 C/C++ 檔並要求你重提一次 commit。 +- clang-tidy 以建議模式執行(不擋提交)。 + +### 5) 建構與執行 +```bash +mkdir -p build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Debug +make -j$(nproc) +./tests ./main ``` -## Linter 配置說明 -### 當前採用階段一設定: -- 只檢查最關鍵的錯誤 -- 不會因 linter 警告而中斷 CI -- 適合舊代碼逐步改進 +## Phase 1 政策(建議 → 強制的起點) +- 格式:以 `.clang-format` 為準;CI 以 `--dry-run --Werror` 強制。 +- 靜態分析:`.clang-tidy` 啟用 `clang-analyzer-*` 與精選 `bugprone-*`;僅回報警告,不讓 CI 失敗。 +- PR:僅分析變更檔案,加速回饋。 -### 後續階段計劃 -- 階段二:增加記憶體和資源管理檢查 -- 階段三:加入可讀性和現代化建議 -- 階段四:強制執行,警告視為錯誤 +## CI 流程(Azure Pipelines) +1. 安裝工具與版本資訊 +2. `clang-format --dry-run --Werror`(不合格式即失敗) +3. 以 CMake 匯出 `build/compile_commands.json` +4. `clang-tidy -p build` 掃描(建議模式,不擋流程) +5. Build 專案與執行 `./tests`、`./main` +6. PR Job 僅對變更檔案跑 `clang-tidy -p build` -## 在 Azure DevOps 中使用 -1. 將此專案推送到 Azure DevOps -2. 設定 Pipeline 使用 azure-pipelines.yml -3. 建立 Pull Request 時會自動執行 linter +## 自定義規則 +- `.clang-tidy`:Phase 1 強調重大缺陷檢查(例如 analyzer、部分 bugprone)。 +- `.clang-format`:基於 LLVM 風格(IndentWidth=4, ColumnLimit=100)。 -## 自定義配置 -編輯 .clang-tidy 檔案來調整檢查規則: -```yaml -# 增加更多檢查 -Checks: '-*,clang-analyzer-*,bugprone-*,readability-*' -``` - -## 檔案說明 -`.clang-tidy:` Linter 配置檔 -`.clang-format`: 程式碼格式化配置 -`azure-pipelines.yml`: Azure DevOps CI/CD 配置 -`scripts/run-linter.sh`: 本地執行 linter 腳本 - -## 注意事項 -- 此模板使用 C++98 標準,適合舊代碼 -- Linter 設定較為寬鬆,適合逐步導入 -- 可以根據團隊需求調整檢查規則 +## 文件 +- `docs/linter-setup.md`:本地與 CI 設定流程 +- `docs/coding-standards.md`:編碼與格式規範(Phase 1) ## 專案結構 cpp-linter-template/ -├── .clang-tidy # linter 配置檔 -├── .clang-format # 格式化配置檔 -├── azure-pipelines.yml # Azure DevOps CI/CD 配置 -├── CMakeLists.txt # 建構配置 -├── README.md # 專案說明 -├── docs/ # 文件目錄 -│ ├── linter-setup.md # Linter 設定指南 -│ └── coding-standards.md # 編碼標準 -├── src/ # 原始碼 -│ ├── main.cpp # 主程式(含各種測試案例) -│ ├── utils.cpp # 工具函數 -│ └── utils.h # 標頭檔 -├── tests/ # 測試程式碼 -│ └── test_main.cpp # 簡單測試 -└── scripts/ # 輔助腳本 - ├── run-linter.sh # 本地執行 linter - └── setup-hooks.sh # 設定 git hooks \ No newline at end of file +├── .clang-tidy # linter 配置檔(Phase 1) +├── .clang-format # 格式化配置 +├── azure-pipelines.yml # Azure DevOps CI/CD 配置 +├── CMakeLists.txt # 建構配置(C++98) +├── README.md # 專案說明(本檔) +├── docs/ # 文件目錄 +│ ├── linter-setup.md # Linter 設定指南 +│ └── coding-standards.md # 編碼標準(Phase 1) +├── src/ # 原始碼 +│ ├── main.cpp # 主程式(含測試片段) +│ ├── utils.cpp # 工具函數 +│ └── utils.h # 標頭檔 +├── tests/ # 測試程式碼 +│ └── test_main.cpp # 最小測試(無框架) +└── scripts/ # 輔助腳本 + ├── run-linter.sh # 本地執行 linter + └── setup-hooks.sh # 安裝 pre-commit hooks + +## 後續階段(預覽) +- Phase 2:對關鍵 tidy 規則轉為失敗門檻(資源管理/未定義行為)。 +- Phase 3:引入 readability/modernize 並在 PR 變更檔上強制。 +- Phase 4:全域強制,警告視為錯誤(需技術債清理完成)。 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index bdefb1b..0d7ddf6 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -32,6 +32,7 @@ stages: sudo apt-get update sudo apt-get install -y clang-tidy clang-format cmake build-essential clang-tidy --version + cmake --version - task: Bash@3 displayName: 'Check Code Format' @@ -42,14 +43,26 @@ stages: find src tests -name "*.cpp" -o -name "*.h" | xargs clang-format --dry-run --Werror - task: Bash@3 - displayName: 'Run clang-tidy on all files' + displayName: 'Configure build (export compile_commands)' inputs: targetType: 'inline' script: | - echo "Running clang-tidy..." - find src tests -name "*.cpp" -o -name "*.h" | xargs clang-tidy --config-file=.clang-tidy + echo "Configuring CMake to export compile_commands.json..." + mkdir -p build + cd build + cmake .. -DCMAKE_BUILD_TYPE=${{ variables.buildConfiguration }} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + ls -la - # 階段一:只顯示警告,不中斷 CI + - task: Bash@3 + displayName: 'Run clang-tidy on all files (advisory)' + inputs: + targetType: 'inline' + script: | + echo "Running clang-tidy (Phase 1: advisory only)..." + FILES=$(find src tests -type f \( -name "*.cpp" -o -name "*.cxx" -o -name "*.cc" -o -name "*.c++" -o -name "*.h" -o -name "*.hpp" -o -name "*.hxx" \)) + if [ -n "$FILES" ]; then + echo "$FILES" | xargs -r clang-tidy -p build --config-file=.clang-tidy || true + fi echo "Linter completed. Review warnings above." - task: Bash@3 @@ -57,7 +70,6 @@ stages: inputs: targetType: 'inline' script: | - mkdir build cd build cmake .. -DCMAKE_BUILD_TYPE=${{ variables.buildConfiguration }} make -j$(nproc) @@ -81,8 +93,18 @@ stages: targetType: 'inline' script: | sudo apt-get update - sudo apt-get install -y clang-tidy + sudo apt-get install -y clang-tidy cmake build-essential + cmake --version + - task: Bash@3 + displayName: 'Configure build (export compile_commands)' + inputs: + targetType: 'inline' + script: | + echo "Configuring CMake to export compile_commands.json for PR analysis..." + mkdir -p build + cd build + cmake .. -DCMAKE_BUILD_TYPE=${{ variables.buildConfiguration }} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - task: Bash@3 displayName: 'Analyze Changed Files Only' inputs: @@ -102,5 +124,5 @@ stages: echo "Changed C++ files:" echo "$CHANGED_FILES" - echo "Running clang-tidy on changed files..." - echo "$CHANGED_FILES" | xargs clang-tidy --config-file=.clang-tidy -- -std=c++98 \ No newline at end of file + echo "Running clang-tidy on changed files (advisory)..." + echo "$CHANGED_FILES" | xargs -r clang-tidy -p build --config-file=.clang-tidy || true diff --git a/docs/coding-standards.md b/docs/coding-standards.md index e69de29..97de97b 100644 --- a/docs/coding-standards.md +++ b/docs/coding-standards.md @@ -0,0 +1,22 @@ +# Coding Standards (Phase 1) + +## 語言與標準 +- C++98 為主要標準(舊代碼相容)。 +- 禁止使用危險 API:偏好安全封裝;必要時以註解說明風險與保護措施。 + +## 格式(由 `.clang-format` 強制) +- BasedOnStyle: LLVM,IndentWidth: 4,ColumnLimit: 100。 +- 以 pre-commit 與 CI 確保格式一致性。 + +## 一般規範 +- 生命週期:new/delete 成對,避免裸指標長期持有;儘量早釋放資源。 +- 控制流:避免過深巢狀;優先早回傳以簡化邏輯。 +- 註解:描述「為何」而非「做什麼」。 +- 介面:標頭檔最小公開;避免不必要的 include,採前置宣告以降低耦合。 + +## Tidy 規則(Phase 1) +- 啟用:`clang-analyzer-*` 與精選 `bugprone-*`,聚焦重大缺陷(洩漏、未定義行為)。 +- 停用:`modernize-*`、`readability-*`(後續階段再逐步導入)。 + +## 例外處理 +- 舊代碼可逐步改善;若需暫時抑制特定告警,請附上註解與追蹤項(TODO/issue)。 diff --git a/docs/linter-setup.md b/docs/linter-setup.md index e69de29..4984343 100644 --- a/docs/linter-setup.md +++ b/docs/linter-setup.md @@ -0,0 +1,39 @@ +# Linter Setup (Phase 1) + +本文件說明如何在本地與 CI 環境執行 Phase 1 的 linter 自動化。 + +## 需求 +- clang-tidy, clang-format +- CMake 3.10+ + +## 本地流程 +1) 安裝工具 + - Ubuntu/Debian: `sudo apt install clang-tidy clang-format cmake` + - macOS: `brew install llvm cmake` + +2) 產出 compile_commands.json(建議,提高 tidy 準確度) + - `mkdir -p build && cd build` + - `cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON` + - `cd -` + +3) 執行 linter + - `./scripts/run-linter.sh` + - 若偵測到 `build/compile_commands.json`,腳本會自動以 `-p build` 方式執行 clang-tidy。 + +4) 安裝 Git hooks(建議) + - `./scripts/setup-hooks.sh` + - pre-commit 會: + - 對 staged C/C++ 檔執行 clang-format 並重新加入索引;若有改動會中止一次提交,請重新檢視並再次提交。 + - 以建議模式執行 clang-tidy(不阻擋提交)。 + +## CI 流程(Azure Pipelines) +- 安裝工具 → `clang-format --dry-run --Werror` → CMake 匯出 `compile_commands.json` → 以 `-p build` 跑 clang-tidy(建議模式)→ Build → Run tests。 +- Pull Request Job 只分析變更檔案,並以建議模式跑 clang-tidy。 + +## Phase 1 政策 +- 格式:必須符合 `.clang-format`,否則 CI 失敗、pre-commit 會自動修正。 +- Tidy:使用 `.clang-tidy` 的安全規則集合,僅回報警告,不使 CI 失敗。 + +## 後續升級(概述) +- Phase 2:對關鍵規則(記憶體/資源/未定義行為)提升為失敗門檻。 +- Phase 3:擴大規則(readability/modernize),對 PR 變更檔強制。 diff --git a/scripts/run-linter.sh b/scripts/run-linter.sh index 7094f1e..e6f9cf7 100755 --- a/scripts/run-linter.sh +++ b/scripts/run-linter.sh @@ -12,7 +12,7 @@ if ! command -v clang-tidy &> /dev/null; then fi # 尋找所有 C++ 檔案 -CPP_FILES=$(find src tests -name "*.cpp" -o -name "*.h" 2>/dev/null) +CPP_FILES=$(find src tests -type f \( -name "*.cpp" -o -name "*.cxx" -o -name "*.cc" -o -name "*.c++" -o -name "*.h" -o -name "*.hpp" -o -name "*.hxx" \) 2>/dev/null) if [ -z "$CPP_FILES" ]; then echo "No C++ files found." @@ -23,7 +23,17 @@ echo "Found files:" echo "$CPP_FILES" echo -# 執行 clang-tidy -echo "$CPP_FILES" | xargs clang-tidy --config-file=.clang-tidy +# 若存在 compile_commands.json,則使用 -p 指向 build 目錄以提高精準度 +TIDY_CMD_BASE=(clang-tidy --config-file=.clang-tidy) +if [ -f "build/compile_commands.json" ]; then + echo "Detected build/compile_commands.json; using -p build" + TIDY_CMD_BASE+=( -p build ) +else + echo "No compile_commands.json detected. For better results, run:" + echo " mkdir -p build && cd build && cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON && cd -" +fi -echo "Linter check completed." \ No newline at end of file +# 執行 clang-tidy(保持 Phase 1:僅回報,不讓流程失敗) +echo "$CPP_FILES" | xargs -r "${TIDY_CMD_BASE[@]}" + +echo "Linter check completed." diff --git a/scripts/setup-hooks.sh b/scripts/setup-hooks.sh index e69de29..8c10e3f 100644 --- a/scripts/setup-hooks.sh +++ b/scripts/setup-hooks.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# 安裝 git hooks(pre-commit)以執行 clang-format 與可選的 clang-tidy 提示 + +set -euo pipefail + +HOOKS_DIR=".git/hooks" +PRE_COMMIT="$HOOKS_DIR/pre-commit" + +if [ ! -d "$HOOKS_DIR" ]; then + echo "This script must be run inside a Git repository." + exit 1 +fi + +cat > "$PRE_COMMIT" <<'EOF' +#!/bin/bash +set -euo pipefail + +echo "[pre-commit] Checking/formatting C++ sources..." + +# Collect staged C/C++ files +STAGED=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(cpp|cxx|cc|c\+\+|h|hpp|hxx)$' || true) + +if [ -z "$STAGED" ]; then + echo "[pre-commit] No C/C++ files staged." + exit 0 +fi + +# 1) clang-format in-place, then re-stage +if command -v clang-format >/dev/null 2>&1; then + echo "$STAGED" | xargs -r clang-format -i + echo "$STAGED" | xargs -r git add +else + echo "[pre-commit] clang-format not found; skipping formatting." >&2 +fi + +# If formatting changed files, fail once to let user review +CHANGES=$(git diff --name-only --diff-filter=ACMR | grep -E '\\.(cpp|cxx|cc|c\\+\\+|h|hpp|hxx)$' || true) +if [ -n "$CHANGES" ]; then + echo "[pre-commit] Formatting applied to:" + echo "$CHANGES" + echo "[pre-commit] Please review changes and re-commit." + exit 1 +fi + +# 2) Optional clang-tidy advisory (Phase 1: do not block commits) +if command -v clang-tidy >/dev/null 2>&1; then + if [ -f build/compile_commands.json ]; then + echo "[pre-commit] Running clang-tidy (advisory)..." + echo "$STAGED" | xargs -r clang-tidy -p build --config-file=.clang-tidy || true + else + echo "[pre-commit] No build/compile_commands.json; skip clang-tidy. Run CMake with -DCMAKE_EXPORT_COMPILE_COMMANDS=ON." + fi +fi + +exit 0 +EOF + +chmod +x "$PRE_COMMIT" +echo "Installed pre-commit hook to $PRE_COMMIT" diff --git a/tests/test_main.cpp b/tests/test_main.cpp index e69de29..e954cd3 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -0,0 +1,21 @@ +#include "utils.h" +#include +#include + +int main() { + std::vector v; + v.push_back(1); + v.push_back(2); + v.push_back(3); + int sum = calculateSum(v); + if (sum != 6) { + std::cerr << "Test failed: expected 6, got " << sum << std::endl; + return 1; + } + if (!isPositive(1) || isPositive(-1)) { + std::cerr << "Test failed: isPositive check" << std::endl; + return 1; + } + std::cout << "All basic tests passed" << std::endl; + return 0; +}