A Discrete-Event Network Simulator
API
Loading...
Searching...
No Matches
check-style-clang-format.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2
3# Copyright (c) 2022 Eduardo Nuno Almeida.
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7# Author: Eduardo Nuno Almeida <enmsa@outlook.pt> [INESC TEC and FEUP, Portugal]
8
9"""Check and apply the ns-3 coding style recursively to all files in the PATH arguments.
10
11The coding style is defined with the clang-format tool, whose definitions are in
12the ".clang-format" file. This script performs the following checks / fixes:
13- Check / apply clang-format. Respects clang-format guards.
14- Check / fix local #include headers with "ns3/" prefix. Respects clang-format guards.
15- Check / fix ns-3 #include headers using angle brackets <> rather than quotes "". Respects clang-format guards.
16- Check / fix Doxygen tags using @ rather than \\. Respects clang-format guards.
17- Check / fix SPDX licenses rather than GPL text. Respects clang-format guards.
18- Check / fix emacs file style comments. Respects clang-format guards.
19- Check / trim trailing whitespace. Always checked.
20- Check / replace tabs with spaces. Respects clang-format guards.
21- Check file encoding. Always checked.
22
23This script can be applied to all text files in a given path or to individual files.
24
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.
28"""
29
30import argparse
31import concurrent.futures
32import itertools
33import os
34import re
35import shutil
36import subprocess
37import sys
38from typing import Callable, Dict, List, Tuple
39
40###########################################################
41# PARAMETERS
42###########################################################
43CLANG_FORMAT_MAX_VERSION = 20
44CLANG_FORMAT_MIN_VERSION = 20
45
46FORMAT_GUARD_ON = [
47 "// clang-format on",
48 "# cmake-format: on",
49 "# fmt: on",
50]
51
52FORMAT_GUARD_OFF = [
53 "// clang-format off",
54 "# cmake-format: off",
55 "# fmt: off",
56]
57
58DIRECTORIES_TO_SKIP = [
59 "__pycache__",
60 ".git",
61 ".venv",
62 "bindings",
63 "build",
64 "cmake-cache",
65 "testpy-output",
66 "venv",
67]
68
69# List of files entirely copied from elsewhere that should not be checked,
70# in order to optimize the performance of this script
71FILES_TO_SKIP = [
72 "valgrind.h",
73]
74
75# List of checks
76CHECKS = [
77 "include_prefixes",
78 "include_quotes",
79 "doxygen_tags",
80 "license",
81 "emacs",
82 "whitespace",
83 "tabs",
84 "formatting",
85 "encoding",
86]
87
88# Files to check
89FILES_TO_CHECK: Dict[str, List[str]] = {c: [] for c in CHECKS}
90
91FILES_TO_CHECK["tabs"] = [
92 ".clang-format",
93 ".clang-tidy",
94 ".codespellrc",
95 "CMakeLists.txt",
96 "codespell-ignored-lines",
97 "codespell-ignored-words",
98 "ns3",
99]
100
101FILES_TO_CHECK["whitespace"] = FILES_TO_CHECK["tabs"] + [
102 "Makefile",
103]
104
105# File extensions to check
106FILE_EXTENSIONS_TO_CHECK: Dict[str, List[str]] = {c: [] for c in CHECKS}
107
108FILE_EXTENSIONS_TO_CHECK["formatting"] = [
109 ".c",
110 ".cc",
111 ".h",
112]
113
114FILE_EXTENSIONS_TO_CHECK["include_prefixes"] = FILE_EXTENSIONS_TO_CHECK["formatting"]
115FILE_EXTENSIONS_TO_CHECK["include_quotes"] = FILE_EXTENSIONS_TO_CHECK["formatting"]
116FILE_EXTENSIONS_TO_CHECK["doxygen_tags"] = FILE_EXTENSIONS_TO_CHECK["formatting"]
117FILE_EXTENSIONS_TO_CHECK["encoding"] = FILE_EXTENSIONS_TO_CHECK["formatting"]
118
119FILE_EXTENSIONS_TO_CHECK["license"] = [
120 ".c",
121 ".cc",
122 ".cmake",
123 ".h",
124 ".py",
125]
126
127FILE_EXTENSIONS_TO_CHECK["emacs"] = [
128 ".c",
129 ".cc",
130 ".h",
131 ".py",
132 ".rst",
133]
134
135FILE_EXTENSIONS_TO_CHECK["tabs"] = [
136 ".c",
137 ".cc",
138 ".cmake",
139 ".css",
140 ".h",
141 ".html",
142 ".js",
143 ".json",
144 ".m",
145 ".md",
146 ".pl",
147 ".py",
148 ".rst",
149 ".sh",
150 ".toml",
151 ".yml",
152]
153
154FILE_EXTENSIONS_TO_CHECK["whitespace"] = FILE_EXTENSIONS_TO_CHECK["tabs"] + [
155 ".click",
156 ".cfg",
157 ".conf",
158 ".dot",
159 ".gnuplot",
160 ".gp",
161 ".mob",
162 ".ns_params",
163 ".ns_movements",
164 ".params",
165 ".plt",
166 ".seqdiag",
167 ".txt",
168]
169
170# Other check parameters
171TAB_SIZE = 4
172FILE_ENCODING = "UTF-8"
173
174
175###########################################################
176# AUXILIARY FUNCTIONS
177###########################################################
178def should_analyze_directory(dirpath: str) -> bool:
179 """Check whether a directory should be analyzed.
180
181 Args:
182 dirpath: Directory path.
183
184 Returns:
185 Whether the directory should be analyzed.
186 """
187
188 _, directory = os.path.split(dirpath)
189
190 return directory not in DIRECTORIES_TO_SKIP
191
192
194 path: str,
195 files_to_check: List[str],
196 file_extensions_to_check: List[str],
197) -> bool:
198 """Check whether a file should be analyzed.
199
200 Args:
201 path: Path to the file.
202 files_to_check: List of files that shall be checked.
203 file_extensions_to_check: List of file extensions that shall be checked.
204
205 Returns:
206 Whether the file should be analyzed.
207 """
208
209 filename = os.path.split(path)[1]
210
211 if filename in FILES_TO_SKIP:
212 return False
213
214 extension = os.path.splitext(filename)[1]
215
216 return filename in files_to_check or extension in file_extensions_to_check
217
218
220 paths: List[str],
221) -> Dict[str, List[str]]:
222 """Find all files to be checked in a given list of paths.
223
224 Args:
225 paths List of paths to the files to check.
226
227 Returns:
228 Dictionary of checks and corresponding list of files to check.
229 Example: {
230 "formatting": list_of_files_to_check_formatting,
231 ...,
232 }
233 """
234
235 # Get list of files found in the given path
236 files_found: List[str] = []
237
238 for path in paths:
239 abs_path = os.path.abspath(os.path.expanduser(path))
240
241 if os.path.isfile(abs_path):
242 files_found.append(path)
243
244 elif os.path.isdir(abs_path):
245 for dirpath, dirnames, filenames in os.walk(path, topdown=True):
246 if not should_analyze_directory(dirpath):
247 # Remove directory and its subdirectories
248 dirnames[:] = []
249 continue
250
251 files_found.extend([os.path.join(dirpath, f) for f in filenames])
252
253 else:
254 raise ValueError(f"{path} is not a valid file nor a directory")
255
256 files_found.sort()
257
258 # Check which files should be checked
259 files_to_check: Dict[str, List[str]] = {c: [] for c in CHECKS}
260
261 for f in files_found:
262 for check in CHECKS:
263 if should_analyze_file(f, FILES_TO_CHECK[check], FILE_EXTENSIONS_TO_CHECK[check]):
264 files_to_check[check].append(f)
265
266 return files_to_check
267
268
270 """Find the path to one of the supported versions of clang-format.
271
272 Returns:
273 Path to clang-format.
274
275 Raises:
276 RuntimeError: If no supported version of clang-format is found.
277 """
278
279 # Find exact version, starting from the most recent one
280 for version in range(CLANG_FORMAT_MAX_VERSION, CLANG_FORMAT_MIN_VERSION - 1, -1):
281 clang_format_path = shutil.which(f"clang-format-{version}")
282
283 if clang_format_path:
284 return clang_format_path
285
286 # Find default version and check if it is supported
287 clang_format_path = shutil.which("clang-format")
288 major_version = None
289
290 if clang_format_path:
291 process = subprocess.run(
292 [clang_format_path, "--version"],
293 capture_output=True,
294 text=True,
295 check=True,
296 )
297
298 clang_format_version = process.stdout.strip()
299 version_regex = re.findall(r"\b(\d+)(\.\d+){0,2}\b", clang_format_version)
300
301 if version_regex:
302 major_version = int(version_regex[0][0])
303
304 if CLANG_FORMAT_MIN_VERSION <= major_version <= CLANG_FORMAT_MAX_VERSION:
305 return clang_format_path
306
307 # No supported version of clang-format found
308 raise RuntimeError(
309 f"Could not find any supported version of clang-format installed on this system. "
310 f"List of supported versions: [{CLANG_FORMAT_MAX_VERSION}-{CLANG_FORMAT_MIN_VERSION}]. "
311 + (f"Found clang-format {major_version}." if major_version else "")
312 )
313
314
315###########################################################
316# CHECK STYLE MAIN FUNCTIONS
317###########################################################
319 paths: List[str],
320 checks_enabled: Dict[str, bool],
321 fix: bool,
322 verbose: bool,
323 n_jobs: int = 1,
324) -> bool:
325 """Check / fix the coding style of a list of files.
326
327 Args:
328 paths: List of paths to the files to check.
329 checks_enabled: Dictionary of checks indicating whether to enable each of them.
330 fix: Whether to fix (True) or just check (False) the file.
331 verbose: Show the lines that are not compliant with the style.
332 n_jobs: Number of parallel jobs.
333
334 Returns:
335 Whether all files are compliant with all enabled style checks.
336 """
337
338 files_to_check = find_files_to_check_style(paths)
339 checks_successful = {c: True for c in CHECKS}
340
341 style_check_strs = {
342 "include_prefixes": '#include headers from the same module with the "ns3/" prefix',
343 "include_quotes": 'ns-3 #include headers using angle brackets <> rather than quotes ""',
344 "doxygen_tags": "Doxygen tags using \\ rather than @",
345 "license": "GPL license text instead of SPDX license",
346 "emacs": "emacs file style comments",
347 "whitespace": "trailing whitespace",
348 "tabs": "tabs",
349 "formatting": "bad code formatting",
350 "encoding": f"bad file encoding ({FILE_ENCODING})",
351 }
352
353 check_style_file_functions_kwargs = {
354 "include_prefixes": {
355 "function": check_manually_file,
356 "kwargs": {
357 "respect_clang_format_guards": True,
358 "check_style_line_function": check_include_prefixes_line,
359 },
360 },
361 "include_quotes": {
362 "function": check_manually_file,
363 "kwargs": {
364 "respect_clang_format_guards": True,
365 "check_style_line_function": check_include_quotes_line,
366 },
367 },
368 "doxygen_tags": {
369 "function": check_manually_file,
370 "kwargs": {
371 "respect_clang_format_guards": True,
372 "check_style_line_function": check_doxygen_tags_line,
373 },
374 },
375 "license": {
376 "function": check_manually_file,
377 "kwargs": {
378 "respect_clang_format_guards": True,
379 "check_style_line_function": check_licenses_line,
380 },
381 },
382 "emacs": {
383 "function": check_manually_file,
384 "kwargs": {
385 "respect_clang_format_guards": True,
386 "check_style_line_function": check_emacs_line,
387 },
388 },
389 "whitespace": {
390 "function": check_manually_file,
391 "kwargs": {
392 "respect_clang_format_guards": False,
393 "check_style_line_function": check_whitespace_line,
394 },
395 },
396 "tabs": {
397 "function": check_manually_file,
398 "kwargs": {
399 "respect_clang_format_guards": True,
400 "check_style_line_function": check_tabs_line,
401 },
402 },
403 "formatting": {
404 "function": check_formatting_file,
405 "kwargs": {}, # The formatting keywords are added below
406 },
407 "encoding": {
408 "function": check_encoding_file,
409 "kwargs": {},
410 },
411 }
412
413 if checks_enabled["formatting"]:
414 check_style_file_functions_kwargs["formatting"]["kwargs"] = {
415 "clang_format_path": find_clang_format_path(),
416 }
417
418 n_checks_enabled = sum(checks_enabled.values())
419 n_check = 0
420
421 for check in CHECKS:
422 if checks_enabled[check]:
423 checks_successful[check] = check_style_files(
424 style_check_strs[check],
425 check_style_file_functions_kwargs[check]["function"],
426 files_to_check[check],
427 fix,
428 verbose,
429 n_jobs,
430 **check_style_file_functions_kwargs[check]["kwargs"],
431 )
432
433 n_check += 1
434
435 if n_check < n_checks_enabled:
436 print("")
437
438 return all(checks_successful.values())
439
440
442 style_check_str: str,
443 check_style_file_function: Callable[..., Tuple[str, bool, List[str]]],
444 filenames: List[str],
445 fix: bool,
446 verbose: bool,
447 n_jobs: int,
448 **kwargs,
449) -> bool:
450 """Check / fix style of a list of files.
451
452 Args:
453 style_check_str: Description of the check to be performed.
454 check_style_file_function: Function used to check the file.
455 filename: Name of the file to be checked.
456 fix: Whether to fix (True) or just check (False) the file (True).
457 verbose: Show the lines that are not compliant with the style.
458 n_jobs: Number of parallel jobs.
459 kwargs: Additional keyword arguments to the check_style_file_function.
460
461 Returns:
462 Whether all files are compliant with the style.
463 """
464
465 # Check files
466 non_compliant_files: List[str] = []
467 files_verbose_infos: Dict[str, List[str]] = {}
468
469 with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
470 non_compliant_files_results = executor.map(
471 check_style_file_function,
472 filenames,
473 itertools.repeat(fix),
474 itertools.repeat(verbose),
475 *[arg if isinstance(arg, list) else itertools.repeat(arg) for arg in kwargs.values()],
476 )
477
478 for filename, is_file_compliant, verbose_infos in non_compliant_files_results:
479 if not is_file_compliant:
480 non_compliant_files.append(filename)
481
482 if verbose:
483 files_verbose_infos[filename] = verbose_infos
484
485 # Output results
486 if not non_compliant_files:
487 print(f"- No files detected with {style_check_str}")
488 return True
489
490 else:
491 n_non_compliant_files = len(non_compliant_files)
492
493 if fix:
494 print(f"- Fixed {style_check_str} in the files ({n_non_compliant_files}):")
495 else:
496 print(f"- Detected {style_check_str} in the files ({n_non_compliant_files}):")
497
498 for f in non_compliant_files:
499 if verbose:
500 print(*[f" {l}" for l in files_verbose_infos[f]], sep="\n")
501 else:
502 print(f" - {f}")
503
504 # If all files were fixed, there are no more non-compliant files
505 return fix
506
507
508###########################################################
509# CHECK STYLE FUNCTIONS
510###########################################################
512 filename: str,
513 fix: bool,
514 verbose: bool,
515 clang_format_path: str,
516) -> Tuple[str, bool, List[str]]:
517 """Check / fix the coding style of a file with clang-format.
518
519 Args:
520 filename: Name of the file to be checked.
521 fix: Whether to fix (True) or just check (False) the style of the file.
522 verbose: Show the lines that are not compliant with the style.
523 clang_format_path: Path to clang-format.
524
525 Returns:
526 Tuple (Filename,
527 Whether the file is compliant with the style (before the check),
528 Verbose information)
529 """
530
531 verbose_infos: List[str] = []
532
533 # Check if the file is well formatted
534 process = subprocess.run(
535 [
536 clang_format_path,
537 filename,
538 "-style=file",
539 "--dry-run",
540 "--Werror",
541 # Optimization: In non-verbose mode, only one error is needed to check that the file is not compliant
542 f"--ferror-limit={0 if verbose else 1}",
543 ],
544 check=False,
545 capture_output=True,
546 text=True,
547 )
548
549 is_file_compliant = process.returncode == 0
550
551 if verbose:
552 verbose_infos = process.stderr.splitlines()
553
554 # Fix file
555 if fix and not is_file_compliant:
556 process = subprocess.run(
557 [
558 clang_format_path,
559 filename,
560 "-style=file",
561 "-i",
562 ],
563 check=False,
564 stdout=subprocess.DEVNULL,
565 stderr=subprocess.DEVNULL,
566 )
567
568 return (filename, is_file_compliant, verbose_infos)
569
570
572 filename: str,
573 fix: bool,
574 verbose: bool,
575) -> Tuple[str, bool, List[str]]:
576 """Check / fix the encoding of a file.
577
578 Args:
579 filename: Name of the file to be checked.
580 fix: Whether to fix (True) or just check (False) the encoding of the file.
581 verbose: Show the lines that are not compliant with the style.
582
583 Returns:
584 Tuple (Filename,
585 Whether the file is compliant with the style (before the check),
586 Verbose information)
587 """
588
589 verbose_infos: List[str] = []
590 is_file_compliant = True
591
592 with open(filename, "rb") as f:
593 file_data = f.read()
594 file_lines = file_data.decode(FILE_ENCODING, errors="replace").splitlines(keepends=True)
595
596 # Check if file has correct encoding
597 try:
598 file_data.decode(FILE_ENCODING)
599
600 except UnicodeDecodeError as e:
601 is_file_compliant = False
602
603 if verbose:
604 # Find line and column with bad encoding
605 bad_char_start_index = e.start
606 n_chars_file_read = 0
607
608 for line_number, line in enumerate(file_lines):
609 n_chars_line = len(line)
610
611 if bad_char_start_index < n_chars_file_read + n_chars_line:
612 bad_char_column = bad_char_start_index - n_chars_file_read
613
614 verbose_infos.extend(
615 [
616 f"{filename}:{line_number + 1}:{bad_char_column + 1}: error: bad {FILE_ENCODING} encoding",
617 f" {line.rstrip()}",
618 f" {'':>{bad_char_column}}^",
619 ]
620 )
621
622 break
623
624 n_chars_file_read += n_chars_line
625
626 # Fix file encoding
627 if fix and not is_file_compliant:
628 with open(filename, "w", encoding=FILE_ENCODING) as f:
629 f.writelines(file_lines)
630
631 return (filename, is_file_compliant, verbose_infos)
632
633
635 filename: str,
636 fix: bool,
637 verbose: bool,
638 respect_clang_format_guards: bool,
639 check_style_line_function: Callable[[str, str, int], Tuple[bool, str, List[str]]],
640) -> Tuple[str, bool, List[str]]:
641 """Check / fix a file manually using a function to check / fix each line.
642
643 Args:
644 filename: Name of the file to be checked.
645 fix: Whether to fix (True) or just check (False) the style of the file.
646 verbose: Show the lines that are not compliant with the style.
647 respect_clang_format_guards: Whether to respect clang-format guards.
648 check_style_line_function: Function used to check each line.
649
650 Returns:
651 Tuple (Filename,
652 Whether the file is compliant with the style (before the check),
653 Verbose information)
654 """
655
656 is_file_compliant = True
657 verbose_infos: List[str] = []
658 clang_format_enabled = True
659
660 with open(filename, "r", encoding=FILE_ENCODING) as f:
661 file_lines = f.readlines()
662
663 for i, line in enumerate(file_lines):
664 # Check clang-format guards
665 if respect_clang_format_guards:
666 line_stripped = line.strip()
667
668 if line_stripped in FORMAT_GUARD_ON:
669 clang_format_enabled = True
670 elif line_stripped in FORMAT_GUARD_OFF:
671 clang_format_enabled = False
672
673 if not clang_format_enabled and line_stripped not in (
674 FORMAT_GUARD_ON + FORMAT_GUARD_OFF
675 ):
676 continue
677
678 # Check if the line is compliant with the style and fix it
679 is_line_compliant, line_fixed, line_verbose_infos = check_style_line_function(
680 line, filename, i
681 )
682
683 if not is_line_compliant:
684 is_file_compliant = False
685 file_lines[i] = line_fixed
686 verbose_infos.extend(line_verbose_infos)
687
688 # Optimization: If running in non-verbose check mode, only one error is needed to check that the file is not compliant
689 if not fix and not verbose:
690 break
691
692 # Update file with the fixed lines
693 if fix and not is_file_compliant:
694 with open(filename, "w", encoding=FILE_ENCODING) as f:
695 f.writelines(file_lines)
696
697 return (filename, is_file_compliant, verbose_infos)
698
699
701 line: str,
702 filename: str,
703 line_number: int,
704) -> Tuple[bool, str, List[str]]:
705 """Check / fix #include headers from the same module with the "ns3/" prefix in a line.
706
707 Args:
708 line: The line to check.
709 filename: Name of the file to be checked.
710 line_number: The number of the line checked.
711
712 Returns:
713 Tuple (Filename,
714 Whether the line is compliant with the style (before the check),
715 Verbose information)
716 """
717
718 is_line_compliant = True
719 line_fixed = line
720 verbose_infos: List[str] = []
721
722 # Check if the line is an #include and extract its header file
723 line_stripped = line.strip()
724 header_file = re.findall(r'^#include ["<]ns3/(.*\.h)[">]', line_stripped)
725
726 if header_file:
727 # Check if the header file belongs to the same module and remove the "ns3/" prefix
728 header_file = header_file[0]
729 parent_path = os.path.split(filename)[0]
730
731 if os.path.exists(os.path.join(parent_path, header_file)):
732 is_line_compliant = False
733 line_fixed = (
734 line_stripped.replace(f"ns3/{header_file}", header_file)
735 .replace("<", '"')
736 .replace(">", '"')
737 + "\n"
738 )
739
740 header_index = len('#include "')
741
742 verbose_infos.extend(
743 [
744 f'{filename}:{line_number + 1}:{header_index + 1}: error: #include headers from the same module with the "ns3/" prefix detected',
745 f" {line_stripped}",
746 f" {'':>{header_index}}^",
747 ]
748 )
749
750 return (is_line_compliant, line_fixed, verbose_infos)
751
752
754 line: str,
755 filename: str,
756 line_number: int,
757) -> Tuple[bool, str, List[str]]:
758 """Check / fix ns-3 #include headers using angle brackets <> rather than quotes "" in a line.
759
760 Args:
761 line: The line to check.
762 filename: Name of the file to be checked.
763 line_number: The number of the line checked.
764
765 Returns:
766 Tuple (Whether the line is compliant with the style (before the check),
767 Fixed line,
768 Verbose information)
769 """
770
771 is_line_compliant = True
772 line_fixed = line
773 verbose_infos: List[str] = []
774
775 # Check if the line is an #include <ns3/...>
776 header_file = re.findall(r"^#include <ns3/.*\.h>", line)
777
778 if header_file:
779 is_line_compliant = False
780 line_fixed = line.replace("<", '"').replace(">", '"')
781
782 header_index = len("#include ")
783
784 verbose_infos = [
785 f"{filename}:{line_number + 1}:{header_index + 1}: error: ns-3 #include headers with angle brackets detected",
786 f" {line}",
787 f" {'':{header_index}}^",
788 ]
789
790 return (is_line_compliant, line_fixed, verbose_infos)
791
792
794 line: str,
795 filename: str,
796 line_number: int,
797) -> Tuple[bool, str, List[str]]:
798 """Check / fix Doxygen tags using \\ rather than @ in a line.
799
800 Args:
801 line: The line to check.
802 filename: Name of the file to be checked.
803 line_number: The number of the line checked.
804
805 Returns:
806 Tuple (Whether the line is compliant with the style (before the check),
807 Fixed line,
808 Verbose information)
809 """
810
811 IGNORED_WORDS = [
812 "\\dots",
813 "\\langle",
814 "\\quad",
815 ]
816
817 is_line_compliant = True
818 line_fixed = line
819 verbose_infos: List[str] = []
820
821 # Match Doxygen tags at the start of the line (e.g., "* \param arg Description")
822 line_stripped = line.rstrip()
823 regex_findings = re.findall(r"^\s*(?:\*|\/\*\*|\/\/\/)\s*(\\\w{3,})(?=(?:\s|$))", line_stripped)
824
825 if regex_findings:
826 doxygen_tag = regex_findings[0]
827
828 if doxygen_tag not in IGNORED_WORDS:
829 is_line_compliant = False
830
831 doxygen_tag_index = line_fixed.find(doxygen_tag)
832 line_fixed = line.replace(doxygen_tag, f"@{doxygen_tag[1:]}")
833
834 verbose_infos.extend(
835 [
836 f"{filename}:{line_number + 1}:{doxygen_tag_index + 1}: error: detected Doxygen tags using \\ rather than @",
837 f" {line_stripped}",
838 f" {'':{doxygen_tag_index}}^",
839 ]
840 )
841
842 return (is_line_compliant, line_fixed, verbose_infos)
843
844
846 line: str,
847 filename: str,
848 line_number: int,
849) -> Tuple[bool, str, List[str]]:
850 """Check / fix SPDX licenses rather than GPL text in a line.
851
852 Args:
853 line: The line to check.
854 filename: Name of the file to be checked.
855 line_number: The number of the line checked.
856
857 Returns:
858 Tuple (Whether the line is compliant with the style (before the check),
859 Fixed line,
860 Verbose information)
861 """
862
863 # fmt: off
864 GPL_LICENSE_LINES = [
865 "This program is free software; you can redistribute it and/or modify",
866 "it under the terms of the GNU General Public License version 2 as",
867 "published by the Free Software Foundation;",
868 "This program is distributed in the hope that it will be useful,",
869 "but WITHOUT ANY WARRANTY; without even the implied warranty of",
870 "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the",
871 "GNU General Public License for more details.",
872 "You should have received a copy of the GNU General Public License",
873 "along with this program; if not, write to the Free Software",
874 "Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA",
875 ]
876 # fmt: on
877
878 # REUSE-IgnoreStart
879 SPDX_LICENSE = "SPDX-License-Identifier: GPL-2.0-only"
880 # REUSE-IgnoreEnd
881
882 is_line_compliant = True
883 line_fixed = line
884 verbose_infos: List[str] = []
885
886 # Check if the line is a GPL license text
887 line_stripped = line.strip()
888 line_stripped_no_leading_comments = line_stripped.strip("*#/").strip()
889
890 if line_stripped_no_leading_comments in GPL_LICENSE_LINES:
891 is_line_compliant = False
892 col_index = 0
893
894 # Replace GPL text with SPDX license.
895 # Replace the first line of the GPL text with SPDX.
896 # Delete the remaining GPL text lines.
897 if line_stripped_no_leading_comments == GPL_LICENSE_LINES[0]:
898 line_fixed = line.replace(line_stripped_no_leading_comments, SPDX_LICENSE)
899 else:
900 line_fixed = ""
901
902 verbose_infos.extend(
903 [
904 f"{filename}:{line_number + 1}:{col_index}: error: GPL license text detected instead of SPDX license",
905 f" {line_stripped}",
906 f" {'':>{col_index}}^",
907 ]
908 )
909
910 return (is_line_compliant, line_fixed, verbose_infos)
911
912
914 line: str,
915 filename: str,
916 line_number: int,
917) -> Tuple[bool, str, List[str]]:
918 """Check / fix emacs file style comment in a line.
919
920 Args:
921 line: The line to check.
922 filename: Name of the file to be checked.
923 line_number: The number of the line checked.
924
925 Returns:
926 Tuple (Whether the line is compliant with the style (before the check),
927 Fixed line,
928 Verbose information)
929 """
930
931 is_line_compliant = True
932 line_fixed = line
933 verbose_infos: List[str] = []
934
935 # Check if line is an emacs file style comment
936 line_stripped = line.strip()
937 # fmt: off
938 emacs_line = re.search(r"c-file-style:|py-indent-offset:", line_stripped)
939 # fmt: on
940
941 if emacs_line:
942 is_line_compliant = False
943 line_fixed = ""
944 col_index = emacs_line.start()
945
946 verbose_infos = [
947 f"{filename}:{line_number + 1}:{col_index}: error: emacs file style comment detected",
948 f" {line_stripped}",
949 f" {'':{col_index}}^",
950 ]
951
952 return (is_line_compliant, line_fixed, verbose_infos)
953
954
956 line: str,
957 filename: str,
958 line_number: int,
959) -> Tuple[bool, str, List[str]]:
960 """Check / fix whitespace in a line.
961
962 Args:
963 line: The line to check.
964 filename: Name of the file to be checked.
965 line_number: The number of the line checked.
966
967 Returns:
968 Tuple (Whether the line is compliant with the style (before the check),
969 Fixed line,
970 Verbose information)
971 """
972
973 is_line_compliant = True
974 line_fixed = line.rstrip() + "\n"
975 verbose_infos: List[str] = []
976
977 if line_fixed != line:
978 is_line_compliant = False
979 line_fixed_stripped_expanded = line_fixed.rstrip().expandtabs(TAB_SIZE)
980
981 verbose_infos = [
982 f"{filename}:{line_number + 1}:{len(line_fixed_stripped_expanded) + 1}: error: Trailing whitespace detected",
983 f" {line_fixed_stripped_expanded}",
984 f" {'':>{len(line_fixed_stripped_expanded)}}^",
985 ]
986
987 return (is_line_compliant, line_fixed, verbose_infos)
988
989
991 line: str,
992 filename: str,
993 line_number: int,
994) -> Tuple[bool, str, List[str]]:
995 """Check / fix tabs in a line.
996
997 Args:
998 line: The line to check.
999 filename: Name of the file to be checked.
1000 line_number: The number of the line checked.
1001
1002 Returns:
1003 Tuple (Whether the line is compliant with the style (before the check),
1004 Fixed line,
1005 Verbose information)
1006 """
1007
1008 is_line_compliant = True
1009 line_fixed = line
1010 verbose_infos: List[str] = []
1011
1012 tab_index = line.find("\t")
1013
1014 if tab_index != -1:
1015 is_line_compliant = False
1016 line_fixed = line.expandtabs(TAB_SIZE)
1017
1018 verbose_infos = [
1019 f"{filename}:{line_number + 1}:{tab_index + 1}: error: Tab detected",
1020 f" {line.rstrip()}",
1021 f" {'':>{tab_index}}^",
1022 ]
1023
1024 return (is_line_compliant, line_fixed, verbose_infos)
1025
1026
1027###########################################################
1028# MAIN
1029###########################################################
1030if __name__ == "__main__":
1031 parser = argparse.ArgumentParser(
1032 description="Check and apply the ns-3 coding style recursively to all files in the given PATHs. "
1033 "The script checks the formatting of the files using clang-format and"
1034 " other coding style rules manually (see script arguments). "
1035 "All checks respect clang-format guards, except trailing whitespace and file encoding,"
1036 " which are always checked. "
1037 'When used in "check mode" (default), the script runs all checks in all files. '
1038 "If it detects non-formatted files, they will be printed and this process exits with a non-zero code. "
1039 'When used in "fix mode", this script automatically fixes the files and exits with 0 code.'
1040 )
1041
1042 parser.add_argument(
1043 "paths",
1044 action="store",
1045 type=str,
1046 nargs="+",
1047 help="List of paths to the files to check",
1048 )
1049
1050 parser.add_argument(
1051 "--no-include-prefixes",
1052 action="store_true",
1053 help='Do not check / fix #include headers from the same module with the "ns3/" prefix (respects clang-format guards)',
1054 )
1055
1056 parser.add_argument(
1057 "--no-include-quotes",
1058 action="store_true",
1059 help='Do not check / fix ns-3 #include headers using angle brackets <> rather than quotes "" (respects clang-format guards)',
1060 )
1061
1062 parser.add_argument(
1063 "--no-doxygen-tags",
1064 action="store_true",
1065 help="Do not check / fix Doxygen tags using @ rather than \\ (respects clang-format guards)",
1066 )
1067
1068 parser.add_argument(
1069 "--no-licenses",
1070 action="store_true",
1071 help="Do not check / fix SPDX licenses rather than GPL text (respects clang-format guards)",
1072 )
1073
1074 parser.add_argument(
1075 "--no-emacs",
1076 action="store_true",
1077 help="Do not check / fix emacs file style comments (respects clang-format guards)",
1078 )
1079
1080 parser.add_argument(
1081 "--no-whitespace",
1082 action="store_true",
1083 help="Do not check / fix trailing whitespace",
1084 )
1085
1086 parser.add_argument(
1087 "--no-tabs",
1088 action="store_true",
1089 help="Do not check / fix tabs (respects clang-format guards)",
1090 )
1091
1092 parser.add_argument(
1093 "--no-formatting",
1094 action="store_true",
1095 help="Do not check / fix code formatting (respects clang-format guards)",
1096 )
1097
1098 parser.add_argument(
1099 "--no-encoding",
1100 action="store_true",
1101 help=f"Do not check / fix file encoding ({FILE_ENCODING})",
1102 )
1103
1104 parser.add_argument(
1105 "--fix",
1106 action="store_true",
1107 help="Fix coding style issues detected in the files",
1108 )
1109
1110 parser.add_argument(
1111 "-v",
1112 "--verbose",
1113 action="store_true",
1114 help="Show the lines that are not well-formatted",
1115 )
1116
1117 parser.add_argument(
1118 "-j",
1119 "--jobs",
1120 type=int,
1121 default=max(1, os.cpu_count() - 1),
1122 help="Number of parallel jobs",
1123 )
1124
1125 args = parser.parse_args()
1126
1127 try:
1128 all_checks_successful = check_style_clang_format(
1129 paths=args.paths,
1130 checks_enabled={
1131 "include_prefixes": not args.no_include_prefixes,
1132 "include_quotes": not args.no_include_quotes,
1133 "doxygen_tags": not args.no_doxygen_tags,
1134 "license": not args.no_licenses,
1135 "emacs": not args.no_emacs,
1136 "whitespace": not args.no_whitespace,
1137 "tabs": not args.no_tabs,
1138 "formatting": not args.no_formatting,
1139 "encoding": not args.no_encoding,
1140 },
1141 fix=args.fix,
1142 verbose=args.verbose,
1143 n_jobs=args.jobs,
1144 )
1145
1146 except Exception as ex:
1147 print("ERROR:", ex)
1148 sys.exit(1)
1149
1150 if not all_checks_successful:
1151 if args.verbose:
1152 print(
1153 "",
1154 "Notes to fix the above formatting issues:",
1155 ' - To fix the formatting of specific files, run this script with the flag "--fix":',
1156 " $ ./utils/check-style-clang-format.py --fix path [path ...]",
1157 " - To fix the formatting of all files modified by this branch, run this script in the following way:",
1158 " $ git diff --name-only master | xargs ./utils/check-style-clang-format.py --fix",
1159 sep="\n",
1160 )
1161
1162 sys.exit(1)
Tuple[str, bool, List[str]] check_encoding_file(str filename, bool fix, bool verbose)
Tuple[bool, str, List[str]] check_doxygen_tags_line(str line, str filename, int line_number)
Tuple[bool, str, List[str]] check_whitespace_line(str line, str filename, int line_number)
Tuple[bool, str, List[str]] check_include_quotes_line(str line, str filename, int line_number)
Tuple[str, bool, List[str]] check_formatting_file(str filename, bool fix, bool verbose, str clang_format_path)
CHECK STYLE FUNCTIONS.
Dict[str, List[str]] find_files_to_check_style(List[str] paths)
bool check_style_clang_format(List[str] paths, Dict[str, bool] checks_enabled, bool fix, bool verbose, int n_jobs=1)
CHECK STYLE MAIN FUNCTIONS.
Tuple[str, bool, List[str]] check_manually_file(str filename, bool fix, bool verbose, bool respect_clang_format_guards, Callable[[str, str, int], Tuple[bool, str, List[str]]] check_style_line_function)
Tuple[bool, str, List[str]] check_emacs_line(str line, str filename, int line_number)
bool check_style_files(str style_check_str, Callable[..., Tuple[str, bool, List[str]]] check_style_file_function, List[str] filenames, bool fix, bool verbose, int n_jobs, **kwargs)
Tuple[bool, str, List[str]] check_include_prefixes_line(str line, str filename, int line_number)
bool should_analyze_file(str path, List[str] files_to_check, List[str] file_extensions_to_check)
Tuple[bool, str, List[str]] check_tabs_line(str line, str filename, int line_number)
bool should_analyze_directory(str dirpath)
AUXILIARY FUNCTIONS.
Tuple[bool, str, List[str]] check_licenses_line(str line, str filename, int line_number)