diff --git a/README.md b/README.md index 923e601..c9c3207 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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: @@ -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] @@ -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 @@ -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] @@ -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] @@ -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 @@ -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` | diff --git a/cpp_linter_hooks/clang_tidy.py b/cpp_linter_hooks/clang_tidy.py index 39b7ca4..b35ca92 100644 --- a/cpp_linter_hooks/clang_tidy.py +++ b/cpp_linter_hooks/clang_tidy.py @@ -60,6 +60,15 @@ The hook finds it in build/ and builddir/. For another directory, pass --compile-commands=.""" +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. @@ -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 @@ -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) diff --git a/examples/cmake/README.md b/examples/cmake/README.md index 105ee52..b26e80a 100644 --- a/examples/cmake/README.md +++ b/examples/cmake/README.md @@ -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: diff --git a/examples/meson/README.md b/examples/meson/README.md index 97efd80..e3e7816 100644 --- a/examples/meson/README.md +++ b/examples/meson/README.md @@ -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 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: diff --git a/pyproject.toml b/pyproject.toml index 89225d7..c0481dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/tests/test_clang_tidy.py b/tests/test_clang_tidy.py index 7a432f7..dbc3c69 100644 --- a/tests/test_clang_tidy.py +++ b/tests/test_clang_tidy.py @@ -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, @@ -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] \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 --- diff --git a/uv.lock b/uv.lock index b5de557..cbe703e 100644 --- a/uv.lock +++ b/uv.lock @@ -11,6 +11,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] +[[package]] +name = "clang-format" +version = "23.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c1/76dcec575f5fa6598e32cc66731c499349610ebdcb0393e76cceaa74c3bb/clang_format-23.1.3.tar.gz", hash = "sha256:9f7a4e7a2b8eb766bdef11da742fa93fc58c2b0e960e6a6041716618326a07c5", size = 11506, upload-time = "2026-10-06T20:27:32.799Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/0f/a1813ede1e149e700ee810baf22dd9c863ca0bd719d73f86e8c10b67a6bb/clang_format-23.1.3-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:0b8e86cc5a870ca42989bd1ccfe5220fbab96e386922d0a8f0ca350db30593df", size = 1565137, upload-time = "2026-10-06T20:27:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/47/a5/2007b7a7a53dfe417e66d0a6b38b2f87f16391bfe6fe522d436d1e50be1c/clang_format-23.1.3-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:79f53ac55434af6dda88eb90eeb65d903ff920c910f78041de41e3ca666dfba5", size = 1552313, upload-time = "2026-10-06T20:27:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/1d/00/a0f99e24dc4892fb120e82fa9cf6e8fcb92714181ffd1165ab7eaf5ba1fd/clang_format-23.1.3-py2.py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:072113527b49a9c0ab9058006d9fe20e583823e6482608a34e1883dd2c49ae7d", size = 1820841, upload-time = "2026-10-06T20:27:08.675Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/c74dbdaaa72dcb447007c6611ca1a50751c451a75c1d58d1965728001b32/clang_format-23.1.3-py2.py3-none-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:8736b2cd4ed510ceeb1cac2ed0687fd03ce32f82ea960c3c3e62020c5ec88b40", size = 1954844, upload-time = "2026-10-06T20:27:10.441Z" }, + { url = "https://files.pythonhosted.org/packages/d3/51/0517ba621f554044201e44a61abb2bf64821fac79e3b6260670ff4ef2398/clang_format-23.1.3-py2.py3-none-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b1ab2973aa6d2b72289709df1b2969e0a3f1e09dccb665745b18ca265d4419f3", size = 2192373, upload-time = "2026-10-06T20:27:11.911Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9a/7c3dd660c8655e2c8d96d7684b05557f0a407a7e6338749d1d47355a7d41/clang_format-23.1.3-py2.py3-none-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d4bf253feb752a3a52cc5ca595bdae88c9e41de7b35a0f470c8b26c316c2643", size = 2161068, upload-time = "2026-10-06T20:27:13.367Z" }, + { url = "https://files.pythonhosted.org/packages/11/d1/658d3da4dcbca98620956868c7cf6f4ba19fccecbdfc30cc055e19c9d4a7/clang_format-23.1.3-py2.py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58bf1804216501669832a39f2415e14284337438a062ff564729792adec664ac", size = 1906888, upload-time = "2026-10-06T20:27:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b6/80d700f3f02a158e6510a1c16ea70a2d9c55e072d3388081152c03cbf44d/clang_format-23.1.3-py2.py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:4e55a20db2d6bc7e9a3341f818a2638f269d45d5a92d2121dfe1c64d47d5b22a", size = 1738515, upload-time = "2026-10-06T20:27:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/19/f1/2680a4545159dae13f96668dcfcc04d49127f3a4bc04e232cd3f5d67e9d8/clang_format-23.1.3-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fdb92131587b1ec71e587d35de552b5a9cfe32c55c5faa9c7e0956c08ee2667f", size = 2804820, upload-time = "2026-10-06T20:27:17.628Z" }, + { url = "https://files.pythonhosted.org/packages/6a/eb/3d80137f08f6c3ae5789e63fca26c3f5f06246b611529593012a32e77898/clang_format-23.1.3-py2.py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07070f0f5aaa584eb0f7613c4608709adc01c34f14a3b7efb75152d8dcfcf948", size = 2580768, upload-time = "2026-10-06T20:27:19.136Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0e/bf8e7202082d2d00c1a4a79833f28725bff715eebcc505bb48d33b837bf9/clang_format-23.1.3-py2.py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b87338ce8862dde24d9844970a2638d5f7fb2c5cd9d868062ccfcbd6e39f4fd", size = 3058043, upload-time = "2026-10-06T20:27:20.938Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/db9b28062cdf54b01bc2eb7545f1368ca231545a712e7090d59327f1c4ac/clang_format-23.1.3-py2.py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:a92fac41b6a553205f69892b99c49e84195ad9b48055dbca53ec62305f2d7275", size = 3250030, upload-time = "2026-10-06T20:27:22.729Z" }, + { url = "https://files.pythonhosted.org/packages/c3/65/cee67955f7f4e3dba78e6eb243b76ce32c1d915d31760ba10abb63ea9ef8/clang_format-23.1.3-py2.py3-none-musllinux_1_2_s390x.whl", hash = "sha256:22095e50d7aa66b0336dba712dbaec76ad79b54f95bd1d4bd4cffa2aadbdab75", size = 3287158, upload-time = "2026-10-06T20:27:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/81/9d/eb9c0d0d7188929a6c07ca5bc83775e3457a63123515f7692a0d310095bf/clang_format-23.1.3-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae316062f0266f4f1c914dff24158fafc6009bef0e407bdebb6a2e2308a40a70", size = 2920230, upload-time = "2026-10-06T20:27:26.512Z" }, + { url = "https://files.pythonhosted.org/packages/32/af/51bb1f77e54863e769d5527a606298da2988520e7d43328648b6d9c6b7b5/clang_format-23.1.3-py2.py3-none-win32.whl", hash = "sha256:1560b5d7b12c9fe6ace14c659b8fae16d580dfe4abfe18eb02583ba290f01ed0", size = 1363845, upload-time = "2026-10-06T20:27:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/23/a6/90148f91544e3c056e78c514f0e764c1b44d50a33344c83fd03011ea97c8/clang_format-23.1.3-py2.py3-none-win_amd64.whl", hash = "sha256:6c2b74c3a3e942ec172d768e7b945f8e136db05a7b735d49ecfc69bda97643ad", size = 1537541, upload-time = "2026-10-06T20:27:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ed/47bda876e360ca71f366f256939dd674e7a5f4f8deae6eddf7819ea21832/clang_format-23.1.3-py2.py3-none-win_arm64.whl", hash = "sha256:583af22e6593b8ea463412cf3411f55731f117546ad13e88b9ce585e7c47475f", size = 1442230, upload-time = "2026-10-06T20:27:31.241Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -152,6 +177,7 @@ wheels = [ name = "cpp-linter-hooks" source = { editable = "." } dependencies = [ + { name = "clang-format" }, { name = "filelock" }, { name = "pip" }, ] @@ -166,6 +192,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "clang-format" }, { name = "filelock", specifier = ">=3.16" }, { name = "pip", specifier = ">=26.1" }, ]