Skip to content

Commit 66668b5

Browse files
committed
[run-clang-tidy.py] Refactor, add progress indicator, add type hints
1 parent 6fedc18 commit 66668b5

File tree

1 file changed

+126
-122
lines changed

1 file changed

+126
-122
lines changed

clang-tools-extra/clang-tidy/tool/run-clang-tidy.py

Lines changed: 126 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,30 @@
3434
http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html
3535
"""
3636

37-
from __future__ import print_function
38-
3937
import argparse
38+
import asyncio
4039
import glob
4140
import json
4241
import multiprocessing
4342
import os
44-
import queue
4543
import re
4644
import shutil
4745
import subprocess
4846
import sys
4947
import tempfile
50-
import threading
5148
import traceback
49+
from types import ModuleType
50+
from typing import Any, Awaitable, Callable, List, Optional, Tuple, TypeVar
51+
5252

53+
yaml: Optional[ModuleType] = None
5354
try:
5455
import yaml
5556
except ImportError:
56-
yaml = None
57+
pass
5758

5859

59-
def strtobool(val):
60+
def strtobool(val: str) -> bool:
6061
"""Convert a string representation of truth to a bool following LLVM's CLI argument parsing."""
6162

6263
val = val.lower()
@@ -67,11 +68,11 @@ def strtobool(val):
6768

6869
# Return ArgumentTypeError so that argparse does not substitute its own error message
6970
raise argparse.ArgumentTypeError(
70-
"'{}' is invalid value for boolean argument! Try 0 or 1.".format(val)
71+
f"'{val}' is invalid value for boolean argument! Try 0 or 1."
7172
)
7273

7374

74-
def find_compilation_database(path):
75+
def find_compilation_database(path: str) -> str:
7576
"""Adjusts the directory until a compilation database is found."""
7677
result = os.path.realpath("./")
7778
while not os.path.isfile(os.path.join(result, path)):
@@ -83,30 +84,24 @@ def find_compilation_database(path):
8384
return result
8485

8586

