File indexing completed on 2025-01-18 09:10:43
0001
0002 import argparse
0003 from concurrent.futures import ThreadPoolExecutor, as_completed
0004 from multiprocessing import cpu_count
0005 import subprocess
0006 from subprocess import check_call, check_output, CalledProcessError
0007 from pathlib import Path
0008 import re
0009 import sys
0010 import os
0011 import threading
0012
0013 import rich.console
0014 import rich.progress
0015 import rich.panel
0016 import rich.live
0017 import rich.text
0018 import rich.table
0019 import rich.rule
0020 import rich.spinner
0021
0022
0023 def which(cmd: str):
0024 try:
0025 return check_output(["command", "-v", cmd]).decode().strip()
0026 except CalledProcessError:
0027 return None
0028
0029
0030 def main():
0031 p = argparse.ArgumentParser()
0032 p.add_argument("--clang-tidy", default=which("clang-tidy"))
0033 p.add_argument("--clang-format", default=which("clang-format"))
0034 p.add_argument("--jobs", "-j", type=int, default=cpu_count())
0035 p.add_argument("--fix", action="store_true")
0036 p.add_argument("--include", action="append", default=[])
0037 p.add_argument("--exclude", action="append", default=[])
0038 p.add_argument("--ignore-compiler-errors", action="store_true")
0039 p.add_argument("build", type=Path)
0040 p.add_argument("source", type=Path)
0041
0042 args = p.parse_args()
0043
0044 assert args.clang_tidy is not None, "clang-tidy not found"
0045 assert args.clang_format is not None, "clang-format not found"
0046
0047 args.include = [re.compile(f) for f in args.include]
0048 args.exclude = [re.compile(f) for f in args.exclude]
0049
0050 check_call([args.clang_tidy, "--version"], stdout=subprocess.DEVNULL)
0051 check_call([args.clang_format, "--version"], stdout=subprocess.DEVNULL)
0052
0053 assert (
0054 args.build.exists() and args.build.is_dir()
0055 ), f"{args.build} is not a directory"
0056 assert (
0057 args.source.exists() and args.source.is_dir()
0058 ), f"{args.source} is not a directory"
0059
0060 futures = []
0061 files = []
0062 active_files = {}
0063 active_file_lock = threading.Lock()
0064
0065 def run(file: Path):
0066 with active_file_lock:
0067 active_files[threading.current_thread().ident] = file
0068 cmd = [args.clang_tidy, "-p", args.build, file]
0069 if args.fix:
0070 cmd.append("-fix")
0071
0072 try:
0073 out = check_output(cmd, stderr=subprocess.STDOUT).decode().strip()
0074 error = False
0075 except CalledProcessError as e:
0076 out = e.output.decode().strip()
0077 if args.ignore_compiler_errors and "Found compiler error(s)." in out:
0078 out = "Found compiler error(s)."
0079 error = False
0080 else:
0081 error = True
0082 finally:
0083 with active_file_lock:
0084 active_files[threading.current_thread().ident] = None
0085 return file, out, error
0086
0087 for dirpath, _, filenames in os.walk(args.source):
0088 dirpath = Path(dirpath)
0089 for file in filenames:
0090 file = dirpath / file
0091 if (
0092 file.suffix in (".hpp", ".cpp", ".ipp")
0093 and (
0094 len(args.include) == 0
0095 or any(flt.match(str(file)) for flt in args.include)
0096 )
0097 and not any(flt.match(str(file)) for flt in args.exclude)
0098 ):
0099 files.append(file)
0100
0101 with ThreadPoolExecutor(args.jobs) as tp:
0102 for file in files:
0103 assert file.exists(), f"{file} does not exist"
0104 futures.append(tp.submit(run, file))
0105
0106 error = False
0107
0108 console = rich.console.Console()
0109
0110 prog = rich.progress.Progress()
0111 log = []
0112
0113 def make_display():
0114 t = rich.table.Table.grid()
0115 t.add_column()
0116 t.add_column()
0117 with active_file_lock:
0118 for f in active_files.values():
0119 if f is None:
0120 t.add_row("")
0121 else:
0122 t.add_row(rich.spinner.Spinner("dots", style="green"), f" {f}")
0123
0124 ot = rich.table.Table.grid(expand=True)
0125 ot.add_column(ratio=1)
0126 ot.add_column(ratio=1)
0127
0128 def emoji(err):
0129 return ":red_circle:" if err else ":green_circle:"
0130
0131 ot.add_row(
0132 t,
0133 rich.console.Group(
0134 *[f"{emoji(err)} {line}" for err, line in log[-args.jobs :]]
0135 ),
0136 )
0137
0138 return rich.console.Group(rich.rule.Rule(), ot, prog)
0139
0140 task = prog.add_task("Running clang-tidy", total=len(futures))
0141
0142 with rich.live.Live(
0143 make_display(), console=console, refresh_per_second=20, transient=False
0144 ) as live:
0145 for f in as_completed(futures):
0146 file, result, this_error = f.result()
0147 log.append((this_error, file))
0148 error = this_error or error
0149 console.print(
0150 rich.panel.Panel(
0151 result, title=str(file), style="red" if this_error else ""
0152 )
0153 )
0154 prog.advance(task)
0155 live.update(make_display())
0156 live.refresh()
0157
0158 if error:
0159 return 1
0160 else:
0161 return 0
0162
0163
0164 if __name__ == "__main__":
0165 sys.exit(main())