Skip to content

clang-format hook support --verbose output #79

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jul 4, 2025
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
101 changes: 64 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,26 @@
[![codecov](https://codecov.io/gh/cpp-linter/cpp-linter-hooks/branch/main/graph/badge.svg?token=L74Z3HZ4Y5)](https://codecov.io/gh/cpp-linter/cpp-linter-hooks)
[![Test](https://github.com/cpp-linter/cpp-linter-hooks/actions/workflows/test.yml/badge.svg)](https://github.com/cpp-linter/cpp-linter-hooks/actions/workflows/test.yml)
[![CodeQL](https://github.com/cpp-linter/cpp-linter-hooks/actions/workflows/codeql.yml/badge.svg)](https://github.com/cpp-linter/cpp-linter-hooks/actions/workflows/codeql.yml)
<!-- [![PyPI - Downloads](https://img.shields.io/pypi/dw/cpp-linter-hooks)](https://pypi.org/project/cpp-linter-hooks/) -->

cpp-linter-hooks is a [pre-commit](https://pre-commit.com/) hook that uses `clang-format` and `clang-tidy` to format C/C++ code.
A powerful [pre-commit](https://pre-commit.com/) hook for auto-formatting and linting C/C++ code with `clang-format` and `clang-tidy`.

> [!NOTE]
> This hook automatically downloads specific versions of `clang-format` or `clang-tidy` [static-binaries](https://github.com/cpp-linter/clang-tools-static-binaries) and installs them on your system.
## Table of Contents

## Usage
- [Quick Start](#quick-start)
- [Custom Configuration Files](#custom-configuration-files)
- [Custom Clang Tool Version](#custom-clang-tool-version)
- [Output](#output)
- [clang-format Output](#clang-format-output)
- [clang-tidy Output](#clang-tidy-output)
- [Troubleshooting](#troubleshooting)
- [Performance Optimization](#performance-optimization)
- [Verbose Output](#verbose-output)
- [Contributing](#contributing)
- [License](#license)

To use cpp-linter-hooks, add the following configuration to your `.pre-commit-config.yaml`:
## Quick Start

### Basic Configuration
Add this configuration to your `.pre-commit-config.yaml` file:

```yaml
repos:
Expand All @@ -29,7 +37,7 @@ repos:
args: [--checks='boost-*,bugprone-*,performance-*,readability-*,portability-*,modernize-*,clang-analyzer-*,cppcoreguidelines-*']
```

### Custom Configuration
### Custom Configuration Files

To use custom configurations like `.clang-format` and `.clang-tidy`:

Expand All @@ -44,6 +52,8 @@ repos:
args: [--checks=.clang-tidy] # Loads checks from .clang-tidy file
```

### Custom Clang Tool Version

To use specific versions of [clang-tools](https://github.com/cpp-linter/clang-tools-pip?tab=readme-ov-file#supported-versions):

```yaml
Expand All @@ -57,33 +67,9 @@ repos:
args: [--checks=.clang-tidy, --version=18] # Specifies version
```

> [!IMPORTANT]
> If your `pre-commit` runs longer than expected, it is highly recommended to add `files` in `.pre-commit-config.yaml` to limit the scope of the hook. This helps improve performance by reducing the number of files being checked and avoids unnecessary processing. Here's an example configuration:


```yaml
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v0.8.1
hooks:
- id: clang-format
args: [--style=file, --version=18]
files: ^(src|include)/.*\.(cpp|cc|cxx|h|hpp)$ # Limits to specific dirs and file types
- id: clang-tidy
args: [--checks=.clang-tidy, --version=18]
files: ^(src|include)/.*\.(cpp|cc|cxx|h|hpp)$
```

Alternatively, if you want to run the hooks manually on only the changed files, you can use the following command:

```bash
pre-commit run --files $(git diff --name-only)
```

This approach ensures that only modified files are checked, further speeding up the linting process during development.

## Output

### clang-format Example
### clang-format Output

```bash
clang-format.............................................................Failed
Expand All @@ -106,8 +92,8 @@ Here’s a sample diff showing the formatting applied:
+ return 0;
+}
```

Use `--dry-run` in `args` of `clang-format` to print instead of changing the format, e.g.:
> [!NOTE]
> Use `--dry-run` in `args` of `clang-format` to print instead of changing the format, e.g.:

```bash
clang-format.............................................................Failed
Expand All @@ -134,7 +120,7 @@ int main() {for (;;) break; printf("Hello world!\n");return 0;}
^
```

### clang-tidy Example
### clang-tidy Output

```bash
clang-tidy...............................................................Failed
Expand All @@ -151,10 +137,51 @@ Use -header-filter=.* to display errors from all non-system headers. Use -system

```

## Troubleshooting

### Performance Optimization

> [!WARNING]
> If your `pre-commit` runs longer than expected, it is highly recommended to add `files` in `.pre-commit-config.yaml` to limit the scope of the hook. This helps improve performance by reducing the number of files being checked and avoids unnecessary processing. Here's an example configuration:

```yaml
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v0.8.1
hooks:
- id: clang-format
args: [--style=file, --version=18]
files: ^(src|include)/.*\.(cpp|cc|cxx|h|hpp)$ # Limits to specific dirs and file types
- id: clang-tidy
args: [--checks=.clang-tidy, --version=18]
files: ^(src|include)/.*\.(cpp|cc|cxx|h|hpp)$
```

Alternatively, if you want to run the hooks manually on only the changed files, you can use the following command:

```bash
pre-commit run --files $(git diff --name-only)
```

This approach ensures that only modified files are checked, further speeding up the linting process during development.

### Verbose Output

> [!NOTE]
> Use `-v` or `--verbose` in `args` of `clang-format` to show the list of processed files e.g.:

```yaml
repos:
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v0.8.1
hooks:
- id: clang-format
args: [--style=file, --version=18, --verbose] # Add -v or --verbose for detailed output
```

## Contributing

We welcome contributions! Whether it's fixing issues, suggesting improvements, or submitting pull requests, your support is greatly appreciated.

## License

cpp-linter-hooks is licensed under the [MIT License](LICENSE)
This project is licensed under the [MIT License](LICENSE).
58 changes: 46 additions & 12 deletions cpp_linter_hooks/clang_format.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import subprocess
import sys
from argparse import ArgumentParser
from typing import Tuple

Expand All @@ -7,34 +8,67 @@

parser = ArgumentParser()
parser.add_argument("--version", default=DEFAULT_CLANG_VERSION)
parser.add_argument(
"-v", "--verbose", action="store_true", help="Enable verbose output"
)


def run_clang_format(args=None) -> Tuple[int, str]:
hook_args, other_args = parser.parse_known_args(args)
path = ensure_installed("clang-format", hook_args.version)
command = [str(path), "-i"]

# Add verbose flag if requested
if hook_args.verbose:
command.append("--verbose")

command.extend(other_args)

retval = 0
output = ""
try:
# Run the clang-format command with captured output
sp = subprocess.run(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)

# Combine stdout and stderr for complete output
output = (sp.stdout or "") + (sp.stderr or "")

# Handle special case for dry-run mode
if "--dry-run" in command:
sp = subprocess.run(command, stdout=subprocess.PIPE, encoding="utf-8")
retval = -1 # Not a fail just identify it's a dry-run.
output = sp.stdout
retval = -1 # Special code to identify dry-run mode
else:
retval = subprocess.run(command, stdout=subprocess.PIPE).returncode
retval = sp.returncode

# Print verbose information if requested
if hook_args.verbose:
_print_verbose_info(command, retval, output)

return retval, output
except FileNotFoundError as stderr:
retval = 1
return retval, str(stderr)

except FileNotFoundError as e:
return 1, str(e)


def _print_verbose_info(command: list, retval: int, output: str) -> None:
"""Print verbose debugging information to stderr."""
print(f"Command executed: {' '.join(command)}", file=sys.stderr)
print(f"Exit code: {retval}", file=sys.stderr)
if output.strip():
print(f"Output: {output}", file=sys.stderr)


def main() -> int:
retval, output = run_clang_format()
if retval != 0:
retval, output = run_clang_format() # pragma: no cover

# Print output for errors, but not for dry-run mode
if retval != 0 and retval != -1 and output.strip(): # pragma: no cover
print(output)
return retval

# Convert dry-run special code to success
return 0 if retval == -1 else retval # pragma: no cover


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ requires-python = ">=3.9"

[project]
name = "cpp_linter_hooks"
description = "Automatically check c/c++ code with clang-format and clang-tidy"
description = "Automatically formats and lints C/C++ code using clang-format and clang-tidy"
readme = "README.md"
keywords = ["clang", "clang-format", "clang-tidy", "pre-commit", "pre-commit-hooks"]
license = "MIT"
Expand Down
11 changes: 11 additions & 0 deletions testing/pre-commit-config-verbose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
repos:
- repo: .
rev: HEAD
hooks:
- id: clang-format
args: [--style=file, --version=16, --verbose] # test with verbose output
- repo: .
rev: HEAD
hooks:
- id: clang-format
args: [--style=file, --version=16, -v] # test with verbose output
34 changes: 26 additions & 8 deletions testing/run.sh
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
rm -f result.txt
#!/bin/bash
echo "==========================="
echo "Test pre-commit-config.yaml"
echo "==========================="
pre-commit clean
pre-commit run -c testing/pre-commit-config.yaml --files testing/main.c | tee -a result.txt || true
git restore testing/main.c

for config in testing/pre-commit-config.yaml testing/pre-commit-config-version.yaml; do
pre-commit clean
pre-commit run -c $config --files testing/main.c | tee -a result.txt || true
git restore testing/main.c
done
echo "===================================="
echo "Test pre-commit-config-version.yaml"
echo "===================================="
pre-commit clean
pre-commit run -c testing/pre-commit-config-version.yaml --files testing/main.c | tee -a result.txt || true
git restore testing/main.c

echo "===================================="
echo "Test pre-commit-config-verbose.yaml"
echo "===================================="
pre-commit clean
pre-commit run -c testing/pre-commit-config-verbose.yaml --files testing/main.c | tee -a result.txt || true
git restore testing/main.c

echo "=================================================================================="
echo "print result.txt"
cat result.txt
echo "=================================================================================="

failed_cases=`grep -c "Failed" result.txt`

echo $failed_cases " cases failed."

if [ $failed_cases -eq 9 ]; then
if [ $failed_cases -eq 10 ]; then
echo "=============================="
echo "Test cpp-linter-hooks success."
echo "=============================="
exit 0
rm result.txt
exit 0
else
echo "============================="
echo "Test cpp-linter-hooks failed."
Expand Down
32 changes: 32 additions & 0 deletions tests/test_clang_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,35 @@ def test_run_clang_format_dry_run(args, expected_retval, tmp_path):
test_file = tmp_path / "main.c"
ret, _ = run_clang_format(["--dry-run", str(test_file)])
assert ret == -1 # Dry run should not fail


def test_run_clang_format_verbose(tmp_path):
"""Test that verbose option works and provides detailed output."""
# copy test file to tmp_path to prevent modifying repo data
test_file = tmp_path / "main.c"
test_file.write_bytes(Path("testing/main.c").read_bytes())

# Test with verbose flag
ret, _ = run_clang_format(["--verbose", "--style=Google", str(test_file)])

# Should succeed
assert ret == 0
# Should have verbose output (will be printed to stderr, not returned)
# The function should still return successfully
assert test_file.read_text() == Path("testing/good.c").read_text()


def test_run_clang_format_verbose_error(tmp_path):
"""Test that verbose option provides useful error information."""
test_file = tmp_path / "main.c"
test_file.write_bytes(Path("testing/main.c").read_bytes())

# Test with verbose flag and invalid style
ret, output = run_clang_format(
["--verbose", "--style=InvalidStyle", str(test_file)]
)

# Should fail
assert ret != 0
# Should have error message in output
assert "Invalid value for -style" in output
Loading