21 Check and apply the ns-3 coding style to all files in the PATH argument.
23 The coding style is defined with the clang-format tool, whose definitions are in
24 the ".clang-format" file. This script performs the following checks / fixes:
25 - Check / apply clang-format.
26 - Check / trim trailing whitespace.
27 - Check / replace tabs with spaces.
29 The clang-format and tabs checks respect clang-format guards, which mark code blocks
30 that should not be checked. Trailing whitespace is always checked regardless of
33 This script can be applied to all text files in a given path or to individual files.
35 NOTE: The formatting check requires clang-format (version >= 14) to be found on the path.
36 Trimming of trailing whitespace and conversion of tabs to spaces (via the "--no-formatting"
37 option) do not depend on clang-format.
41 import concurrent.futures
47 from typing
import List, Tuple
53 CLANG_FORMAT_VERSIONS = [
59 CLANG_FORMAT_GUARD_ON =
'// clang-format on'
60 CLANG_FORMAT_GUARD_OFF =
'// clang-format off'
62 DIRECTORIES_TO_SKIP = [
77 FILE_EXTENSIONS_TO_CHECK_FORMATTING = [
83 FILE_EXTENSIONS_TO_CHECK_WHITESPACE = [
113 FILES_TO_CHECK_WHITESPACE = [
118 FILE_EXTENSIONS_TO_CHECK_TABS = [
136 Check if a directory should be skipped.
138 @param dirpath Directory path.
139 @return Whether the directory should be skipped or not.
142 _, directory = os.path.split(dirpath)
144 return (directory
in DIRECTORIES_TO_SKIP
or
145 (directory.startswith(
'.')
and directory !=
'.'))
150 Check if a file should be skipped from formatting analysis.
152 @param path Path to the file.
153 @return Whether the file should be skipped or not.
156 filename = os.path.split(path)[1]
158 if filename
in FILES_TO_SKIP:
161 _, extension = os.path.splitext(filename)
163 return extension
not in FILE_EXTENSIONS_TO_CHECK_FORMATTING
168 Check if a file should be skipped from trailing whitespace analysis.
170 @param path Path to the file.
171 @return Whether the file should be skipped or not.
174 filename = os.path.split(path)[1]
176 if filename
in FILES_TO_SKIP:
179 basename, extension = os.path.splitext(filename)
181 return (basename
not in FILES_TO_CHECK_WHITESPACE
and
182 extension
not in FILE_EXTENSIONS_TO_CHECK_WHITESPACE)
187 Check if a file should be skipped from tabs analysis.
189 @param path Path to the file.
190 @return Whether the file should be skipped or not.
193 filename = os.path.split(path)[1]
195 if filename
in FILES_TO_SKIP:
198 _, extension = os.path.splitext(filename)
200 return extension
not in FILE_EXTENSIONS_TO_CHECK_TABS
205 Find all files to be checked in a given path.
207 @param path Path to check.
208 @return Tuple [List of files to check formatting,
209 List of files to check trailing whitespace,
210 List of files to check tabs].
213 files_to_check_formatting: List[str] = []
214 files_to_check_whitespace: List[str] = []
215 files_to_check_tabs: List[str] = []
217 abs_path = os.path.normpath(os.path.abspath(os.path.expanduser(path)))
219 if os.path.isfile(abs_path):
221 files_to_check_formatting.append(path)
224 files_to_check_whitespace.append(path)
227 files_to_check_tabs.append(path)
229 elif os.path.isdir(abs_path):
230 for dirpath, dirnames, filenames
in os.walk(path, topdown=
True):
236 filenames = [os.path.join(dirpath, f)
for f
in filenames]
240 files_to_check_formatting.append(f)
243 files_to_check_whitespace.append(f)
246 files_to_check_tabs.append(f)
249 raise ValueError(f
'Error: {path} is not a file nor a directory')
252 files_to_check_formatting,
253 files_to_check_whitespace,
260 Find the path to one of the supported versions of clang-format.
261 If no supported version of clang-format is found, raise an exception.
263 @return Path to clang-format.
267 for version
in CLANG_FORMAT_VERSIONS:
268 clang_format_path = shutil.which(f
'clang-format-{version}')
270 if clang_format_path:
271 return clang_format_path
274 clang_format_path = shutil.which(
'clang-format')
276 if clang_format_path:
277 process = subprocess.run(
278 [clang_format_path,
'--version'],
284 version = process.stdout.strip().split(
' ')[-1]
285 major_version =
int(version.split(
'.')[0])
287 if major_version
in CLANG_FORMAT_VERSIONS:
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_VERSIONS}.'
301 enable_check_formatting: bool,
302 enable_check_whitespace: bool,
303 enable_check_tabs: bool,
308 Check / fix the coding style of a list of files, including formatting and
311 @param path Path to the files.
312 @param fix Whether to fix the style of the file (True) or
313 just check if the file is well-formatted (False).
314 @param enable_check_formatting Whether to enable code formatting checking.
315 @param enable_check_whitespace Whether to enable trailing whitespace checking.
316 @param enable_check_tabs Whether to enable tabs checking.
317 @param n_jobs Number of parallel jobs.
320 (files_to_check_formatting,
321 files_to_check_whitespace,
324 check_formatting_successful =
True
325 check_whitespace_successful =
True
326 check_tabs_successful =
True
328 if enable_check_formatting:
330 files_to_check_formatting, fix, n_jobs)
334 if enable_check_whitespace:
336 files_to_check_whitespace, fix, n_jobs)
340 if enable_check_tabs:
342 files_to_check_tabs, fix, n_jobs)
344 if check_formatting_successful
and \
345 check_whitespace_successful
and \
346 check_tabs_successful:
357 Check / fix the coding style of a list of files with clang-format.
359 @param filenames List of filenames to be checked.
360 @param fix Whether to fix the formatting of the file (True) or
361 just check if the file is well-formatted (False).
362 @param n_jobs Number of parallel jobs.
363 @return True if all files are well formatted after the check process.
364 False if there are non-formatted files after the check process.
369 files_not_formatted: List[str] = []
371 with concurrent.futures.ProcessPoolExecutor(n_jobs)
as executor:
372 files_not_formatted_results = executor.map(
373 check_formatting_file,
375 itertools.repeat(clang_format_path),
376 itertools.repeat(fix),
379 for (filename, formatted)
in files_not_formatted_results:
381 files_not_formatted.append(filename)
383 files_not_formatted.sort()
386 if not files_not_formatted:
387 print(
'All files are well formatted')
391 n_non_formatted_files = len(files_not_formatted)
394 print(f
'Fixed formatting of the files ({n_non_formatted_files}):')
396 print(f
'Detected bad formatting in the files ({n_non_formatted_files}):')
398 for f
in files_not_formatted:
406 clang_format_path: str,
408 ) -> Tuple[str, bool]:
410 Check / fix the coding style of a file with clang-format.
412 @param filename Name of the file to be checked.
413 @param clang_format_path Path to clang-format.
414 @param fix Whether to fix the style of the file (True) or
415 just check if the file is well-formatted (False).
416 @return Tuple [Filename, Whether the file is well-formatted].
420 process = subprocess.run(
431 stdout=subprocess.DEVNULL,
432 stderr=subprocess.DEVNULL,
435 file_formatted = (process.returncode == 0)
438 if fix
and not file_formatted:
439 process = subprocess.run(
447 stdout=subprocess.DEVNULL,
448 stderr=subprocess.DEVNULL,
451 return (filename, file_formatted)
459 Check / fix trailing whitespace in a list of files.
461 @param filename Name of the file to be checked.
462 @param fix Whether to fix the file (True) or
463 just check if it has trailing whitespace (False).
464 @param n_jobs Number of parallel jobs.
465 @return True if no files have trailing whitespace after the check process.
466 False if there are trailing whitespace after the check process.
470 files_with_whitespace: List[str] = []
472 with concurrent.futures.ProcessPoolExecutor(n_jobs)
as executor:
473 files_with_whitespace_results = executor.map(
474 check_trailing_whitespace_file,
476 itertools.repeat(fix),
479 for (filename, has_whitespace)
in files_with_whitespace_results:
481 files_with_whitespace.append(filename)
483 files_with_whitespace.sort()
486 if not files_with_whitespace:
487 print(
'No files detected with trailing whitespace')
491 n_files_with_whitespace = len(files_with_whitespace)
495 f
'Fixed trailing whitespace in the files ({n_files_with_whitespace}):')
498 f
'Detected trailing whitespace in the files ({n_files_with_whitespace}):')
500 for f
in files_with_whitespace:
509 Check / fix trailing whitespace in a file.
511 @param filename Name of the file to be checked.
512 @param fix Whether to fix the file (True) or
513 just check if it has trailing whitespace (False).
514 @return Tuple [Filename, Whether the file has trailing whitespace].
517 has_trailing_whitespace =
False
519 with open(filename,
'r', encoding=
'utf-8')
as f:
520 file_lines = f.readlines()
523 for (i, line)
in enumerate(file_lines):
524 line_fixed = line.rstrip() +
'\n'
526 if line_fixed != line:
527 has_trailing_whitespace =
True
533 file_lines[i] = line_fixed
536 if fix
and has_trailing_whitespace:
537 with open(filename,
'w', encoding=
'utf-8')
as f:
538 f.writelines(file_lines)
540 return (filename, has_trailing_whitespace)
546 def check_tabs(filenames: List[str], fix: bool, n_jobs: int) -> bool:
548 Check / fix tabs in a list of files.
550 @param filename Name of the file to be checked.
551 @param fix Whether to fix the file (True) or just check if it has tabs (False).
552 @param n_jobs Number of parallel jobs.
553 @return True if no files have tabs after the check process.
554 False if there are tabs after the check process.
558 files_with_tabs: List[str] = []
560 with concurrent.futures.ProcessPoolExecutor(n_jobs)
as executor:
561 files_with_tabs_results = executor.map(
564 itertools.repeat(fix),
567 for (filename, has_tabs)
in files_with_tabs_results:
569 files_with_tabs.append(filename)
571 files_with_tabs.sort()
574 if not files_with_tabs:
575 print(
'No files detected with tabs')
579 n_files_with_tabs = len(files_with_tabs)
583 f
'Fixed tabs in the files ({n_files_with_tabs}):')
586 f
'Detected tabs in the files ({n_files_with_tabs}):')
588 for f
in files_with_tabs:
597 Check / fix tabs in a file.
599 @param filename Name of the file to be checked.
600 @param fix Whether to fix the file (True) or just check if it has tabs (False).
601 @return Tuple [Filename, Whether the file has tabs].
605 clang_format_enabled =
True
607 with open(filename,
'r', encoding=
'utf-8')
as f:
608 file_lines = f.readlines()
610 for (i, line)
in enumerate(file_lines):
613 line_stripped = line.strip()
615 if line_stripped == CLANG_FORMAT_GUARD_ON:
616 clang_format_enabled =
True
617 elif line_stripped == CLANG_FORMAT_GUARD_OFF:
618 clang_format_enabled =
False
620 if (
not clang_format_enabled
and
621 line_stripped
not in (CLANG_FORMAT_GUARD_ON, CLANG_FORMAT_GUARD_OFF)):
625 if line.find(
'\t') != -1:
632 file_lines[i] = line.expandtabs(TAB_SIZE)
636 with open(filename,
'w', encoding=
'utf-8')
as f:
637 f.writelines(file_lines)
639 return (filename, has_tabs)
645 if __name__ ==
'__main__':
647 parser = argparse.ArgumentParser(
648 description=
'Check and apply the ns-3 coding style to all files in a given PATH. '
649 'The script checks the formatting of the file with clang-format. '
650 'Additionally, it checks the presence of trailing whitespace and tabs. '
651 'Formatting and tabs checks respect clang-format guards. '
652 'When used in "check mode" (default), the script checks if all files are well '
653 'formatted and do not have trailing whitespace nor tabs. '
654 'If it detects non-formatted files, they will be printed and this process exits with a '
655 'non-zero code. When used in "fix mode", this script automatically fixes the files.')
657 parser.add_argument(
'path', action=
'store', type=str,
658 help=
'Path to the files to check')
660 parser.add_argument(
'--no-formatting', action=
'store_true',
661 help=
'Do not check / fix code formatting')
663 parser.add_argument(
'--no-whitespace', action=
'store_true',
664 help=
'Do not check / fix trailing whitespace')
666 parser.add_argument(
'--no-tabs', action=
'store_true',
667 help=
'Do not check / fix tabs')
669 parser.add_argument(
'--fix', action=
'store_true',
670 help=
'Fix coding style issues detected in the files')
672 parser.add_argument(
'-j',
'--jobs', type=int, default=
max(1, os.cpu_count() - 1),
673 help=
'Number of parallel jobs')
675 args = parser.parse_args()
680 enable_check_formatting=(
not args.no_formatting),
681 enable_check_whitespace=(
not args.no_whitespace),
682 enable_check_tabs=(
not args.no_tabs),
687 except Exception
as e: