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