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
This commit is contained in:
2025-09-09 22:52:34 +08:00
parent d5350c35a8
commit f55d10ee07
9 changed files with 296 additions and 81 deletions

11
.clang-format Normal file
View File

@@ -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

31
.clang-tidy Normal file
View File

@@ -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'

120
README.md
View File

@@ -1,94 +1,94 @@
# C++ Linter Template # C++ Linter Template
這是一個展示如何在 C++ 專案中整合 linter 工具的模板專案 這是一個符合最佳實務、可漸進式導入的 C++ linter 自動化模板。現為 Phase 1格式嚴格、靜態分析建議不擋流程
## 目 ## 目
- 展示 clang-tidy 在舊 C++ 代碼中的應用 - 對舊 C++ 專案導入 `clang-tidy``clang-format`
- 提供漸進式導入 linter 的範例 - 提供「先格式、後強化」的漸進策略
- Azure DevOps CI/CD 整合範例 - 內建 Azure DevOps CI/CD 串接與 PR 差異分析
## 快速開始 ## 快速開始Phase 1
### 1. 本地開發環境設定 ### 1) 安裝工具
```bash ```bash
# 安裝相依套件 (Ubuntu/Debian) # Ubuntu/Debian
sudo apt install clang-tidy clang-format cmake build-essential sudo apt install clang-tidy clang-format cmake build-essential
# 安裝相依套件 (macOS) # macOS
brew install llvm cmake brew install llvm cmake
# 克隆專案
git clone <your-repo-url>
cd cpp-linter-template
``` ```
### 2. 執行 Linter ### 2) 產出 compile_commands.json建議
```bash ```bash
# 使用提供的腳本 mkdir -p build && cd build
cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cd -
```
### 3) 執行 Linter
```bash
# 使用腳本(會自動偵測 build/compile_commands.json
./scripts/run-linter.sh ./scripts/run-linter.sh
# 或手動執行
clang-tidy src/*.cpp src/*.h tests/*.cpp --config-file=.clang-tidy
``` ```
### 3. 建構專案 ### 4) 安裝 Git hooks建議
```bash ```bash
mkdir build && cd build ./scripts/setup-hooks.sh
cmake .. ```
make - 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 ./main
``` ```
## Linter 配置說明 ## Phase 1 政策(建議 → 強制的起點)
### 當前採用階段一設定: - 格式:以 `.clang-format` 為準CI 以 `--dry-run --Werror` 強制。
- 只檢查最關鍵的錯誤 - 靜態分析:`.clang-tidy` 啟用 `clang-analyzer-*` 與精選 `bugprone-*`;僅回報警告,不讓 CI 失敗。
- 不會因 linter 警告而中斷 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 - `.clang-tidy`Phase 1 強調重大缺陷檢查(例如 analyzer、部分 bugprone
2. 設定 Pipeline 使用 azure-pipelines.yml - `.clang-format`:基於 LLVM 風格IndentWidth=4, ColumnLimit=100
3. 建立 Pull Request 時會自動執行 linter
## 自定義配置 ## 文件
編輯 .clang-tidy 檔案來調整檢查規則: - `docs/linter-setup.md`:本地與 CI 設定流程
```yaml - `docs/coding-standards.md`編碼與格式規範Phase 1
# 增加更多檢查
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 設定較為寬鬆,適合逐步導入
- 可以根據團隊需求調整檢查規則
## 專案結構 ## 專案結構
cpp-linter-template/ cpp-linter-template/
├── .clang-tidy # linter 配置檔 ├── .clang-tidy # linter 配置檔Phase 1
├── .clang-format # 格式化配置 ├── .clang-format # 格式化配置
├── azure-pipelines.yml # Azure DevOps CI/CD 配置 ├── azure-pipelines.yml # Azure DevOps CI/CD 配置
├── CMakeLists.txt # 建構配置 ├── CMakeLists.txt # 建構配置C++98
├── README.md # 專案說明 ├── README.md # 專案說明(本檔)
├── docs/ # 文件目錄 ├── docs/ # 文件目錄
│ ├── linter-setup.md # Linter 設定指南 │ ├── linter-setup.md # Linter 設定指南
│ └── coding-standards.md # 編碼標準 │ └── coding-standards.md # 編碼標準Phase 1
├── src/ # 原始碼 ├── src/ # 原始碼
│ ├── main.cpp # 主程式(含各種測試案例 │ ├── main.cpp # 主程式(含測試片段
│ ├── utils.cpp # 工具函數 │ ├── utils.cpp # 工具函數
│ └── utils.h # 標頭檔 │ └── utils.h # 標頭檔
├── tests/ # 測試程式碼 ├── tests/ # 測試程式碼
│ └── test_main.cpp # 簡單測試 │ └── test_main.cpp # 最小測試(無框架)
└── scripts/ # 輔助腳本 └── scripts/ # 輔助腳本
├── run-linter.sh # 本地執行 linter ├── run-linter.sh # 本地執行 linter
└── setup-hooks.sh # 設定 git hooks └── setup-hooks.sh # 安裝 pre-commit hooks
## 後續階段(預覽)
- Phase 2對關鍵 tidy 規則轉為失敗門檻(資源管理/未定義行為)。
- Phase 3引入 readability/modernize 並在 PR 變更檔上強制。
- Phase 4全域強制警告視為錯誤需技術債清理完成

View File

@@ -32,6 +32,7 @@ stages:
sudo apt-get update sudo apt-get update
sudo apt-get install -y clang-tidy clang-format cmake build-essential sudo apt-get install -y clang-tidy clang-format cmake build-essential
clang-tidy --version clang-tidy --version
cmake --version
- task: Bash@3 - task: Bash@3
displayName: 'Check Code Format' displayName: 'Check Code Format'
@@ -42,14 +43,26 @@ stages:
find src tests -name "*.cpp" -o -name "*.h" | xargs clang-format --dry-run --Werror find src tests -name "*.cpp" -o -name "*.h" | xargs clang-format --dry-run --Werror
- task: Bash@3 - task: Bash@3
displayName: 'Run clang-tidy on all files' displayName: 'Configure build (export compile_commands)'
inputs: inputs:
targetType: 'inline' targetType: 'inline'
script: | script: |
echo "Running clang-tidy..." echo "Configuring CMake to export compile_commands.json..."
find src tests -name "*.cpp" -o -name "*.h" | xargs clang-tidy --config-file=.clang-tidy 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." echo "Linter completed. Review warnings above."
- task: Bash@3 - task: Bash@3
@@ -57,7 +70,6 @@ stages:
inputs: inputs:
targetType: 'inline' targetType: 'inline'
script: | script: |
mkdir build
cd build cd build
cmake .. -DCMAKE_BUILD_TYPE=${{ variables.buildConfiguration }} cmake .. -DCMAKE_BUILD_TYPE=${{ variables.buildConfiguration }}
make -j$(nproc) make -j$(nproc)
@@ -81,8 +93,18 @@ stages:
targetType: 'inline' targetType: 'inline'
script: | script: |
sudo apt-get update 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 - task: Bash@3
displayName: 'Analyze Changed Files Only' displayName: 'Analyze Changed Files Only'
inputs: inputs:
@@ -102,5 +124,5 @@ stages:
echo "Changed C++ files:" echo "Changed C++ files:"
echo "$CHANGED_FILES" echo "$CHANGED_FILES"
echo "Running clang-tidy on changed files..." echo "Running clang-tidy on changed files (advisory)..."
echo "$CHANGED_FILES" | xargs clang-tidy --config-file=.clang-tidy -- -std=c++98 echo "$CHANGED_FILES" | xargs -r clang-tidy -p build --config-file=.clang-tidy || true

View File

@@ -0,0 +1,22 @@
# Coding Standards (Phase 1)
## 語言與標準
- C++98 為主要標準(舊代碼相容)。
- 禁止使用危險 API偏好安全封裝必要時以註解說明風險與保護措施。
## 格式(由 `.clang-format` 強制)
- BasedOnStyle: LLVMIndentWidth: 4ColumnLimit: 100。
- 以 pre-commit 與 CI 確保格式一致性。
## 一般規範
- 生命週期new/delete 成對,避免裸指標長期持有;儘量早釋放資源。
- 控制流:避免過深巢狀;優先早回傳以簡化邏輯。
- 註解:描述「為何」而非「做什麼」。
- 介面:標頭檔最小公開;避免不必要的 include採前置宣告以降低耦合。
## Tidy 規則Phase 1
- 啟用:`clang-analyzer-*` 與精選 `bugprone-*`,聚焦重大缺陷(洩漏、未定義行為)。
- 停用:`modernize-*``readability-*`(後續階段再逐步導入)。
## 例外處理
- 舊代碼可逐步改善若需暫時抑制特定告警請附上註解與追蹤項TODO/issue

View File

@@ -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 變更檔強制。

View File

@@ -12,7 +12,7 @@ if ! command -v clang-tidy &> /dev/null; then
fi fi
# 尋找所有 C++ 檔案 # 尋找所有 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 if [ -z "$CPP_FILES" ]; then
echo "No C++ files found." echo "No C++ files found."
@@ -23,7 +23,17 @@ echo "Found files:"
echo "$CPP_FILES" echo "$CPP_FILES"
echo echo
# 執行 clang-tidy # 若存在 compile_commands.json則使用 -p 指向 build 目錄以提高精準度
echo "$CPP_FILES" | xargs clang-tidy --config-file=.clang-tidy 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
# 執行 clang-tidy保持 Phase 1僅回報不讓流程失敗
echo "$CPP_FILES" | xargs -r "${TIDY_CMD_BASE[@]}"
echo "Linter check completed." echo "Linter check completed."

View File

@@ -0,0 +1,59 @@
#!/bin/bash
# 安裝 git hookspre-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"

View File

@@ -0,0 +1,21 @@
#include "utils.h"
#include <iostream>
#include <vector>
int main() {
std::vector<int> 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;
}