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 diff --git a/README.md b/README.md index 9c30d5d..d21b5d4 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,6 +73,35 @@ 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](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. + +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: 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=.*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) + ] +``` + ## Output ### clang-format Output diff --git a/cpp_linter_hooks/clang_tidy.py b/cpp_linter_hooks/clang_tidy.py index 1a5c8f0..e003722 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,70 @@ 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 + break + + 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..2ea11bb 100644 --- a/tests/test_clang_tidy.py +++ b/tests/test_clang_tidy.py @@ -53,3 +53,107 @@ 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)