21Check and apply the ns-3 coding style to all files in the PATH argument.
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.
33Trimming of trailing whitespace and conversion of tabs to spaces (via the "--no-formatting"
34option) do not depend on clang-format.
38import concurrent.futures
46from typing
import Callable, Dict, List, Tuple
51CLANG_FORMAT_VERSIONS = [
57CLANG_FORMAT_GUARD_ON =
'// clang-format on'
58CLANG_FORMAT_GUARD_OFF =
'// clang-format off'
60DIRECTORIES_TO_SKIP = [
75FILE_EXTENSIONS_TO_CHECK_FORMATTING = [
81FILE_EXTENSIONS_TO_CHECK_INCLUDE_PREFIXES = FILE_EXTENSIONS_TO_CHECK_FORMATTING
83FILE_EXTENSIONS_TO_CHECK_WHITESPACE = [
113FILES_TO_CHECK_WHITESPACE = [
118FILE_EXTENSIONS_TO_CHECK_TABS = [
136 Check whether a directory should be analyzed.
138 @param dirpath Directory path.
139 @return Whether the directory should be analyzed.
142 _, directory = os.path.split(dirpath)
144 return not (directory
in DIRECTORIES_TO_SKIP
or
145 (directory.startswith(
'.')
and directory !=
'.'))
149 files_to_check: List[str],
150 file_extensions_to_check: List[str],
153 Check whether a file should be analyzed.
155 @param path Path to the file.
156 @param files_to_check List of files that shall be checked.
157 @param file_extensions_to_check List of file extensions that shall be checked.
158 @return Whether the file should be analyzed.
161 filename = os.path.split(path)[1]
163 if filename
in FILES_TO_SKIP:
166 basename, extension = os.path.splitext(filename)
168 return (basename
in files_to_check
or
169 extension
in file_extensions_to_check)
174 Find all files to be checked in a given path.
176 @param path Path to check.
177 @return Tuple [List of files to check include prefixes,
178 List of files to check formatting,
179 List of files to check trailing whitespace,
180 List of files to check tabs].
183 files_to_check: List[str] = []
184 abs_path = os.path.abspath(os.path.expanduser(path))
186 if os.path.isfile(abs_path):
187 files_to_check = [path]
189 elif os.path.isdir(abs_path):
190 for dirpath, dirnames, filenames
in os.walk(path, topdown=
True):
196 files_to_check.extend([os.path.join(dirpath, f)
for f
in filenames])
199 raise ValueError(f
'Error: {path} is not a file nor a directory')
201 files_to_check.sort()
203 files_to_check_include_prefixes: List[str] = []
204 files_to_check_formatting: List[str] = []
205 files_to_check_whitespace: List[str] = []
206 files_to_check_tabs: List[str] = []
208 for f
in files_to_check:
210 files_to_check_include_prefixes.append(f)
213 files_to_check_formatting.append(f)
216 files_to_check_whitespace.append(f)
219 files_to_check_tabs.append(f)
222 files_to_check_include_prefixes,
223 files_to_check_formatting,
224 files_to_check_whitespace,
231 Find the path to one of the supported versions of clang-format.
232 If no supported version of clang-format is found,
raise an exception.
234 @return Path to clang-format.
238 for version
in CLANG_FORMAT_VERSIONS:
239 clang_format_path = shutil.which(f
'clang-format-{version}')
241 if clang_format_path:
242 return clang_format_path
245 clang_format_path = shutil.which(
'clang-format')
247 if clang_format_path:
248 process = subprocess.run(
249 [clang_format_path,
'--version'],
255 version = process.stdout.strip().split(
' ')[-1]
256 major_version =
int(version.split(
'.')[0])
258 if major_version
in CLANG_FORMAT_VERSIONS:
259 return clang_format_path
263 f
'Could not find any supported version of clang-format installed on this system. '
264 f
'List of supported versions: {CLANG_FORMAT_VERSIONS}.'
272 enable_check_include_prefixes: bool,
273 enable_check_formatting: bool,
274 enable_check_whitespace: bool,
275 enable_check_tabs: bool,
281 Check / fix the coding style of a list of files.
283 @param path Path to the files.
284 @param enable_check_include_prefixes Whether to enable checking
285 @param enable_check_formatting Whether to enable checking code formatting.
286 @param enable_check_whitespace Whether to enable checking trailing whitespace.
287 @param enable_check_tabs Whether to enable checking tabs.
288 @param fix Whether to fix (
True)
or just check (
False) the file.
289 @param verbose Show the lines that are
not compliant
with the style.
290 @param n_jobs Number of parallel jobs.
291 @return Whether all files are compliant
with all enabled style checks.
294 (files_to_check_include_prefixes,
295 files_to_check_formatting,
296 files_to_check_whitespace,
299 check_include_prefixes_successful = True
300 check_formatting_successful =
True
301 check_whitespace_successful =
True
302 check_tabs_successful =
True
304 if enable_check_include_prefixes:
306 files_to_check_include_prefixes,
307 check_include_prefixes_file,
308 '#include headers from the same module with the "ns3/" prefix',
316 if enable_check_formatting:
318 files_to_check_formatting,
319 check_formatting_file,
320 'bad code formatting',
329 if enable_check_whitespace:
331 files_to_check_whitespace,
332 check_trailing_whitespace_file,
333 'trailing whitespace',
341 if enable_check_tabs:
352 check_include_prefixes_successful,
353 check_formatting_successful,
354 check_whitespace_successful,
355 check_tabs_successful,
360 check_style_file_function: Callable,
361 style_check_str: str,
368 Check / fix style of a list of files.
370 @param filename Name of the file to be checked.
371 @param check_style_file_function Function used to check the file.
372 @param style_check_str Description of the check to be performed.
373 @param fix Whether to fix (
True)
or just check (
False) the file (
True).
374 @param verbose Show the lines that are
not compliant
with the style.
375 @param n_jobs Number of parallel jobs.
376 @param kwargs Additional keyword arguments to the check_style_file_function.
377 @return Whether all files are compliant
with the style.
381 non_compliant_files: List[str] = []
382 files_verbose_infos: Dict[str, List[str]] = {}
384 with concurrent.futures.ProcessPoolExecutor(n_jobs)
as executor:
385 non_compliant_files_results = executor.map(
386 check_style_file_function,
388 itertools.repeat(fix),
389 itertools.repeat(verbose),
390 *[arg
if isinstance(arg, list)
else itertools.repeat(arg)
for arg
in kwargs.values()],
393 for (filename, is_file_compliant, verbose_infos)
in non_compliant_files_results:
394 if not is_file_compliant:
395 non_compliant_files.append(filename)
398 files_verbose_infos[filename] = verbose_infos
401 if not non_compliant_files:
402 print(f
'- No files detected with {style_check_str}')
406 n_non_compliant_files = len(non_compliant_files)
409 print(f
'- Fixed {style_check_str} in the files ({n_non_compliant_files}):')
411 print(f
'- Detected {style_check_str} in the files ({n_non_compliant_files}):')
413 for f
in non_compliant_files:
415 print(*[f
' {l}' for l
in files_verbose_infos[f]], sep=
'\n')
429 ) -> Tuple[str, bool, List[str]]:
433 @param filename Name of the file to be checked.
434 @param fix Whether to fix (
True)
or just check (
False) the style of the file (
True).
435 @param verbose Show the lines that are
not compliant
with the style.
436 @return Tuple [Filename,
437 Whether the file
is compliant
with the style (before the check),
438 Verbose information].
441 is_file_compliant = True
442 clang_format_enabled =
True
444 verbose_infos: List[str] = []
446 with open(filename,
'r', encoding=
'utf-8')
as f:
447 file_lines = f.readlines()
449 for (i, line)
in enumerate(file_lines):
452 line_stripped = line.strip()
454 if line_stripped == CLANG_FORMAT_GUARD_ON:
455 clang_format_enabled =
True
456 elif line_stripped == CLANG_FORMAT_GUARD_OFF:
457 clang_format_enabled =
False
459 if (
not clang_format_enabled
and
460 line_stripped
not in (CLANG_FORMAT_GUARD_ON, CLANG_FORMAT_GUARD_OFF)):
464 header_file = re.findall(
r'^#include ["<]ns3/(.*\.h)[">]', line_stripped)
470 header_file = header_file[0]
471 parent_path = os.path.split(filename)[0]
473 if not os.path.exists(os.path.join(parent_path, header_file)):
476 is_file_compliant =
False
477 file_lines[i] = line_stripped.replace(
478 f
'ns3/{header_file}', header_file).replace(
'<',
'"').replace(
'>',
'"') +
'\n'
481 header_index = len(
'#include "')
483 verbose_infos.extend([
484 f
'{filename}:{i + 1}:{header_index + 1}: error: #include headers from the same module with the "ns3/" prefix detected',
486 f
' {"":{header_index}}^',
490 if not fix
and not verbose:
494 if fix
and not is_file_compliant:
495 with open(filename,
'w', encoding=
'utf-8')
as f:
496 f.writelines(file_lines)
498 return (filename, is_file_compliant, verbose_infos)
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 (
True).
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)
561 ) -> Tuple[str, bool, List[str]]:
563 Check / fix trailing whitespace in a file.
565 @param filename Name of the file to be checked.
566 @param fix Whether to fix (
True)
or just check (
False) the style of the file (
True).
567 @param verbose Show the lines that are
not compliant
with the style.
568 @return Tuple [Filename,
569 Whether the file
is compliant
with the style (before the check),
570 Verbose information].
573 is_file_compliant = True
574 verbose_infos: List[str] = []
576 with open(filename,
'r', encoding=
'utf-8')
as f:
577 file_lines = f.readlines()
580 for (i, line)
in enumerate(file_lines):
581 line_fixed = line.rstrip() +
'\n'
583 if line_fixed == line:
586 is_file_compliant =
False
587 file_lines[i] = line_fixed
590 line_fixed_stripped_expanded = line_fixed.rstrip().expandtabs(TAB_SIZE)
592 verbose_infos.extend([
593 f
'{filename}:{i + 1}:{len(line_fixed_stripped_expanded) + 1}: error: Trailing whitespace detected',
594 f
' {line_fixed_stripped_expanded}',
595 f
' {"":{len(line_fixed_stripped_expanded)}}^',
599 if not fix
and not verbose:
603 if fix
and not is_file_compliant:
604 with open(filename,
'w', encoding=
'utf-8')
as f:
605 f.writelines(file_lines)
607 return (filename, is_file_compliant, verbose_infos)
613 ) -> Tuple[str, bool, List[str]]:
615 Check / fix tabs in a file.
617 @param filename Name of the file to be checked.
618 @param fix Whether to fix (
True)
or just check (
False) the style of the file (
True).
619 @param verbose Show the lines that are
not compliant
with the style.
620 @return Tuple [Filename,
621 Whether the file
is compliant
with the style (before the check),
622 Verbose information].
625 is_file_compliant = True
626 clang_format_enabled =
True
628 verbose_infos: List[str] = []
630 with open(filename,
'r', encoding=
'utf-8')
as f:
631 file_lines = f.readlines()
633 for (i, line)
in enumerate(file_lines):
636 line_stripped = line.strip()
638 if line_stripped == CLANG_FORMAT_GUARD_ON:
639 clang_format_enabled =
True
640 elif line_stripped == CLANG_FORMAT_GUARD_OFF:
641 clang_format_enabled =
False
643 if (
not clang_format_enabled
and
644 line_stripped
not in (CLANG_FORMAT_GUARD_ON, CLANG_FORMAT_GUARD_OFF)):
648 tab_index = line.find(
'\t')
653 is_file_compliant =
False
654 file_lines[i] = line.expandtabs(TAB_SIZE)
657 verbose_infos.extend([
658 f
'{filename}:{i + 1}:{tab_index + 1}: error: Tab detected',
660 f
' {"":{tab_index}}^',
664 if not fix
and not verbose:
668 if fix
and not is_file_compliant:
669 with open(filename,
'w', encoding=
'utf-8')
as f:
670 f.writelines(file_lines)
672 return (filename, is_file_compliant, verbose_infos)
678if __name__ ==
'__main__':
680 parser = argparse.ArgumentParser(
681 description=
'Check and apply the ns-3 coding style to all files in a given PATH. '
682 'The script checks the formatting of the file with clang-format. '
683 'Additionally, it checks #include headers from the same module with the "ns3/" prefix, '
684 'the presence of trailing whitespace and tabs. '
685 'Formatting, local #include "ns3/" prefixes and tabs checks respect clang-format guards. '
686 'When used in "check mode" (default), the script checks if all files are well '
687 'formatted and do not have trailing whitespace nor tabs. '
688 'If it detects non-formatted files, they will be printed and this process exits with a '
689 'non-zero code. When used in "fix mode", this script automatically fixes the files.')
691 parser.add_argument(
'path', action=
'store', type=str,
692 help=
'Path to the files to check')
694 parser.add_argument(
'--no-include-prefixes', action=
'store_true',
695 help=
'Do not check / fix #include headers from the same module with the "ns3/" prefix')
697 parser.add_argument(
'--no-formatting', action=
'store_true',
698 help=
'Do not check / fix code formatting')
700 parser.add_argument(
'--no-whitespace', action=
'store_true',
701 help=
'Do not check / fix trailing whitespace')
703 parser.add_argument(
'--no-tabs', action=
'store_true',
704 help=
'Do not check / fix tabs')
706 parser.add_argument(
'--fix', action=
'store_true',
707 help=
'Fix coding style issues detected in the files')
709 parser.add_argument(
'-v',
'--verbose', action=
'store_true',
710 help=
'Show the lines that are not well-formatted')
712 parser.add_argument(
'-j',
'--jobs', type=int, default=
max(1, os.cpu_count() - 1),
713 help=
'Number of parallel jobs')
715 args = parser.parse_args()
720 enable_check_include_prefixes=(
not args.no_include_prefixes),
721 enable_check_formatting=(
not args.no_formatting),
722 enable_check_whitespace=(
not args.no_whitespace),
723 enable_check_tabs=(
not args.no_tabs),
725 verbose=args.verbose,
729 except Exception
as e:
733 if all_checks_successful: