Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
10 changes: 10 additions & 0 deletions linters/clang-tidy-utils/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Environment
venv*/
__pycache__

# Packaging
*.egg-info

# Dev
.mypy_cache
.ruff_cache
44 changes: 44 additions & 0 deletions linters/clang-tidy-utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# clang-tidy-utils

`clang-tidy-utils` is a command-line tool for running [clang-tidy][clang-tidy-home] checks efficiently.

## Requirements
- [uv] (for package and environment management)

## Installation

### Virtual environment installation (Recommended)

For installation within a virtual environment:
```shell
uv venv
uv pip install .
```

### System-wide installation

To install the tool globally:
```shell
uv tool install .
```

## Usage

Run the tool using:
```shell
uv run clang-tidy-utils [-h] [-j NUM_JOBS] FILE [FILE ...] [-- CLANG-TIDY-ARGS ...]
```
- By default, the number of jobs (`-j`) is set to the number of available CPU cores.
- Arguments after `--` are directly passed to `clang-tidy`.

## Development:
To format and lint the code, run:
```shell
uv tool run mypy src
uv tool run docformatter -i src
uv tool run black src
uv tool run ruff check --fix src
```

[clang-tidy-home]: https://clang.llvm.org/extra/clang-tidy/
[uv]: https://docs.astral.sh/uv/getting-started/installation/
45 changes: 45 additions & 0 deletions linters/clang-tidy-utils/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[project]
name = "clang-tidy-utils"
version = "0.0.1"
authors = [
{ name="zhihao lin", email="[email protected]" },
]
requires-python = ">=3.10"
dependencies = [
"clang-tidy >= 19.1.0",
]

[dependency-groups]
dev = [
"black >= 24.10.0",
"docformatter >= 1.7.5",
"mypy >= 1.14.1",
"ruff >= 0.9.2",
]

[project.scripts]
clang-tidy-utils = "clang_tidy_utils.cli:main"

[tool.black]
line-length = 100
color = true
preview = true

[tool.docformatter]
make-summary-multi-line = true
pre-summary-newline = true
recursive = true
wrap-summaries = 100
wrap-descriptions = 100

[tool.mypy]
explicit_package_bases = true
strict = true
pretty = true

[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = ["E", "I", "F"]
isort.order-by-type = false
175 changes: 175 additions & 0 deletions linters/clang-tidy-utils/src/clang_tidy_utils/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import argparse
import asyncio
import dataclasses
import multiprocessing
import subprocess
import sys
from typing import List, Optional


@dataclasses.dataclass
class ClangTidyResult:
"""
Class that represents clang-tidy's execution results.
"""

file_name: str
ret_code: int
stdout: str
stderr: str


def _create_clang_tidy_task_arg_list(file: str, clang_tidy_args: List[str]) -> List[str]:
"""
:param file: The file to check.
:param clang_tidy_args: The clang-tidy cli arguments.
:return: A list of arguments to run clang-tidy to check the given file with the given args.
"""
args: List[str] = ["clang-tidy", file]
args.extend(clang_tidy_args)
return args


async def _execute_clang_tidy_task(file: str, clang_tidy_args: List[str]) -> ClangTidyResult:
"""
Executes a single clang-tidy task by checking one file using a process managed by asyncio.

:param file: The file to check.
:param clang_tidy_args: The clang-tidy cli arguments.
:return: Execution results represented by an instance of `ClangTidyResult`.
"""
task_args: List[str] = _create_clang_tidy_task_arg_list(file, clang_tidy_args)
try:
process = await asyncio.create_subprocess_exec(
*task_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
stdout, stderr = await process.communicate()
except asyncio.CancelledError:
process.terminate()
await process.wait()
raise

assert process.returncode is not None
return ClangTidyResult(
file,
process.returncode,
stdout.decode("UTF-8"),
stderr.decode("UTF-8"),
)
Comment on lines +59 to +84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Handle potential Unicode decode errors

The stdout and stderr decoding could fail if clang-tidy outputs non-UTF-8 characters. Consider using error handling for the decode operations.

Apply this diff to handle potential decode errors:

     assert process.returncode is not None
     return ClangTidyResult(
         file,
         process.returncode,
-        stdout.decode("UTF-8"),
-        stderr.decode("UTF-8"),
+        stdout.decode("UTF-8", errors="replace"),
+        stderr.decode("UTF-8", errors="replace"),
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def _execute_clang_tidy_task(file: str, clang_tidy_args: List[str]) -> ClangTidyResult:
"""
Executes a single clang-tidy task by checking one file using a process managed by asyncio.
:param file: The file to check.
:param clang_tidy_args: The clang-tidy cli arguments.
:return: Execution results represented by an instance of `ClangTidyResult`.
"""
task_args: List[str] = _create_clang_tidy_task_arg_list(file, clang_tidy_args)
try:
process = await asyncio.create_subprocess_exec(
*task_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
stdout, stderr = await process.communicate()
except asyncio.CancelledError:
process.terminate()
await process.wait()
raise
assert process.returncode is not None
return ClangTidyResult(
file,
process.returncode,
stdout.decode("UTF-8"),
stderr.decode("UTF-8"),
)
assert process.returncode is not None
return ClangTidyResult(
file,
process.returncode,
stdout.decode("UTF-8", errors="replace"),
stderr.decode("UTF-8", errors="replace"),
)
🤖 Prompt for AI Agents
In linters/clang-tidy-utils/src/clang_tidy_utils/cli.py around lines 59-84, the
code decodes stdout and stderr with .decode("UTF-8") which can raise
UnicodeDecodeError for non-UTF-8 output; change the decoding to handle errors
(e.g., use stdout.decode("utf-8", errors="replace") and stderr.decode("utf-8",
errors="replace") or wrap the decode in a try/except UnicodeDecodeError and fall
back to decoding with errors="replace") so the function always returns safe
strings instead of raising.



async def _execute_clang_tidy_task_with_sem(
sem: asyncio.Semaphore, file: str, clang_tidy_args: List[str]
) -> ClangTidyResult:
"""
Wrapper of `execute_clang_tidy_task` with a global semaphore for concurrency control.

:param sem: The global semaphore for concurrency control.
:param file: The file to check.
:param clang_tidy_args: The clang-tidy cli arguments.
:return: Forwards `execute_clang_tidy_task`'s return values.
"""
async with sem:
return await _execute_clang_tidy_task(file, clang_tidy_args)


async def _clang_tidy_parallel_execution_entry(
num_jobs: int,
files: List[str],
clang_tidy_args: List[str],
) -> int:
"""
Async entry for running clang-tidy checks in parallel.

:param num_jobs: The maximum number of jobs allowed to run in parallel.
:param files: The list of files to check. :clang_tidy_args: The clang-tidy cli arguments.
"""
sem: asyncio.Semaphore = asyncio.Semaphore(num_jobs)
tasks: List[asyncio.Task[ClangTidyResult]] = [
asyncio.create_task(_execute_clang_tidy_task_with_sem(sem, file, clang_tidy_args))
for file in files
]
num_total_files: int = len(files)

ret_code: int = 0
try:
for idx, clang_tidy_task in enumerate(asyncio.as_completed(tasks)):
result: ClangTidyResult = await clang_tidy_task
if 0 != result.ret_code:
ret_code = 1
print(f"[{idx + 1}/{num_total_files}]: {result.file_name}")
print(result.stdout)
print(result.stderr)
except asyncio.CancelledError as e:
print(f"\nAll tasks cancelled: {e}")
for task in tasks:
task.cancel()

return ret_code


def main() -> None:
parser: argparse.ArgumentParser = argparse.ArgumentParser(
description="clang-tidy-utils cli options.",
)

parser.add_argument(
"-j",
"--num-jobs",
type=int,
help="Number of jobs to run for parallel processing.",
)

parser.add_argument(
"input_files",
metavar="FILE",
type=str,
nargs="+",
help="Input files to process.",
)

default_parser_usage: str = parser.format_usage()
if default_parser_usage.endswith("\n"):
default_parser_usage = default_parser_usage[:-1]
usage_prefix: str = "usage: "
if default_parser_usage.startswith(usage_prefix):
default_parser_usage = default_parser_usage[len(usage_prefix) :]
usage: str = default_parser_usage + " [-- CLANG-TIDY-ARGS ...]"
parser.usage = usage

args: List[str] = sys.argv[1:]
delimiter_idx: Optional[int] = None
try:
delimiter_idx = args.index("--")
except ValueError:
pass

cli_args: List[str] = args
clang_tidy_args: List[str] = []
if delimiter_idx is not None:
cli_args = args[:delimiter_idx]
clang_tidy_args = args[delimiter_idx + 1 :]

parsed_cli_args: argparse.Namespace = parser.parse_args(cli_args)

num_jobs: int
if parsed_cli_args.num_jobs is not None:
num_jobs = parsed_cli_args.num_jobs
else:
num_jobs = multiprocessing.cpu_count()

Comment on lines +184 to +189
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for num_jobs parameter

The num_jobs parameter should be validated to ensure it's a positive integer.

Apply this diff to add validation:

     num_jobs: int
     if parsed_cli_args.num_jobs is not None:
         num_jobs = parsed_cli_args.num_jobs
+        if num_jobs <= 0:
+            parser.error("Number of jobs must be a positive integer")
     else:
         num_jobs = multiprocessing.cpu_count()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
num_jobs: int
if parsed_cli_args.num_jobs is not None:
num_jobs = parsed_cli_args.num_jobs
else:
num_jobs = multiprocessing.cpu_count()
num_jobs: int
if parsed_cli_args.num_jobs is not None:
num_jobs = parsed_cli_args.num_jobs
if num_jobs <= 0:
parser.error("Number of jobs must be a positive integer")
else:
num_jobs = multiprocessing.cpu_count()

ret_code: int = 0
try:
ret_code = asyncio.run(
_clang_tidy_parallel_execution_entry(
num_jobs, parsed_cli_args.input_files, clang_tidy_args
)
)
except KeyboardInterrupt:
pass

exit(ret_code)


if "__main__" == __name__:
main()