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 / trim trailing whitespace. Always checked.
19- Check / replace tabs with spaces. Respects clang-format guards.
20- Check / fix SPDX licenses rather than GPL text. 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 = 17
44CLANG_FORMAT_MIN_VERSION = 14
53 "// clang-format off",
54 "# cmake-format: off",
58DIRECTORIES_TO_SKIP = [
88FILES_TO_CHECK: Dict[str, List[str]] = {c: []
for c
in CHECKS}
90FILES_TO_CHECK[
"tabs"] = [
95 "codespell-ignored-lines",
96 "codespell-ignored-words",
100FILES_TO_CHECK[
"whitespace"] = FILES_TO_CHECK[
"tabs"] + [
105FILE_EXTENSIONS_TO_CHECK: Dict[str, List[str]] = {c: []
for c
in CHECKS}
107FILE_EXTENSIONS_TO_CHECK[
"formatting"] = [
113FILE_EXTENSIONS_TO_CHECK[
"include_prefixes"] = FILE_EXTENSIONS_TO_CHECK[
"formatting"]
114FILE_EXTENSIONS_TO_CHECK[
"include_quotes"] = FILE_EXTENSIONS_TO_CHECK[
"formatting"]
115FILE_EXTENSIONS_TO_CHECK[
"doxygen_tags"] = FILE_EXTENSIONS_TO_CHECK[
"formatting"]
116FILE_EXTENSIONS_TO_CHECK[
"encoding"] = FILE_EXTENSIONS_TO_CHECK[
"formatting"]
118FILE_EXTENSIONS_TO_CHECK[
"tabs"] = [
137FILE_EXTENSIONS_TO_CHECK[
"whitespace"] = FILE_EXTENSIONS_TO_CHECK[
"tabs"] + [
153FILE_EXTENSIONS_TO_CHECK[
"license"] = [
163FILE_ENCODING =
"UTF-8"
171 Check whether a directory should be analyzed.
173 @param dirpath Directory path.
174 @return Whether the directory should be analyzed.
177 _, directory = os.path.split(dirpath)
179 return directory
not in DIRECTORIES_TO_SKIP
184 files_to_check: List[str],
185 file_extensions_to_check: List[str],
188 Check whether a file should be analyzed.
190 @param path Path to the file.
191 @param files_to_check List of files that shall be checked.
192 @param file_extensions_to_check List of file extensions that shall be checked.
193 @return Whether the file should be analyzed.
196 filename = os.path.split(path)[1]
198 if filename
in FILES_TO_SKIP:
201 extension = os.path.splitext(filename)[1]
203 return filename
in files_to_check
or extension
in file_extensions_to_check
208) -> Dict[str, List[str]]:
210 Find all files to be checked in a given list of paths.
212 @param paths List of paths to the files to check.
213 @return Dictionary of checks and corresponding list of files to check.
215 "formatting": list_of_files_to_check_formatting,
221 files_found: List[str] = []
224 abs_path = os.path.abspath(os.path.expanduser(path))
226 if os.path.isfile(abs_path):
227 files_found.append(path)
229 elif os.path.isdir(abs_path):
230 for dirpath, dirnames, filenames
in os.walk(path, topdown=
True):
236 files_found.extend([os.path.join(dirpath, f)
for f
in filenames])
239 raise ValueError(f
"{path} is not a valid file nor a directory")
244 files_to_check: Dict[str, List[str]] = {c: []
for c
in CHECKS}
246 for f
in files_found:
249 files_to_check[check].append(f)
251 return files_to_check
256 Find the path to one of the supported versions of clang-format.
257 If no supported version of clang-format is found, raise an exception.
259 @return Path to clang-format.
263 for version
in range(CLANG_FORMAT_MAX_VERSION, CLANG_FORMAT_MIN_VERSION - 1, -1):
264 clang_format_path = shutil.which(f
"clang-format-{version}")
266 if clang_format_path:
267 return clang_format_path
270 clang_format_path = shutil.which(
"clang-format")
273 if clang_format_path:
274 process = subprocess.run(
275 [clang_format_path,
"--version"],
281 clang_format_version = process.stdout.strip()
282 version_regex = re.findall(
r"\b(\d+)(\.\d+){0,2}\b", clang_format_version)
285 major_version = int(version_regex[0][0])
287 if CLANG_FORMAT_MIN_VERSION <= major_version <= CLANG_FORMAT_MAX_VERSION:
288 return clang_format_path
292 f
"Could not find any supported version of clang-format installed on this system. "
293 f
"List of supported versions: [{CLANG_FORMAT_MAX_VERSION}-{CLANG_FORMAT_MIN_VERSION}]. "
294 + (f
"Found clang-format {major_version}." if major_version
else "")
303 checks_enabled: Dict[str, bool],
309 Check / fix the coding style of a list of files.
311 @param paths List of paths to the files to check.
312 @param checks_enabled Dictionary of checks indicating whether to enable each of them.
313 @param fix Whether to fix (True) or just check (False) the file.
314 @param verbose Show the lines that are not compliant with the style.
315 @param n_jobs Number of parallel jobs.
316 @return Whether all files are compliant with all enabled style checks.
320 checks_successful = {c:
True for c
in CHECKS}
323 "include_prefixes":
'#include headers from the same module with the "ns3/" prefix',
324 "include_quotes":
'ns-3 #include headers using angle brackets <> rather than quotes ""',
325 "doxygen_tags":
"Doxygen tags using \\ rather than @",
326 "whitespace":
"trailing whitespace",
328 "license":
"GPL license text instead of SPDX license",
329 "formatting":
"bad code formatting",
330 "encoding": f
"bad file encoding ({FILE_ENCODING})",
333 check_style_file_functions_kwargs = {
334 "include_prefixes": {
335 "function": check_manually_file,
337 "respect_clang_format_guards":
True,
338 "check_style_line_function": check_include_prefixes_line,
342 "function": check_manually_file,
344 "respect_clang_format_guards":
True,
345 "check_style_line_function": check_include_quotes_line,
349 "function": check_manually_file,
351 "respect_clang_format_guards":
True,
352 "check_style_line_function": check_doxygen_tags_line,
356 "function": check_manually_file,
358 "respect_clang_format_guards":
False,
359 "check_style_line_function": check_whitespace_line,
363 "function": check_manually_file,
365 "respect_clang_format_guards":
True,
366 "check_style_line_function": check_tabs_line,
370 "function": check_manually_file,
372 "respect_clang_format_guards":
True,
373 "check_style_line_function": check_licenses_line,
377 "function": check_formatting_file,
381 "function": check_encoding_file,
386 if checks_enabled[
"formatting"]:
387 check_style_file_functions_kwargs[
"formatting"][
"kwargs"] = {
391 n_checks_enabled = sum(checks_enabled.values())
395 if checks_enabled[check]:
397 style_check_strs[check],
398 check_style_file_functions_kwargs[check][
"function"],
399 files_to_check[check],
403 **check_style_file_functions_kwargs[check][
"kwargs"],
408 if n_check < n_checks_enabled:
411 return all(checks_successful.values())
415 style_check_str: str,
416 check_style_file_function: Callable[..., Tuple[str, bool, List[str]]],
417 filenames: List[str],
424 Check / fix style of a list of files.
426 @param style_check_str Description of the check to be performed.
427 @param check_style_file_function Function used to check the file.
428 @param filename Name of the file to be checked.
429 @param fix Whether to fix (True) or just check (False) the file (True).
430 @param verbose Show the lines that are not compliant with the style.
431 @param n_jobs Number of parallel jobs.
432 @param kwargs Additional keyword arguments to the check_style_file_function.
433 @return Whether all files are compliant with the style.
437 non_compliant_files: List[str] = []
438 files_verbose_infos: Dict[str, List[str]] = {}
440 with concurrent.futures.ProcessPoolExecutor(n_jobs)
as executor:
441 non_compliant_files_results = executor.map(
442 check_style_file_function,
444 itertools.repeat(fix),
445 itertools.repeat(verbose),
446 *[arg
if isinstance(arg, list)
else itertools.repeat(arg)
for arg
in kwargs.values()],
449 for filename, is_file_compliant, verbose_infos
in non_compliant_files_results:
450 if not is_file_compliant:
451 non_compliant_files.append(filename)
454 files_verbose_infos[filename] = verbose_infos
457 if not non_compliant_files:
458 print(f
"- No files detected with {style_check_str}")
462 n_non_compliant_files = len(non_compliant_files)
465 print(f
"- Fixed {style_check_str} in the files ({n_non_compliant_files}):")
467 print(f
"- Detected {style_check_str} in the files ({n_non_compliant_files}):")
469 for f
in non_compliant_files:
471 print(*[f
" {l}" for l
in files_verbose_infos[f]], sep=
"\n")
486 clang_format_path: str,
487) -> Tuple[str, bool, List[str]]:
489 Check / fix the coding style of a file with clang-format.
491 @param filename Name of the file to be checked.
492 @param fix Whether to fix (True) or just check (False) the style of the file.
493 @param verbose Show the lines that are not compliant with the style.
494 @param clang_format_path Path to clang-format.
495 @return Tuple [Filename,
496 Whether the file is compliant with the style (before the check),
497 Verbose information].
500 verbose_infos: List[str] = []
503 process = subprocess.run(
511 f
"--ferror-limit={0 if verbose else 1}",
518 is_file_compliant = process.returncode == 0
521 verbose_infos = process.stderr.splitlines()
524 if fix
and not is_file_compliant:
525 process = subprocess.run(
533 stdout=subprocess.DEVNULL,
534 stderr=subprocess.DEVNULL,
537 return (filename, is_file_compliant, verbose_infos)
544) -> Tuple[str, bool, List[str]]:
546 Check / fix the encoding of a file.
548 @param filename Name of the file to be checked.
549 @param fix Whether to fix (True) or just check (False) the encoding of the file.
550 @param verbose Show the lines that are not compliant with the style.
551 @return Tuple [Filename,
552 Whether the file is compliant with the style (before the check),
553 Verbose information].
556 verbose_infos: List[str] = []
557 is_file_compliant =
True
559 with open(filename,
"rb")
as f:
561 file_lines = file_data.decode(FILE_ENCODING, errors=
"replace").splitlines(keepends=
True)
565 file_data.decode(FILE_ENCODING)
567 except UnicodeDecodeError
as e:
568 is_file_compliant =
False
572 bad_char_start_index = e.start
573 n_chars_file_read = 0
575 for line_number, line
in enumerate(file_lines):
576 n_chars_line = len(line)
578 if bad_char_start_index < n_chars_file_read + n_chars_line:
579 bad_char_column = bad_char_start_index - n_chars_file_read
581 verbose_infos.extend(
583 f
"{filename}:{line_number + 1}:{bad_char_column + 1}: error: bad {FILE_ENCODING} encoding",
585 f
" {'':>{bad_char_column}}^",
591 n_chars_file_read += n_chars_line
594 if fix
and not is_file_compliant:
595 with open(filename,
"w", encoding=FILE_ENCODING)
as f:
596 f.writelines(file_lines)
598 return (filename, is_file_compliant, verbose_infos)
605 respect_clang_format_guards: bool,
606 check_style_line_function: Callable[[str, str, int], Tuple[bool, str, List[str]]],
607) -> Tuple[str, bool, List[str]]:
609 Check / fix a file manually using a function to check / fix each line.
611 @param filename Name of the file to be checked.
612 @param fix Whether to fix (True) or just check (False) the style of the file.
613 @param verbose Show the lines that are not compliant with the style.
614 @param respect_clang_format_guards Whether to respect clang-format guards.
615 @param check_style_line_function Function used to check each line.
616 @return Tuple [Filename,
617 Whether the file is compliant with the style (before the check),
618 Verbose information].
621 is_file_compliant =
True
622 verbose_infos: List[str] = []
623 clang_format_enabled =
True
625 with open(filename,
"r", encoding=FILE_ENCODING)
as f:
626 file_lines = f.readlines()
628 for i, line
in enumerate(file_lines):
630 if respect_clang_format_guards:
631 line_stripped = line.strip()
633 if line_stripped
in FORMAT_GUARD_ON:
634 clang_format_enabled =
True
635 elif line_stripped
in FORMAT_GUARD_OFF:
636 clang_format_enabled =
False
638 if not clang_format_enabled
and line_stripped
not in (
639 FORMAT_GUARD_ON + FORMAT_GUARD_OFF
644 (is_line_compliant, line_fixed, line_verbose_infos) = check_style_line_function(
648 if not is_line_compliant:
649 is_file_compliant =
False
650 file_lines[i] = line_fixed
651 verbose_infos.extend(line_verbose_infos)
654 if not fix
and not verbose:
658 if fix
and not is_file_compliant:
659 with open(filename,
"w", encoding=FILE_ENCODING)
as f:
660 f.writelines(file_lines)
662 return (filename, is_file_compliant, verbose_infos)
669) -> Tuple[bool, str, List[str]]:
671 Check / fix #include headers from the same module with the "ns3/" prefix in a line.
673 @param line The line to check.
674 @param filename Name of the file to be checked.
675 @param line_number The number of the line checked.
676 @return Tuple [Whether the line is compliant with the style (before the check),
678 Verbose information].
681 is_line_compliant =
True
683 verbose_infos: List[str] = []
686 line_stripped = line.strip()
687 header_file = re.findall(
r'^#include ["<]ns3/(.*\.h)[">]', line_stripped)
691 header_file = header_file[0]
692 parent_path = os.path.split(filename)[0]
694 if os.path.exists(os.path.join(parent_path, header_file)):
695 is_line_compliant =
False
697 line_stripped.replace(f
"ns3/{header_file}", header_file)
703 header_index = len(
'#include "')
705 verbose_infos.extend(
707 f
'{filename}:{line_number + 1}:{header_index + 1}: error: #include headers from the same module with the "ns3/" prefix detected',
709 f
" {'':>{header_index}}^",
713 return (is_line_compliant, line_fixed, verbose_infos)
720) -> Tuple[bool, str, List[str]]:
722 Check / fix ns-3 #include headers using angle brackets <> rather than quotes "" in a line.
724 @param line The line to check.
725 @param filename Name of the file to be checked.
726 @param line_number The number of the line checked.
727 @return Tuple [Whether the line is compliant with the style (before the check),
729 Verbose information].
732 is_line_compliant =
True
734 verbose_infos: List[str] = []
737 header_file = re.findall(
r"^#include <ns3/.*\.h>", line)
740 is_line_compliant =
False
741 line_fixed = line.replace(
"<",
'"').replace(
">",
'"')
743 header_index = len(
"#include ")
746 f
"{filename}:{line_number + 1}:{header_index + 1}: error: ns-3 #include headers with angle brackets detected",
748 f
' {"":{header_index}}^',
751 return (is_line_compliant, line_fixed, verbose_infos)
758) -> Tuple[bool, str, List[str]]:
760 Check / fix Doxygen tags using \\ rather than @ in a line.
762 @param line The line to check.
763 @param filename Name of the file to be checked.
764 @param line_number The number of the line checked.
765 @return Tuple [Whether the line is compliant with the style (before the check),
767 Verbose information].
776 is_line_compliant =
True
778 verbose_infos: List[str] = []
781 line_stripped = line.rstrip()
782 regex_findings = re.findall(
r"^\s*(?:\*|\/\*\*|\/\/\/)\s*(\\\w{3,})(?=(?:\s|$))", line_stripped)
785 doxygen_tag = regex_findings[0]
787 if doxygen_tag
not in IGNORED_WORDS:
788 is_line_compliant =
False
790 doxygen_tag_index = line_fixed.find(doxygen_tag)
791 line_fixed = line.replace(doxygen_tag, f
"@{doxygen_tag[1:]}")
793 verbose_infos.extend(
795 f
"{filename}:{line_number + 1}:{doxygen_tag_index + 1}: error: detected Doxygen tags using \\ rather than @",
797 f
' {"":{doxygen_tag_index}}^',
801 return (is_line_compliant, line_fixed, verbose_infos)
808) -> Tuple[bool, str, List[str]]:
810 Check / fix whitespace in a line.
812 @param line The line to check.
813 @param filename Name of the file to be checked.
814 @param line_number The number of the line checked.
815 @return Tuple [Whether the line is compliant with the style (before the check),
817 Verbose information].
820 is_line_compliant =
True
821 line_fixed = line.rstrip() +
"\n"
822 verbose_infos: List[str] = []
824 if line_fixed != line:
825 is_line_compliant =
False
826 line_fixed_stripped_expanded = line_fixed.rstrip().expandtabs(TAB_SIZE)
829 f
"{filename}:{line_number + 1}:{len(line_fixed_stripped_expanded) + 1}: error: Trailing whitespace detected",
830 f
" {line_fixed_stripped_expanded}",
831 f
" {'':>{len(line_fixed_stripped_expanded)}}^",
834 return (is_line_compliant, line_fixed, verbose_infos)
841) -> Tuple[bool, str, List[str]]:
843 Check / fix tabs in a line.
845 @param line The line to check.
846 @param filename Name of the file to be checked.
847 @param line_number The number of the line checked.
848 @return Tuple [Whether the line is compliant with the style (before the check),
850 Verbose information].
853 is_line_compliant =
True
855 verbose_infos: List[str] = []
857 tab_index = line.find(
"\t")
860 is_line_compliant =
False
861 line_fixed = line.expandtabs(TAB_SIZE)
864 f
"{filename}:{line_number + 1}:{tab_index + 1}: error: Tab detected",
866 f
" {'':>{tab_index}}^",
869 return (is_line_compliant, line_fixed, verbose_infos)
876) -> Tuple[bool, str, List[str]]:
878 Check / fix SPDX licenses rather than GPL text in a line.
880 @param line The line to check.
881 @param filename Name of the file to be checked.
882 @param line_number The number of the line checked.
883 @return Tuple [Whether the line is compliant with the style (before the check),
885 Verbose information].
889 GPL_LICENSE_LINES = [
890 "This program is free software; you can redistribute it and/or modify",
891 "it under the terms of the GNU General Public License version 2 as",
892 "published by the Free Software Foundation;",
893 "This program is distributed in the hope that it will be useful,",
894 "but WITHOUT ANY WARRANTY; without even the implied warranty of",
895 "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the",
896 "GNU General Public License for more details.",
897 "You should have received a copy of the GNU General Public License",
898 "along with this program; if not, write to the Free Software",
899 "Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA",
903 SPDX_LICENSE =
"SPDX-License-Identifier: GPL-2.0-only"
905 is_line_compliant =
True
907 verbose_infos: List[str] = []
910 line_stripped = line.strip()
911 line_stripped_no_leading_comments = line_stripped.strip(
"*#/").strip()
913 if line_stripped_no_leading_comments
in GPL_LICENSE_LINES:
914 is_line_compliant =
False
920 if line_stripped_no_leading_comments == GPL_LICENSE_LINES[0]:
921 line_fixed = line.replace(line_stripped_no_leading_comments, SPDX_LICENSE)
925 verbose_infos.extend(
927 f
"{filename}:{line_number + 1}:{col_index}: error: GPL license text detected instead of SPDX license",
929 f
" {'':>{col_index}}^",
933 return (is_line_compliant, line_fixed, verbose_infos)
939if __name__ ==
"__main__":
940 parser = argparse.ArgumentParser(
941 description=
"Check and apply the ns-3 coding style recursively to all files in the given PATHs. "
942 "The script checks the formatting of the files using clang-format and"
943 " other coding style rules manually (see script arguments). "
944 "All checks respect clang-format guards, except trailing whitespace and file encoding,"
945 " which are always checked. "
946 'When used in "check mode" (default), the script runs all checks in all files. '
947 "If it detects non-formatted files, they will be printed and this process exits with a non-zero code. "
948 'When used in "fix mode", this script automatically fixes the files and exits with 0 code.'
956 help=
"List of paths to the files to check",
960 "--no-include-prefixes",
962 help=
'Do not check / fix #include headers from the same module with the "ns3/" prefix (respects clang-format guards)',
966 "--no-include-quotes",
968 help=
'Do not check / fix ns-3 #include headers using angle brackets <> rather than quotes "" (respects clang-format guards)',
974 help=
"Do not check / fix Doxygen tags using @ rather than \\ (respects clang-format guards)",
980 help=
"Do not check / fix trailing whitespace",
986 help=
"Do not check / fix tabs (respects clang-format guards)",
992 help=
"Do not check / fix SPDX licenses rather than GPL text (respects clang-format guards)",
998 help=
"Do not check / fix code formatting (respects clang-format guards)",
1001 parser.add_argument(
1003 action=
"store_true",
1004 help=f
"Do not check / fix file encoding ({FILE_ENCODING})",
1007 parser.add_argument(
1009 action=
"store_true",
1010 help=
"Fix coding style issues detected in the files",
1013 parser.add_argument(
1016 action=
"store_true",
1017 help=
"Show the lines that are not well-formatted",
1020 parser.add_argument(
1024 default=max(1, os.cpu_count() - 1),
1025 help=
"Number of parallel jobs",
1028 args = parser.parse_args()
1034 "include_prefixes":
not args.no_include_prefixes,
1035 "include_quotes":
not args.no_include_quotes,
1036 "doxygen_tags":
not args.no_doxygen_tags,
1037 "whitespace":
not args.no_whitespace,
1038 "tabs":
not args.no_tabs,
1039 "license":
not args.no_licenses,
1040 "formatting":
not args.no_formatting,
1041 "encoding":
not args.no_encoding,
1044 verbose=args.verbose,
1048 except Exception
as ex:
1052 if not all_checks_successful:
1056 "Notes to fix the above formatting issues:",
1057 ' - To fix the formatting of specific files, run this script with the flag "--fix":',
1058 " $ ./utils/check-style-clang-format.py --fix path [path ...]",
1059 " - To fix the formatting of all files modified by this branch, run this script in the following way:",
1060 " $ git diff --name-only master | xargs ./utils/check-style-clang-format.py --fix",