Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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 cli/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
41 changes: 41 additions & 0 deletions cli/clang-tidy-utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# yscope-clang-tidy-utils

This project is a CLI scripts for running [clang-tidy][clang-tidy-home] checks.

## Requirements
- [uv]

## Installation
To install the tool system-wide (permanently), run:
```shell
uv tool install .
```

Or, installing the tool in a virtual environment:
```shell
uv venv
uv pip install .
```
Chech [here] for uv's virtual environment behaviour.

## Usage
```shell
yscope-clang-tidy-utils [-h] [-j NUM_JOBS] FILE [FILE ...] [-- CLANG-TIDY-ARGS ...]
```
Note:
- By default, the number of jobs will be set to the number of cores in the running environment.
- Anything after `--` will be considered as clang-tidy arguments and will be directly passed into
clang-tidy.

## Development:
Run linting tools with the following commands:
```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/
[uv-venv]: https://docs.astral.sh/uv/pip/compatibility/#virtual-environments-by-default
45 changes: 45 additions & 0 deletions cli/clang-tidy-utils/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[project]
name = "yscope-clang-tidy-utils"
Copy link
Member Author

Choose a reason for hiding this comment

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

We may drop yscope- prefix

Copy link
Member

Choose a reason for hiding this comment

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

I don't have a strong preference, but if we ever do upload this to pypi it probably makes more sense to have the prefix. If we never upload it to pypi the name won't ever really matter right?

Copy link
Member

Choose a reason for hiding this comment

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

I prefer to drop the prefix for now since it's likely this will be an internal tool for the foreseeable future.

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed.

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]
yscope-clang-tidy-utils = "yscope_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 cli/clang-tidy-utils/src/yscope_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"),
)


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="yscope-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()

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()