34
34
http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html
35
35
"""
36
36
37
- from __future__ import print_function
38
-
39
37
import argparse
38
+ import asyncio
40
39
import glob
41
40
import json
42
41
import multiprocessing
43
42
import os
44
- import queue
45
43
import re
46
44
import shutil
47
45
import subprocess
48
46
import sys
49
47
import tempfile
50
- import threading
51
48
import traceback
49
+ from types import ModuleType
50
+ from typing import Any , Awaitable , Callable , List , Optional , Tuple , TypeVar
51
+
52
52
53
+ yaml : Optional [ModuleType ] = None
53
54
try :
54
55
import yaml
55
56
except ImportError :
56
- yaml = None
57
+ pass
57
58
58
59
59
- def strtobool (val ) :
60
+ def strtobool (val : str ) -> bool :
60
61
"""Convert a string representation of truth to a bool following LLVM's CLI argument parsing."""
61
62
62
63
val = val .lower ()
@@ -67,11 +68,11 @@ def strtobool(val):
67
68
68
69
# Return ArgumentTypeError so that argparse does not substitute its own error message
69
70
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."
71
72
)
72
73
73
74
74
- def find_compilation_database (path ) :
75
+ def find_compilation_database (path : str ) -> str :
75
76
"""Adjusts the directory until a compilation database is found."""
76
77
result = os .path .realpath ("./" )
77
78
while not os .path .isfile (os .path .join (result , path )):
@@ -83,30 +84,24 @@ def find_compilation_database(path):
83
84
return result
84
85
85
86
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
-
92
87
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 ] :
110
105
"""Gets a command line for clang-tidy."""
111
106
start = [clang_tidy_binary ]
112
107
if allow_enabling_alpha_checkers :
@@ -130,9 +125,9 @@ def get_tidy_invocation(
130
125
os .close (handle )
131
126
start .append (name )
132
127
for arg in extra_arg :
133
- start .append ("-extra-arg=%s" % arg )
128
+ start .append (f "-extra-arg={ arg } " )
134
129
for arg in extra_arg_before :
135
- start .append ("-extra-arg-before=%s" % arg )
130
+ start .append (f "-extra-arg-before={ arg } " )
136
131
start .append ("-p=" + build_path )
137
132
if quiet :
138
133
start .append ("-quiet" )
@@ -148,8 +143,9 @@ def get_tidy_invocation(
148
143
return start
149
144
150
145
151
- def merge_replacement_files (tmpdir , mergefile ) :
146
+ def merge_replacement_files (tmpdir : str , mergefile : str ) -> None :
152
147
"""Merge all replacement files in a directory into a single file"""
148
+ assert yaml
153
149
# The fixes suggested by clang-tidy >= 4.0.0 are given under
154
150
# the top level key 'Diagnostics' in the output yaml files
155
151
mergekey = "Diagnostics"
@@ -173,29 +169,27 @@ def merge_replacement_files(tmpdir, mergefile):
173
169
open (mergefile , "w" ).close ()
174
170
175
171
176
- def find_binary (arg , name , build_path ) :
172
+ def find_binary (arg : str , name : str , build_path : str ) -> str :
177
173
"""Get the path for a binary or exit"""
178
174
if arg :
179
175
if shutil .which (arg ):
180
176
return arg
181
177
else :
182
178
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"
186
180
)
187
181
188
182
built_path = os .path .join (build_path , "bin" , name )
189
183
binary = shutil .which (name ) or shutil .which (built_path )
190
184
if binary :
191
185
return binary
192
186
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 } " )
196
188
197
189
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 :
199
193
"""Calls clang-apply-fixes on a given directory."""
200
194
invocation = [clang_apply_replacements_binary ]
201
195
invocation .append ("-ignore-insert-conflict" )
@@ -207,47 +201,59 @@ def apply_fixes(args, clang_apply_replacements_binary, tmpdir):
207
201
subprocess .call (invocation )
208
202
209
203
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
+ )
232
247
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 :
251
257
parser = argparse .ArgumentParser (
252
258
description = "Runs clang-tidy over all files "
253
259
"in a compilation database. Requires "
@@ -408,7 +414,7 @@ def main():
408
414
)
409
415
410
416
combine_fixes = False
411
- export_fixes_dir = None
417
+ export_fixes_dir : Optional [ str ] = None
412
418
delete_fixes_dir = False
413
419
if args .export_fixes is not None :
414
420
# if a directory is given, create it if it does not exist
@@ -464,10 +470,9 @@ def main():
464
470
sys .exit (1 )
465
471
466
472
# 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 }
471
476
472
477
# Filter source files from compilation database.
473
478
if args .source_filter :
@@ -488,70 +493,69 @@ def main():
488
493
489
494
# Build up a big regexy filter from all command line arguments.
490
495
file_name_re = re .compile ("|" .join (args .files ))
496
+ files = {f for f in files if file_name_re .search (f )}
491
497
492
- return_code = 0
498
+ returncode = 0
493
499
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 ,
511
510
)
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 )
525
525
except KeyboardInterrupt :
526
526
# This is a sad hack. Unfortunately subprocess goes
527
527
# bonkers with ctrl-c and we start forking merrily.
528
528
print ("\n Ctrl-C detected, goodbye." )
529
529
if delete_fixes_dir :
530
+ assert export_fixes_dir
530
531
shutil .rmtree (export_fixes_dir )
531
532
os .kill (0 , 9 )
532
533
533
534
if combine_fixes :
534
535
print ("Writing fixes to " + args .export_fixes + " ..." )
535
536
try :
537
+ assert export_fixes_dir
536
538
merge_replacement_files (export_fixes_dir , args .export_fixes )
537
539
except :
538
540
print ("Error exporting fixes.\n " , file = sys .stderr )
539
541
traceback .print_exc ()
540
- return_code = 1
542
+ returncode = 1
541
543
542
544
if args .fix :
543
545
print ("Applying fixes ..." )
544
546
try :
547
+ assert export_fixes_dir
545
548
apply_fixes (args , clang_apply_replacements_binary , export_fixes_dir )
546
549
except :
547
550
print ("Error applying fixes.\n " , file = sys .stderr )
548
551
traceback .print_exc ()
549
- return_code = 1
552
+ returncode = 1
550
553
551
554
if delete_fixes_dir :
555
+ assert export_fixes_dir
552
556
shutil .rmtree (export_fixes_dir )
553
- sys .exit (return_code )
557
+ sys .exit (returncode )
554
558
555
559
556
560
if __name__ == "__main__" :
557
- main ()
561
+ asyncio . run ( main () )
0 commit comments