Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
ci:
autofix_prs: true

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
Expand All @@ -10,7 +13,7 @@ repos:
- id: check-toml
- id: requirements-txt-fixer
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.2
rev: v0.15.8
hooks:
# Run the linter.
- id: ruff-check
Expand Down
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ A pre-commit hook that automatically formats and lints your C/C++ code using `cl
- [Quick Start](#quick-start)
- [Custom Configuration Files](#custom-configuration-files)
- [Custom Clang Tool Version](#custom-clang-tool-version)
- [Compilation Database (CMake/Meson Projects)](#compilation-database-cmakemeson-projects)
- [Output](#output)
- [clang-format Output](#clang-format-output)
- [clang-tidy Output](#clang-tidy-output)
Expand Down Expand Up @@ -72,6 +73,52 @@ repos:
args: [--checks=.clang-tidy, --version=21] # Specifies version
```

### Compilation Database (CMake/Meson Projects)

For CMake or Meson projects, clang-tidy works best with a `compile_commands.json`
file that records the exact compiler flags used for each file. Without it, clang-tidy
may report false positives from missing include paths or wrong compiler flags.

The hook auto-detects `compile_commands.json` in common build directories (`build/`,
`out/`, `cmake-build-debug/`, `_build/`) and passes `-p <dir>` to clang-tidy
automatically — no configuration needed for most projects:

```yaml
repos:
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.2.0
hooks:
- id: clang-tidy
args: [--checks=.clang-tidy]
# Auto-detects ./build/compile_commands.json if present
```

To specify the build directory explicitly:

```yaml
- id: clang-tidy
args: [--compile-commands=build, --checks=.clang-tidy]
```

To disable auto-detection (e.g. in a monorepo where auto-detect might pick the wrong database):

```yaml
- id: clang-tidy
args: [--no-compile-commands, --checks=.clang-tidy]
```

To see which `compile_commands.json` the hook is using, add `-v`:

```yaml
- id: clang-tidy
args: [--compile-commands=build, -v, --checks=.clang-tidy]
```

> [!NOTE]
> Generate `compile_commands.json` with CMake using `cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -Bbuild .`
> or add `set(CMAKE_EXPORT_COMPILE_COMMANDS ON)` to your `CMakeLists.txt`.
> `--compile-commands` takes the **directory** containing `compile_commands.json`, not the file path itself.

## Output

### clang-format Output
Expand Down Expand Up @@ -172,15 +219,19 @@ This approach ensures that only modified files are checked, further speeding up
### Verbose Output

> [!NOTE]
> Use `-v` or `--verbose` in `args` of `clang-format` to show the list of processed files e.g.:
> Use `-v` or `--verbose` in `args` to enable verbose output.
> For `clang-format`, it shows the list of processed files.
> For `clang-tidy`, it prints which `compile_commands.json` is being used (when auto-detected or explicitly set).

```yaml
repos:
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.2.0
hooks:
- id: clang-format
args: [--style=file, --version=21, --verbose] # Add -v or --verbose for detailed output
args: [--style=file, --version=21, --verbose] # Shows processed files
- id: clang-tidy
args: [--checks=.clang-tidy, --verbose] # Shows which compile_commands.json is used
```

## FAQ
Expand All @@ -196,6 +247,7 @@ repos:
| Supports passing format style string | ✅ via `--style` | ❌ |
| Verbose output | ✅ via `--verbose` | ❌ |
| Dry-run mode | ✅ via `--dry-run` | ❌ |
| Compilation database support | ✅ auto-detect or `--compile-commands` | ❌ |


<!-- > [!TIP]
Expand Down
90 changes: 76 additions & 14 deletions cpp_linter_hooks/clang_tidy.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,96 @@
import subprocess
import sys
from argparse import ArgumentParser
from typing import Tuple
from pathlib import Path
from typing import Optional, Tuple

from cpp_linter_hooks.util import resolve_install, DEFAULT_CLANG_TIDY_VERSION

COMPILE_DB_SEARCH_DIRS = ["build", "out", "cmake-build-debug", "_build"]

parser = ArgumentParser()
parser.add_argument("--version", default=DEFAULT_CLANG_TIDY_VERSION)
parser.add_argument("--compile-commands", default=None, dest="compile_commands")
parser.add_argument(
"--no-compile-commands", action="store_true", dest="no_compile_commands"
)
parser.add_argument("-v", "--verbose", action="store_true")


def run_clang_tidy(args=None) -> Tuple[int, str]:
hook_args, other_args = parser.parse_known_args(args)
if hook_args.version:
resolve_install("clang-tidy", hook_args.version)
command = ["clang-tidy"] + other_args
def _find_compile_commands() -> Optional[str]:
for d in COMPILE_DB_SEARCH_DIRS:
if (Path(d) / "compile_commands.json").exists():
return d
return None


def _resolve_compile_db(
hook_args, other_args
) -> Tuple[Optional[str], Optional[Tuple[int, str]]]:
"""Resolve the compile_commands.json directory to pass as -p to clang-tidy.

Returns (db_path, None) on success or (None, (retval, message)) on error.
"""
if hook_args.no_compile_commands:
return None, None

# Covers both "-p ./build" (two tokens) and "-p=./build" (one token)
has_p = any(a == "-p" or a.startswith("-p=") for a in other_args)

if hook_args.compile_commands:
if has_p:
print(
"Warning: --compile-commands ignored; -p already in args",
file=sys.stderr,
)
return None, None
p = Path(hook_args.compile_commands)
if not p.is_dir() or not (p / "compile_commands.json").exists():
return None, (
1,
f"--compile-commands: no compile_commands.json"
f" in '{hook_args.compile_commands}'",
)
return hook_args.compile_commands, None

if not has_p:
return _find_compile_commands(), None

return None, None

retval = 0
output = ""

def _exec_clang_tidy(command) -> Tuple[int, str]:
"""Run clang-tidy and return (retval, output)."""
try:
sp = subprocess.run(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8"
)
retval = sp.returncode
output = (sp.stdout or "") + (sp.stderr or "")
if "warning:" in output or "error:" in output:
retval = 1
retval = (
1 if sp.returncode != 0 or "warning:" in output or "error:" in output else 0
)
return retval, output
except FileNotFoundError as stderr:
retval = 1
return retval, str(stderr)
except FileNotFoundError as e:
return 1, str(e)


def run_clang_tidy(args=None) -> Tuple[int, str]:
hook_args, other_args = parser.parse_known_args(args)
if hook_args.version:
resolve_install("clang-tidy", hook_args.version)

compile_db_path, error = _resolve_compile_db(hook_args, other_args)
if error is not None:
return error

if compile_db_path:
if hook_args.verbose:
print(
f"Using compile_commands.json from: {compile_db_path}", file=sys.stderr
)
other_args = ["-p", compile_db_path] + other_args

return _exec_clang_tidy(["clang-tidy"] + other_args)


def main() -> int:
Expand Down
Loading
Loading