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
88 changes: 41 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,77 +19,67 @@ Add this configuration to your `.pre-commit-config.yaml` file:
```yaml
repos:
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.8.0
rev: v1.8.1
hooks:
- id: clang-format
args: [--style=file]
additional_dependencies: [clang-format==21.1.8]
- id: clang-tidy
additional_dependencies: [clang-tidy==21.1.6]
```

Run `pre-commit install` once in each clone. `--style=file` loads the style from your
`.clang-format` file, and clang-tidy reads your `.clang-tidy` file by itself. The clang-tidy hook
`.clang-format` file, and clang-tidy reads your `.clang-tidy` file by itself. clang-tidy 22 and
newer run no checks without one, so add a `.clang-tidy` file that contains, for example,
`Checks: 'bugprone-*,performance-*,readability-*'`. The clang-tidy hook
reports only problems on the lines that your commit adds or changes (see
[Only changed lines](https://github.com/cpp-linter/cpp-linter-hooks#only-changed-lines)). It needs
a `compile_commands.json`, which it looks for in `build/` and a few other directories; without one
it skips clang-tidy on your machine and fails in CI (see
[Compilation database](https://github.com/cpp-linter/cpp-linter-hooks#compilation-database)).

`additional_dependencies` pins the clang-format and clang-tidy versions, and pre-commit installs
them once, with the hook (see
[Install the tool with the hook](https://github.com/cpp-linter/cpp-linter-hooks#install-the-tool-with-the-hook)).
This uses the newest clang-format and clang-tidy. To choose a version, add `--version` (see
[Clang tool version](https://github.com/cpp-linter/cpp-linter-hooks#clang-tool-version)).

## Usage

### Custom clang tool version

> [!TIP]
> The `rev` tag (e.g. `v1.8.0`) is the **project** version, not the clang tool version. Without
> `additional_dependencies` or `--version`, each hook installs the newest clang-format or
> clang-tidy wheel on PyPI the first time it runs and keeps it until pre-commit rebuilds the hook
> environment, so two machines can run different LLVM versions with the same configuration. Pin
> the version in one of the two places, not both.
### Clang tool version

Instead of `additional_dependencies`, you can give the version in `--version`. The hook then
picks and installs the wheel the first time it runs:
The `rev` tag (e.g. `v1.8.1`) is the version of these hooks, not of clang-format or clang-tidy.
Choose the clang tool version with `--version`:

```yaml
- id: clang-format
args: [--style=file, --version=21]
- id: clang-tidy
args: [--version=21]
```

- `--version=21` installs the newest 21.x wheel, and `--version=21.1.8` pins an exact release.
- Without `--version`, the hooks use the newest wheel on PyPI: pre-commit installs the newest
clang-format together with the hook, and the clang-tidy hook installs the newest clang-tidy the
first time it runs. Both are kept until pre-commit rebuilds the hook (a new `rev` or
`pre-commit clean`), so two machines can end up with different versions. Teams and CI should
set `--version`.
- `--version=21` installs the newest 21.x wheel, and `--version=21.1.8` an exact release.
clang-tidy wheels are released separately from clang-format wheels and skip some releases, so
give clang-tidy the major version.
- clang-format wheels cover LLVM 6 to 23 and clang-tidy wheels LLVM 13 to 23. For a version without
a wheel, the hook fails and lists some of the versions that exist.
- Each `--version` is installed once, into its own directory in the hook environment, and later
runs use it without contacting pypi.org. pre-commit shares the hook environment between all
your projects that use the same `rev`, so projects that ask for different versions keep their
own copy instead of reinstalling. A wheel from `additional_dependencies` that matches
`--version` is used as well.
- Only a run that finds no matching wheel installed looks the version up on pypi.org and
installs it, so that run needs network access. pre-commit runs the hook on groups of files in
parallel; one of them installs the wheel and the others wait for it.

### Install the tool with the hook

We recommend giving the version in `additional_dependencies`, as in the Quick start. pre-commit
then installs the clang tool once, when it creates the hook environment, the same way
[mirrors-clang-format](https://github.com/pre-commit/mirrors-clang-format) does. The hook uses
that wheel, so do not set `--version` as well.
- Each version is installed once, the first time the hook runs with it, and later runs use it
without contacting pypi.org. pre-commit shares the hook between all your projects that use the
same `rev`, and projects that ask for different versions each keep their own copy.

pre-commit installs `additional_dependencies` with the hook (for example on
`pre-commit install-hooks`), through the same package index it uses for the hook itself. The hooks
then never install anything or contact pypi.org when they run, so this works:
#### Without network access

- on [pre-commit.ci](https://pre-commit.ci/), which runs hooks without network access,
- behind a firewall or with a company PyPI mirror, where pypi.org may be blocked or slow,
- for teams and CI, where every machine gets exactly the same wheel.
clang-format without `--version` is installed together with the hook and runs offline. clang-tidy
without `--version`, and any `--version` that is not installed yet, is installed the first time
the hook runs, which needs network access. On [pre-commit.ci](https://pre-commit.ci/), which runs
hooks without network access, or where pypi.org is blocked, pin the version with pre-commit's own
`additional_dependencies` instead of `--version`. pre-commit installs it together with the hook, through the package index it uses for
the hook itself:

If you set both and they do not match, `--version` wins and the hook installs that wheel at run
time, which needs network access.
```yaml
- id: clang-tidy
additional_dependencies: [clang-tidy==21.1.6]
```

### clang-format

Expand Down Expand Up @@ -152,6 +142,10 @@ int main() {for (;;) break; printf("Hello world!\n");return 0;}

### clang-tidy

clang-tidy 22 and newer enable no checks by themselves. Without a `.clang-tidy` file or
`--checks`, the hook fails and says how to choose checks, for example with a `.clang-tidy` file
that contains `Checks: 'bugprone-*,performance-*,readability-*'`.

To set the checks in `args` instead of your `.clang-tidy` file, quote the whole option: inside
`[...]`, YAML splits an unquoted value at each comma. When that happens, the hook joins the pieces
again and adds a note on how to fix the configuration. `--checks` takes check names, not a file:
Expand Down Expand Up @@ -220,7 +214,7 @@ See [Performance optimization](https://github.com/cpp-linter/cpp-linter-hooks#pe
```yaml
repos:
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.8.0 # includes --fix support
rev: v1.8.1 # includes --fix support
hooks:
- id: clang-tidy
args: [--fix]
Expand All @@ -243,7 +237,7 @@ repository root works too.
```yaml
repos:
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.8.0
rev: v1.8.1
hooks:
- id: clang-tidy
# Auto-detects ./build/compile_commands.json if present
Expand Down Expand Up @@ -298,7 +292,7 @@ Three self-contained templates plus quick snippets for other common setups.

```yaml
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.8.0
rev: v1.8.1
hooks:
- id: clang-format
args: [--style=file]
Expand All @@ -312,7 +306,7 @@ or `-j`:

```yaml
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.8.0
rev: v1.8.1
hooks:
- id: clang-tidy
args: [--jobs=4]
Expand Down Expand Up @@ -349,7 +343,7 @@ This approach ensures that only modified files are checked, further speeding up
```yaml
repos:
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.8.0
rev: v1.8.1
hooks:
- id: clang-format
args: [--style=file, --verbose] # Shows processed files
Expand All @@ -366,7 +360,7 @@ mirror of the clang-format wheel.
|----------------------------------|-------------------------------------------|----------------------------------------|
| Supports `clang-format` and `clang-tidy` | Both | `clang-format` only |
| Custom configuration files | `.clang-format`, `.clang-tidy` | `.clang-format` |
| Specify tool version | `additional_dependencies` or `--version` | via `rev` tag (e.g. `rev: v21.1.8`) |
| Specify tool version | `--version` | via `rev` tag (e.g. `rev: v21.1.8`) |
| `rev` tag meaning | Project version, not the tool version | Equals the clang-format version directly |
| Default file types | C, C++ | C, C++, C#, CUDA, Java, JavaScript, JSON, Objective-C, proto, textproto, Metal |
| Supports passing format style string | via `--style` | via `--style` |
Expand Down
18 changes: 18 additions & 0 deletions cpp_linter_hooks/clang_tidy.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@
The hook finds it in build/ and builddir/. For another directory, pass
--compile-commands=<dir>."""

NO_CHECKS_ERROR = "Error: no checks enabled."
NO_CHECKS_HINT = """\
clang-tidy found no checks to run for some files. Their .clang-tidy file turns
every check off (for example Checks: '-*'), or they have none and clang-tidy 22
and newer enable no checks by default. To check them, set the checks you want
in a .clang-tidy file, for example
Checks: 'bugprone-*,performance-*,readability-*'
or in args: ["--checks=bugprone-*,performance-*"]."""

MSVC_HINT = """\
Windows/MSVC clang-tidy hints:
- Run from a Visual Studio Developer Command Prompt, or call vcvars64.bat first.
Expand Down Expand Up @@ -393,6 +402,9 @@ def _exec_clang_tidy(command) -> tuple[int, str]:
try:
sp = subprocess.run(command, capture_output=True, encoding="utf-8", check=False)
output = (sp.stdout or "") + (sp.stderr or "")
if NO_CHECKS_ERROR in output:
# Keep the error line, not the whole --help text that follows it.
return 1, NO_CHECKS_ERROR
output = _append_guidance(output)
retval = (
1 if sp.returncode != 0 or "warning:" in output or "error:" in output else 0
Expand Down Expand Up @@ -705,6 +717,12 @@ def run_clang_tidy(args=None) -> tuple[int, str]:
[executable] + clang_tidy_args, source_files, lines
)

if NO_CHECKS_ERROR in output:
# Other runs' warnings stay; the hint explains the error once.
output = "\n".join(
line for line in output.splitlines() if line != NO_CHECKS_ERROR
)
notes.append(NO_CHECKS_HINT)
if not hook_args.verbose:
output = _strip_noise(output)
return retval, _append_notes(output, notes)
Expand Down
14 changes: 10 additions & 4 deletions examples/cmake/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
# CMake Project

If you already have a CMake project, you need **two files**:
If you already have a CMake project, you need **three files**:

## .pre-commit-config.yaml

```yaml
repos:
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.8.0
rev: v1.8.1
hooks:
- id: clang-format
args: [--style=file]
additional_dependencies: [clang-format==21.1.8]
files: ^(src|include)/.*\.(cpp|cc|cxx|h|hpp)$

- id: clang-tidy
additional_dependencies: [clang-tidy==21.1.6]
files: ^(src|include)/.*\.(cpp|cc|cxx)$
```

## .clang-tidy

clang-tidy 22 and newer run no checks without one. Choose the checks you want, for example:

```yaml
Checks: 'bugprone-*,performance-*,readability-*'
```

## CMakeLists.txt

Add one line so `clang-tidy` can see your include paths:
Expand Down
15 changes: 11 additions & 4 deletions examples/meson/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
# Meson Project

If you already have a Meson project, configure the hooks and generate a compilation database.
If you already have a Meson project, configure the hooks and the clang-tidy checks, and generate a compilation
database.

## .pre-commit-config.yaml

```yaml
repos:
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.8.0
rev: v1.8.1
Comment thread
coderabbitai[bot] marked this conversation as resolved.
hooks:
- id: clang-format
args: [--style=file]
additional_dependencies: [clang-format==21.1.8]
files: ^(src|include)/.*\.(cpp|cc|cxx|h|hpp)$

- id: clang-tidy
additional_dependencies: [clang-tidy==21.1.6]
files: ^(src|include)/.*\.(cpp|cc|cxx)$
```

## .clang-tidy

clang-tidy 22 and newer run no checks without one. Choose the checks you want, for example:

```yaml
Checks: 'bugprone-*,performance-*,readability-*'
```

## Generate the compilation database

Run from the project root, where `meson.build` lives:
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ classifiers = [
"Topic :: Software Development :: Build Tools",
]
dependencies = [
# Unpinned, so a new hook environment gets the latest clang-format at install
# time; additional_dependencies can pin it, and --version runs another version.
"clang-format",
"filelock>=3.16", # One hook process at a time installs clang tools
"pip>=26.1", # Required to install clang-format and clang-tidy
"pip>=26.1", # Required to install other clang-format and clang-tidy versions
]
dynamic = ["version"]

Expand Down
38 changes: 38 additions & 0 deletions tests/test_clang_tidy.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
COMPILE_COMMANDS_HINT,
COMPILE_DB_SEARCH_DIRS,
MSVC_HINT,
NO_CHECKS_ERROR,
NO_CHECKS_HINT,
_append_guidance,
_combine_outputs,
_exec_clang_tidy,
Expand Down Expand Up @@ -880,6 +882,42 @@ def test_main_reads_arguments_from_command_line(monkeypatch, mock_resolve, mock_
mock_exec.assert_called_once_with(["clang-tidy", "-quiet", "a.cpp"])


# --- no checks enabled (LLVM 22+ without a Checks setting) ---


def test_exec_keeps_only_the_no_checks_error():
help_text = "Error: no checks enabled.\nUSAGE: clang-tidy [options] <source0>\n..."
with patch("cpp_linter_hooks.clang_tidy.subprocess.run") as mock_run:
mock_run.return_value = subprocess.CompletedProcess(
["clang-tidy"], 1, stdout="", stderr=help_text
)
assert _exec_clang_tidy(["clang-tidy", "a.cpp"]) == (1, NO_CHECKS_ERROR)


@pytest.mark.parametrize("verbose", [[], ["--verbose"]])
def test_no_checks_enabled_shows_hint(mock_exec, verbose):
mock_exec.return_value = (1, NO_CHECKS_ERROR)

retval, output = run_clang_tidy(verbose + ["--no-compile-commands", "a.cpp"])

assert retval == 1
assert output == NO_CHECKS_HINT


def test_no_checks_hint_keeps_other_files_warnings(mock_exec):
warning = "b.cpp:1:5: warning: use a trailing return type [modernize-x]"
mock_exec.side_effect = lambda command: (
(1, NO_CHECKS_ERROR) if command[-1] == "sub/c.cpp" else (1, warning)
)

retval, output = run_clang_tidy(
["--jobs=2", "--no-compile-commands", "b.cpp", "sub/c.cpp"]
)

assert retval == 1
assert output == warning + "\n\n" + NO_CHECKS_HINT


# --- quiet output ---


Expand Down
Loading
Loading