9"""Check and apply the ns-3 coding style recursively to all files in the PATH arguments.
11The coding style is defined with the clang-format tool, whose definitions are in
12the ".clang-format" file. This script performs the following checks / fixes:
13- Check / apply clang-format. Respects clang-format guards.
14- Check / fix local #include headers with "ns3/" prefix. Respects clang-format guards.
15- Check / fix ns-3 #include headers using angle brackets <> rather than quotes "". Respects clang-format guards.
16- Check / fix Doxygen tags using @ rather than \\. Respects clang-format guards.
17- Check / fix SPDX licenses rather than GPL text. Respects clang-format guards.
18- Check / fix emacs file style comments. Respects clang-format guards.
19- Check / trim trailing whitespace. Always checked.
20- Check / replace tabs with spaces. Respects clang-format guards.
21- Check file encoding. Always checked.
23This script can be applied to all text files in a given path or to individual files.
25NOTE: The formatting check requires clang-format to be found on the path (see the supported versions below).
26The remaining checks do not depend on clang-format and can be executed by disabling clang-format
27checking with the "--no-formatting" option.
31import concurrent.futures
38from typing
import Callable, Dict, List, Tuple
43CLANG_FORMAT_MAX_VERSION = 20
44CLANG_FORMAT_MIN_VERSION = 20
53 "// clang-format off",
54 "# cmake-format: off",
58DIRECTORIES_TO_SKIP = [
89FILES_TO_CHECK: Dict[str, List[str]] = {c: []
for c
in CHECKS}
91FILES_TO_CHECK[
"tabs"] = [
96 "codespell-ignored-lines",
97 "codespell-ignored-words",
101FILES_TO_CHECK[
"whitespace"] = FILES_TO_CHECK[
"tabs"] + [
106FILE_EXTENSIONS_TO_CHECK: Dict[str, List[str]] = {c: []
for c
in CHECKS}
108FILE_EXTENSIONS_TO_CHECK[
"formatting"] = [
114FILE_EXTENSIONS_TO_CHECK[
"include_prefixes"] = FILE_EXTENSIONS_TO_CHECK[
"formatting"]
115FILE_EXTENSIONS_TO_CHECK[
"include_quotes"] = FILE_EXTENSIONS_TO_CHECK[
"formatting"]
116FILE_EXTENSIONS_TO_CHECK[
"doxygen_tags"] = FILE_EXTENSIONS_TO_CHECK[
"formatting"]
117FILE_EXTENSIONS_TO_CHECK[
"encoding"] = FILE_EXTENSIONS_TO_CHECK[
"formatting"]
119FILE_EXTENSIONS_TO_CHECK[
"license"] = [
127FILE_EXTENSIONS_TO_CHECK[
"emacs"] = [
135FILE_EXTENSIONS_TO_CHECK[
"tabs"] = [
154FILE_EXTENSIONS_TO_CHECK[
"whitespace"] = FILE_EXTENSIONS_TO_CHECK[
"tabs"] + [
172FILE_ENCODING =
"UTF-8"
179 """Check whether a directory should be analyzed.
182 dirpath: Directory path.
185 Whether the directory should be analyzed.
188 _, directory = os.path.split(dirpath)
190 return directory
not in DIRECTORIES_TO_SKIP
195 files_to_check: List[str],
196 file_extensions_to_check: List[str],
198 """Check whether a file should be analyzed.
201 path: Path to the file.
202 files_to_check: List of files that shall be checked.
203 file_extensions_to_check: List of file extensions that shall be checked.
206 Whether the file should be analyzed.
209 filename = os.path.split(path)[1]
211 if filename
in FILES_TO_SKIP:
214 extension = os.path.splitext(filename)[1]
216 return filename
in files_to_check
or extension
in file_extensions_to_check
221) -> Dict[str, List[str]]:
222 """Find all files to be checked in a given list of paths.
225 paths List of paths to the files to check.
228 Dictionary of checks and corresponding list of files to check.
230 "formatting": list_of_files_to_check_formatting,
236 files_found: List[str] = []
239 abs_path = os.path.abspath(os.path.expanduser(path))
241 if os.path.isfile(abs_path):
242 files_found.append(path)
244 elif os.path.isdir(abs_path):
245 for dirpath, dirnames, filenames
in os.walk(path, topdown=
True):
251 files_found.extend([os.path.join(dirpath, f)
for f
in filenames])
254 raise ValueError(f
"{path} is not a valid file nor a directory")
259 files_to_check: Dict[str, List[str]] = {c: []
for c
in CHECKS}
261 for f
in files_found:
264 files_to_check[check].append(f)
266 return files_to_check
270 """Find the path to one of the supported versions of clang-format.
273 Path to clang-format.
276 RuntimeError: If no supported version of clang-format is found.
280 for version
in range(CLANG_FORMAT_MAX_VERSION, CLANG_FORMAT_MIN_VERSION - 1, -1):
281 clang_format_path = shutil.which(f
"clang-format-{version}")
283 if clang_format_path:
284 return clang_format_path
287 clang_format_path = shutil.which(
"clang-format")
290 if clang_format_path:
291 process = subprocess.run(
292 [clang_format_path,
"--version"],
298 clang_format_version = process.stdout.strip()
299 version_regex = re.findall(
r"\b(\d+)(\.\d+){0,2}\b", clang_format_version)
302 major_version = int(version_regex[0][0])
304 if CLANG_FORMAT_MIN_VERSION <= major_version <= CLANG_FORMAT_MAX_VERSION:
305 return clang_format_path
309 f
"Could not find any supported version of clang-format installed on this system. "
310 f
"List of supported versions: [{CLANG_FORMAT_MAX_VERSION}-{CLANG_FORMAT_MIN_VERSION}]. "
311 + (f
"Found clang-format {major_version}." if major_version
else "")
320 checks_enabled: Dict[str, bool],
325 """Check / fix the coding style of a list of files.
328 paths: List of paths to the files to check.
329 checks_enabled: Dictionary of checks indicating whether to enable each of them.
330 fix: Whether to fix (True) or just check (False) the file.
331 verbose: Show the lines that are not compliant with the style.
332 n_jobs: Number of parallel jobs.
335 Whether all files are compliant with all enabled style checks.
339 checks_successful = {c:
True for c
in CHECKS}
342 "include_prefixes":
'#include headers from the same module with the "ns3/" prefix',
343 "include_quotes":
'ns-3 #include headers using angle brackets <> rather than quotes ""',
344 "doxygen_tags":
"Doxygen tags using \\ rather than @",
345 "license":
"GPL license text instead of SPDX license",
346 "emacs":
"emacs file style comments",
347 "whitespace":
"trailing whitespace",
349 "formatting":
"bad code formatting",
350 "encoding": f
"bad file encoding ({FILE_ENCODING})",
353 check_style_file_functions_kwargs = {
354 "include_prefixes": {
355 "function": check_manually_file,
357 "respect_clang_format_guards":
True,
358 "check_style_line_function": check_include_prefixes_line,
362 "function": check_manually_file,
364 "respect_clang_format_guards":
True,
365 "check_style_line_function": check_include_quotes_line,
369 "function": check_manually_file,
371 "respect_clang_format_guards":
True,
372 "check_style_line_function": check_doxygen_tags_line,
376 "function": check_manually_file,
378 "respect_clang_format_guards":
True,
379 "check_style_line_function": check_licenses_line,
383 "function": check_manually_file,
385 "respect_clang_format_guards":
True,
386 "check_style_line_function": check_emacs_line,
390 "function": check_manually_file,
392 "respect_clang_format_guards":
False,
393 "check_style_line_function": check_whitespace_line,
397 "function": check_manually_file,
399 "respect_clang_format_guards":
True,
400 "check_style_line_function": check_tabs_line,
404 "function": check_formatting_file,
408 "function": check_encoding_file,
413 if checks_enabled[
"formatting"]:
414 check_style_file_functions_kwargs[
"formatting"][
"kwargs"] = {
418 n_checks_enabled = sum(checks_enabled.values())
422 if checks_enabled[check]:
424 style_check_strs[check],
425 check_style_file_functions_kwargs[check][
"function"],
426 files_to_check[check],
430 **check_style_file_functions_kwargs[check][
"kwargs"],
435 if n_check < n_checks_enabled:
438 return all(checks_successful.values())
442 style_check_str: str,
443 check_style_file_function: Callable[..., Tuple[str, bool, List[str]]],
444 filenames: List[str],
450 """Check / fix style of a list of files.
453 style_check_str: Description of the check to be performed.
454 check_style_file_function: Function used to check the file.
455 filename: Name of the file to be checked.
456 fix: Whether to fix (True) or just check (False) the file (True).
457 verbose: Show the lines that are not compliant with the style.
458 n_jobs: Number of parallel jobs.
459 kwargs: Additional keyword arguments to the check_style_file_function.
462 Whether all files are compliant with the style.
466 non_compliant_files: List[str] = []
467 files_verbose_infos: Dict[str, List[str]] = {}
469 with concurrent.futures.ProcessPoolExecutor(n_jobs)
as executor:
470 non_compliant_files_results = executor.map(
471 check_style_file_function,
473 itertools.repeat(fix),
474 itertools.repeat(verbose),
475 *[arg
if isinstance(arg, list)
else itertools.repeat(arg)
for arg
in kwargs.values()],
478 for filename, is_file_compliant, verbose_infos
in non_compliant_files_results:
479 if not is_file_compliant:
480 non_compliant_files.append(filename)
483 files_verbose_infos[filename] = verbose_infos
486 if not non_compliant_files:
487 print(f
"- No files detected with {style_check_str}")
491 n_non_compliant_files = len(non_compliant_files)
494 print(f
"- Fixed {style_check_str} in the files ({n_non_compliant_files}):")
496 print(f
"- Detected {style_check_str} in the files ({n_non_compliant_files}):")
498 for f
in non_compliant_files:
500 print(*[f
" {l}" for l
in files_verbose_infos[f]], sep=
"\n")
515 clang_format_path: str,
516) -> Tuple[str, bool, List[str]]:
517 """Check / fix the coding style of a file with clang-format.
520 filename: Name of the file to be checked.
521 fix: Whether to fix (True) or just check (False) the style of the file.
522 verbose: Show the lines that are not compliant with the style.
523 clang_format_path: Path to clang-format.
527 Whether the file is compliant with the style (before the check),
531 verbose_infos: List[str] = []
534 process = subprocess.run(
542 f
"--ferror-limit={0 if verbose else 1}",
549 is_file_compliant = process.returncode == 0
552 verbose_infos = process.stderr.splitlines()
555 if fix
and not is_file_compliant:
556 process = subprocess.run(
564 stdout=subprocess.DEVNULL,
565 stderr=subprocess.DEVNULL,
568 return (filename, is_file_compliant, verbose_infos)
575) -> Tuple[str, bool, List[str]]:
576 """Check / fix the encoding of a file.
579 filename: Name of the file to be checked.
580 fix: Whether to fix (True) or just check (False) the encoding of the file.
581 verbose: Show the lines that are not compliant with the style.
585 Whether the file is compliant with the style (before the check),
589 verbose_infos: List[str] = []
590 is_file_compliant =
True
592 with open(filename,
"rb")
as f:
594 file_lines = file_data.decode(FILE_ENCODING, errors=
"replace").splitlines(keepends=
True)
598 file_data.decode(FILE_ENCODING)
600 except UnicodeDecodeError
as e:
601 is_file_compliant =
False
605 bad_char_start_index = e.start
606 n_chars_file_read = 0
608 for line_number, line
in enumerate(file_lines):
609 n_chars_line = len(line)
611 if bad_char_start_index < n_chars_file_read + n_chars_line:
612 bad_char_column = bad_char_start_index - n_chars_file_read
614 verbose_infos.extend(
616 f
"{filename}:{line_number + 1}:{bad_char_column + 1}: error: bad {FILE_ENCODING} encoding",
618 f
" {'':>{bad_char_column}}^",
624 n_chars_file_read += n_chars_line
627 if fix
and not is_file_compliant:
628 with open(filename,
"w", encoding=FILE_ENCODING)
as f:
629 f.writelines(file_lines)
631 return (filename, is_file_compliant, verbose_infos)
638 respect_clang_format_guards: bool,
639 check_style_line_function: Callable[[str, str, int], Tuple[bool, str, List[str]]],
640) -> Tuple[str, bool, List[str]]:
641 """Check / fix a file manually using a function to check / fix each line.
644 filename: Name of the file to be checked.
645 fix: Whether to fix (True) or just check (False) the style of the file.
646 verbose: Show the lines that are not compliant with the style.
647 respect_clang_format_guards: Whether to respect clang-format guards.
648 check_style_line_function: Function used to check each line.
652 Whether the file is compliant with the style (before the check),
656 is_file_compliant =
True
657 verbose_infos: List[str] = []
658 clang_format_enabled =
True
660 with open(filename,
"r", encoding=FILE_ENCODING)
as f:
661 file_lines = f.readlines()
663 for i, line
in enumerate(file_lines):
665 if respect_clang_format_guards:
666 line_stripped = line.strip()
668 if line_stripped
in FORMAT_GUARD_ON:
669 clang_format_enabled =
True
670 elif line_stripped
in FORMAT_GUARD_OFF:
671 clang_format_enabled =
False
673 if not clang_format_enabled
and line_stripped
not in (
674 FORMAT_GUARD_ON + FORMAT_GUARD_OFF
679 is_line_compliant, line_fixed, line_verbose_infos = check_style_line_function(
683 if not is_line_compliant:
684 is_file_compliant =
False
685 file_lines[i] = line_fixed
686 verbose_infos.extend(line_verbose_infos)
689 if not fix
and not verbose:
693 if fix
and not is_file_compliant:
694 with open(filename,
"w", encoding=FILE_ENCODING)
as f:
695 f.writelines(file_lines)
697 return (filename, is_file_compliant, verbose_infos)
704) -> Tuple[bool, str, List[str]]:
705 """Check / fix #include headers from the same module with the "ns3/" prefix in a line.
708 line: The line to check.
709 filename: Name of the file to be checked.
710 line_number: The number of the line checked.
714 Whether the line is compliant with the style (before the check),
718 is_line_compliant =
True
720 verbose_infos: List[str] = []
723 line_stripped = line.strip()
724 header_file = re.findall(
r'^#include ["<]ns3/(.*\.h)[">]', line_stripped)
728 header_file = header_file[0]
729 parent_path = os.path.split(filename)[0]
731 if os.path.exists(os.path.join(parent_path, header_file)):
732 is_line_compliant =
False
734 line_stripped.replace(f
"ns3/{header_file}", header_file)
740 header_index = len(
'#include "')
742 verbose_infos.extend(
744 f
'{filename}:{line_number + 1}:{header_index + 1}: error: #include headers from the same module with the "ns3/" prefix detected',
746 f
" {'':>{header_index}}^",
750 return (is_line_compliant, line_fixed, verbose_infos)
757) -> Tuple[bool, str, List[str]]:
758 """Check / fix ns-3 #include headers using angle brackets <> rather than quotes "" in a line.
761 line: The line to check.
762 filename: Name of the file to be checked.
763 line_number: The number of the line checked.
766 Tuple (Whether the line is compliant with the style (before the check),
771 is_line_compliant =
True
773 verbose_infos: List[str] = []
776 header_file = re.findall(
r"^#include <ns3/.*\.h>", line)
779 is_line_compliant =
False
780 line_fixed = line.replace(
"<",
'"').replace(
">",
'"')
782 header_index = len(
"#include ")
785 f
"{filename}:{line_number + 1}:{header_index + 1}: error: ns-3 #include headers with angle brackets detected",
787 f
" {'':{header_index}}^",
790 return (is_line_compliant, line_fixed, verbose_infos)
797) -> Tuple[bool, str, List[str]]:
798 """Check / fix Doxygen tags using \\ rather than @ in a line.
801 line: The line to check.
802 filename: Name of the file to be checked.
803 line_number: The number of the line checked.
806 Tuple (Whether the line is compliant with the style (before the check),
817 is_line_compliant =
True
819 verbose_infos: List[str] = []
822 line_stripped = line.rstrip()
823 regex_findings = re.findall(
r"^\s*(?:\*|\/\*\*|\/\/\/)\s*(\\\w{3,})(?=(?:\s|$))", line_stripped)
826 doxygen_tag = regex_findings[0]
828 if doxygen_tag
not in IGNORED_WORDS:
829 is_line_compliant =
False
831 doxygen_tag_index = line_fixed.find(doxygen_tag)
832 line_fixed = line.replace(doxygen_tag, f
"@{doxygen_tag[1:]}")
834 verbose_infos.extend(
836 f
"{filename}:{line_number + 1}:{doxygen_tag_index + 1}: error: detected Doxygen tags using \\ rather than @",
838 f
" {'':{doxygen_tag_index}}^",
842 return (is_line_compliant, line_fixed, verbose_infos)
849) -> Tuple[bool, str, List[str]]:
850 """Check / fix SPDX licenses rather than GPL text in a line.
853 line: The line to check.
854 filename: Name of the file to be checked.
855 line_number: The number of the line checked.
858 Tuple (Whether the line is compliant with the style (before the check),
864 GPL_LICENSE_LINES = [
865 "This program is free software; you can redistribute it and/or modify",
866 "it under the terms of the GNU General Public License version 2 as",
867 "published by the Free Software Foundation;",
868 "This program is distributed in the hope that it will be useful,",
869 "but WITHOUT ANY WARRANTY; without even the implied warranty of",
870 "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the",
871 "GNU General Public License for more details.",
872 "You should have received a copy of the GNU General Public License",
873 "along with this program; if not, write to the Free Software",
874 "Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA",
879 SPDX_LICENSE =
"SPDX-License-Identifier: GPL-2.0-only"
882 is_line_compliant =
True
884 verbose_infos: List[str] = []
887 line_stripped = line.strip()
888 line_stripped_no_leading_comments = line_stripped.strip(
"*#/").strip()
890 if line_stripped_no_leading_comments
in GPL_LICENSE_LINES:
891 is_line_compliant =
False
897 if line_stripped_no_leading_comments == GPL_LICENSE_LINES[0]:
898 line_fixed = line.replace(line_stripped_no_leading_comments, SPDX_LICENSE)
902 verbose_infos.extend(
904 f
"{filename}:{line_number + 1}:{col_index}: error: GPL license text detected instead of SPDX license",
906 f
" {'':>{col_index}}^",
910 return (is_line_compliant, line_fixed, verbose_infos)
917) -> Tuple[bool, str, List[str]]:
918 """Check / fix emacs file style comment in a line.
921 line: The line to check.
922 filename: Name of the file to be checked.
923 line_number: The number of the line checked.
926 Tuple (Whether the line is compliant with the style (before the check),
931 is_line_compliant =
True
933 verbose_infos: List[str] = []
936 line_stripped = line.strip()
938 emacs_line = re.search(
r"c-file-style:|py-indent-offset:", line_stripped)
942 is_line_compliant =
False
944 col_index = emacs_line.start()
947 f
"{filename}:{line_number + 1}:{col_index}: error: emacs file style comment detected",
949 f
" {'':{col_index}}^",
952 return (is_line_compliant, line_fixed, verbose_infos)
959) -> Tuple[bool, str, List[str]]:
960 """Check / fix whitespace in a line.
963 line: The line to check.
964 filename: Name of the file to be checked.
965 line_number: The number of the line checked.
968 Tuple (Whether the line is compliant with the style (before the check),
973 is_line_compliant =
True
974 line_fixed = line.rstrip() +
"\n"
975 verbose_infos: List[str] = []
977 if line_fixed != line:
978 is_line_compliant =
False
979 line_fixed_stripped_expanded = line_fixed.rstrip().expandtabs(TAB_SIZE)
982 f
"{filename}:{line_number + 1}:{len(line_fixed_stripped_expanded) + 1}: error: Trailing whitespace detected",
983 f
" {line_fixed_stripped_expanded}",
984 f
" {'':>{len(line_fixed_stripped_expanded)}}^",
987 return (is_line_compliant, line_fixed, verbose_infos)
994) -> Tuple[bool, str, List[str]]:
995 """Check / fix tabs in a line.
998 line: The line to check.
999 filename: Name of the file to be checked.
1000 line_number: The number of the line checked.
1003 Tuple (Whether the line is compliant with the style (before the check),
1005 Verbose information)
1008 is_line_compliant =
True
1010 verbose_infos: List[str] = []
1012 tab_index = line.find(
"\t")
1015 is_line_compliant =
False
1016 line_fixed = line.expandtabs(TAB_SIZE)
1019 f
"{filename}:{line_number + 1}:{tab_index + 1}: error: Tab detected",
1020 f
" {line.rstrip()}",
1021 f
" {'':>{tab_index}}^",
1024 return (is_line_compliant, line_fixed, verbose_infos)
1030if __name__ ==
"__main__":
1031 parser = argparse.ArgumentParser(
1032 description=
"Check and apply the ns-3 coding style recursively to all files in the given PATHs. "
1033 "The script checks the formatting of the files using clang-format and"
1034 " other coding style rules manually (see script arguments). "
1035 "All checks respect clang-format guards, except trailing whitespace and file encoding,"
1036 " which are always checked. "
1037 'When used in "check mode" (default), the script runs all checks in all files. '
1038 "If it detects non-formatted files, they will be printed and this process exits with a non-zero code. "
1039 'When used in "fix mode", this script automatically fixes the files and exits with 0 code.'
1042 parser.add_argument(
1047 help=
"List of paths to the files to check",
1050 parser.add_argument(
1051 "--no-include-prefixes",
1052 action=
"store_true",
1053 help=
'Do not check / fix #include headers from the same module with the "ns3/" prefix (respects clang-format guards)',
1056 parser.add_argument(
1057 "--no-include-quotes",
1058 action=
"store_true",
1059 help=
'Do not check / fix ns-3 #include headers using angle brackets <> rather than quotes "" (respects clang-format guards)',
1062 parser.add_argument(
1063 "--no-doxygen-tags",
1064 action=
"store_true",
1065 help=
"Do not check / fix Doxygen tags using @ rather than \\ (respects clang-format guards)",
1068 parser.add_argument(
1070 action=
"store_true",
1071 help=
"Do not check / fix SPDX licenses rather than GPL text (respects clang-format guards)",
1074 parser.add_argument(
1076 action=
"store_true",
1077 help=
"Do not check / fix emacs file style comments (respects clang-format guards)",
1080 parser.add_argument(
1082 action=
"store_true",
1083 help=
"Do not check / fix trailing whitespace",
1086 parser.add_argument(
1088 action=
"store_true",
1089 help=
"Do not check / fix tabs (respects clang-format guards)",
1092 parser.add_argument(
1094 action=
"store_true",
1095 help=
"Do not check / fix code formatting (respects clang-format guards)",
1098 parser.add_argument(
1100 action=
"store_true",
1101 help=f
"Do not check / fix file encoding ({FILE_ENCODING})",
1104 parser.add_argument(
1106 action=
"store_true",
1107 help=
"Fix coding style issues detected in the files",
1110 parser.add_argument(
1113 action=
"store_true",
1114 help=
"Show the lines that are not well-formatted",
1117 parser.add_argument(
1121 default=max(1, os.cpu_count() - 1),
1122 help=
"Number of parallel jobs",
1125 args = parser.parse_args()
1131 "include_prefixes":
not args.no_include_prefixes,
1132 "include_quotes":
not args.no_include_quotes,
1133 "doxygen_tags":
not args.no_doxygen_tags,
1134 "license":
not args.no_licenses,
1135 "emacs":
not args.no_emacs,
1136 "whitespace":
not args.no_whitespace,
1137 "tabs":
not args.no_tabs,
1138 "formatting":
not args.no_formatting,
1139 "encoding":
not args.no_encoding,
1142 verbose=args.verbose,
1146 except Exception
as ex:
1150 if not all_checks_successful:
1154 "Notes to fix the above formatting issues:",
1155 ' - To fix the formatting of specific files, run this script with the flag "--fix":',
1156 " $ ./utils/check-style-clang-format.py --fix path [path ...]",
1157 " - To fix the formatting of all files modified by this branch, run this script in the following way:",
1158 " $ git diff --name-only master | xargs ./utils/check-style-clang-format.py --fix",