10Check and apply the ns-3 coding style recursively to all files in the PATH arguments.
12The coding style is defined with the clang-format tool, whose definitions are in
13the ".clang-format" file. This script performs the following checks / fixes:
14- Check / apply clang-format. Respects clang-format guards.
15- Check / fix local #include headers with "ns3/" prefix. Respects clang-format guards.
16- Check / fix ns-3 #include headers using angle brackets <> rather than quotes "". Respects clang-format guards.
17- Check / fix Doxygen tags using @ rather than \\. Respects clang-format guards.
18- Check / fix SPDX licenses rather than GPL text. Respects clang-format guards.
19- Check / fix emacs file style comments. Respects clang-format guards.
20- Check / trim trailing whitespace. Always checked.
21- Check / replace tabs with spaces. Respects clang-format guards.
22- Check file encoding. Always checked.
24This script can be applied to all text files in a given path or to individual files.
26NOTE: The formatting check requires clang-format to be found on the path (see the supported versions below).
27The remaining checks do not depend on clang-format and can be executed by disabling clang-format
28checking with the "--no-formatting" option.
32import concurrent.futures
39from typing
import Callable, Dict, List, Tuple
44CLANG_FORMAT_MAX_VERSION = 17
45CLANG_FORMAT_MIN_VERSION = 15
54 "// clang-format off",
55 "# cmake-format: off",
59DIRECTORIES_TO_SKIP = [
90FILES_TO_CHECK: Dict[str, List[str]] = {c: []
for c
in CHECKS}
92FILES_TO_CHECK[
"tabs"] = [
97 "codespell-ignored-lines",
98 "codespell-ignored-words",
102FILES_TO_CHECK[
"whitespace"] = FILES_TO_CHECK[
"tabs"] + [
107FILE_EXTENSIONS_TO_CHECK: Dict[str, List[str]] = {c: []
for c
in CHECKS}
109FILE_EXTENSIONS_TO_CHECK[
"formatting"] = [
115FILE_EXTENSIONS_TO_CHECK[
"include_prefixes"] = FILE_EXTENSIONS_TO_CHECK[
"formatting"]
116FILE_EXTENSIONS_TO_CHECK[
"include_quotes"] = FILE_EXTENSIONS_TO_CHECK[
"formatting"]
117FILE_EXTENSIONS_TO_CHECK[
"doxygen_tags"] = FILE_EXTENSIONS_TO_CHECK[
"formatting"]
118FILE_EXTENSIONS_TO_CHECK[
"encoding"] = FILE_EXTENSIONS_TO_CHECK[
"formatting"]
120FILE_EXTENSIONS_TO_CHECK[
"license"] = [
128FILE_EXTENSIONS_TO_CHECK[
"emacs"] = [
136FILE_EXTENSIONS_TO_CHECK[
"tabs"] = [
155FILE_EXTENSIONS_TO_CHECK[
"whitespace"] = FILE_EXTENSIONS_TO_CHECK[
"tabs"] + [
173FILE_ENCODING =
"UTF-8"
181 Check whether a directory should be analyzed.
183 @param dirpath Directory path.
184 @return Whether the directory should be analyzed.
187 _, directory = os.path.split(dirpath)
189 return directory
not in DIRECTORIES_TO_SKIP
194 files_to_check: List[str],
195 file_extensions_to_check: List[str],
198 Check whether a file should be analyzed.
200 @param path Path to the file.
201 @param files_to_check List of files that shall be checked.
202 @param file_extensions_to_check List of file extensions that shall be checked.
203 @return Whether the file should be analyzed.
206 filename = os.path.split(path)[1]
208 if filename
in FILES_TO_SKIP:
211 extension = os.path.splitext(filename)[1]
213 return filename
in files_to_check
or extension
in file_extensions_to_check
218) -> Dict[str, List[str]]:
220 Find all files to be checked in a given list of paths.
222 @param paths List of paths to the files to check.
223 @return Dictionary of checks and corresponding list of files to check.
225 "formatting": list_of_files_to_check_formatting,
231 files_found: List[str] = []
234 abs_path = os.path.abspath(os.path.expanduser(path))
236 if os.path.isfile(abs_path):
237 files_found.append(path)
239 elif os.path.isdir(abs_path):
240 for dirpath, dirnames, filenames
in os.walk(path, topdown=
True):
246 files_found.extend([os.path.join(dirpath, f)
for f
in filenames])
249 raise ValueError(f
"{path} is not a valid file nor a directory")
254 files_to_check: Dict[str, List[str]] = {c: []
for c
in CHECKS}
256 for f
in files_found:
259 files_to_check[check].append(f)
261 return files_to_check
266 Find the path to one of the supported versions of clang-format.
267 If no supported version of clang-format is found, raise an exception.
269 @return Path to clang-format.
273 for version
in range(CLANG_FORMAT_MAX_VERSION, CLANG_FORMAT_MIN_VERSION - 1, -1):
274 clang_format_path = shutil.which(f
"clang-format-{version}")
276 if clang_format_path:
277 return clang_format_path
280 clang_format_path = shutil.which(
"clang-format")
283 if clang_format_path:
284 process = subprocess.run(
285 [clang_format_path,
"--version"],
291 clang_format_version = process.stdout.strip()
292 version_regex = re.findall(
r"\b(\d+)(\.\d+){0,2}\b", clang_format_version)
295 major_version = int(version_regex[0][0])
297 if CLANG_FORMAT_MIN_VERSION <= major_version <= CLANG_FORMAT_MAX_VERSION:
298 return clang_format_path
302 f
"Could not find any supported version of clang-format installed on this system. "
303 f
"List of supported versions: [{CLANG_FORMAT_MAX_VERSION}-{CLANG_FORMAT_MIN_VERSION}]. "
304 + (f
"Found clang-format {major_version}." if major_version
else "")
313 checks_enabled: Dict[str, bool],
319 Check / fix the coding style of a list of files.
321 @param paths List of paths to the files to check.
322 @param checks_enabled Dictionary of checks indicating whether to enable each of them.
323 @param fix Whether to fix (True) or just check (False) the file.
324 @param verbose Show the lines that are not compliant with the style.
325 @param n_jobs Number of parallel jobs.
326 @return Whether all files are compliant with all enabled style checks.
330 checks_successful = {c:
True for c
in CHECKS}
333 "include_prefixes":
'#include headers from the same module with the "ns3/" prefix',
334 "include_quotes":
'ns-3 #include headers using angle brackets <> rather than quotes ""',
335 "doxygen_tags":
"Doxygen tags using \\ rather than @",
336 "license":
"GPL license text instead of SPDX license",
337 "emacs":
"emacs file style comments",
338 "whitespace":
"trailing whitespace",
340 "formatting":
"bad code formatting",
341 "encoding": f
"bad file encoding ({FILE_ENCODING})",
344 check_style_file_functions_kwargs = {
345 "include_prefixes": {
346 "function": check_manually_file,
348 "respect_clang_format_guards":
True,
349 "check_style_line_function": check_include_prefixes_line,
353 "function": check_manually_file,
355 "respect_clang_format_guards":
True,
356 "check_style_line_function": check_include_quotes_line,
360 "function": check_manually_file,
362 "respect_clang_format_guards":
True,
363 "check_style_line_function": check_doxygen_tags_line,
367 "function": check_manually_file,
369 "respect_clang_format_guards":
True,
370 "check_style_line_function": check_licenses_line,
374 "function": check_manually_file,
376 "respect_clang_format_guards":
True,
377 "check_style_line_function": check_emacs_line,
381 "function": check_manually_file,
383 "respect_clang_format_guards":
False,
384 "check_style_line_function": check_whitespace_line,
388 "function": check_manually_file,
390 "respect_clang_format_guards":
True,
391 "check_style_line_function": check_tabs_line,
395 "function": check_formatting_file,
399 "function": check_encoding_file,
404 if checks_enabled[
"formatting"]:
405 check_style_file_functions_kwargs[
"formatting"][
"kwargs"] = {
409 n_checks_enabled = sum(checks_enabled.values())
413 if checks_enabled[check]:
415 style_check_strs[check],
416 check_style_file_functions_kwargs[check][
"function"],
417 files_to_check[check],
421 **check_style_file_functions_kwargs[check][
"kwargs"],
426 if n_check < n_checks_enabled:
429 return all(checks_successful.values())
433 style_check_str: str,
434 check_style_file_function: Callable[..., Tuple[str, bool, List[str]]],
435 filenames: List[str],
442 Check / fix style of a list of files.
444 @param style_check_str Description of the check to be performed.
445 @param check_style_file_function Function used to check the file.
446 @param filename Name of the file to be checked.
447 @param fix Whether to fix (True) or just check (False) the file (True).
448 @param verbose Show the lines that are not compliant with the style.
449 @param n_jobs Number of parallel jobs.
450 @param kwargs Additional keyword arguments to the check_style_file_function.
451 @return Whether all files are compliant with the style.
455 non_compliant_files: List[str] = []
456 files_verbose_infos: Dict[str, List[str]] = {}
458 with concurrent.futures.ProcessPoolExecutor(n_jobs)
as executor:
459 non_compliant_files_results = executor.map(
460 check_style_file_function,
462 itertools.repeat(fix),
463 itertools.repeat(verbose),
464 *[arg
if isinstance(arg, list)
else itertools.repeat(arg)
for arg
in kwargs.values()],
467 for filename, is_file_compliant, verbose_infos
in non_compliant_files_results:
468 if not is_file_compliant:
469 non_compliant_files.append(filename)
472 files_verbose_infos[filename] = verbose_infos
475 if not non_compliant_files:
476 print(f
"- No files detected with {style_check_str}")
480 n_non_compliant_files = len(non_compliant_files)
483 print(f
"- Fixed {style_check_str} in the files ({n_non_compliant_files}):")
485 print(f
"- Detected {style_check_str} in the files ({n_non_compliant_files}):")
487 for f
in non_compliant_files:
489 print(*[f
" {l}" for l
in files_verbose_infos[f]], sep=
"\n")
504 clang_format_path: str,
505) -> Tuple[str, bool, List[str]]:
507 Check / fix the coding style of a file with clang-format.
509 @param filename Name of the file to be checked.
510 @param fix Whether to fix (True) or just check (False) the style of the file.
511 @param verbose Show the lines that are not compliant with the style.
512 @param clang_format_path Path to clang-format.
513 @return Tuple [Filename,
514 Whether the file is compliant with the style (before the check),
515 Verbose information].
518 verbose_infos: List[str] = []
521 process = subprocess.run(
529 f
"--ferror-limit={0 if verbose else 1}",
536 is_file_compliant = process.returncode == 0
539 verbose_infos = process.stderr.splitlines()
542 if fix
and not is_file_compliant:
543 process = subprocess.run(
551 stdout=subprocess.DEVNULL,
552 stderr=subprocess.DEVNULL,
555 return (filename, is_file_compliant, verbose_infos)
562) -> Tuple[str, bool, List[str]]:
564 Check / fix the encoding of a file.
566 @param filename Name of the file to be checked.
567 @param fix Whether to fix (True) or just check (False) the encoding of the file.
568 @param verbose Show the lines that are not compliant with the style.
569 @return Tuple [Filename,
570 Whether the file is compliant with the style (before the check),
571 Verbose information].
574 verbose_infos: List[str] = []
575 is_file_compliant =
True
577 with open(filename,
"rb")
as f:
579 file_lines = file_data.decode(FILE_ENCODING, errors=
"replace").splitlines(keepends=
True)
583 file_data.decode(FILE_ENCODING)
585 except UnicodeDecodeError
as e:
586 is_file_compliant =
False
590 bad_char_start_index = e.start
591 n_chars_file_read = 0
593 for line_number, line
in enumerate(file_lines):
594 n_chars_line = len(line)
596 if bad_char_start_index < n_chars_file_read + n_chars_line:
597 bad_char_column = bad_char_start_index - n_chars_file_read
599 verbose_infos.extend(
601 f
"{filename}:{line_number + 1}:{bad_char_column + 1}: error: bad {FILE_ENCODING} encoding",
603 f
" {'':>{bad_char_column}}^",
609 n_chars_file_read += n_chars_line
612 if fix
and not is_file_compliant:
613 with open(filename,
"w", encoding=FILE_ENCODING)
as f:
614 f.writelines(file_lines)
616 return (filename, is_file_compliant, verbose_infos)
623 respect_clang_format_guards: bool,
624 check_style_line_function: Callable[[str, str, int], Tuple[bool, str, List[str]]],
625) -> Tuple[str, bool, List[str]]:
627 Check / fix a file manually using a function to check / fix each line.
629 @param filename Name of the file to be checked.
630 @param fix Whether to fix (True) or just check (False) the style of the file.
631 @param verbose Show the lines that are not compliant with the style.
632 @param respect_clang_format_guards Whether to respect clang-format guards.
633 @param check_style_line_function Function used to check each line.
634 @return Tuple [Filename,
635 Whether the file is compliant with the style (before the check),
636 Verbose information].
639 is_file_compliant =
True
640 verbose_infos: List[str] = []
641 clang_format_enabled =
True
643 with open(filename,
"r", encoding=FILE_ENCODING)
as f:
644 file_lines = f.readlines()
646 for i, line
in enumerate(file_lines):
648 if respect_clang_format_guards:
649 line_stripped = line.strip()
651 if line_stripped
in FORMAT_GUARD_ON:
652 clang_format_enabled =
True
653 elif line_stripped
in FORMAT_GUARD_OFF:
654 clang_format_enabled =
False
656 if not clang_format_enabled
and line_stripped
not in (
657 FORMAT_GUARD_ON + FORMAT_GUARD_OFF
662 (is_line_compliant, line_fixed, line_verbose_infos) = check_style_line_function(
666 if not is_line_compliant:
667 is_file_compliant =
False
668 file_lines[i] = line_fixed
669 verbose_infos.extend(line_verbose_infos)
672 if not fix
and not verbose:
676 if fix
and not is_file_compliant:
677 with open(filename,
"w", encoding=FILE_ENCODING)
as f:
678 f.writelines(file_lines)
680 return (filename, is_file_compliant, verbose_infos)
687) -> Tuple[bool, str, List[str]]:
689 Check / fix #include headers from the same module with the "ns3/" prefix in a line.
691 @param line The line to check.
692 @param filename Name of the file to be checked.
693 @param line_number The number of the line checked.
694 @return Tuple [Whether the line is compliant with the style (before the check),
696 Verbose information].
699 is_line_compliant =
True
701 verbose_infos: List[str] = []
704 line_stripped = line.strip()
705 header_file = re.findall(
r'^#include ["<]ns3/(.*\.h)[">]', line_stripped)
709 header_file = header_file[0]
710 parent_path = os.path.split(filename)[0]
712 if os.path.exists(os.path.join(parent_path, header_file)):
713 is_line_compliant =
False
715 line_stripped.replace(f
"ns3/{header_file}", header_file)
721 header_index = len(
'#include "')
723 verbose_infos.extend(
725 f
'{filename}:{line_number + 1}:{header_index + 1}: error: #include headers from the same module with the "ns3/" prefix detected',
727 f
" {'':>{header_index}}^",
731 return (is_line_compliant, line_fixed, verbose_infos)
738) -> Tuple[bool, str, List[str]]:
740 Check / fix ns-3 #include headers using angle brackets <> rather than quotes "" in a line.
742 @param line The line to check.
743 @param filename Name of the file to be checked.
744 @param line_number The number of the line checked.
745 @return Tuple [Whether the line is compliant with the style (before the check),
747 Verbose information].
750 is_line_compliant =
True
752 verbose_infos: List[str] = []
755 header_file = re.findall(
r"^#include <ns3/.*\.h>", line)
758 is_line_compliant =
False
759 line_fixed = line.replace(
"<",
'"').replace(
">",
'"')
761 header_index = len(
"#include ")
764 f
"{filename}:{line_number + 1}:{header_index + 1}: error: ns-3 #include headers with angle brackets detected",
766 f
" {'':{header_index}}^",
769 return (is_line_compliant, line_fixed, verbose_infos)
776) -> Tuple[bool, str, List[str]]:
778 Check / fix Doxygen tags using \\ rather than @ in a line.
780 @param line The line to check.
781 @param filename Name of the file to be checked.
782 @param line_number The number of the line checked.
783 @return Tuple [Whether the line is compliant with the style (before the check),
785 Verbose information].
794 is_line_compliant =
True
796 verbose_infos: List[str] = []
799 line_stripped = line.rstrip()
800 regex_findings = re.findall(
r"^\s*(?:\*|\/\*\*|\/\/\/)\s*(\\\w{3,})(?=(?:\s|$))", line_stripped)
803 doxygen_tag = regex_findings[0]
805 if doxygen_tag
not in IGNORED_WORDS:
806 is_line_compliant =
False
808 doxygen_tag_index = line_fixed.find(doxygen_tag)
809 line_fixed = line.replace(doxygen_tag, f
"@{doxygen_tag[1:]}")
811 verbose_infos.extend(
813 f
"{filename}:{line_number + 1}:{doxygen_tag_index + 1}: error: detected Doxygen tags using \\ rather than @",
815 f
" {'':{doxygen_tag_index}}^",
819 return (is_line_compliant, line_fixed, verbose_infos)
826) -> Tuple[bool, str, List[str]]:
828 Check / fix SPDX licenses rather than GPL text in a line.
830 @param line The line to check.
831 @param filename Name of the file to be checked.
832 @param line_number The number of the line checked.
833 @return Tuple [Whether the line is compliant with the style (before the check),
835 Verbose information].
839 GPL_LICENSE_LINES = [
840 "This program is free software; you can redistribute it and/or modify",
841 "it under the terms of the GNU General Public License version 2 as",
842 "published by the Free Software Foundation;",
843 "This program is distributed in the hope that it will be useful,",
844 "but WITHOUT ANY WARRANTY; without even the implied warranty of",
845 "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the",
846 "GNU General Public License for more details.",
847 "You should have received a copy of the GNU General Public License",
848 "along with this program; if not, write to the Free Software",
849 "Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA",
853 SPDX_LICENSE =
"SPDX-License-Identifier: GPL-2.0-only"
855 is_line_compliant =
True
857 verbose_infos: List[str] = []
860 line_stripped = line.strip()
861 line_stripped_no_leading_comments = line_stripped.strip(
"*#/").strip()
863 if line_stripped_no_leading_comments
in GPL_LICENSE_LINES:
864 is_line_compliant =
False
870 if line_stripped_no_leading_comments == GPL_LICENSE_LINES[0]:
871 line_fixed = line.replace(line_stripped_no_leading_comments, SPDX_LICENSE)
875 verbose_infos.extend(
877 f
"{filename}:{line_number + 1}:{col_index}: error: GPL license text detected instead of SPDX license",
879 f
" {'':>{col_index}}^",
883 return (is_line_compliant, line_fixed, verbose_infos)
890) -> Tuple[bool, str, List[str]]:
892 Check / fix emacs file style comment in a line.
894 @param line The line to check.
895 @param filename Name of the file to be checked.
896 @param line_number The number of the line checked.
897 @return Tuple [Whether the line is compliant with the style (before the check),
899 Verbose information].
902 is_line_compliant =
True
904 verbose_infos: List[str] = []
907 line_stripped = line.strip()
909 emacs_line = re.search(
r"c-file-style:|py-indent-offset:", line_stripped)
913 is_line_compliant =
False
915 col_index = emacs_line.start()
918 f
"{filename}:{line_number + 1}:{col_index}: error: emacs file style comment detected",
920 f
" {'':{col_index}}^",
923 return (is_line_compliant, line_fixed, verbose_infos)
930) -> Tuple[bool, str, List[str]]:
932 Check / fix whitespace in a line.
934 @param line The line to check.
935 @param filename Name of the file to be checked.
936 @param line_number The number of the line checked.
937 @return Tuple [Whether the line is compliant with the style (before the check),
939 Verbose information].
942 is_line_compliant =
True
943 line_fixed = line.rstrip() +
"\n"
944 verbose_infos: List[str] = []
946 if line_fixed != line:
947 is_line_compliant =
False
948 line_fixed_stripped_expanded = line_fixed.rstrip().expandtabs(TAB_SIZE)
951 f
"{filename}:{line_number + 1}:{len(line_fixed_stripped_expanded) + 1}: error: Trailing whitespace detected",
952 f
" {line_fixed_stripped_expanded}",
953 f
" {'':>{len(line_fixed_stripped_expanded)}}^",
956 return (is_line_compliant, line_fixed, verbose_infos)
963) -> Tuple[bool, str, List[str]]:
965 Check / fix tabs in a line.
967 @param line The line to check.
968 @param filename Name of the file to be checked.
969 @param line_number The number of the line checked.
970 @return Tuple [Whether the line is compliant with the style (before the check),
972 Verbose information].
975 is_line_compliant =
True
977 verbose_infos: List[str] = []
979 tab_index = line.find(
"\t")
982 is_line_compliant =
False
983 line_fixed = line.expandtabs(TAB_SIZE)
986 f
"{filename}:{line_number + 1}:{tab_index + 1}: error: Tab detected",
988 f
" {'':>{tab_index}}^",
991 return (is_line_compliant, line_fixed, verbose_infos)
997if __name__ ==
"__main__":
998 parser = argparse.ArgumentParser(
999 description=
"Check and apply the ns-3 coding style recursively to all files in the given PATHs. "
1000 "The script checks the formatting of the files using clang-format and"
1001 " other coding style rules manually (see script arguments). "
1002 "All checks respect clang-format guards, except trailing whitespace and file encoding,"
1003 " which are always checked. "
1004 'When used in "check mode" (default), the script runs all checks in all files. '
1005 "If it detects non-formatted files, they will be printed and this process exits with a non-zero code. "
1006 'When used in "fix mode", this script automatically fixes the files and exits with 0 code.'
1009 parser.add_argument(
1014 help=
"List of paths to the files to check",
1017 parser.add_argument(
1018 "--no-include-prefixes",
1019 action=
"store_true",
1020 help=
'Do not check / fix #include headers from the same module with the "ns3/" prefix (respects clang-format guards)',
1023 parser.add_argument(
1024 "--no-include-quotes",
1025 action=
"store_true",
1026 help=
'Do not check / fix ns-3 #include headers using angle brackets <> rather than quotes "" (respects clang-format guards)',
1029 parser.add_argument(
1030 "--no-doxygen-tags",
1031 action=
"store_true",
1032 help=
"Do not check / fix Doxygen tags using @ rather than \\ (respects clang-format guards)",
1035 parser.add_argument(
1037 action=
"store_true",
1038 help=
"Do not check / fix SPDX licenses rather than GPL text (respects clang-format guards)",
1041 parser.add_argument(
1043 action=
"store_true",
1044 help=
"Do not check / fix emacs file style comments (respects clang-format guards)",
1047 parser.add_argument(
1049 action=
"store_true",
1050 help=
"Do not check / fix trailing whitespace",
1053 parser.add_argument(
1055 action=
"store_true",
1056 help=
"Do not check / fix tabs (respects clang-format guards)",
1059 parser.add_argument(
1061 action=
"store_true",
1062 help=
"Do not check / fix code formatting (respects clang-format guards)",
1065 parser.add_argument(
1067 action=
"store_true",
1068 help=f
"Do not check / fix file encoding ({FILE_ENCODING})",
1071 parser.add_argument(
1073 action=
"store_true",
1074 help=
"Fix coding style issues detected in the files",
1077 parser.add_argument(
1080 action=
"store_true",
1081 help=
"Show the lines that are not well-formatted",
1084 parser.add_argument(
1088 default=max(1, os.cpu_count() - 1),
1089 help=
"Number of parallel jobs",
1092 args = parser.parse_args()
1098 "include_prefixes":
not args.no_include_prefixes,
1099 "include_quotes":
not args.no_include_quotes,
1100 "doxygen_tags":
not args.no_doxygen_tags,
1101 "license":
not args.no_licenses,
1102 "emacs":
not args.no_emacs,
1103 "whitespace":
not args.no_whitespace,
1104 "tabs":
not args.no_tabs,
1105 "formatting":
not args.no_formatting,
1106 "encoding":
not args.no_encoding,
1109 verbose=args.verbose,
1113 except Exception
as ex:
1117 if not all_checks_successful:
1121 "Notes to fix the above formatting issues:",
1122 ' - To fix the formatting of specific files, run this script with the flag "--fix":',
1123 " $ ./utils/check-style-clang-format.py --fix path [path ...]",
1124 " - To fix the formatting of all files modified by this branch, run this script in the following way:",
1125 " $ git diff --name-only master | xargs ./utils/check-style-clang-format.py --fix",