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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
61 changes: 60 additions & 1 deletion cpp_linter_hooks/clang_tidy.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,78 @@
import subprocess
from argparse import ArgumentParser
from typing import Tuple
import re

from cpp_linter_hooks.util import resolve_install, DEFAULT_CLANG_TIDY_VERSION


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 = ""
Expand Down
104 changes: 104 additions & 0 deletions tests/test_clang_tidy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading