Skip to content
Open
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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,28 @@ Use -header-filter=.* to display errors from all non-system headers. Use -system

```

> [!NOTE]
> Add `--fix` to `args` to automatically apply clang-tidy fixes in place (equivalent to
> passing `-fix` to clang-tidy directly). This is **opt-in** and **not the default** because
> auto-fixing can modify source files in unexpected ways. A valid `compile_commands.json` is
> strongly recommended when using `--fix`.
>
> For cases where compiler errors exist alongside style issues, pass `-fix-errors` directly
> in `args` instead (clang-tidy native flag).

```yaml
repos:
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.2.0
hooks:
- id: clang-tidy
args: [--checks=.clang-tidy, --fix]
```

> [!WARNING]
> When `--fix` (or `-fix-errors`) is active, parallel execution via `--jobs`/`-j` is
> automatically disabled to prevent concurrent writes to the same header file.

## Troubleshooting

### Performance Optimization
Expand Down Expand Up @@ -266,6 +288,7 @@ repos:
| Supports passing format style string | ✅ via `--style` | ❌ |
| Verbose output | ✅ via `--verbose` | ❌ |
| Dry-run mode | ✅ via `--dry-run` | ❌ |
| Auto-fix mode | ✅ via `--fix` (clang-tidy only) | ❌ |
| Compilation database support | ✅ auto-detect or `--compile-commands` | ❌ |


Expand Down
15 changes: 11 additions & 4 deletions cpp_linter_hooks/clang_tidy.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def _positive_int(value: str) -> int:
)
parser.add_argument("-j", "--jobs", type=_positive_int, default=1)
parser.add_argument("-v", "--verbose", action="store_true")
parser.add_argument("--fix", action="store_true", help="Apply fixes in place (-fix)")


def _find_compile_commands() -> Optional[str]:
Expand Down Expand Up @@ -154,11 +155,17 @@ def run_clang_tidy(args=None) -> Tuple[int, str]:

clang_tidy_args, source_files = _split_source_files(other_args)

if hook_args.fix:
clang_tidy_args.append("-fix")

# Parallel execution is unsafe when arguments include flags that write to a
# shared output path (e.g., --export-fixes fixes.yaml). In that case, force
# serial execution to avoid concurrent writes/overwrites.
# shared output path (e.g., --export-fixes fixes.yaml) or that apply in-place
# fixes (-fix, -fix-errors), since multiple clang-tidy processes may attempt
# to modify the same header file concurrently.
unsafe_parallel = any(
arg == "--export-fixes" or arg.startswith("--export-fixes=")
arg == "--export-fixes"
or arg.startswith("--export-fixes=")
or arg in ("-fix", "-fix-errors")
for arg in clang_tidy_args
)

Expand All @@ -167,7 +174,7 @@ def run_clang_tidy(args=None) -> Tuple[int, str]:
["clang-tidy"] + clang_tidy_args, source_files, hook_args.jobs
)

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


def main() -> int:
Expand Down
47 changes: 47 additions & 0 deletions tests/test_clang_tidy.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,50 @@ def test_jobs_with_export_fixes_forces_serial_execution():
"b.cpp",
]
)


def test_fix_flag_appends_fix_to_command():
with (
patch(
"cpp_linter_hooks.clang_tidy._exec_clang_tidy", return_value=(0, "")
) as mock_exec,
patch("cpp_linter_hooks.clang_tidy.resolve_install"),
):
run_clang_tidy(["--fix", "-p", "./build", "dummy.cpp"])

cmd = mock_exec.call_args[0][0]
assert "-fix" in cmd


def test_fix_flag_forces_serial_execution():
with (
patch(
"cpp_linter_hooks.clang_tidy._exec_clang_tidy", return_value=(0, "")
) as mock_exec,
patch("cpp_linter_hooks.clang_tidy.resolve_install"),
):
run_clang_tidy(["--fix", "--jobs=4", "-p", "./build", "a.cpp", "b.cpp"])

mock_exec.assert_called_once()
cmd = mock_exec.call_args[0][0]
assert "-fix" in cmd
assert "a.cpp" in cmd
assert "b.cpp" in cmd


def test_fix_errors_in_args_forces_serial_execution():
with (
patch(
"cpp_linter_hooks.clang_tidy._exec_clang_tidy", return_value=(0, "")
) as mock_exec,
patch("cpp_linter_hooks.clang_tidy.resolve_install"),
):
run_clang_tidy(
["--jobs=4", "-p", "./build", "-fix-errors", "a.cpp", "b.cpp"]
)

mock_exec.assert_called_once()
cmd = mock_exec.call_args[0][0]
assert "-fix-errors" in cmd
assert "a.cpp" in cmd
assert "b.cpp" in cmd
Loading