21Check and apply the ns-3 coding style recursively to all files in the PATH arguments.
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.
30This script can be applied to all text files in a given path or to individual files.
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.
38import concurrent.futures
45from typing
import Callable, Dict, List, Tuple
50CLANG_FORMAT_VERSIONS = [
57CLANG_FORMAT_GUARD_ON =
"// clang-format on"
58CLANG_FORMAT_GUARD_OFF =
"// clang-format off"
60DIRECTORIES_TO_SKIP = [
77FILE_EXTENSIONS_TO_CHECK_FORMATTING = [
83FILE_EXTENSIONS_TO_CHECK_INCLUDE_PREFIXES = FILE_EXTENSIONS_TO_CHECK_FORMATTING
85FILE_EXTENSIONS_TO_CHECK_TABS = [
104FILES_TO_CHECK_TABS = [
109 "codespell-ignored-lines",
110 "codespell-ignored-words",
114FILE_EXTENSIONS_TO_CHECK_WHITESPACE = FILE_EXTENSIONS_TO_CHECK_TABS + [
130FILES_TO_CHECK_WHITESPACE = FILES_TO_CHECK_TABS + [
142 Check whether a directory should be analyzed.
144 @param dirpath Directory path.
145 @return Whether the directory should be analyzed.
148 _, directory = os.path.split(dirpath)
150 return directory
not in DIRECTORIES_TO_SKIP
155 files_to_check: List[str],
156 file_extensions_to_check: List[str],
159 Check whether a file should be analyzed.
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.
167 filename = os.path.split(path)[1]
169 if filename
in FILES_TO_SKIP:
172 extension = os.path.splitext(filename)[1]
174 return filename
in files_to_check
or extension
in file_extensions_to_check
179) -> Tuple[List[str], List[str], List[str], List[str]]:
181 Find all files to be checked in a given list of paths.
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].
190 files_to_check: List[str] = []
193 abs_path = os.path.abspath(os.path.expanduser(path))
195 if os.path.isfile(abs_path):
196 files_to_check.append(path)
198 elif os.path.isdir(abs_path):
199 for dirpath, dirnames, filenames
in os.walk(path, topdown=
True):
205 files_to_check.extend([os.path.join(dirpath, f)
for f
in filenames])
208 raise ValueError(f
"Error: {path} is not a file nor a directory")
210 files_to_check.sort()
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] = []
217 for f
in files_to_check:
219 files_to_check_include_prefixes.append(f)
222 files_to_check_formatting.append(f)
225 files_to_check_whitespace.append(f)
228 files_to_check_tabs.append(f)
231 files_to_check_include_prefixes,
232 files_to_check_formatting,
233 files_to_check_whitespace,
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.
243 @return Path to clang-format.
247 for version
in CLANG_FORMAT_VERSIONS:
248 clang_format_path = shutil.which(f
"clang-format-{version}")
250 if clang_format_path:
251 return clang_format_path
254 clang_format_path = shutil.which(
"clang-format")
256 if clang_format_path:
257 process = subprocess.run(
258 [clang_format_path,
"--version"],
264 version = process.stdout.strip().split(
" ")[-1]
265 major_version = int(version.split(
".")[0])
267 if major_version
in CLANG_FORMAT_VERSIONS:
268 return clang_format_path
272 f
"Could not find any supported version of clang-format installed on this system. "
273 f
"List of supported versions: {CLANG_FORMAT_VERSIONS}."
282 enable_check_include_prefixes: bool,
283 enable_check_formatting: bool,
284 enable_check_whitespace: bool,
285 enable_check_tabs: bool,
291 Check / fix the coding style of a list of files.
293 @param paths List of paths to the files to check.
294 @param enable_check_include_prefixes Whether to enable checking
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.
305 files_to_check_include_prefixes,
306 files_to_check_formatting,
307 files_to_check_whitespace,
311 check_include_prefixes_successful = True
312 check_formatting_successful =
True
313 check_whitespace_successful =
True
314 check_tabs_successful =
True
316 if enable_check_include_prefixes:
318 '#include headers from the same module with the "ns3/" prefix',
320 files_to_check_include_prefixes,
324 respect_clang_format_guards=
True,
325 check_style_line_function=check_include_prefixes_line,
330 if enable_check_formatting:
332 "bad code formatting",
333 check_formatting_file,
334 files_to_check_formatting,
343 if enable_check_whitespace:
345 "trailing whitespace",
347 files_to_check_whitespace,
351 respect_clang_format_guards=
False,
352 check_style_line_function=check_whitespace_line,
357 if enable_check_tabs:
365 respect_clang_format_guards=
True,
366 check_style_line_function=check_tabs_line,
371 check_include_prefixes_successful,
372 check_formatting_successful,
373 check_whitespace_successful,
374 check_tabs_successful,
380 style_check_str: str,
381 check_style_file_function: Callable[..., Tuple[str, bool, List[str]]],
382 filenames: List[str],
389 Check / fix style of a list of files.
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.
402 non_compliant_files: List[str] = []
403 files_verbose_infos: Dict[str, List[str]] = {}
405 with concurrent.futures.ProcessPoolExecutor(n_jobs)
as executor:
406 non_compliant_files_results = executor.map(
407 check_style_file_function,
409 itertools.repeat(fix),
410 itertools.repeat(verbose),
411 *[arg
if isinstance(arg, list)
else itertools.repeat(arg)
for arg
in kwargs.values()],
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)
419 files_verbose_infos[filename] = verbose_infos
422 if not non_compliant_files:
423 print(f
"- No files detected with {style_check_str}")
427 n_non_compliant_files = len(non_compliant_files)
430 print(f
"- Fixed {style_check_str} in the files ({n_non_compliant_files}):")
432 print(f
"- Detected {style_check_str} in the files ({n_non_compliant_files}):")
434 for f
in non_compliant_files:
436 print(*[f
" {l}" for l
in files_verbose_infos[f]], sep=
"\n")
451 clang_format_path: str,
452) -> Tuple[str, bool, List[str]]:
454 Check / fix the coding style of a file with clang-format.
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].
465 verbose_infos: List[str] = []
468 process = subprocess.run(
476 f
"--ferror-limit={0 if verbose else 1}",
483 is_file_compliant = process.returncode == 0
486 verbose_infos = process.stderr.splitlines()
489 if fix
and not is_file_compliant:
490 process = subprocess.run(
498 stdout=subprocess.DEVNULL,
499 stderr=subprocess.DEVNULL,
502 return (filename, is_file_compliant, verbose_infos)
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]]:
513 Check / fix a file manually using a function to check / fix each line.
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].
525 is_file_compliant = True
526 verbose_infos: List[str] = []
527 clang_format_enabled =
True
529 with open(filename,
"r", encoding=
"utf-8")
as f:
530 file_lines = f.readlines()
532 for i, line
in enumerate(file_lines):
534 if respect_clang_format_guards:
535 line_stripped = line.strip()
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
542 if not clang_format_enabled
and line_stripped
not in (
543 CLANG_FORMAT_GUARD_ON,
544 CLANG_FORMAT_GUARD_OFF,
549 (is_line_compliant, line_fixed, line_verbose_infos) = check_style_line_function(
553 if not is_line_compliant:
554 is_file_compliant =
False
555 file_lines[i] = line_fixed
556 verbose_infos.extend(line_verbose_infos)
559 if not fix
and not verbose:
563 if fix
and not is_file_compliant:
564 with open(filename,
"w", encoding=
"utf-8")
as f:
565 f.writelines(file_lines)
567 return (filename, is_file_compliant, verbose_infos)
574) -> Tuple[bool, str, List[str]]:
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),
583 Verbose information].
586 is_line_compliant = True
588 verbose_infos: List[str] = []
591 line_stripped = line.strip()
592 header_file = re.findall(
r'^#include ["<]ns3/(.*\.h)[">]', line_stripped)
596 header_file = header_file[0]
597 parent_path = os.path.split(filename)[0]
599 if os.path.exists(os.path.join(parent_path, header_file)):
600 is_line_compliant =
False
602 line_stripped.replace(f
"ns3/{header_file}", header_file)
608 header_index = len(
'#include "')
610 verbose_infos.extend(
612 f
'{filename}:{line_number + 1}:{header_index + 1}: error: #include headers from the same module with the "ns3/" prefix detected',
614 f
' {"":{header_index}}^',
618 return (is_line_compliant, line_fixed, verbose_infos)
625) -> Tuple[bool, str, List[str]]:
627 Check / fix whitespace in a line.
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),
634 Verbose information].
637 is_line_compliant = True
638 line_fixed = line.rstrip() +
"\n"
639 verbose_infos: List[str] = []
641 if line_fixed != line:
642 is_line_compliant =
False
643 line_fixed_stripped_expanded = line_fixed.rstrip().expandtabs(TAB_SIZE)
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)}}^',
651 return (is_line_compliant, line_fixed, verbose_infos)
658) -> Tuple[bool, str, List[str]]:
660 Check / fix tabs in a line.
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),
667 Verbose information].
670 is_line_compliant = True
672 verbose_infos: List[str] = []
674 tab_index = line.find(
"\t")
677 is_line_compliant =
False
678 line_fixed = line.expandtabs(TAB_SIZE)
681 f
"{filename}:{line_number + 1}:{tab_index + 1}: error: Tab detected",
683 f
' {"":{tab_index}}^',
686 return (is_line_compliant, line_fixed, verbose_infos)
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.'
710 help=
"List of paths to the files to check",
714 "--no-include-prefixes",
716 help=
'Do not check / fix #include headers from the same module with the "ns3/" prefix',
722 help=
"Do not check / fix code formatting",
728 help=
"Do not check / fix trailing whitespace",
734 help=
"Do not check / fix tabs",
740 help=
"Fix coding style issues detected in the files",
747 help=
"Show the lines that are not well-formatted",
754 default=max(1, os.cpu_count() - 1),
755 help=
"Number of parallel jobs",
758 args = parser.parse_args()
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),
768 verbose=args.verbose,
772 except Exception
as e:
776 if not all_checks_successful:
779 print(
'NOTE: To fix the files automatically, run this script with the flag "--fix"')