A Discrete-Event Network Simulator
API
Loading...
Searching...
No Matches
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
47
48from typing import Dict, List, Tuple
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 verbose: bool,
306 n_jobs: int = 1,
307 ) -> None:
308 """
309 Check / fix the coding style of a list of files, including formatting and
310 trailing whitespace.
311
312 @param path Path to the files.
313 @param fix Whether to fix the style of the file (True) or
314 just check if the file is well-formatted (False).
315 @param enable_check_formatting Whether to enable code formatting checking.
316 @param enable_check_whitespace Whether to enable trailing whitespace checking.
317 @param enable_check_tabs Whether to enable tabs checking.
318 @param verbose Show the lines that are not well-formatted.
319 @param n_jobs Number of parallel jobs.
320 """
321
322 (files_to_check_formatting,
323 files_to_check_whitespace,
324 files_to_check_tabs) = find_files_to_check_style(path)
325
326 check_formatting_successful = True
327 check_whitespace_successful = True
328 check_tabs_successful = True
329
330 if enable_check_formatting:
331 check_formatting_successful = check_formatting(
332 files_to_check_formatting,
333 fix,
334 verbose,
335 n_jobs,
336 )
337
338 print('')
339
340 if enable_check_whitespace:
341 check_whitespace_successful = check_trailing_whitespace(
342 files_to_check_whitespace,
343 fix,
344 verbose,
345 n_jobs,
346 )
347
348 print('')
349
350 if enable_check_tabs:
351 check_tabs_successful = check_tabs(
352 files_to_check_tabs,
353 fix,
354 verbose,
355 n_jobs,
356 )
357
358 if all([
359 check_formatting_successful,
360 check_whitespace_successful,
361 check_tabs_successful,
362 ]):
363 sys.exit(0)
364 else:
365 sys.exit(1)
366
367
368
371def check_formatting(filenames: List[str],
372 fix: bool,
373 verbose: bool,
374 n_jobs: int,
375 ) -> bool:
376 """
377 Check / fix the coding style of a list of files with clang-format.
378
379 @param filenames List of filenames to be checked.
380 @param fix Whether to fix the formatting of the file (True) or
381 just check if the file is well-formatted (False).
382 @param verbose Show the lines that are not well-formatted.
383 @param n_jobs Number of parallel jobs.
384 @return True if all files are well formatted after the check process.
385 False if there are non-formatted files after the check process.
386 """
387
388 # Check files
389 clang_format_path = find_clang_format_path()
390 files_not_formatted: List[str] = []
391 files_verbose_infos: Dict[str, List[str]] = {}
392
393 with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
394 files_not_formatted_results = executor.map(
395 check_formatting_file,
396 filenames,
397 itertools.repeat(clang_format_path),
398 itertools.repeat(fix),
399 itertools.repeat(verbose),
400 )
401
402 for (filename, formatted, verbose_infos) in files_not_formatted_results:
403 if not formatted:
404 files_not_formatted.append(filename)
405
406 if verbose:
407 files_verbose_infos[filename] = verbose_infos
408
409 # Output results
410 if not files_not_formatted:
411 print('- All files are well formatted')
412 return True
413
414 else:
415 n_non_formatted_files = len(files_not_formatted)
416
417 if fix:
418 print(f'- Fixed formatting of the files ({n_non_formatted_files}):')
419 else:
420 print(f'- Detected bad formatting in the files ({n_non_formatted_files}):')
421
422 for f in files_not_formatted:
423 if verbose:
424 print(*[f' {l}' for l in files_verbose_infos[f]], sep='\n')
425 else:
426 print(f' - {f}')
427
428 # Return True if all files were fixed
429 return fix
430
431
432def check_formatting_file(filename: str,
433 clang_format_path: str,
434 fix: bool,
435 verbose: bool,
436 ) -> Tuple[str, bool, List[str]]:
437 """
438 Check / fix the coding style of a file with clang-format.
439
440 @param filename Name of the file to be checked.
441 @param clang_format_path Path to clang-format.
442 @param fix Whether to fix the style of the file (True) or
443 just check if the file is well-formatted (False).
444 @param verbose Show the lines that are not well-formatted.
445 @return Tuple [Filename, Whether the file is well-formatted, Verbose information].
446 """
447
448 verbose_infos: List[str] = []
449
450 # Check if the file is well formatted
451 process = subprocess.run(
452 [
453 clang_format_path,
454 filename,
455 '-style=file',
456 '--dry-run',
457 '--Werror',
458 # Optimization: In non-verbose mode, only 1 error is needed to check that the file is not formatted
459 f'--ferror-limit={0 if verbose else 1}',
460 ],
461 check=False,
462 capture_output=True,
463 text=True,
464 )
465
466 file_formatted = (process.returncode == 0)
467
468 if verbose:
469 verbose_infos = process.stderr.splitlines()
470
471 # Fix file
472 if fix and not file_formatted:
473 process = subprocess.run(
474 [
475 clang_format_path,
476 filename,
477 '-style=file',
478 '-i',
479 ],
480 check=False,
481 stdout=subprocess.DEVNULL,
482 stderr=subprocess.DEVNULL,
483 )
484
485 return (filename, file_formatted, verbose_infos)
486
487
488
491def check_trailing_whitespace(filenames: List[str],
492 fix: bool,
493 verbose: bool,
494 n_jobs: int,
495 ) -> bool:
496 """
497 Check / fix trailing whitespace in a list of files.
498
499 @param filename Name of the file to be checked.
500 @param fix Whether to fix the file (True) or
501 just check if it has trailing whitespace (False).
502 @param verbose Show the lines that are not well-formatted.
503 @param n_jobs Number of parallel jobs.
504 @return True if no files have trailing whitespace after the check process.
505 False if there are trailing whitespace after the check process.
506 """
507
508 # Check files
509 files_with_whitespace: List[str] = []
510 files_verbose_infos: Dict[str, List[str]] = {}
511
512 with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
513 files_with_whitespace_results = executor.map(
514 check_trailing_whitespace_file,
515 filenames,
516 itertools.repeat(fix),
517 itertools.repeat(verbose),
518 )
519
520 for (filename, has_whitespace, verbose_infos) in files_with_whitespace_results:
521 if has_whitespace:
522 files_with_whitespace.append(filename)
523
524 if verbose:
525 files_verbose_infos[filename] = verbose_infos
526
527 # Output results
528 if not files_with_whitespace:
529 print('- No files detected with trailing whitespace')
530 return True
531
532 else:
533 n_files_with_whitespace = len(files_with_whitespace)
534
535 if fix:
536 print(
537 f'- Fixed trailing whitespace in the files ({n_files_with_whitespace}):')
538 else:
539 print(
540 f'- Detected trailing whitespace in the files ({n_files_with_whitespace}):')
541
542 for f in files_with_whitespace:
543 if verbose:
544 print(*[f' {l}' for l in files_verbose_infos[f]], sep='\n')
545 else:
546 print(f' - {f}')
547
548 # If all files were fixed, there are no more trailing whitespace
549 return fix
550
551
553 fix: bool,
554 verbose: bool,
555 ) -> Tuple[str, bool, List[str]]:
556 """
557 Check / fix trailing whitespace in a file.
558
559 @param filename Name of the file to be checked.
560 @param fix Whether to fix the file (True) or
561 just check if it has trailing whitespace (False).
562 @param verbose Show the lines that are not well-formatted.
563 @return Tuple [Filename, Whether the file has trailing whitespace, Verbose information].
564 """
565
566 has_trailing_whitespace = False
567 verbose_infos: List[str] = []
568
569 with open(filename, 'r', encoding='utf-8') as f:
570 file_lines = f.readlines()
571
572 # Check if there are trailing whitespace and fix them
573 for (i, line) in enumerate(file_lines):
574 line_fixed = line.rstrip() + '\n'
575
576 if line_fixed != line:
577 has_trailing_whitespace = True
578 file_lines[i] = line_fixed
579
580 if verbose:
581 verbose_infos.extend([
582 f'{filename}:{i + 1}: error: Trailing whitespace detected',
583 f' {line_fixed.rstrip()}',
584 f' {"":{len(line_fixed) - 1}}^',
585 ])
586
587 # Optimization: If running in non-verbose check mode, only one error is needed to check that the file is not formatted
588 if not fix and not verbose:
589 break
590
591 # Update file with the fixed lines
592 if fix and has_trailing_whitespace:
593 with open(filename, 'w', encoding='utf-8') as f:
594 f.writelines(file_lines)
595
596 return (filename, has_trailing_whitespace, verbose_infos)
597
598
599
602def check_tabs(filenames: List[str],
603 fix: bool,
604 verbose: bool,
605 n_jobs: int,
606 ) -> bool:
607 """
608 Check / fix tabs in a list of files.
609
610 @param filename Name of the file to be checked.
611 @param fix Whether to fix the file (True) or just check if it has tabs (False).
612 @param verbose Show the lines that are not well-formatted.
613 @param n_jobs Number of parallel jobs.
614 @return True if no files have tabs after the check process.
615 False if there are tabs after the check process.
616 """
617
618 # Check files
619 files_with_tabs: List[str] = []
620 files_verbose_infos: Dict[str, List[str]] = {}
621
622 with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
623 files_with_tabs_results = executor.map(
624 check_tabs_file,
625 filenames,
626 itertools.repeat(fix),
627 itertools.repeat(verbose),
628 )
629
630 for (filename, has_tabs, verbose_infos) in files_with_tabs_results:
631 if has_tabs:
632 files_with_tabs.append(filename)
633
634 if verbose:
635 files_verbose_infos[filename] = verbose_infos
636
637 # Output results
638 if not files_with_tabs:
639 print('- No files detected with tabs')
640 return True
641
642 else:
643 n_files_with_tabs = len(files_with_tabs)
644
645 if fix:
646 print(
647 f'- Fixed tabs in the files ({n_files_with_tabs}):')
648 else:
649 print(
650 f'- Detected tabs in the files ({n_files_with_tabs}):')
651
652 for f in files_with_tabs:
653 if verbose:
654 print(*[f' {l}' for l in files_verbose_infos[f]], sep='\n')
655 else:
656 print(f' - {f}')
657
658 # If all files were fixed, there are no more trailing whitespace
659 return fix
660
661
662def check_tabs_file(filename: str,
663 fix: bool,
664 verbose: bool,
665 ) -> Tuple[str, bool, List[str]]:
666 """
667 Check / fix tabs in a file.
668
669 @param filename Name of the file to be checked.
670 @param fix Whether to fix the file (True) or just check if it has tabs (False).
671 @param verbose Show the lines that are not well-formatted.
672 @return Tuple [Filename, Whether the file has tabs, Verbose information].
673 """
674
675 has_tabs = False
676 clang_format_enabled = True
677
678 verbose_infos: List[str] = []
679
680 with open(filename, 'r', encoding='utf-8') as f:
681 file_lines = f.readlines()
682
683 for (i, line) in enumerate(file_lines):
684
685 # Check clang-format guards
686 line_stripped = line.strip()
687
688 if line_stripped == CLANG_FORMAT_GUARD_ON:
689 clang_format_enabled = True
690 elif line_stripped == CLANG_FORMAT_GUARD_OFF:
691 clang_format_enabled = False
692
693 if (not clang_format_enabled and
694 line_stripped not in (CLANG_FORMAT_GUARD_ON, CLANG_FORMAT_GUARD_OFF)):
695 continue
696
697 # Check if there are tabs and fix them
698 tab_index = line.find('\t')
699
700 if tab_index != -1:
701 has_tabs = True
702 file_lines[i] = line.expandtabs(TAB_SIZE)
703
704 if verbose:
705 verbose_infos.extend([
706 f'{filename}:{i + 1}:{tab_index + 1}: error: Tab detected',
707 f' {line.rstrip()}',
708 f' {"":{tab_index}}^',
709 ])
710
711 # Optimization: If running in non-verbose check mode, only one error is needed to check that the file is not formatted
712 if not fix and not verbose:
713 break
714
715 # Update file with the fixed lines
716 if fix and has_tabs:
717 with open(filename, 'w', encoding='utf-8') as f:
718 f.writelines(file_lines)
719
720 return (filename, has_tabs, verbose_infos)
721
722
723
726if __name__ == '__main__':
727
728 parser = argparse.ArgumentParser(
729 description='Check and apply the ns-3 coding style to all files in a given PATH. '
730 'The script checks the formatting of the file with clang-format. '
731 'Additionally, it checks the presence of trailing whitespace and tabs. '
732 'Formatting and tabs checks respect clang-format guards. '
733 'When used in "check mode" (default), the script checks if all files are well '
734 'formatted and do not have trailing whitespace nor tabs. '
735 'If it detects non-formatted files, they will be printed and this process exits with a '
736 'non-zero code. When used in "fix mode", this script automatically fixes the files.')
737
738 parser.add_argument('path', action='store', type=str,
739 help='Path to the files to check')
740
741 parser.add_argument('--no-formatting', action='store_true',
742 help='Do not check / fix code formatting')
743
744 parser.add_argument('--no-whitespace', action='store_true',
745 help='Do not check / fix trailing whitespace')
746
747 parser.add_argument('--no-tabs', action='store_true',
748 help='Do not check / fix tabs')
749
750 parser.add_argument('--fix', action='store_true',
751 help='Fix coding style issues detected in the files')
752
753 parser.add_argument('-v', '--verbose', action='store_true',
754 help='Show the lines that are not well-formatted')
755
756 parser.add_argument('-j', '--jobs', type=int, default=max(1, os.cpu_count() - 1),
757 help='Number of parallel jobs')
758
759 args = parser.parse_args()
760
761 try:
763 path=args.path,
764 enable_check_formatting=(not args.no_formatting),
765 enable_check_whitespace=(not args.no_whitespace),
766 enable_check_tabs=(not args.no_tabs),
767 fix=args.fix,
768 verbose=args.verbose,
769 n_jobs=args.jobs,
770 )
771
772 except Exception as e:
773 print(e)
774 sys.exit(1)
#define max(a, b)
Definition: 80211b.c:42
Tuple[str, bool, List[str]] check_trailing_whitespace_file(str filename, bool fix, bool verbose)
Tuple[str, bool, List[str]] check_tabs_file(str filename, bool fix, bool verbose)
Tuple[List[str], List[str], List[str]] find_files_to_check_style(str path)
bool check_trailing_whitespace(List[str] filenames, bool fix, bool verbose, int n_jobs)
CHECK TRAILING WHITESPACE.
None check_style(str path, bool enable_check_formatting, bool enable_check_whitespace, bool enable_check_tabs, bool fix, bool verbose, int n_jobs=1)
CHECK STYLE.
bool check_tabs(List[str] filenames, bool fix, bool verbose, int n_jobs)
CHECK TABS.
Tuple[str, bool, List[str]] check_formatting_file(str filename, str clang_format_path, bool fix, bool verbose)
bool check_formatting(List[str] filenames, bool fix, bool verbose, int n_jobs)
CHECK FORMATTING.
bool skip_directory(str dirpath)
AUXILIARY FUNCTIONS.