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"""
10Check and apply the ns-3 coding style recursively to all files in the PATH arguments.
11
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.
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 = 17
44CLANG_FORMAT_MIN_VERSION = 14
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 "whitespace",
81 "tabs",
82 "license",
83 "formatting",
84 "encoding",
85]
86
87# Files to check
88FILES_TO_CHECK: Dict[str, List[str]] = {c: [] for c in CHECKS}
89
90FILES_TO_CHECK["tabs"] = [
91 ".clang-format",
92 ".clang-tidy",
93 ".codespellrc",
94 "CMakeLists.txt",
95 "codespell-ignored-lines",
96 "codespell-ignored-words",
97 "ns3",
98]
99
100FILES_TO_CHECK["whitespace"] = FILES_TO_CHECK["tabs"] + [
101 "Makefile",
102]
103
104# File extensions to check
105FILE_EXTENSIONS_TO_CHECK: Dict[str, List[str]] = {c: [] for c in CHECKS}
106
107FILE_EXTENSIONS_TO_CHECK["formatting"] = [
108 ".c",
109 ".cc",
110 ".h",
111]
112
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"]
117
118FILE_EXTENSIONS_TO_CHECK["tabs"] = [
119 ".c",
120 ".cc",
121 ".cmake",
122 ".css",
123 ".h",
124 ".html",
125 ".js",
126 ".json",
127 ".m",
128 ".md",
129 ".pl",
130 ".py",
131 ".rst",
132 ".sh",
133 ".toml",
134 ".yml",
135]
136
137FILE_EXTENSIONS_TO_CHECK["whitespace"] = FILE_EXTENSIONS_TO_CHECK["tabs"] + [
138 ".click",
139 ".cfg",
140 ".conf",
141 ".dot",
142 ".gnuplot",
143 ".gp",
144 ".mob",
145 ".ns_params",
146 ".ns_movements",
147 ".params",
148 ".plt",
149 ".seqdiag",
150 ".txt",
151]
152
153FILE_EXTENSIONS_TO_CHECK["license"] = [
154 ".c",
155 ".cc",
156 ".cmake",
157 ".h",
158 ".py",
159]
160
161# Other check parameters
162TAB_SIZE = 4
163FILE_ENCODING = "UTF-8"
164
165
166###########################################################
167# AUXILIARY FUNCTIONS
168###########################################################
169def should_analyze_directory(dirpath: str) -> bool:
170 """
171 Check whether a directory should be analyzed.
172
173 @param dirpath Directory path.
174 @return Whether the directory should be analyzed.
175 """
176
177 _, directory = os.path.split(dirpath)
178
179 return directory not in DIRECTORIES_TO_SKIP
180
181
183 path: str,
184 files_to_check: List[str],
185 file_extensions_to_check: List[str],
186) -> bool:
187 """
188 Check whether a file should be analyzed.
189
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.
194 """
195
196 filename = os.path.split(path)[1]
197
198 if filename in FILES_TO_SKIP:
199 return False
200
201 extension = os.path.splitext(filename)[1]
202
203 return filename in files_to_check or extension in file_extensions_to_check
204
205
207 paths: List[str],
208) -> Dict[str, List[str]]:
209 """
210 Find all files to be checked in a given list of paths.
211
212 @param paths List of paths to the files to check.
213 @return Dictionary of checks and corresponding list of files to check.
214 Example: {
215 "formatting": list_of_files_to_check_formatting,
216 ...,
217 }
218 """
219
220 # Get list of files found in the given path
221 files_found: List[str] = []
222
223 for path in paths:
224 abs_path = os.path.abspath(os.path.expanduser(path))
225
226 if os.path.isfile(abs_path):
227 files_found.append(path)
228
229 elif os.path.isdir(abs_path):
230 for dirpath, dirnames, filenames in os.walk(path, topdown=True):
231 if not should_analyze_directory(dirpath):
232 # Remove directory and its subdirectories
233 dirnames[:] = []
234 continue
235
236 files_found.extend([os.path.join(dirpath, f) for f in filenames])
237
238 else:
239 raise ValueError(f"{path} is not a valid file nor a directory")
240
241 files_found.sort()
242
243 # Check which files should be checked
244 files_to_check: Dict[str, List[str]] = {c: [] for c in CHECKS}
245
246 for f in files_found:
247 for check in CHECKS:
248 if should_analyze_file(f, FILES_TO_CHECK[check], FILE_EXTENSIONS_TO_CHECK[check]):
249 files_to_check[check].append(f)
250
251 return files_to_check
252
253
255 """
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.
258
259 @return Path to clang-format.
260 """
261
262 # Find exact version, starting from the most recent one
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}")
265
266 if clang_format_path:
267 return clang_format_path
268
269 # Find default version and check if it is supported
270 clang_format_path = shutil.which("clang-format")
271 major_version = None
272
273 if clang_format_path:
274 process = subprocess.run(
275 [clang_format_path, "--version"],
276 capture_output=True,
277 text=True,
278 check=True,
279 )
280
281 clang_format_version = process.stdout.strip()
282 version_regex = re.findall(r"\b(\d+)(\.\d+){0,2}\b", clang_format_version)
283
284 if version_regex:
285 major_version = int(version_regex[0][0])
286
287 if CLANG_FORMAT_MIN_VERSION <= major_version <= CLANG_FORMAT_MAX_VERSION:
288 return clang_format_path
289
290 # No supported version of clang-format found
291 raise RuntimeError(
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 "")
295 )
296
297
298###########################################################
299# CHECK STYLE MAIN FUNCTIONS
300###########################################################
302 paths: List[str],
303 checks_enabled: Dict[str, bool],
304 fix: bool,
305 verbose: bool,
306 n_jobs: int = 1,
307) -> bool:
308 """
309 Check / fix the coding style of a list of files.
310
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.
317 """
318
319 files_to_check = find_files_to_check_style(paths)
320 checks_successful = {c: True for c in CHECKS}
321
322 style_check_strs = {
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",
327 "tabs": "tabs",
328 "license": "GPL license text instead of SPDX license",
329 "formatting": "bad code formatting",
330 "encoding": f"bad file encoding ({FILE_ENCODING})",
331 }
332
333 check_style_file_functions_kwargs = {
334 "include_prefixes": {
335 "function": check_manually_file,
336 "kwargs": {
337 "respect_clang_format_guards": True,
338 "check_style_line_function": check_include_prefixes_line,
339 },
340 },
341 "include_quotes": {
342 "function": check_manually_file,
343 "kwargs": {
344 "respect_clang_format_guards": True,
345 "check_style_line_function": check_include_quotes_line,
346 },
347 },
348 "doxygen_tags": {
349 "function": check_manually_file,
350 "kwargs": {
351 "respect_clang_format_guards": True,
352 "check_style_line_function": check_doxygen_tags_line,
353 },
354 },
355 "whitespace": {
356 "function": check_manually_file,
357 "kwargs": {
358 "respect_clang_format_guards": False,
359 "check_style_line_function": check_whitespace_line,
360 },
361 },
362 "tabs": {
363 "function": check_manually_file,
364 "kwargs": {
365 "respect_clang_format_guards": True,
366 "check_style_line_function": check_tabs_line,
367 },
368 },
369 "license": {
370 "function": check_manually_file,
371 "kwargs": {
372 "respect_clang_format_guards": True,
373 "check_style_line_function": check_licenses_line,
374 },
375 },
376 "formatting": {
377 "function": check_formatting_file,
378 "kwargs": {}, # The formatting keywords are added below
379 },
380 "encoding": {
381 "function": check_encoding_file,
382 "kwargs": {},
383 },
384 }
385
386 if checks_enabled["formatting"]:
387 check_style_file_functions_kwargs["formatting"]["kwargs"] = {
388 "clang_format_path": find_clang_format_path(),
389 }
390
391 n_checks_enabled = sum(checks_enabled.values())
392 n_check = 0
393
394 for check in CHECKS:
395 if checks_enabled[check]:
396 checks_successful[check] = check_style_files(
397 style_check_strs[check],
398 check_style_file_functions_kwargs[check]["function"],
399 files_to_check[check],
400 fix,
401 verbose,
402 n_jobs,
403 **check_style_file_functions_kwargs[check]["kwargs"],
404 )
405
406 n_check += 1
407
408 if n_check < n_checks_enabled:
409 print("")
410
411 return all(checks_successful.values())
412
413
415 style_check_str: str,
416 check_style_file_function: Callable[..., Tuple[str, bool, List[str]]],
417 filenames: List[str],
418 fix: bool,
419 verbose: bool,
420 n_jobs: int,
421 **kwargs,
422) -> bool:
423 """
424 Check / fix style of a list of files.
425
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.
434 """
435
436 # Check files
437 non_compliant_files: List[str] = []
438 files_verbose_infos: Dict[str, List[str]] = {}
439
440 with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
441 non_compliant_files_results = executor.map(
442 check_style_file_function,
443 filenames,
444 itertools.repeat(fix),
445 itertools.repeat(verbose),
446 *[arg if isinstance(arg, list) else itertools.repeat(arg) for arg in kwargs.values()],
447 )
448
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)
452
453 if verbose:
454 files_verbose_infos[filename] = verbose_infos
455
456 # Output results
457 if not non_compliant_files:
458 print(f"- No files detected with {style_check_str}")
459 return True
460
461 else:
462 n_non_compliant_files = len(non_compliant_files)
463
464 if fix:
465 print(f"- Fixed {style_check_str} in the files ({n_non_compliant_files}):")
466 else:
467 print(f"- Detected {style_check_str} in the files ({n_non_compliant_files}):")
468
469 for f in non_compliant_files:
470 if verbose:
471 print(*[f" {l}" for l in files_verbose_infos[f]], sep="\n")
472 else:
473 print(f" - {f}")
474
475 # If all files were fixed, there are no more non-compliant files
476 return fix
477
478
479###########################################################
480# CHECK STYLE FUNCTIONS
481###########################################################
483 filename: str,
484 fix: bool,
485 verbose: bool,
486 clang_format_path: str,
487) -> Tuple[str, bool, List[str]]:
488 """
489 Check / fix the coding style of a file with clang-format.
490
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].
498 """
499
500 verbose_infos: List[str] = []
501
502 # Check if the file is well formatted
503 process = subprocess.run(
504 [
505 clang_format_path,
506 filename,
507 "-style=file",
508 "--dry-run",
509 "--Werror",
510 # Optimization: In non-verbose mode, only one error is needed to check that the file is not compliant
511 f"--ferror-limit={0 if verbose else 1}",
512 ],
513 check=False,
514 capture_output=True,
515 text=True,
516 )
517
518 is_file_compliant = process.returncode == 0
519
520 if verbose:
521 verbose_infos = process.stderr.splitlines()
522
523 # Fix file
524 if fix and not is_file_compliant:
525 process = subprocess.run(
526 [
527 clang_format_path,
528 filename,
529 "-style=file",
530 "-i",
531 ],
532 check=False,
533 stdout=subprocess.DEVNULL,
534 stderr=subprocess.DEVNULL,
535 )
536
537 return (filename, is_file_compliant, verbose_infos)
538
539
541 filename: str,
542 fix: bool,
543 verbose: bool,
544) -> Tuple[str, bool, List[str]]:
545 """
546 Check / fix the encoding of a file.
547
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].
554 """
555
556 verbose_infos: List[str] = []
557 is_file_compliant = True
558
559 with open(filename, "rb") as f:
560 file_data = f.read()
561 file_lines = file_data.decode(FILE_ENCODING, errors="replace").splitlines(keepends=True)
562
563 # Check if file has correct encoding
564 try:
565 file_data.decode(FILE_ENCODING)
566
567 except UnicodeDecodeError as e:
568 is_file_compliant = False
569
570 if verbose:
571 # Find line and column with bad encoding
572 bad_char_start_index = e.start
573 n_chars_file_read = 0
574
575 for line_number, line in enumerate(file_lines):
576 n_chars_line = len(line)
577
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
580
581 verbose_infos.extend(
582 [
583 f"{filename}:{line_number + 1}:{bad_char_column + 1}: error: bad {FILE_ENCODING} encoding",
584 f" {line.rstrip()}",
585 f" {'':>{bad_char_column}}^",
586 ]
587 )
588
589 break
590
591 n_chars_file_read += n_chars_line
592
593 # Fix file encoding
594 if fix and not is_file_compliant:
595 with open(filename, "w", encoding=FILE_ENCODING) as f:
596 f.writelines(file_lines)
597
598 return (filename, is_file_compliant, verbose_infos)
599
600
602 filename: str,
603 fix: bool,
604 verbose: bool,
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]]:
608 """
609 Check / fix a file manually using a function to check / fix each line.
610
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].
619 """
620
621 is_file_compliant = True
622 verbose_infos: List[str] = []
623 clang_format_enabled = True
624
625 with open(filename, "r", encoding=FILE_ENCODING) as f:
626 file_lines = f.readlines()
627
628 for i, line in enumerate(file_lines):
629 # Check clang-format guards
630 if respect_clang_format_guards:
631 line_stripped = line.strip()
632
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
637
638 if not clang_format_enabled and line_stripped not in (
639 FORMAT_GUARD_ON + FORMAT_GUARD_OFF
640 ):
641 continue
642
643 # Check if the line is compliant with the style and fix it
644 (is_line_compliant, line_fixed, line_verbose_infos) = check_style_line_function(
645 line, filename, i
646 )
647
648 if not is_line_compliant:
649 is_file_compliant = False
650 file_lines[i] = line_fixed
651 verbose_infos.extend(line_verbose_infos)
652
653 # Optimization: If running in non-verbose check mode, only one error is needed to check that the file is not compliant
654 if not fix and not verbose:
655 break
656
657 # Update file with the fixed lines
658 if fix and not is_file_compliant:
659 with open(filename, "w", encoding=FILE_ENCODING) as f:
660 f.writelines(file_lines)
661
662 return (filename, is_file_compliant, verbose_infos)
663
664
666 line: str,
667 filename: str,
668 line_number: int,
669) -> Tuple[bool, str, List[str]]:
670 """
671 Check / fix #include headers from the same module with the "ns3/" prefix in a line.
672
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),
677 Fixed line,
678 Verbose information].
679 """
680
681 is_line_compliant = True
682 line_fixed = line
683 verbose_infos: List[str] = []
684
685 # Check if the line is an #include and extract its header file
686 line_stripped = line.strip()
687 header_file = re.findall(r'^#include ["<]ns3/(.*\.h)[">]', line_stripped)
688
689 if header_file:
690 # Check if the header file belongs to the same module and remove the "ns3/" prefix
691 header_file = header_file[0]
692 parent_path = os.path.split(filename)[0]
693
694 if os.path.exists(os.path.join(parent_path, header_file)):
695 is_line_compliant = False
696 line_fixed = (
697 line_stripped.replace(f"ns3/{header_file}", header_file)
698 .replace("<", '"')
699 .replace(">", '"')
700 + "\n"
701 )
702
703 header_index = len('#include "')
704
705 verbose_infos.extend(
706 [
707 f'{filename}:{line_number + 1}:{header_index + 1}: error: #include headers from the same module with the "ns3/" prefix detected',
708 f" {line_stripped}",
709 f" {'':>{header_index}}^",
710 ]
711 )
712
713 return (is_line_compliant, line_fixed, verbose_infos)
714
715
717 line: str,
718 filename: str,
719 line_number: int,
720) -> Tuple[bool, str, List[str]]:
721 """
722 Check / fix ns-3 #include headers using angle brackets <> rather than quotes "" in a line.
723
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),
728 Fixed line,
729 Verbose information].
730 """
731
732 is_line_compliant = True
733 line_fixed = line
734 verbose_infos: List[str] = []
735
736 # Check if the line is an #include <ns3/...>
737 header_file = re.findall(r"^#include <ns3/.*\.h>", line)
738
739 if header_file:
740 is_line_compliant = False
741 line_fixed = line.replace("<", '"').replace(">", '"')
742
743 header_index = len("#include ")
744
745 verbose_infos = [
746 f"{filename}:{line_number + 1}:{header_index + 1}: error: ns-3 #include headers with angle brackets detected",
747 f" {line}",
748 f' {"":{header_index}}^',
749 ]
750
751 return (is_line_compliant, line_fixed, verbose_infos)
752
753
755 line: str,
756 filename: str,
757 line_number: int,
758) -> Tuple[bool, str, List[str]]:
759 """
760 Check / fix Doxygen tags using \\ rather than @ in a line.
761
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),
766 Fixed line,
767 Verbose information].
768 """
769
770 IGNORED_WORDS = [
771 "\\dots",
772 "\\langle",
773 "\\quad",
774 ]
775
776 is_line_compliant = True
777 line_fixed = line
778 verbose_infos: List[str] = []
779
780 # Match Doxygen tags at the start of the line (e.g., "* \param arg Description")
781 line_stripped = line.rstrip()
782 regex_findings = re.findall(r"^\s*(?:\*|\/\*\*|\/\/\/)\s*(\\\w{3,})(?=(?:\s|$))", line_stripped)
783
784 if regex_findings:
785 doxygen_tag = regex_findings[0]
786
787 if doxygen_tag not in IGNORED_WORDS:
788 is_line_compliant = False
789
790 doxygen_tag_index = line_fixed.find(doxygen_tag)
791 line_fixed = line.replace(doxygen_tag, f"@{doxygen_tag[1:]}")
792
793 verbose_infos.extend(
794 [
795 f"{filename}:{line_number + 1}:{doxygen_tag_index + 1}: error: detected Doxygen tags using \\ rather than @",
796 f" {line_stripped}",
797 f' {"":{doxygen_tag_index}}^',
798 ]
799 )
800
801 return (is_line_compliant, line_fixed, verbose_infos)
802
803
805 line: str,
806 filename: str,
807 line_number: int,
808) -> Tuple[bool, str, List[str]]:
809 """
810 Check / fix whitespace in a line.
811
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),
816 Fixed line,
817 Verbose information].
818 """
819
820 is_line_compliant = True
821 line_fixed = line.rstrip() + "\n"
822 verbose_infos: List[str] = []
823
824 if line_fixed != line:
825 is_line_compliant = False
826 line_fixed_stripped_expanded = line_fixed.rstrip().expandtabs(TAB_SIZE)
827
828 verbose_infos = [
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)}}^",
832 ]
833
834 return (is_line_compliant, line_fixed, verbose_infos)
835
836
838 line: str,
839 filename: str,
840 line_number: int,
841) -> Tuple[bool, str, List[str]]:
842 """
843 Check / fix tabs in a line.
844
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),
849 Fixed line,
850 Verbose information].
851 """
852
853 is_line_compliant = True
854 line_fixed = line
855 verbose_infos: List[str] = []
856
857 tab_index = line.find("\t")
858
859 if tab_index != -1:
860 is_line_compliant = False
861 line_fixed = line.expandtabs(TAB_SIZE)
862
863 verbose_infos = [
864 f"{filename}:{line_number + 1}:{tab_index + 1}: error: Tab detected",
865 f" {line.rstrip()}",
866 f" {'':>{tab_index}}^",
867 ]
868
869 return (is_line_compliant, line_fixed, verbose_infos)
870
871
873 line: str,
874 filename: str,
875 line_number: int,
876) -> Tuple[bool, str, List[str]]:
877 """
878 Check / fix SPDX licenses rather than GPL text in a line.
879
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),
884 Fixed line,
885 Verbose information].
886 """
887
888 # fmt: off
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",
900 ]
901 # fmt: on
902
903 SPDX_LICENSE = "SPDX-License-Identifier: GPL-2.0-only"
904
905 is_line_compliant = True
906 line_fixed = line
907 verbose_infos: List[str] = []
908
909 # Check if the line is a GPL license text
910 line_stripped = line.strip()
911 line_stripped_no_leading_comments = line_stripped.strip("*#/").strip()
912
913 if line_stripped_no_leading_comments in GPL_LICENSE_LINES:
914 is_line_compliant = False
915 col_index = 0
916
917 # Replace GPL text with SPDX license.
918 # Replace the first line of the GPL text with SPDX.
919 # Delete the remaining GPL text lines.
920 if line_stripped_no_leading_comments == GPL_LICENSE_LINES[0]:
921 line_fixed = line.replace(line_stripped_no_leading_comments, SPDX_LICENSE)
922 else:
923 line_fixed = ""
924
925 verbose_infos.extend(
926 [
927 f"{filename}:{line_number + 1}:{col_index}: error: GPL license text detected instead of SPDX license",
928 f" {line_stripped}",
929 f" {'':>{col_index}}^",
930 ]
931 )
932
933 return (is_line_compliant, line_fixed, verbose_infos)
934
935
936###########################################################
937# MAIN
938###########################################################
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.'
949 )
950
951 parser.add_argument(
952 "paths",
953 action="store",
954 type=str,
955 nargs="+",
956 help="List of paths to the files to check",
957 )
958
959 parser.add_argument(
960 "--no-include-prefixes",
961 action="store_true",
962 help='Do not check / fix #include headers from the same module with the "ns3/" prefix (respects clang-format guards)',
963 )
964
965 parser.add_argument(
966 "--no-include-quotes",
967 action="store_true",
968 help='Do not check / fix ns-3 #include headers using angle brackets <> rather than quotes "" (respects clang-format guards)',
969 )
970
971 parser.add_argument(
972 "--no-doxygen-tags",
973 action="store_true",
974 help="Do not check / fix Doxygen tags using @ rather than \\ (respects clang-format guards)",
975 )
976
977 parser.add_argument(
978 "--no-whitespace",
979 action="store_true",
980 help="Do not check / fix trailing whitespace",
981 )
982
983 parser.add_argument(
984 "--no-tabs",
985 action="store_true",
986 help="Do not check / fix tabs (respects clang-format guards)",
987 )
988
989 parser.add_argument(
990 "--no-licenses",
991 action="store_true",
992 help="Do not check / fix SPDX licenses rather than GPL text (respects clang-format guards)",
993 )
994
995 parser.add_argument(
996 "--no-formatting",
997 action="store_true",
998 help="Do not check / fix code formatting (respects clang-format guards)",
999 )
1000
1001 parser.add_argument(
1002 "--no-encoding",
1003 action="store_true",
1004 help=f"Do not check / fix file encoding ({FILE_ENCODING})",
1005 )
1006
1007 parser.add_argument(
1008 "--fix",
1009 action="store_true",
1010 help="Fix coding style issues detected in the files",
1011 )
1012
1013 parser.add_argument(
1014 "-v",
1015 "--verbose",
1016 action="store_true",
1017 help="Show the lines that are not well-formatted",
1018 )
1019
1020 parser.add_argument(
1021 "-j",
1022 "--jobs",
1023 type=int,
1024 default=max(1, os.cpu_count() - 1),
1025 help="Number of parallel jobs",
1026 )
1027
1028 args = parser.parse_args()
1029
1030 try:
1031 all_checks_successful = check_style_clang_format(
1032 paths=args.paths,
1033 checks_enabled={
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,
1042 },
1043 fix=args.fix,
1044 verbose=args.verbose,
1045 n_jobs=args.jobs,
1046 )
1047
1048 except Exception as ex:
1049 print("ERROR:", ex)
1050 sys.exit(1)
1051
1052 if not all_checks_successful:
1053 if args.verbose:
1054 print(
1055 "",
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",
1061 sep="\n",
1062 )
1063
1064 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)
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)