From b173efd639df8bba409f652759c5b96818b52e25 Mon Sep 17 00:00:00 2001 From: Patrick Vreeburg <96523159+quasariumm@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:35:05 +0100 Subject: [PATCH 1/8] feat: Clang-tidy prefixes and regexes --- cpp_linter_hooks/clang_tidy.py | 55 +++++++++++++++++++++++++++++++++- tests/test_clang_tidy.py | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/cpp_linter_hooks/clang_tidy.py b/cpp_linter_hooks/clang_tidy.py index 1a5c8f0..208842f 100644 --- a/cpp_linter_hooks/clang_tidy.py +++ b/cpp_linter_hooks/clang_tidy.py @@ -1,6 +1,7 @@ import subprocess from argparse import ArgumentParser from typing import Tuple +import re from cpp_linter_hooks.util import resolve_install, DEFAULT_CLANG_TIDY_VERSION @@ -8,12 +9,64 @@ parser = ArgumentParser() parser.add_argument("--version", default=DEFAULT_CLANG_TIDY_VERSION) +# [Optional] Used for adding a prefix to the clang executable +# You can call this multiple times if relevant. +# +# This is useful when using platform-specific versions or +# cross-compilation toolchains where the tools are named +# with a prefix, such as 'x86_64-linux-gnu-clang-tidy' or +# 'aarch64-linux-gnu-clang-format'. +# +# Leave empty to just use 'clang-tidy'. +parser.add_argument("--clang-tool-prefix", action='append', type=str, default=[]) + +# [Optional] Specifies regex for '--clang-tool-prefix' +# You can call this multiple times if relevant. +# +# This is useful for when you have a project with multiple +# platforms that need a specific clang-tidy executable +# to perform linting. +# +# Since the regex is applied to the clang-tidy arguments, +# be careful with the regex string. +# +# Any prefix that does not have a regex linked will use '.*' +parser.add_argument("--prefix-regex", action='append', type=str, default=[]) + def run_clang_tidy(args=None) -> Tuple[int, str]: hook_args, other_args = parser.parse_known_args(args) if hook_args.version: resolve_install("clang-tidy", hook_args.version) - command = ["clang-tidy"] + other_args + + clang_args = ' '.join(other_args) + + prefix = '' + + num_prefixes = len(hook_args.clang_tool_prefix) + num_regexes = len(hook_args.prefix_regex) + if num_prefixes > 0 \ + or num_regexes > 0: + # If there are two or more prefixes than there are regexes, I throw an error + # since I have no clue which one is meant + if num_prefixes - num_regexes >= 2: + return 2, "Too many prefixes provided. Please provide no more than two more prefixes than regexes." + if num_regexes > num_prefixes: + return 3, "More regexes than prefixes provided. Please use the 'files' argument in the hook for filtering instead of this argument." + + # This loops over all the specified prefixes and tests the regex on the file name + for i, ct_prefix in enumerate(hook_args.clang_tool_prefix): + regex_string = '' + + try: + regex_string = hook_args.prefix_regex[i] + except IndexError: + regex_string = '.*' + + if re.search(regex_string, clang_args) is not None: + prefix = ct_prefix + + command = [prefix + "clang-tidy"] + other_args retval = 0 output = "" diff --git a/tests/test_clang_tidy.py b/tests/test_clang_tidy.py index 1ef35ee..0202762 100644 --- a/tests/test_clang_tidy.py +++ b/tests/test_clang_tidy.py @@ -53,3 +53,58 @@ def test_run_clang_tidy_invalid(args, expected_retval, tmp_path): ret, _ = run_clang_tidy(args + [str(test_file)]) assert ret == expected_retval + + +# This test covers proper handling of prefixes. +# Note that the prefixes I use here are not valid. +# The whole point of the test is to see if the prefixes +# are caught. +@pytest.mark.benchmark +@pytest.mark.parametrize( + ("args", "expected_retval"), + ( + # Should give the usual warnings + (['--checks="boost-*"'], 1), + # Should use testclang-tidy -> FileNotFoundError + (['--checks="-*"', '--clang-tool-prefix', 'test', '--prefix-regex', r'.*'], 1), + # Should use testclang-tidy -> FileNotFoundError + (['--checks="-*"', '--clang-tool-prefix', 'test', '--prefix-regex', r'.*main\.(c|cpp|h|hpp)'], 1), + # Should use testclang-tidy -> FileNotFoundError + (['--checks="-*"', '--clang-tool-prefix', 'test', '--prefix-regex', r'.*\.c'], 1), + # Should use clang-tidy -> usual warnings + (['--checks="-*"', '--clang-tool-prefix', 'test', '--prefix-regex', r'shouldnotmatch'], 1), + # Should use testclang-tidy -> FileNotFoundError + (['--checks="-*"', '--clang-tool-prefix', 'test', '--prefix-regex', r'.*\.c', '--clang-tool-prefix', 'test2', '--prefix-regex', 'main'], 1), + ) +) +def test_run_clang_tidy_prefixes_valid(args, expected_retval): + # copy test file to tmp_path to prevent modifying repo data + test_file = Path("testing/main.c") + test_file.write_bytes(Path("testing/main.c").read_bytes()) + ret, output = run_clang_tidy(args + [str(test_file)]) + assert ret == expected_retval + print(output) + + +# This test covers cases where the user has either specified +# too many prefixes or regexes. +@pytest.mark.benchmark +@pytest.mark.parametrize( + ("args", "expected_retval"), + ( + # More regexes than prefixes is invalid. + # For this type of filtering, I strongly + # suggest the user to use the files arg. + (['--checks="boost-*"', '--prefix-regex', r'.*'], 3), + # Two or more prefixes than there are + # regexes is confusing + (['--checks="boost-*"', '--clang-tool-prefix', 'test', '--clang-tool-prefix', 'test2'], 2), + ) +) +def test_run_clang_tidy_prefixes_invalid(args, expected_retval): + # copy test file to tmp_path to prevent modifying repo data + test_file = Path("testing/main.c") + test_file.write_bytes(Path("testing/main.c").read_bytes()) + ret, output = run_clang_tidy(args + [str(test_file)]) + assert ret == expected_retval + print(output) \ No newline at end of file From 7a8a4bbdea463bded983ec9cd54641e792704f73 Mon Sep 17 00:00:00 2001 From: Patrick Vreeburg <96523159+quasariumm@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:37:26 +0100 Subject: [PATCH 2/8] fix: Forgot to initialise pre_commit. Applied its changes --- cpp_linter_hooks/clang_tidy.py | 31 ++++++++------- tests/test_clang_tidy.py | 69 +++++++++++++++++++++++++++++----- 2 files changed, 77 insertions(+), 23 deletions(-) diff --git a/cpp_linter_hooks/clang_tidy.py b/cpp_linter_hooks/clang_tidy.py index 208842f..8506e6e 100644 --- a/cpp_linter_hooks/clang_tidy.py +++ b/cpp_linter_hooks/clang_tidy.py @@ -12,13 +12,13 @@ # [Optional] Used for adding a prefix to the clang executable # You can call this multiple times if relevant. # -# This is useful when using platform-specific versions or -# cross-compilation toolchains where the tools are named +# This is useful when using platform-specific versions or +# cross-compilation toolchains where the tools are named # with a prefix, such as 'x86_64-linux-gnu-clang-tidy' or # 'aarch64-linux-gnu-clang-format'. # # Leave empty to just use 'clang-tidy'. -parser.add_argument("--clang-tool-prefix", action='append', type=str, default=[]) +parser.add_argument("--clang-tool-prefix", action="append", type=str, default=[]) # [Optional] Specifies regex for '--clang-tool-prefix' # You can call this multiple times if relevant. @@ -27,11 +27,11 @@ # platforms that need a specific clang-tidy executable # to perform linting. # -# Since the regex is applied to the clang-tidy arguments, +# Since the regex is applied to the clang-tidy arguments, # be careful with the regex string. # # Any prefix that does not have a regex linked will use '.*' -parser.add_argument("--prefix-regex", action='append', type=str, default=[]) +parser.add_argument("--prefix-regex", action="append", type=str, default=[]) def run_clang_tidy(args=None) -> Tuple[int, str]: @@ -39,29 +39,34 @@ def run_clang_tidy(args=None) -> Tuple[int, str]: if hook_args.version: resolve_install("clang-tidy", hook_args.version) - clang_args = ' '.join(other_args) + clang_args = " ".join(other_args) - prefix = '' + prefix = "" num_prefixes = len(hook_args.clang_tool_prefix) num_regexes = len(hook_args.prefix_regex) - if num_prefixes > 0 \ - or num_regexes > 0: + if num_prefixes > 0 or num_regexes > 0: # If there are two or more prefixes than there are regexes, I throw an error # since I have no clue which one is meant if num_prefixes - num_regexes >= 2: - return 2, "Too many prefixes provided. Please provide no more than two more prefixes than regexes." + return ( + 2, + "Too many prefixes provided. Please provide no more than two more prefixes than regexes.", + ) if num_regexes > num_prefixes: - return 3, "More regexes than prefixes provided. Please use the 'files' argument in the hook for filtering instead of this argument." + return ( + 3, + "More regexes than prefixes provided. Please use the 'files' argument in the hook for filtering instead of this argument.", + ) # This loops over all the specified prefixes and tests the regex on the file name for i, ct_prefix in enumerate(hook_args.clang_tool_prefix): - regex_string = '' + regex_string = "" try: regex_string = hook_args.prefix_regex[i] except IndexError: - regex_string = '.*' + regex_string = ".*" if re.search(regex_string, clang_args) is not None: prefix = ct_prefix diff --git a/tests/test_clang_tidy.py b/tests/test_clang_tidy.py index 0202762..2ea11bb 100644 --- a/tests/test_clang_tidy.py +++ b/tests/test_clang_tidy.py @@ -66,16 +66,56 @@ def test_run_clang_tidy_invalid(args, expected_retval, tmp_path): # Should give the usual warnings (['--checks="boost-*"'], 1), # Should use testclang-tidy -> FileNotFoundError - (['--checks="-*"', '--clang-tool-prefix', 'test', '--prefix-regex', r'.*'], 1), + (['--checks="-*"', "--clang-tool-prefix", "test", "--prefix-regex", r".*"], 1), # Should use testclang-tidy -> FileNotFoundError - (['--checks="-*"', '--clang-tool-prefix', 'test', '--prefix-regex', r'.*main\.(c|cpp|h|hpp)'], 1), + ( + [ + '--checks="-*"', + "--clang-tool-prefix", + "test", + "--prefix-regex", + r".*main\.(c|cpp|h|hpp)", + ], + 1, + ), # Should use testclang-tidy -> FileNotFoundError - (['--checks="-*"', '--clang-tool-prefix', 'test', '--prefix-regex', r'.*\.c'], 1), + ( + [ + '--checks="-*"', + "--clang-tool-prefix", + "test", + "--prefix-regex", + r".*\.c", + ], + 1, + ), # Should use clang-tidy -> usual warnings - (['--checks="-*"', '--clang-tool-prefix', 'test', '--prefix-regex', r'shouldnotmatch'], 1), + ( + [ + '--checks="-*"', + "--clang-tool-prefix", + "test", + "--prefix-regex", + r"shouldnotmatch", + ], + 1, + ), # Should use testclang-tidy -> FileNotFoundError - (['--checks="-*"', '--clang-tool-prefix', 'test', '--prefix-regex', r'.*\.c', '--clang-tool-prefix', 'test2', '--prefix-regex', 'main'], 1), - ) + ( + [ + '--checks="-*"', + "--clang-tool-prefix", + "test", + "--prefix-regex", + r".*\.c", + "--clang-tool-prefix", + "test2", + "--prefix-regex", + "main", + ], + 1, + ), + ), ) def test_run_clang_tidy_prefixes_valid(args, expected_retval): # copy test file to tmp_path to prevent modifying repo data @@ -95,11 +135,20 @@ def test_run_clang_tidy_prefixes_valid(args, expected_retval): # More regexes than prefixes is invalid. # For this type of filtering, I strongly # suggest the user to use the files arg. - (['--checks="boost-*"', '--prefix-regex', r'.*'], 3), + (['--checks="boost-*"', "--prefix-regex", r".*"], 3), # Two or more prefixes than there are # regexes is confusing - (['--checks="boost-*"', '--clang-tool-prefix', 'test', '--clang-tool-prefix', 'test2'], 2), - ) + ( + [ + '--checks="boost-*"', + "--clang-tool-prefix", + "test", + "--clang-tool-prefix", + "test2", + ], + 2, + ), + ), ) def test_run_clang_tidy_prefixes_invalid(args, expected_retval): # copy test file to tmp_path to prevent modifying repo data @@ -107,4 +156,4 @@ def test_run_clang_tidy_prefixes_invalid(args, expected_retval): test_file.write_bytes(Path("testing/main.c").read_bytes()) ret, output = run_clang_tidy(args + [str(test_file)]) assert ret == expected_retval - print(output) \ No newline at end of file + print(output) From 15e3a312ac0bb3e3d4f7e6b659a213febf109699 Mon Sep 17 00:00:00 2001 From: Patrick Vreeburg <96523159+quasariumm@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:50:16 +0100 Subject: [PATCH 3/8] fix: Set prefix as first that matches regex --- cpp_linter_hooks/clang_tidy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp_linter_hooks/clang_tidy.py b/cpp_linter_hooks/clang_tidy.py index 8506e6e..e003722 100644 --- a/cpp_linter_hooks/clang_tidy.py +++ b/cpp_linter_hooks/clang_tidy.py @@ -70,6 +70,7 @@ def run_clang_tidy(args=None) -> Tuple[int, str]: if re.search(regex_string, clang_args) is not None: prefix = ct_prefix + break command = [prefix + "clang-tidy"] + other_args From b12c16290e0c58ee5176dd3b9ddc9e674569a005 Mon Sep 17 00:00:00 2001 From: Patrick Vreeburg <96523159+quasariumm@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:50:43 +0100 Subject: [PATCH 4/8] docs: updated README.md with prefixes documentation --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 9c30d5d..3241af5 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,38 @@ repos: args: [--checks=.clang-tidy, --version=21] # Specifies version ``` +### Clang-tidy prefixes + +Since some platforms have their own version of clang-tidy, you might need a specific prefix. + +Any prefix can use a regex. The regex matches all other arguments that are given to clang-tidy, so not only the file that is being checked. + +When one prefix is specified without a regex, it is used for all files that are not matched by any other specified regex. + +The prefix that is being applied is the **first** one whose regex matches.. + +To specify this, add the following arguments to the hook: + +```yaml +repos: + - repo: https://github.com/BredaUniversityGames/cpp-linter-hooks + rev: 7a8a4bb + hooks: + - id: clang-format + args: [--style=file] + - id: clang-tidy + args: [ + --checks=.clang-tidy, + --clang-tool-prefix, + x86_64-linux-gnu-, # Specifies prefix 0 + --prefix-regex, + .*, # Specifies a regex for prefix 0 + --clang-tool-prefix, + aarch64-linux-gnu-, # Specifies prefix 1, + ... + ] +``` + ## Output ### clang-format Output From 61efae784e829dcb6cb61dff7aa26f70d21d9181 Mon Sep 17 00:00:00 2001 From: Patrick Vreeburg <96523159+quasariumm@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:21:41 +0100 Subject: [PATCH 5/8] docs: updated README.md with tag --- README.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3241af5..5e487d5 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A pre-commit hook that automatically formats and lints your C/C++ code using `cl - [Quick Start](#quick-start) - [Custom Configuration Files](#custom-configuration-files) - [Custom Clang Tool Version](#custom-clang-tool-version) + - [Clang-tidy prefixes](#clang-tidy-prefixes) - [Output](#output) - [clang-format Output](#clang-format-output) - [clang-tidy Output](#clang-tidy-output) @@ -72,11 +73,11 @@ repos: args: [--checks=.clang-tidy, --version=21] # Specifies version ``` -### Clang-tidy prefixes +### Clang-tidy Prefixes Since some platforms have their own version of clang-tidy, you might need a specific prefix. -Any prefix can use a regex. The regex matches all other arguments that are given to clang-tidy, so not only the file that is being checked. +Any prefix can use a [regex](https://en.wikipedia.org/wiki/Regular_expression). The regex matches all other arguments that are given to clang-tidy, so not only the file that is being checked. When one prefix is specified without a regex, it is used for all files that are not matched by any other specified regex. @@ -87,19 +88,16 @@ To specify this, add the following arguments to the hook: ```yaml repos: - repo: https://github.com/BredaUniversityGames/cpp-linter-hooks - rev: 7a8a4bb + rev: v1.1.11-prefix # Prefixes are added in this version hooks: - id: clang-format args: [--style=file] - id: clang-tidy args: [ --checks=.clang-tidy, - --clang-tool-prefix, - x86_64-linux-gnu-, # Specifies prefix 0 - --prefix-regex, - .*, # Specifies a regex for prefix 0 - --clang-tool-prefix, - aarch64-linux-gnu-, # Specifies prefix 1, + --clang-tool-prefix=x86_64-linux-gnu-, # Specifies prefix 0 + --prefix-regex=.*, # Specifies a regex for prefix 0 + --clang-tool-prefix=aarch64-linux-gnu-, # Specifies prefix 1, ... ] ``` From 2eea9be6bafe5ffc9500394803ce11f56511c117 Mon Sep 17 00:00:00 2001 From: Patrick Vreeburg <96523159+quasariumm@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:23:35 +0100 Subject: [PATCH 6/8] docs: more clear prefix usage docs --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5e487d5..4c9ca11 100644 --- a/README.md +++ b/README.md @@ -96,9 +96,9 @@ repos: args: [ --checks=.clang-tidy, --clang-tool-prefix=x86_64-linux-gnu-, # Specifies prefix 0 - --prefix-regex=.*, # Specifies a regex for prefix 0 - --clang-tool-prefix=aarch64-linux-gnu-, # Specifies prefix 1, - ... + --prefix-regex=.*x64_linux.*, # Specifies a regex for prefix 0 + --clang-tool-prefix=aarch64-linux-gnu-, # Specifies prefix 1 + # Leaving this empty will set its regex to .* (capturing the remaining files) ] ``` From 8561964ef33b67fae8cf22cc84c038e641721df2 Mon Sep 17 00:00:00 2001 From: Patrick Vreeburg <96523159+quasariumm@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:24:11 +0100 Subject: [PATCH 7/8] docs: punctuation error --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c9ca11..d21b5d4 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Any prefix can use a [regex](https://en.wikipedia.org/wiki/Regular_expression). When one prefix is specified without a regex, it is used for all files that are not matched by any other specified regex. -The prefix that is being applied is the **first** one whose regex matches.. +The prefix that is being applied is the **first** one whose regex matches. To specify this, add the following arguments to the hook: From e068d8966ecae01d72690611236af239d5d5e985 Mon Sep 17 00:00:00 2001 From: Patrick Vreeburg <96523159+quasariumm@users.noreply.github.com> Date: Sun, 15 Feb 2026 16:45:58 +0100 Subject: [PATCH 8/8] fix: made codecov upload optional --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b410a6b..fc02a7e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml - fail_ci_if_error: true # optional (default = false) + fail_ci_if_error: false # optional (default = false) verbose: true # optional (default = false) - name: Test cpp-linter-hooks run: bash testing/run.sh