86-
def make_absolute(f, directory):
87-
if os.path.isabs(f):
88-
return f
89-
return os.path.normpath(os.path.join(directory, f))
90-
91-
9287
def get_tidy_invocation(
93-
f,
94-
clang_tidy_binary,
95-
checks,
96-
tmpdir,
97-
build_path,
98-
header_filter,
99-
allow_enabling_alpha_checkers,
100-
extra_arg,
101-
extra_arg_before,
102-
quiet,
103-
config_file_path,
104-
config,
105-
line_filter,
106-
use_color,
107-
plugins,
108-
warnings_as_errors,
109-
):
88+
f: str,
89+
clang_tidy_binary: str,
90+
checks: str,
91+
tmpdir: Optional[str],
92+
build_path: str,
93+
header_filter: Optional[str],
94+
allow_enabling_alpha_checkers: bool,
95+
extra_arg: List[str],
96+
extra_arg_before: List[str],
97+
quiet: bool,
98+
config_file_path: str,
99+
config: str,
100+
line_filter: Optional[str],
101+
use_color: bool,
102+
plugins: List[str],
103+
warnings_as_errors: Optional[str],
104+
) -> List[str]:
110105
"""Gets a command line for clang-tidy."""
111106
start = [clang_tidy_binary]
112107
if allow_enabling_alpha_checkers:
@@ -130,9 +125,9 @@ def get_tidy_invocation(
130125
os.close(handle)
131126
start.append(name)
132127
for arg in extra_arg:
133-
start.append("-extra-arg=%s" % arg)
128+
start.append(f"-extra-arg={arg}")
134129
for arg in extra_arg_before:
135-
start.append("-extra-arg-before=%s" % arg)
130+
start.append(f"-extra-arg-before={arg}")
136131
start.append("-p=" + build_path)
137132
if quiet:
138133
start.append("-quiet")
@@ -148,8 +143,9 @@ def get_tidy_invocation(
148143
return start
149144

150145

151-
def merge_replacement_files(tmpdir, mergefile):
146+
def merge_replacement_files(tmpdir: str, mergefile: str) -> None:
152147
"""Merge all replacement files in a directory into a single file"""
148+
assert yaml
153149
# The fixes suggested by clang-tidy >= 4.0.0 are given under
154150
# the top level key 'Diagnostics' in the output yaml files
155151
mergekey = "Diagnostics"
@@ -173,29 +169,27 @@ def merge_replacement_files(tmpdir, mergefile):
173169
open(mergefile, "w").close()
174170

175171

176-
def find_binary(arg, name, build_path):
172+
def find_binary(arg: str, name: str, build_path: str) -> str:
177173
"""Get the path for a binary or exit"""
178174
if arg:
179175
if shutil.which(arg):
180176
return arg
181177
else:
182178
raise SystemExit(
183-
"error: passed binary '{}' was not found or is not executable".format(
184-
arg
185-
)
179+
f"error: passed binary '{arg}' was not found or is not executable"
186180
)
187181

188182
built_path = os.path.join(build_path, "bin", name)
189183
binary = shutil.which(name) or shutil.which(built_path)
190184
if binary:
191185
return binary
192186
else:
193-
raise SystemExit(
194-
"error: failed to find {} in $PATH or at {}".format(name, built_path)
195-
)
187+
raise SystemExit(f"error: failed to find {name} in $PATH or at {built_path}")
196188

197189

198-
def apply_fixes(args, clang_apply_replacements_binary, tmpdir):
190+
def apply_fixes(
191+
args: argparse.Namespace, clang_apply_replacements_binary: str, tmpdir: str
192+
) -> None:
199193
"""Calls clang-apply-fixes on a given directory."""
200194
invocation = [clang_apply_replacements_binary]
201195
invocation.append("-ignore-insert-conflict")
@@ -207,47 +201,59 @@ def apply_fixes(args, clang_apply_replacements_binary, tmpdir):
207201
subprocess.call(invocation)
208202

209203

210-
def run_tidy(args, clang_tidy_binary, tmpdir, build_path, queue, lock, failed_files):
211-
"""Takes filenames out of queue and runs clang-tidy on them."""
212-
while True:
213-
name = queue.get()
214-
invocation = get_tidy_invocation(
215-
name,
216-
clang_tidy_binary,
217-
args.checks,
218-
tmpdir,
219-
build_path,
220-
args.header_filter,
221-
args.allow_enabling_alpha_checkers,
222-
args.extra_arg,
223-
args.extra_arg_before,
224-
args.quiet,
225-
args.config_file,
226-
args.config,
227-
args.line_filter,
228-
args.use_color,
229-
args.plugins,
230-
args.warnings_as_errors,
231-
)
204+
# FIXME: From Python 3.12, this can be simplified out with run_with_semaphore[T](...).
205+
T = TypeVar("T")
206+
207+
208+
async def run_with_semaphore(
209+
semaphore: asyncio.Semaphore,
210+
f: Callable[..., Awaitable[T]],
211+
*args: Any,
212+
**kwargs: Any,
213+
) -> T:
214+
async with semaphore:
215+
return await f(*args, **kwargs)
216+
217+
218+
async def run_tidy(
219+
args: argparse.Namespace,
220+
name: str,
221+
clang_tidy_binary: str,
222+
tmpdir: str,
223+
build_path: str,
224+
) -> Tuple[str, int, str, str]:
225+
"""
226+
Runs clang-tidy on a single file and returns the result.
227+
The returned value is a tuple of the file name, return code, stdout and stderr.
228+
"""
229+
invocation = get_tidy_invocation(
230+
name,
231+
clang_tidy_binary,
232+
args.checks,
233+
tmpdir,
234+
build_path,
235+
args.header_filter,
236+
args.allow_enabling_alpha_checkers,
237+
args.extra_arg,
238+
args.extra_arg_before,
239+
args.quiet,
240+
args.config_file,
241+
args.config,
242+
args.line_filter,
243+
args.use_color,
244+
args.plugins,
245+
args.warnings_as_errors,
246+
)
232247

233-
proc = subprocess.Popen(
234-
invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE
235-
)
236-
output, err = proc.communicate()
237-
if proc.returncode != 0:
238-
if proc.returncode < 0:
239-
msg = "%s: terminated by signal %d\n" % (name, -proc.returncode)
240-
err += msg.encode("utf-8")
241-
failed_files.append(name)
242-
with lock:
243-
sys.stdout.write(" ".join(invocation) + "\n" + output.decode("utf-8"))
244-
if len(err) > 0:
245-
sys.stdout.flush()
246-
sys.stderr.write(err.decode("utf-8"))
247-
queue.task_done()
248-
249-
250-
def main():
248+
process = await asyncio.create_subprocess_exec(
249+
*invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE
250+
)
251+
stdout, stderr = await process.communicate()
252+
assert process.returncode is not None
253+
return name, process.returncode, stdout.decode("UTF-8"), stderr.decode("UTF-8")
254+
255+
256+
async def main() -> None:
251257
parser = argparse.ArgumentParser(
252258
description="Runs clang-tidy over all files "
253259
"in a compilation database. Requires "
@@ -408,7 +414,7 @@ def main():
408414
)
409415

410416
combine_fixes = False
411-
export_fixes_dir = None
417+
export_fixes_dir: Optional[str] = None
412418
delete_fixes_dir = False
413419
if args.export_fixes is not None:
414420
# if a directory is given, create it if it does not exist
@@ -464,10 +470,9 @@ def main():
464470
sys.exit(1)
465471

466472
# Load the database and extract all files.
467-
database = json.load(open(os.path.join(build_path, db_path)))
468-
files = set(
469-
[make_absolute(entry["file"], entry["directory"]) for entry in database]
470-
)
473+
with open(os.path.join(build_path, db_path)) as f:
474+
database = json.load(f)
475+
files = {os.path.abspath(os.path.join(e["directory"], e["file"])) for e in database}
471476

472477
# Filter source files from compilation database.
473478
if args.source_filter:
@@ -488,70 +493,69 @@ def main():
488493

489494
# Build up a big regexy filter from all command line arguments.
490495
file_name_re = re.compile("|".join(args.files))
496+
files = {f for f in files if file_name_re.search(f)}
491497

492-
return_code = 0
498+
returncode = 0
493499
try:
494-
# Spin up a bunch of tidy-launching threads.
495-
task_queue = queue.Queue(max_task)
496-
# List of files with a non-zero return code.
497-
failed_files = []
498-
lock = threading.Lock()
499-
for _ in range(max_task):
500-
t = threading.Thread(
501-
target=run_tidy,
502-
args=(
503-
args,
504-
clang_tidy_binary,
505-
export_fixes_dir,
506-
build_path,
507-
task_queue,
508-
lock,
509-
failed_files,
510-
),
500+
semaphore = asyncio.Semaphore(max_task)
501+
tasks = [
502+
run_with_semaphore(
503+
semaphore,
504+
run_tidy,
505+
args,
506+
f,
507+
clang_tidy_binary,
508+
export_fixes_dir,
509+
build_path,
511510
)
512-
t.daemon = True
513-
t.start()
514-
515-
# Fill the queue with files.
516-
for name in files:
517-
if file_name_re.search(name):
518-
task_queue.put(name)
519-
520-
# Wait for all threads to be done.
521-
task_queue.join()
522-
if len(failed_files):
523-
return_code = 1
524-
511+
for f in files
512+
]
513+
514+
for i, coro in enumerate(asyncio.as_completed(tasks)):
515+
name, process_returncode, stdout, stderr = await coro
516+
if process_returncode != 0:
517+
returncode = 1
518+
if process_returncode < 0:
519+
stderr += f"{name}: terminated by signal {-process_returncode}\n"
520+
print(f"[{i + 1}/{len(files)}] {name}")
521+
if stdout:
522+
print(stdout)
523+
if stderr:
524+
print(stderr, file=sys.stderr)
525525
except KeyboardInterrupt:
526526
# This is a sad hack. Unfortunately subprocess goes
527527
# bonkers with ctrl-c and we start forking merrily.
528528
print("\nCtrl-C detected, goodbye.")
529529
if delete_fixes_dir:
530+
assert export_fixes_dir
530531
shutil.rmtree(export_fixes_dir)
531532
os.kill(0, 9)
532533

533534
if combine_fixes:
534535
print("Writing fixes to " + args.export_fixes + " ...")
535536
try:
537+
assert export_fixes_dir
536538
merge_replacement_files(export_fixes_dir, args.export_fixes)
537539
except:
538540
print("Error exporting fixes.\n", file=sys.stderr)
539541
traceback.print_exc()
540-
return_code = 1
542+
returncode = 1
541543

542544
if args.fix:
543545
print("Applying fixes ...")
544546
try:
547+
assert export_fixes_dir
545548
apply_fixes(args, clang_apply_replacements_binary, export_fixes_dir)
546549
except:
547550
print("Error applying fixes.\n", file=sys.stderr)
548551
traceback.print_exc()
549-
return_code = 1
552+
returncode = 1
550553

551554
if delete_fixes_dir:
555+
assert export_fixes_dir
552556
shutil.rmtree(export_fixes_dir)
553-
sys.exit(return_code)
557+
sys.exit(returncode)
554558

555559

556560
if __name__ == "__main__":
557-
main()
561+
asyncio.run(main())

0 commit comments

Comments
 (0)