diff --git a/README.md b/README.md index cd4b8bf..8162b8b 100644 --- a/README.md +++ b/README.md @@ -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) - -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: @@ -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`: @@ -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 @@ -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 @@ -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 @@ -134,7 +120,7 @@ int main() {for (;;) break; printf("Hello world!\n");return 0;} ^ ``` -### clang-tidy Example +### clang-tidy Output ```bash clang-tidy...............................................................Failed @@ -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). diff --git a/cpp_linter_hooks/clang_format.py b/cpp_linter_hooks/clang_format.py index cd0eb91..de3319f 100644 --- a/cpp_linter_hooks/clang_format.py +++ b/cpp_linter_hooks/clang_format.py @@ -1,4 +1,5 @@ import subprocess +import sys from argparse import ArgumentParser from typing import Tuple @@ -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__": diff --git a/pyproject.toml b/pyproject.toml index fc9ac2d..8ad28ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/testing/pre-commit-config-verbose.yaml b/testing/pre-commit-config-verbose.yaml new file mode 100644 index 0000000..adac182 --- /dev/null +++ b/testing/pre-commit-config-verbose.yaml @@ -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 diff --git a/testing/run.sh b/testing/run.sh index 34725f1..b176b06 100644 --- a/testing/run.sh +++ b/testing/run.sh @@ -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." diff --git a/tests/test_clang_format.py b/tests/test_clang_format.py index 38a8fe0..e7ed8ee 100644 --- a/tests/test_clang_format.py +++ b/tests/test_clang_format.py @@ -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