BmnRoot
Loading...
Searching...
No Matches
run-clang-format.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""A wrapper script around clang-format, suitable for linting multiple files
3and to use for continuous integration.
4
5This is an alternative API for the clang-format command line.
6It runs over multiple files and directories in parallel.
7A diff output is produced and a sensible exit code is returned.
8
9Adopted from https://github.com/Sarcasm/run-clang-format/tree/master
10"""
11
12from __future__ import print_function, unicode_literals
13
14import argparse
15import codecs
16import difflib
17import fnmatch
18import io
19import errno
20import multiprocessing
21import os
22import signal
23import subprocess
24import sys
25import traceback
26
27from functools import partial
28
29try:
30 from subprocess import DEVNULL # py3k
31except ImportError:
32 DEVNULL = open(os.devnull, "wb")
33
34
35DEFAULT_EXTENSIONS = 'c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx'
36DEFAULT_CLANG_FORMAT_IGNORE = '.clang-format-ignore'
37
38
40 SUCCESS = 0
41 DIFF = 1
42 TROUBLE = 2
43
44def excludes_from_file(ignore_file):
45 excludes = []
46 try:
47 with io.open(ignore_file, 'r', encoding='utf-8') as f:
48 for line in f:
49 if line.startswith('#'):
50 # ignore comments
51 continue
52 pattern = line.rstrip()
53 if not pattern:
54 # allow empty lines
55 continue
56 excludes.append(pattern)
57 except EnvironmentError as e:
58 if e.errno != errno.ENOENT:
59 raise
60 return excludes;
61
62def list_files(files, recursive=False, extensions=None, exclude=None):
63 if extensions is None:
64 extensions = []
65 if exclude is None:
66 exclude = []
67
68 out = []
69 for file in files:
70 if recursive and os.path.isdir(file):
71 for dirpath, dnames, fnames in os.walk(file):
72 fpaths = [os.path.join(dirpath, fname) for fname in fnames]
73 for pattern in exclude:
74 # os.walk() supports trimming down the dnames list
75 # by modifying it in-place,
76 # to avoid unnecessary directory listings.
77 dnames[:] = [
78 x for x in dnames
79 if
80 not fnmatch.fnmatch(os.path.join(dirpath, x), pattern)
81 ]
82 fpaths = [
83 x for x in fpaths if not fnmatch.fnmatch(x, pattern)
84 ]
85 for f in fpaths:
86 ext = os.path.splitext(f)[1][1:]
87 if ext in extensions:
88 out.append(f)
89 else:
90 out.append(file)
91 return out
92
93
94def make_diff(file, original, reformatted):
95 return list(
96 difflib.unified_diff(
97 original,
98 reformatted,
99 fromfile='{}\t(original)'.format(file),
100 tofile='{}\t(reformatted)'.format(file),
101 n=3))
102
103
104class DiffError(Exception):
105 def __init__(self, message, errs=None):
106 super(DiffError, self).__init__(message)
107 self.errs = errs or []
108
109
110class UnexpectedError(Exception):
111 def __init__(self, message, exc=None):
112 super(UnexpectedError, self).__init__(message)
113 self.formatted_traceback = traceback.format_exc()
114 self.exc = exc
115
116
118 try:
119 ret = run_clang_format_diff(args, file)
120 return ret
121 except DiffError:
122 raise
123 except Exception as e:
124 raise UnexpectedError('{}: {}: {}'.format(file, e.__class__.__name__,
125 e), e)
126
127
128def run_clang_format_diff(args, file):
129 try:
130 with io.open(file, 'r', encoding='utf-8') as f:
131 original = f.readlines()
132 except IOError as exc:
133 raise DiffError(str(exc))
134
135 if args.in_place:
136 invocation = [args.clang_format_executable, '-i', file]
137 else:
138 invocation = [args.clang_format_executable, file]
139
140 if args.style:
141 invocation.extend(['--style', args.style])
142
143 if args.dry_run:
144 print(" ".join(invocation))
145 return [], []
146
147 # Use of utf-8 to decode the process output.
148 #
149 # Hopefully, this is the correct thing to do.
150 #
151 # It's done due to the following assumptions (which may be incorrect):
152 # - clang-format will returns the bytes read from the files as-is,
153 # without conversion, and it is already assumed that the files use utf-8.
154 # - if the diagnostics were internationalized, they would use utf-8:
155 # > Adding Translations to Clang
156 # >
157 # > Not possible yet!
158 # > Diagnostic strings should be written in UTF-8,
159 # > the client can translate to the relevant code page if needed.
160 # > Each translation completely replaces the format string
161 # > for the diagnostic.
162 # > -- http://clang.llvm.org/docs/InternalsManual.html#internals-diag-translation
163 #
164 # It's not pretty, due to Python 2 & 3 compatibility.
165 encoding_py3 = {}
166 if sys.version_info[0] >= 3:
167 encoding_py3['encoding'] = 'utf-8'
168
169 try:
170 proc = subprocess.Popen(
171 invocation,
172 stdout=subprocess.PIPE,
173 stderr=subprocess.PIPE,
174 universal_newlines=True,
175 **encoding_py3)
176 except OSError as exc:
177 raise DiffError(
178 "Command '{}' failed to start: {}".format(
179 subprocess.list2cmdline(invocation), exc
180 )
181 )
182 proc_stdout = proc.stdout
183 proc_stderr = proc.stderr
184 if sys.version_info[0] < 3:
185 # make the pipes compatible with Python 3,
186 # reading lines should output unicode
187 encoding = 'utf-8'
188 proc_stdout = codecs.getreader(encoding)(proc_stdout)
189 proc_stderr = codecs.getreader(encoding)(proc_stderr)
190 # hopefully the stderr pipe won't get full and block the process
191 outs = list(proc_stdout.readlines())
192 errs = list(proc_stderr.readlines())
193 proc.wait()
194 if proc.returncode:
195 raise DiffError(
196 "Command '{}' returned non-zero exit status {}".format(
197 subprocess.list2cmdline(invocation), proc.returncode
198 ),
199 errs,
200 )
201 if args.in_place:
202 return [], errs
203 return make_diff(file, original, outs), errs
204
205
206def bold_red(s):
207 return '\x1b[1m\x1b[31m' + s + '\x1b[0m'
208
209
210def colorize(diff_lines):
211 def bold(s):
212 return '\x1b[1m' + s + '\x1b[0m'
213
214 def cyan(s):
215 return '\x1b[36m' + s + '\x1b[0m'
216
217 def green(s):
218 return '\x1b[32m' + s + '\x1b[0m'
219
220 def red(s):
221 return '\x1b[31m' + s + '\x1b[0m'
222
223 for line in diff_lines:
224 if line[:4] in ['--- ', '+++ ']:
225 yield bold(line)
226 elif line.startswith('@@ '):
227 yield cyan(line)
228 elif line.startswith('+'):
229 yield green(line)
230 elif line.startswith('-'):
231 yield red(line)
232 else:
233 yield line
234
235
236def print_diff(diff_lines, use_color):
237 if use_color:
238 diff_lines = colorize(diff_lines)
239 if sys.version_info[0] < 3:
240 sys.stdout.writelines((l.encode('utf-8') for l in diff_lines))
241 else:
242 sys.stdout.writelines(diff_lines)
243
244
245def print_trouble(prog, message, use_colors):
246 error_text = 'error:'
247 if use_colors:
248 error_text = bold_red(error_text)
249 print("{}: {} {}".format(prog, error_text, message), file=sys.stderr)
250
251
252def main():
253 parser = argparse.ArgumentParser(description=__doc__)
254 parser.add_argument(
255 '--clang-format-executable',
256 metavar='EXECUTABLE',
257 help='path to the clang-format executable',
258 default='clang-format')
259 parser.add_argument(
260 '--extensions',
261 help='comma separated list of file extensions (default: {})'.format(
262 DEFAULT_EXTENSIONS),
263 default=DEFAULT_EXTENSIONS)
264 parser.add_argument(
265 '-r',
266 '--recursive',
267 action='store_true',
268 help='run recursively over directories')
269 parser.add_argument(
270 '-d',
271 '--dry-run',
272 action='store_true',
273 help='just print the list of files')
274 parser.add_argument(
275 '-i',
276 '--in-place',
277 action='store_true',
278 help='format file instead of printing differences')
279 parser.add_argument('files', metavar='file', nargs='+')
280 parser.add_argument(
281 '-q',
282 '--quiet',
283 action='store_true',
284 help="disable output, useful for the exit code")
285 parser.add_argument(
286 '-j',
287 metavar='N',
288 type=int,
289 default=0,
290 help='run N clang-format jobs in parallel'
291 ' (default number of cpus + 1)')
292 parser.add_argument(
293 '--color',
294 default='auto',
295 choices=['auto', 'always', 'never'],
296 help='show colored diff (default: auto)')
297 parser.add_argument(
298 '-e',
299 '--exclude',
300 metavar='PATTERN',
301 action='append',
302 default=[],
303 help='exclude paths matching the given glob-like pattern(s)'
304 ' from recursive search')
305 parser.add_argument(
306 '--style',
307 help='formatting style to apply (LLVM, Google, Chromium, Mozilla, WebKit)')
308
309 args = parser.parse_args()
310
311 # use default signal handling, like diff return SIGINT value on ^C
312 # https://bugs.python.org/issue14229#msg156446
313 signal.signal(signal.SIGINT, signal.SIG_DFL)
314 try:
315 signal.SIGPIPE
316 except AttributeError:
317 # compatibility, SIGPIPE does not exist on Windows
318 pass
319 else:
320 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
321
322 colored_stdout = False
323 colored_stderr = False
324 if args.color == 'always':
325 colored_stdout = True
326 colored_stderr = True
327 elif args.color == 'auto':
328 colored_stdout = sys.stdout.isatty()
329 colored_stderr = sys.stderr.isatty()
330
331 version_invocation = [args.clang_format_executable, str("--version")]
332 try:
333 subprocess.check_call(version_invocation, stdout=DEVNULL)
334 except subprocess.CalledProcessError as e:
335 print_trouble(parser.prog, str(e), use_colors=colored_stderr)
336 return ExitStatus.TROUBLE
337 except OSError as e:
339 parser.prog,
340 "Command '{}' failed to start: {}".format(
341 subprocess.list2cmdline(version_invocation), e
342 ),
343 use_colors=colored_stderr,
344 )
345 return ExitStatus.TROUBLE
346
347 retcode = ExitStatus.SUCCESS
348
349 excludes = excludes_from_file(DEFAULT_CLANG_FORMAT_IGNORE)
350 excludes.extend(args.exclude)
351
352 files = list_files(
353 args.files,
354 recursive=args.recursive,
355 exclude=excludes,
356 extensions=args.extensions.split(','))
357
358 if not files:
359 return
360
361 njobs = args.j
362 if njobs == 0:
363 njobs = multiprocessing.cpu_count() + 1
364 njobs = min(len(files), njobs)
365
366 if njobs == 1:
367 # execute directly instead of in a pool,
368 # less overhead, simpler stacktraces
369 it = (run_clang_format_diff_wrapper(args, file) for file in files)
370 pool = None
371 else:
372 pool = multiprocessing.Pool(njobs)
373 it = pool.imap_unordered(
374 partial(run_clang_format_diff_wrapper, args), files)
375 pool.close()
376 while True:
377 try:
378 outs, errs = next(it)
379 except StopIteration:
380 break
381 except DiffError as e:
382 print_trouble(parser.prog, str(e), use_colors=colored_stderr)
383 retcode = ExitStatus.TROUBLE
384 sys.stderr.writelines(e.errs)
385 except UnexpectedError as e:
386 print_trouble(parser.prog, str(e), use_colors=colored_stderr)
387 sys.stderr.write(e.formatted_traceback)
388 retcode = ExitStatus.TROUBLE
389 # stop at the first unexpected error,
390 # something could be very wrong,
391 # don't process all files unnecessarily
392 if pool:
393 pool.terminate()
394 break
395 else:
396 sys.stderr.writelines(errs)
397 if outs == []:
398 continue
399 if not args.quiet:
400 print_diff(outs, use_color=colored_stdout)
401 if retcode == ExitStatus.SUCCESS:
402 retcode = ExitStatus.DIFF
403 if pool:
404 pool.join()
405 return retcode
406
407
408if __name__ == '__main__':
409 sys.exit(main())
friend F32vec4 min(const F32vec4 &a, const F32vec4 &b)
Definition P4_F32vec4.h:30
__init__(self, message, errs=None)
__init__(self, message, exc=None)
run_clang_format_diff_wrapper(args, file)
list_files(files, recursive=False, extensions=None, exclude=None)
make_diff(file, original, reformatted)
excludes_from_file(ignore_file)
run_clang_format_diff(args, file)
print_diff(diff_lines, use_color)
print_trouble(prog, message, use_colors)