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 recursively to all files in the PATH arguments.
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 / fix local #include headers with "ns3/" prefix. Respects clang-format guards.
26- Check / apply clang-format. Respects clang-format guards.
27- Check / trim trailing whitespace. Always checked.
28- Check / replace tabs with spaces. Respects clang-format guards.
29
30This script can be applied to all text files in a given path or to individual files.
31
32NOTE: The formatting check requires clang-format (version >= 14) to be found on the path.
33The remaining checks do not depend on clang-format and can be executed by disabling clang-format
34checking with the "--no-formatting" option.
35"""
36
37import argparse
38import concurrent.futures
39import itertools
40import os
41import re
42import shutil
43import subprocess
44import sys
45from typing import Callable, Dict, List, Tuple
46
47
50CLANG_FORMAT_VERSIONS = [
51 17,
52 16,
53 15,
54 14,
55]
56
57CLANG_FORMAT_GUARD_ON = "// clang-format on"
58CLANG_FORMAT_GUARD_OFF = "// clang-format off"
59
60DIRECTORIES_TO_SKIP = [
61 "__pycache__",
62 ".git",
63 "bindings",
64 "build",
65 "cmake-cache",
66 "testpy-output",
67]
68
69# List of files entirely copied from elsewhere that should not be checked,
70# in order to optimize the performance of this script
71FILES_TO_SKIP = [
72 "valgrind.h",
73]
74
75FILE_EXTENSIONS_TO_CHECK_FORMATTING = [
76 ".c",
77 ".cc",
78 ".h",
79]
80
81FILE_EXTENSIONS_TO_CHECK_INCLUDE_PREFIXES = FILE_EXTENSIONS_TO_CHECK_FORMATTING
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 should_analyze_directory(dirpath: str) -> bool:
135 """
136 Check whether a directory should be analyzed.
137
138 @param dirpath Directory path.
139 @return Whether the directory should be analyzed.
140 """
141
142 _, directory = os.path.split(dirpath)
143
144 return not (
145 directory in DIRECTORIES_TO_SKIP or (directory.startswith(".") and directory != ".")
146 )
147
148
150 path: str,
151 files_to_check: List[str],
152 file_extensions_to_check: List[str],
153) -> bool:
154 """
155 Check whether a file should be analyzed.
156
157 @param path Path to the file.
158 @param files_to_check List of files that shall be checked.
159 @param file_extensions_to_check List of file extensions that shall be checked.
160 @return Whether the file should be analyzed.
161 """
162
163 filename = os.path.split(path)[1]
164
165 if filename in FILES_TO_SKIP:
166 return False
167
168 basename, extension = os.path.splitext(filename)
169
170 return basename in files_to_check or extension in file_extensions_to_check
171
172
174 paths: List[str],
175) -> Tuple[List[str], List[str], List[str], List[str]]:
176 """
177 Find all files to be checked in a given list of paths.
178
179 @param paths List of paths to the files to check.
180 @return Tuple [List of files to check include prefixes,
181 List of files to check formatting,
182 List of files to check trailing whitespace,
183 List of files to check tabs].
184 """
185
186 files_to_check: List[str] = []
187
188 for path in paths:
189 abs_path = os.path.abspath(os.path.expanduser(path))
190
191 if os.path.isfile(abs_path):
192 files_to_check.append(path)
193
194 elif os.path.isdir(abs_path):
195 for dirpath, dirnames, filenames in os.walk(path, topdown=True):
196 if not should_analyze_directory(dirpath):
197 # Remove directory and its subdirectories
198 dirnames[:] = []
199 continue
200
201 files_to_check.extend([os.path.join(dirpath, f) for f in filenames])
202
203 else:
204 raise ValueError(f"Error: {path} is not a file nor a directory")
205
206 files_to_check.sort()
207
208 files_to_check_include_prefixes: List[str] = []
209 files_to_check_formatting: List[str] = []
210 files_to_check_whitespace: List[str] = []
211 files_to_check_tabs: List[str] = []
212
213 for f in files_to_check:
214 if should_analyze_file(f, [], FILE_EXTENSIONS_TO_CHECK_INCLUDE_PREFIXES):
215 files_to_check_include_prefixes.append(f)
216
217 if should_analyze_file(f, [], FILE_EXTENSIONS_TO_CHECK_FORMATTING):
218 files_to_check_formatting.append(f)
219
220 if should_analyze_file(f, FILES_TO_CHECK_WHITESPACE, FILE_EXTENSIONS_TO_CHECK_WHITESPACE):
221 files_to_check_whitespace.append(f)
222
223 if should_analyze_file(f, [], FILE_EXTENSIONS_TO_CHECK_TABS):
224 files_to_check_tabs.append(f)
225
226 return (
227 files_to_check_include_prefixes,
228 files_to_check_formatting,
229 files_to_check_whitespace,
230 files_to_check_tabs,
231 )
232
233
235 """
236 Find the path to one of the supported versions of clang-format.
237 If no supported version of clang-format is found, raise an exception.
238
239 @return Path to clang-format.
240 """
241
242 # Find exact version
243 for version in CLANG_FORMAT_VERSIONS:
244 clang_format_path = shutil.which(f"clang-format-{version}")
245
246 if clang_format_path:
247 return clang_format_path
248
249 # Find default version and check if it is supported
250 clang_format_path = shutil.which("clang-format")
251
252 if clang_format_path:
253 process = subprocess.run(
254 [clang_format_path, "--version"],
255 capture_output=True,
256 text=True,
257 check=True,
258 )
259
260 version = process.stdout.strip().split(" ")[-1]
261 major_version = int(version.split(".")[0])
262
263 if major_version in CLANG_FORMAT_VERSIONS:
264 return clang_format_path
265
266 # No supported version of clang-format found
267 raise RuntimeError(
268 f"Could not find any supported version of clang-format installed on this system. "
269 f"List of supported versions: {CLANG_FORMAT_VERSIONS}."
270 )
271
272
273
277 paths: List[str],
278 enable_check_include_prefixes: bool,
279 enable_check_formatting: bool,
280 enable_check_whitespace: bool,
281 enable_check_tabs: bool,
282 fix: bool,
283 verbose: bool,
284 n_jobs: int = 1,
285) -> bool:
286 """
287 Check / fix the coding style of a list of files.
288
289 @param paths List of paths to the files to check.
290 @param enable_check_include_prefixes Whether to enable checking #include headers from the same module with the "ns3/" prefix.
291 @param enable_check_formatting Whether to enable checking code formatting.
292 @param enable_check_whitespace Whether to enable checking trailing whitespace.
293 @param enable_check_tabs Whether to enable checking tabs.
294 @param fix Whether to fix (True) or just check (False) the file.
295 @param verbose Show the lines that are not compliant with the style.
296 @param n_jobs Number of parallel jobs.
297 @return Whether all files are compliant with all enabled style checks.
298 """
299
300 (
301 files_to_check_include_prefixes,
302 files_to_check_formatting,
303 files_to_check_whitespace,
304 files_to_check_tabs,
306
307 check_include_prefixes_successful = True
308 check_formatting_successful = True
309 check_whitespace_successful = True
310 check_tabs_successful = True
311
312 if enable_check_include_prefixes:
313 check_include_prefixes_successful = check_style_files(
314 '#include headers from the same module with the "ns3/" prefix',
315 check_manually_file,
316 files_to_check_include_prefixes,
317 fix,
318 verbose,
319 n_jobs,
320 respect_clang_format_guards=True,
321 check_style_line_function=check_include_prefixes_line,
322 )
323
324 print("")
325
326 if enable_check_formatting:
327 check_formatting_successful = check_style_files(
328 "bad code formatting",
329 check_formatting_file,
330 files_to_check_formatting,
331 fix,
332 verbose,
333 n_jobs,
334 clang_format_path=find_clang_format_path(),
335 )
336
337 print("")
338
339 if enable_check_whitespace:
340 check_whitespace_successful = check_style_files(
341 "trailing whitespace",
342 check_manually_file,
343 files_to_check_whitespace,
344 fix,
345 verbose,
346 n_jobs,
347 respect_clang_format_guards=False,
348 check_style_line_function=check_whitespace_line,
349 )
350
351 print("")
352
353 if enable_check_tabs:
354 check_tabs_successful = check_style_files(
355 "tabs",
356 check_manually_file,
357 files_to_check_tabs,
358 fix,
359 verbose,
360 n_jobs,
361 respect_clang_format_guards=True,
362 check_style_line_function=check_tabs_line,
363 )
364
365 return all(
366 [
367 check_include_prefixes_successful,
368 check_formatting_successful,
369 check_whitespace_successful,
370 check_tabs_successful,
371 ]
372 )
373
374
376 style_check_str: str,
377 check_style_file_function: Callable[..., Tuple[str, bool, List[str]]],
378 filenames: List[str],
379 fix: bool,
380 verbose: bool,
381 n_jobs: int,
382 **kwargs,
383) -> bool:
384 """
385 Check / fix style of a list of files.
386
387 @param style_check_str Description of the check to be performed.
388 @param check_style_file_function Function used to check the file.
389 @param filename Name of the file to be checked.
390 @param fix Whether to fix (True) or just check (False) the file (True).
391 @param verbose Show the lines that are not compliant with the style.
392 @param n_jobs Number of parallel jobs.
393 @param kwargs Additional keyword arguments to the check_style_file_function.
394 @return Whether all files are compliant with the style.
395 """
396
397 # Check files
398 non_compliant_files: List[str] = []
399 files_verbose_infos: Dict[str, List[str]] = {}
400
401 with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
402 non_compliant_files_results = executor.map(
403 check_style_file_function,
404 filenames,
405 itertools.repeat(fix),
406 itertools.repeat(verbose),
407 *[arg if isinstance(arg, list) else itertools.repeat(arg) for arg in kwargs.values()],
408 )
409
410 for filename, is_file_compliant, verbose_infos in non_compliant_files_results:
411 if not is_file_compliant:
412 non_compliant_files.append(filename)
413
414 if verbose:
415 files_verbose_infos[filename] = verbose_infos
416
417 # Output results
418 if not non_compliant_files:
419 print(f"- No files detected with {style_check_str}")
420 return True
421
422 else:
423 n_non_compliant_files = len(non_compliant_files)
424
425 if fix:
426 print(f"- Fixed {style_check_str} in the files ({n_non_compliant_files}):")
427 else:
428 print(f"- Detected {style_check_str} in the files ({n_non_compliant_files}):")
429
430 for f in non_compliant_files:
431 if verbose:
432 print(*[f" {l}" for l in files_verbose_infos[f]], sep="\n")
433 else:
434 print(f" - {f}")
435
436 # If all files were fixed, there are no more non-compliant files
437 return fix
438
439
440
444 filename: str,
445 fix: bool,
446 verbose: bool,
447 clang_format_path: str,
448) -> Tuple[str, bool, List[str]]:
449 """
450 Check / fix the coding style of a file with clang-format.
451
452 @param filename Name of the file to be checked.
453 @param fix Whether to fix (True) or just check (False) the style of the file (True).
454 @param verbose Show the lines that are not compliant with the style.
455 @param clang_format_path Path to clang-format.
456 @return Tuple [Filename,
457 Whether the file is compliant with the style (before the check),
458 Verbose information].
459 """
460
461 verbose_infos: List[str] = []
462
463 # Check if the file is well formatted
464 process = subprocess.run(
465 [
466 clang_format_path,
467 filename,
468 "-style=file",
469 "--dry-run",
470 "--Werror",
471 # Optimization: In non-verbose mode, only one error is needed to check that the file is not compliant
472 f"--ferror-limit={0 if verbose else 1}",
473 ],
474 check=False,
475 capture_output=True,
476 text=True,
477 )
478
479 is_file_compliant = process.returncode == 0
480
481 if verbose:
482 verbose_infos = process.stderr.splitlines()
483
484 # Fix file
485 if fix and not is_file_compliant:
486 process = subprocess.run(
487 [
488 clang_format_path,
489 filename,
490 "-style=file",
491 "-i",
492 ],
493 check=False,
494 stdout=subprocess.DEVNULL,
495 stderr=subprocess.DEVNULL,
496 )
497
498 return (filename, is_file_compliant, verbose_infos)
499
500
502 filename: str,
503 fix: bool,
504 verbose: bool,
505 respect_clang_format_guards: bool,
506 check_style_line_function: Callable[[str, str, int], Tuple[bool, str, List[str]]],
507) -> Tuple[str, bool, List[str]]:
508 """
509 Check / fix a file manually using a function to check / fix each line.
510
511 @param filename Name of the file to be checked.
512 @param fix Whether to fix (True) or just check (False) the style of the file (True).
513 @param verbose Show the lines that are not compliant with the style.
514 @param respect_clang_format_guards Whether to respect clang-format guards.
515 @param check_style_line_function Function used to check each line.
516 @return Tuple [Filename,
517 Whether the file is compliant with the style (before the check),
518 Verbose information].
519 """
520
521 is_file_compliant = True
522 verbose_infos: List[str] = []
523 clang_format_enabled = True
524
525 with open(filename, "r", encoding="utf-8") as f:
526 file_lines = f.readlines()
527
528 for i, line in enumerate(file_lines):
529 # Check clang-format guards
530 if respect_clang_format_guards:
531 line_stripped = line.strip()
532
533 if line_stripped == CLANG_FORMAT_GUARD_ON:
534 clang_format_enabled = True
535 elif line_stripped == CLANG_FORMAT_GUARD_OFF:
536 clang_format_enabled = False
537
538 if not clang_format_enabled and line_stripped not in (
539 CLANG_FORMAT_GUARD_ON,
540 CLANG_FORMAT_GUARD_OFF,
541 ):
542 continue
543
544 # Check if the line is compliant with the style and fix it
545 (is_line_compliant, line_fixed, line_verbose_infos) = check_style_line_function(
546 line, filename, i
547 )
548
549 if not is_line_compliant:
550 is_file_compliant = False
551 file_lines[i] = line_fixed
552 verbose_infos.extend(line_verbose_infos)
553
554 # Optimization: If running in non-verbose check mode, only one error is needed to check that the file is not compliant
555 if not fix and not verbose:
556 break
557
558 # Update file with the fixed lines
559 if fix and not is_file_compliant:
560 with open(filename, "w", encoding="utf-8") as f:
561 f.writelines(file_lines)
562
563 return (filename, is_file_compliant, verbose_infos)
564
565
567 line: str,
568 filename: str,
569 line_number: int,
570) -> Tuple[bool, str, List[str]]:
571 """
572 Check / fix #include headers from the same module with the "ns3/" prefix in a line.
573
574 @param line The line to check.
575 @param filename Name of the file to be checked.
576 @param line_number The number of the line checked.
577 @return Tuple [Whether the line is compliant with the style (before the check),
578 Fixed line,
579 Verbose information].
580 """
581
582 is_line_compliant = True
583 line_fixed = line
584 verbose_infos: List[str] = []
585
586 # Check if the line is an #include and extract its header file
587 line_stripped = line.strip()
588 header_file = re.findall(r'^#include ["<]ns3/(.*\.h)[">]', line_stripped)
589
590 if header_file:
591 # Check if the header file belongs to the same module and remove the "ns3/" prefix
592 header_file = header_file[0]
593 parent_path = os.path.split(filename)[0]
594
595 if os.path.exists(os.path.join(parent_path, header_file)):
596 is_line_compliant = False
597 line_fixed = (
598 line_stripped.replace(f"ns3/{header_file}", header_file)
599 .replace("<", '"')
600 .replace(">", '"')
601 + "\n"
602 )
603
604 header_index = len('#include "')
605
606 verbose_infos.extend(
607 [
608 f'{filename}:{line_number + 1}:{header_index + 1}: error: #include headers from the same module with the "ns3/" prefix detected',
609 f" {line_stripped}",
610 f' {"":{header_index}}^',
611 ]
612 )
613
614 return (is_line_compliant, line_fixed, verbose_infos)
615
616
618 line: str,
619 filename: str,
620 line_number: int,
621) -> Tuple[bool, str, List[str]]:
622 """
623 Check / fix whitespace in a line.
624
625 @param line The line to check.
626 @param filename Name of the file to be checked.
627 @param line_number The number of the line checked.
628 @return Tuple [Whether the line is compliant with the style (before the check),
629 Fixed line,
630 Verbose information].
631 """
632
633 is_line_compliant = True
634 line_fixed = line.rstrip() + "\n"
635 verbose_infos: List[str] = []
636
637 if line_fixed != line:
638 is_line_compliant = False
639 line_fixed_stripped_expanded = line_fixed.rstrip().expandtabs(TAB_SIZE)
640
641 verbose_infos = [
642 f"{filename}:{line_number + 1}:{len(line_fixed_stripped_expanded) + 1}: error: Trailing whitespace detected",
643 f" {line_fixed_stripped_expanded}",
644 f' {"":{len(line_fixed_stripped_expanded)}}^',
645 ]
646
647 return (is_line_compliant, line_fixed, verbose_infos)
648
649
651 line: str,
652 filename: str,
653 line_number: int,
654) -> Tuple[bool, str, List[str]]:
655 """
656 Check / fix tabs in a line.
657
658 @param line The line to check.
659 @param filename Name of the file to be checked.
660 @param line_number The number of the line checked.
661 @return Tuple [Whether the line is compliant with the style (before the check),
662 Fixed line,
663 Verbose information].
664 """
665
666 is_line_compliant = True
667 line_fixed = line
668 verbose_infos: List[str] = []
669
670 tab_index = line.find("\t")
671
672 if tab_index != -1:
673 is_line_compliant = False
674 line_fixed = line.expandtabs(TAB_SIZE)
675
676 verbose_infos = [
677 f"{filename}:{line_number + 1}:{tab_index + 1}: error: Tab detected",
678 f" {line.rstrip()}",
679 f' {"":{tab_index}}^',
680 ]
681
682 return (is_line_compliant, line_fixed, verbose_infos)
683
684
685
688if __name__ == "__main__":
689 parser = argparse.ArgumentParser(
690 description="Check and apply the ns-3 coding style recursively to all files in the given PATHs. "
691 "The script checks the formatting of the file with clang-format. "
692 'Additionally, it checks #include headers from the same module with the "ns3/" prefix, '
693 "the presence of trailing whitespace and tabs. "
694 'Formatting, local #include "ns3/" prefixes and tabs checks respect clang-format guards. '
695 'When used in "check mode" (default), the script checks if all files are well '
696 "formatted and do not have trailing whitespace nor tabs. "
697 "If it detects non-formatted files, they will be printed and this process exits with a "
698 'non-zero code. When used in "fix mode", this script automatically fixes the files.'
699 )
700
701 parser.add_argument(
702 "paths",
703 action="store",
704 type=str,
705 nargs="+",
706 help="List of paths to the files to check",
707 )
708
709 parser.add_argument(
710 "--no-include-prefixes",
711 action="store_true",
712 help='Do not check / fix #include headers from the same module with the "ns3/" prefix',
713 )
714
715 parser.add_argument(
716 "--no-formatting",
717 action="store_true",
718 help="Do not check / fix code formatting",
719 )
720
721 parser.add_argument(
722 "--no-whitespace",
723 action="store_true",
724 help="Do not check / fix trailing whitespace",
725 )
726
727 parser.add_argument(
728 "--no-tabs",
729 action="store_true",
730 help="Do not check / fix tabs",
731 )
732
733 parser.add_argument(
734 "--fix",
735 action="store_true",
736 help="Fix coding style issues detected in the files",
737 )
738
739 parser.add_argument(
740 "-v",
741 "--verbose",
742 action="store_true",
743 help="Show the lines that are not well-formatted",
744 )
745
746 parser.add_argument(
747 "-j",
748 "--jobs",
749 type=int,
750 default=max(1, os.cpu_count() - 1),
751 help="Number of parallel jobs",
752 )
753
754 args = parser.parse_args()
755
756 try:
757 all_checks_successful = check_style_clang_format(
758 paths=args.paths,
759 enable_check_include_prefixes=(not args.no_include_prefixes),
760 enable_check_formatting=(not args.no_formatting),
761 enable_check_whitespace=(not args.no_whitespace),
762 enable_check_tabs=(not args.no_tabs),
763 fix=args.fix,
764 verbose=args.verbose,
765 n_jobs=args.jobs,
766 )
767
768 except Exception as e:
769 print(e)
770 sys.exit(1)
771
772 if not all_checks_successful:
773 if args.verbose:
774 print("")
775 print('NOTE: To fix the files automatically, run this script with the flag "--fix"')
776
777 sys.exit(1)
Tuple[List[str], List[str], List[str], List[str]] find_files_to_check_style(List[str] paths)
Tuple[bool, str, List[str]] check_whitespace_line(str line, str filename, int line_number)
Tuple[str, bool, List[str]] check_formatting_file(str filename, bool fix, bool verbose, str clang_format_path)
CHECK STYLE FUNCTIONS.
Tuple[str, bool, List[str]] check_manually_file(str filename, bool fix, bool verbose, bool respect_clang_format_guards, Callable[[str, str, int], Tuple[bool, str, List[str]]] check_style_line_function)
bool check_style_files(str style_check_str, Callable[..., Tuple[str, bool, List[str]]] check_style_file_function, List[str] filenames, bool fix, bool verbose, int n_jobs, **kwargs)
Tuple[bool, str, List[str]] check_include_prefixes_line(str line, str filename, int line_number)
bool should_analyze_file(str path, List[str] files_to_check, List[str] file_extensions_to_check)
Tuple[bool, str, List[str]] check_tabs_line(str line, str filename, int line_number)
bool should_analyze_directory(str dirpath)
AUXILIARY FUNCTIONS.
bool check_style_clang_format(List[str] paths, bool enable_check_include_prefixes, bool enable_check_formatting, bool enable_check_whitespace, bool enable_check_tabs, bool fix, bool verbose, int n_jobs=1)
CHECK STYLE MAIN FUNCTIONS.