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