A Discrete-Event Network Simulator
API
check-style-clang-format.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2
3# Copyright (c) 2022 Eduardo Nuno Almeida.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 2 as
7# published by the Free Software Foundation;
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17#
18# Author: Eduardo Nuno Almeida <enmsa@outlook.pt> [INESC TEC and FEUP, Portugal]
19
20"""
21Check and apply the ns-3 coding style to all files in the PATH argument.
22
23The coding style is defined with the clang-format tool, whose definitions are in
24the ".clang-format" file. This script performs the following checks / fixes:
25- Check / apply clang-format.
26- Check / trim trailing whitespace.
27- Check / replace tabs with spaces.
28
29The clang-format and tabs checks respect clang-format guards, which mark code blocks
30that should not be checked. Trailing whitespace is always checked regardless of
31clang-format guards.
32
33This script can be applied to all text files in a given path or to individual files.
34
35NOTE: The formatting check requires clang-format (version >= 14) to be found on the path.
36Trimming of trailing whitespace and conversion of tabs to spaces (via the "--no-formatting"
37option) do not depend on clang-format.
38"""
39
40import argparse
41import concurrent.futures
42import itertools
43import os
44import shutil
45import subprocess
46import sys
47from typing import List, Tuple
48
49
50
53CLANG_FORMAT_VERSIONS = [
54 16,
55 15,
56 14,
57]
58
59CLANG_FORMAT_GUARD_ON = '// clang-format on'
60CLANG_FORMAT_GUARD_OFF = '// clang-format off'
61
62DIRECTORIES_TO_SKIP = [
63 '__pycache__',
64 '.vscode',
65 'bindings',
66 'build',
67 'cmake-cache',
68 'testpy-output',
69]
70
71# List of files entirely copied from elsewhere that should not be checked,
72# in order to optimize the performance of this script
73FILES_TO_SKIP = [
74 'valgrind.h',
75]
76
77FILE_EXTENSIONS_TO_CHECK_FORMATTING = [
78 '.c',
79 '.cc',
80 '.h',
81]
82
83FILE_EXTENSIONS_TO_CHECK_WHITESPACE = [
84 '.c',
85 '.cc',
86 '.click',
87 '.cmake',
88 '.conf',
89 '.css',
90 '.dot',
91 '.gnuplot',
92 '.gp',
93 '.h',
94 '.html',
95 '.js',
96 '.json',
97 '.m',
98 '.md',
99 '.mob',
100 '.ns_params',
101 '.ns_movements',
102 '.params',
103 '.pl',
104 '.plt',
105 '.py',
106 '.rst',
107 '.seqdiag',
108 '.sh',
109 '.txt',
110 '.yml',
111]
112
113FILES_TO_CHECK_WHITESPACE = [
114 'Makefile',
115 'ns3',
116]
117
118FILE_EXTENSIONS_TO_CHECK_TABS = [
119 '.c',
120 '.cc',
121 '.h',
122 '.md',
123 '.py',
124 '.rst',
125 '.sh',
126 '.yml',
127]
128TAB_SIZE = 4
129
130
131
134def skip_directory(dirpath: str) -> bool:
135 """
136 Check if a directory should be skipped.
137
138 @param dirpath Directory path.
139 @return Whether the directory should be skipped or not.
140 """
141
142 _, directory = os.path.split(dirpath)
143
144 return (directory in DIRECTORIES_TO_SKIP or
145 (directory.startswith('.') and directory != '.'))
146
147
148def skip_file_formatting(path: str) -> bool:
149 """
150 Check if a file should be skipped from formatting analysis.
151
152 @param path Path to the file.
153 @return Whether the file should be skipped or not.
154 """
155
156 filename = os.path.split(path)[1]
157
158 if filename in FILES_TO_SKIP:
159 return True
160
161 _, extension = os.path.splitext(filename)
162
163 return extension not in FILE_EXTENSIONS_TO_CHECK_FORMATTING
164
165
166def skip_file_whitespace(path: str) -> bool:
167 """
168 Check if a file should be skipped from trailing whitespace analysis.
169
170 @param path Path to the file.
171 @return Whether the file should be skipped or not.
172 """
173
174 filename = os.path.split(path)[1]
175
176 if filename in FILES_TO_SKIP:
177 return True
178
179 basename, extension = os.path.splitext(filename)
180
181 return (basename not in FILES_TO_CHECK_WHITESPACE and
182 extension not in FILE_EXTENSIONS_TO_CHECK_WHITESPACE)
183
184
185def skip_file_tabs(path: str) -> bool:
186 """
187 Check if a file should be skipped from tabs analysis.
188
189 @param path Path to the file.
190 @return Whether the file should be skipped or not.
191 """
192
193 filename = os.path.split(path)[1]
194
195 if filename in FILES_TO_SKIP:
196 return True
197
198 _, extension = os.path.splitext(filename)
199
200 return extension not in FILE_EXTENSIONS_TO_CHECK_TABS
201
202
203def find_files_to_check_style(path: str) -> Tuple[List[str], List[str], List[str]]:
204 """
205 Find all files to be checked in a given path.
206
207 @param path Path to check.
208 @return Tuple [List of files to check formatting,
209 List of files to check trailing whitespace,
210 List of files to check tabs].
211 """
212
213 files_to_check_formatting: List[str] = []
214 files_to_check_whitespace: List[str] = []
215 files_to_check_tabs: List[str] = []
216
217 abs_path = os.path.normpath(os.path.abspath(os.path.expanduser(path)))
218
219 if os.path.isfile(abs_path):
220 if not skip_file_formatting(path):
221 files_to_check_formatting.append(path)
222
223 if not skip_file_whitespace(path):
224 files_to_check_whitespace.append(path)
225
226 if not skip_file_tabs(path):
227 files_to_check_tabs.append(path)
228
229 elif os.path.isdir(abs_path):
230 for dirpath, dirnames, filenames in os.walk(path, topdown=True):
231 if skip_directory(dirpath):
232 # Remove directory and its subdirectories
233 dirnames[:] = []
234 continue
235
236 filenames = [os.path.join(dirpath, f) for f in filenames]
237
238 for f in filenames:
239 if not skip_file_formatting(f):
240 files_to_check_formatting.append(f)
241
242 if not skip_file_whitespace(f):
243 files_to_check_whitespace.append(f)
244
245 if not skip_file_tabs(f):
246 files_to_check_tabs.append(f)
247
248 else:
249 raise ValueError(f'Error: {path} is not a file nor a directory')
250
251 return (
252 files_to_check_formatting,
253 files_to_check_whitespace,
254 files_to_check_tabs,
255 )
256
257
259 """
260 Find the path to one of the supported versions of clang-format.
261 If no supported version of clang-format is found, raise an exception.
262
263 @return Path to clang-format.
264 """
265
266 # Find exact version
267 for version in CLANG_FORMAT_VERSIONS:
268 clang_format_path = shutil.which(f'clang-format-{version}')
269
270 if clang_format_path:
271 return clang_format_path
272
273 # Find default version and check if it is supported
274 clang_format_path = shutil.which('clang-format')
275
276 if clang_format_path:
277 process = subprocess.run(
278 [clang_format_path, '--version'],
279 capture_output=True,
280 text=True,
281 check=True,
282 )
283
284 version = process.stdout.strip().split(' ')[-1]
285 major_version = int(version.split('.')[0])
286
287 if major_version in CLANG_FORMAT_VERSIONS:
288 return clang_format_path
289
290 # No supported version of clang-format found
291 raise RuntimeError(
292 f'Could not find any supported version of clang-format installed on this system. '
293 f'List of supported versions: {CLANG_FORMAT_VERSIONS}.'
294 )
295
296
297
300def check_style(path: str,
301 enable_check_formatting: bool,
302 enable_check_whitespace: bool,
303 enable_check_tabs: bool,
304 fix: bool,
305 n_jobs: int = 1,
306 ) -> None:
307 """
308 Check / fix the coding style of a list of files, including formatting and
309 trailing whitespace.
310
311 @param path Path to the files.
312 @param fix Whether to fix the style of the file (True) or
313 just check if the file is well-formatted (False).
314 @param enable_check_formatting Whether to enable code formatting checking.
315 @param enable_check_whitespace Whether to enable trailing whitespace checking.
316 @param enable_check_tabs Whether to enable tabs checking.
317 @param n_jobs Number of parallel jobs.
318 """
319
320 (files_to_check_formatting,
321 files_to_check_whitespace,
322 files_to_check_tabs) = find_files_to_check_style(path)
323
324 check_formatting_successful = True
325 check_whitespace_successful = True
326 check_tabs_successful = True
327
328 if enable_check_formatting:
329 check_formatting_successful = check_formatting(
330 files_to_check_formatting, fix, n_jobs)
331
332 print('')
333
334 if enable_check_whitespace:
335 check_whitespace_successful = check_trailing_whitespace(
336 files_to_check_whitespace, fix, n_jobs)
337
338 print('')
339
340 if enable_check_tabs:
341 check_tabs_successful = check_tabs(
342 files_to_check_tabs, fix, n_jobs)
343
344 if check_formatting_successful and \
345 check_whitespace_successful and \
346 check_tabs_successful:
347 sys.exit(0)
348 else:
349 sys.exit(1)
350
351
352
355def check_formatting(filenames: List[str], fix: bool, n_jobs: int) -> bool:
356 """
357 Check / fix the coding style of a list of files with clang-format.
358
359 @param filenames List of filenames to be checked.
360 @param fix Whether to fix the formatting of the file (True) or
361 just check if the file is well-formatted (False).
362 @param n_jobs Number of parallel jobs.
363 @return True if all files are well formatted after the check process.
364 False if there are non-formatted files after the check process.
365 """
366
367 # Check files
368 clang_format_path = find_clang_format_path()
369 files_not_formatted: List[str] = []
370
371 with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
372 files_not_formatted_results = executor.map(
373 check_formatting_file,
374 filenames,
375 itertools.repeat(clang_format_path),
376 itertools.repeat(fix),
377 )
378
379 for (filename, formatted) in files_not_formatted_results:
380 if not formatted:
381 files_not_formatted.append(filename)
382
383 files_not_formatted.sort()
384
385 # Output results
386 if not files_not_formatted:
387 print('All files are well formatted')
388 return True
389
390 else:
391 n_non_formatted_files = len(files_not_formatted)
392
393 if fix:
394 print(f'Fixed formatting of the files ({n_non_formatted_files}):')
395 else:
396 print(f'Detected bad formatting in the files ({n_non_formatted_files}):')
397
398 for f in files_not_formatted:
399 print(f'- {f}')
400
401 # Return True if all files were fixed
402 return fix
403
404
405def check_formatting_file(filename: str,
406 clang_format_path: str,
407 fix: bool,
408 ) -> Tuple[str, bool]:
409 """
410 Check / fix the coding style of a file with clang-format.
411
412 @param filename Name of the file to be checked.
413 @param clang_format_path Path to clang-format.
414 @param fix Whether to fix the style of the file (True) or
415 just check if the file is well-formatted (False).
416 @return Tuple [Filename, Whether the file is well-formatted].
417 """
418
419 # Check if the file is well formatted
420 process = subprocess.run(
421 [
422 clang_format_path,
423 filename,
424 '-style=file',
425 '--dry-run',
426 '--Werror',
427 # Optimization: Only 1 error is needed to check that the file is not formatted
428 '--ferror-limit=1',
429 ],
430 check=False,
431 stdout=subprocess.DEVNULL,
432 stderr=subprocess.DEVNULL,
433 )
434
435 file_formatted = (process.returncode == 0)
436
437 # Fix file
438 if fix and not file_formatted:
439 process = subprocess.run(
440 [
441 clang_format_path,
442 filename,
443 '-style=file',
444 '-i',
445 ],
446 check=False,
447 stdout=subprocess.DEVNULL,
448 stderr=subprocess.DEVNULL,
449 )
450
451 return (filename, file_formatted)
452
453
454
457def check_trailing_whitespace(filenames: List[str], fix: bool, n_jobs: int) -> bool:
458 """
459 Check / fix trailing whitespace in a list of files.
460
461 @param filename Name of the file to be checked.
462 @param fix Whether to fix the file (True) or
463 just check if it has trailing whitespace (False).
464 @param n_jobs Number of parallel jobs.
465 @return True if no files have trailing whitespace after the check process.
466 False if there are trailing whitespace after the check process.
467 """
468
469 # Check files
470 files_with_whitespace: List[str] = []
471
472 with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
473 files_with_whitespace_results = executor.map(
474 check_trailing_whitespace_file,
475 filenames,
476 itertools.repeat(fix),
477 )
478
479 for (filename, has_whitespace) in files_with_whitespace_results:
480 if has_whitespace:
481 files_with_whitespace.append(filename)
482
483 files_with_whitespace.sort()
484
485 # Output results
486 if not files_with_whitespace:
487 print('No files detected with trailing whitespace')
488 return True
489
490 else:
491 n_files_with_whitespace = len(files_with_whitespace)
492
493 if fix:
494 print(
495 f'Fixed trailing whitespace in the files ({n_files_with_whitespace}):')
496 else:
497 print(
498 f'Detected trailing whitespace in the files ({n_files_with_whitespace}):')
499
500 for f in files_with_whitespace:
501 print(f'- {f}')
502
503 # If all files were fixed, there are no more trailing whitespace
504 return fix
505
506
507def check_trailing_whitespace_file(filename: str, fix: bool) -> Tuple[str, bool]:
508 """
509 Check / fix trailing whitespace in a file.
510
511 @param filename Name of the file to be checked.
512 @param fix Whether to fix the file (True) or
513 just check if it has trailing whitespace (False).
514 @return Tuple [Filename, Whether the file has trailing whitespace].
515 """
516
517 has_trailing_whitespace = False
518
519 with open(filename, 'r', encoding='utf-8') as f:
520 file_lines = f.readlines()
521
522 # Check if there are trailing whitespace and fix them
523 for (i, line) in enumerate(file_lines):
524 line_fixed = line.rstrip() + '\n'
525
526 if line_fixed != line:
527 has_trailing_whitespace = True
528
529 # Optimization: if only checking, skip the rest of the file
530 if not fix:
531 break
532
533 file_lines[i] = line_fixed
534
535 # Update file with the fixed lines
536 if fix and has_trailing_whitespace:
537 with open(filename, 'w', encoding='utf-8') as f:
538 f.writelines(file_lines)
539
540 return (filename, has_trailing_whitespace)
541
542
543
546def check_tabs(filenames: List[str], fix: bool, n_jobs: int) -> bool:
547 """
548 Check / fix tabs in a list of files.
549
550 @param filename Name of the file to be checked.
551 @param fix Whether to fix the file (True) or just check if it has tabs (False).
552 @param n_jobs Number of parallel jobs.
553 @return True if no files have tabs after the check process.
554 False if there are tabs after the check process.
555 """
556
557 # Check files
558 files_with_tabs: List[str] = []
559
560 with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
561 files_with_tabs_results = executor.map(
562 check_tabs_file,
563 filenames,
564 itertools.repeat(fix),
565 )
566
567 for (filename, has_tabs) in files_with_tabs_results:
568 if has_tabs:
569 files_with_tabs.append(filename)
570
571 files_with_tabs.sort()
572
573 # Output results
574 if not files_with_tabs:
575 print('No files detected with tabs')
576 return True
577
578 else:
579 n_files_with_tabs = len(files_with_tabs)
580
581 if fix:
582 print(
583 f'Fixed tabs in the files ({n_files_with_tabs}):')
584 else:
585 print(
586 f'Detected tabs in the files ({n_files_with_tabs}):')
587
588 for f in files_with_tabs:
589 print(f'- {f}')
590
591 # If all files were fixed, there are no more trailing whitespace
592 return fix
593
594
595def check_tabs_file(filename: str, fix: bool) -> Tuple[str, bool]:
596 """
597 Check / fix tabs in a file.
598
599 @param filename Name of the file to be checked.
600 @param fix Whether to fix the file (True) or just check if it has tabs (False).
601 @return Tuple [Filename, Whether the file has tabs].
602 """
603
604 has_tabs = False
605 clang_format_enabled = True
606
607 with open(filename, 'r', encoding='utf-8') as f:
608 file_lines = f.readlines()
609
610 for (i, line) in enumerate(file_lines):
611
612 # Check clang-format guards
613 line_stripped = line.strip()
614
615 if line_stripped == CLANG_FORMAT_GUARD_ON:
616 clang_format_enabled = True
617 elif line_stripped == CLANG_FORMAT_GUARD_OFF:
618 clang_format_enabled = False
619
620 if (not clang_format_enabled and
621 line_stripped not in (CLANG_FORMAT_GUARD_ON, CLANG_FORMAT_GUARD_OFF)):
622 continue
623
624 # Check if there are tabs and fix them
625 if line.find('\t') != -1:
626 has_tabs = True
627
628 # Optimization: if only checking, skip the rest of the file
629 if not fix:
630 break
631
632 file_lines[i] = line.expandtabs(TAB_SIZE)
633
634 # Update file with the fixed lines
635 if fix and has_tabs:
636 with open(filename, 'w', encoding='utf-8') as f:
637 f.writelines(file_lines)
638
639 return (filename, has_tabs)
640
641
642
645if __name__ == '__main__':
646
647 parser = argparse.ArgumentParser(
648 description='Check and apply the ns-3 coding style to all files in a given PATH. '
649 'The script checks the formatting of the file with clang-format. '
650 'Additionally, it checks the presence of trailing whitespace and tabs. '
651 'Formatting and tabs checks respect clang-format guards. '
652 'When used in "check mode" (default), the script checks if all files are well '
653 'formatted and do not have trailing whitespace nor tabs. '
654 'If it detects non-formatted files, they will be printed and this process exits with a '
655 'non-zero code. When used in "fix mode", this script automatically fixes the files.')
656
657 parser.add_argument('path', action='store', type=str,
658 help='Path to the files to check')
659
660 parser.add_argument('--no-formatting', action='store_true',
661 help='Do not check / fix code formatting')
662
663 parser.add_argument('--no-whitespace', action='store_true',
664 help='Do not check / fix trailing whitespace')
665
666 parser.add_argument('--no-tabs', action='store_true',
667 help='Do not check / fix tabs')
668
669 parser.add_argument('--fix', action='store_true',
670 help='Fix coding style issues detected in the files')
671
672 parser.add_argument('-j', '--jobs', type=int, default=max(1, os.cpu_count() - 1),
673 help='Number of parallel jobs')
674
675 args = parser.parse_args()
676
677 try:
679 path=args.path,
680 enable_check_formatting=(not args.no_formatting),
681 enable_check_whitespace=(not args.no_whitespace),
682 enable_check_tabs=(not args.no_tabs),
683 fix=args.fix,
684 n_jobs=args.jobs,
685 )
686
687 except Exception as e:
688 print(e)
689 sys.exit(1)
#define max(a, b)
Definition: 80211b.c:43
Tuple[str, bool] check_tabs_file(str filename, bool fix)
bool check_trailing_whitespace(List[str] filenames, bool fix, int n_jobs)
CHECK TRAILING WHITESPACE.
Tuple[List[str], List[str], List[str]] find_files_to_check_style(str path)
None check_style(str path, bool enable_check_formatting, bool enable_check_whitespace, bool enable_check_tabs, bool fix, int n_jobs=1)
CHECK STYLE.
Tuple[str, bool] check_trailing_whitespace_file(str filename, bool fix)
bool check_formatting(List[str] filenames, bool fix, int n_jobs)
CHECK FORMATTING.
Tuple[str, bool] check_formatting_file(str filename, str clang_format_path, bool fix)
bool skip_directory(str dirpath)
AUXILIARY FUNCTIONS.
bool check_tabs(List[str] filenames, bool fix, int n_jobs)
CHECK TABS.