A Discrete-Event Network Simulator
API
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 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License version 2 as
7 # published by the Free Software Foundation;
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 #
18 # Author: Eduardo Nuno Almeida <enmsa@outlook.pt> [INESC TEC and FEUP, Portugal]
19 
20 """
21 Check and apply the ns-3 coding style recursively to all files in the PATH arguments.
22 
23 The coding style is defined with the clang-format tool, whose definitions are in
24 the ".clang-format" file. This script performs the following checks / fixes:
25 - Check / fix local #include headers with "ns3/" prefix. Respects clang-format guards.
26 - Check / apply clang-format. Respects clang-format guards.
27 - Check / trim trailing whitespace. Always checked.
28 - Check / replace tabs with spaces. Respects clang-format guards.
29 
30 This script can be applied to all text files in a given path or to individual files.
31 
32 NOTE: The formatting check requires clang-format (version >= 14) to be found on the path.
33 The remaining checks do not depend on clang-format and can be executed by disabling clang-format
34 checking with the "--no-formatting" option.
35 """
36 
37 import argparse
38 import concurrent.futures
39 import itertools
40 import os
41 import re
42 import shutil
43 import subprocess
44 import sys
45 from typing import Callable, Dict, List, Tuple
46 
47 
50 CLANG_FORMAT_VERSIONS = [
51  17,
52  16,
53  15,
54  14,
55 ]
56 
57 CLANG_FORMAT_GUARD_ON = "// clang-format on"
58 CLANG_FORMAT_GUARD_OFF = "// clang-format off"
59 
60 DIRECTORIES_TO_SKIP = [
61  "__pycache__",
62  ".git",
63  "bindings",
64  "build",
65  "cmake-cache",
66  "testpy-output",
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
71 FILES_TO_SKIP = [
72  "valgrind.h",
73 ]
74 
75 FILE_EXTENSIONS_TO_CHECK_FORMATTING = [
76  ".c",
77  ".cc",
78  ".h",
79 ]
80 
81 FILE_EXTENSIONS_TO_CHECK_INCLUDE_PREFIXES = FILE_EXTENSIONS_TO_CHECK_FORMATTING
82 
83 FILE_EXTENSIONS_TO_CHECK_WHITESPACE = [
84  ".c",
85  ".cc",
86  ".click",
87  ".cmake",
88  ".conf",
89  ".css",
90  ".dot",
91  ".gnuplot",
92  ".gp",
93  ".h",
94  ".html",
95  ".js",
96  ".json",
97  ".m",
98  ".md",
99  ".mob",
100  ".ns_params",
101  ".ns_movements",
102  ".params",
103  ".pl",
104  ".plt",
105  ".py",
106  ".rst",
107  ".seqdiag",
108  ".sh",
109  ".txt",
110  ".yml",
111 ]
112 
113 FILES_TO_CHECK_WHITESPACE = [
114  "Makefile",
115  "ns3",
116 ]
117 
118 FILE_EXTENSIONS_TO_CHECK_TABS = [
119  ".c",
120  ".cc",
121  ".h",
122  ".md",
123  ".py",
124  ".rst",
125  ".sh",
126  ".yml",
127 ]
128 TAB_SIZE = 4
129 
130 
131 
134 def should_analyze_directory(dirpath: str) -> bool:
135  """
136  Check whether a directory should be analyzed.
137 
138  @param dirpath Directory path.
139  @return Whether the directory should be analyzed.
140  """
141 
142  _, directory = os.path.split(dirpath)
143 
144  return not (
145  directory in DIRECTORIES_TO_SKIP or (directory.startswith(".") and directory != ".")
146  )
147 
148 
150  path: str,
151  files_to_check: List[str],
152  file_extensions_to_check: List[str],
153 ) -> bool:
154  """
155  Check whether a file should be analyzed.
156 
157  @param path Path to the file.
158  @param files_to_check List of files that shall be checked.
159  @param file_extensions_to_check List of file extensions that shall be checked.
160  @return Whether the file should be analyzed.
161  """
162 
163  filename = os.path.split(path)[1]
164 
165  if filename in FILES_TO_SKIP:
166  return False
167 
168  basename, extension = os.path.splitext(filename)
169 
170  return basename in files_to_check or extension in file_extensions_to_check
171 
172 
174  paths: List[str],
175 ) -> Tuple[List[str], List[str], List[str], List[str]]:
176  """
177  Find all files to be checked in a given list of paths.
178 
179  @param paths List of paths to the files to check.
180  @return Tuple [List of files to check include prefixes,
181  List of files to check formatting,
182  List of files to check trailing whitespace,
183  List of files to check tabs].
184  """
185 
186  files_to_check: List[str] = []
187 
188  for path in paths:
189  abs_path = os.path.abspath(os.path.expanduser(path))
190 
191  if os.path.isfile(abs_path):
192  files_to_check.append(path)
193 
194  elif os.path.isdir(abs_path):
195  for dirpath, dirnames, filenames in os.walk(path, topdown=True):
196  if not should_analyze_directory(dirpath):
197  # Remove directory and its subdirectories
198  dirnames[:] = []
199  continue
200 
201  files_to_check.extend([os.path.join(dirpath, f) for f in filenames])
202 
203  else:
204  raise ValueError(f"Error: {path} is not a file nor a directory")
205 
206  files_to_check.sort()
207 
208  files_to_check_include_prefixes: List[str] = []
209  files_to_check_formatting: List[str] = []
210  files_to_check_whitespace: List[str] = []
211  files_to_check_tabs: List[str] = []
212 
213  for f in files_to_check:
214  if should_analyze_file(f, [], FILE_EXTENSIONS_TO_CHECK_INCLUDE_PREFIXES):
215  files_to_check_include_prefixes.append(f)
216 
217  if should_analyze_file(f, [], FILE_EXTENSIONS_TO_CHECK_FORMATTING):
218  files_to_check_formatting.append(f)
219 
220  if should_analyze_file(f, FILES_TO_CHECK_WHITESPACE, FILE_EXTENSIONS_TO_CHECK_WHITESPACE):
221  files_to_check_whitespace.append(f)
222 
223  if should_analyze_file(f, [], FILE_EXTENSIONS_TO_CHECK_TABS):
224  files_to_check_tabs.append(f)
225 
226  return (
227  files_to_check_include_prefixes,
228  files_to_check_formatting,
229  files_to_check_whitespace,
230  files_to_check_tabs,
231  )
232 
233 
235  """
236  Find the path to one of the supported versions of clang-format.
237  If no supported version of clang-format is found, raise an exception.
238 
239  @return Path to clang-format.
240  """
241 
242  # Find exact version
243  for version in CLANG_FORMAT_VERSIONS:
244  clang_format_path = shutil.which(f"clang-format-{version}")
245 
246  if clang_format_path:
247  return clang_format_path
248 
249  # Find default version and check if it is supported
250  clang_format_path = shutil.which("clang-format")
251 
252  if clang_format_path:
253  process = subprocess.run(
254  [clang_format_path, "--version"],
255  capture_output=True,
256  text=True,
257  check=True,
258  )
259 
260  version = process.stdout.strip().split(" ")[-1]
261  major_version = int(version.split(".")[0])
262 
263  if major_version in CLANG_FORMAT_VERSIONS:
264  return clang_format_path
265 
266  # No supported version of clang-format found
267  raise RuntimeError(
268  f"Could not find any supported version of clang-format installed on this system. "
269  f"List of supported versions: {CLANG_FORMAT_VERSIONS}."
270  )
271 
272 
273 
277  paths: List[str],
278  enable_check_include_prefixes: bool,
279  enable_check_formatting: bool,
280  enable_check_whitespace: bool,
281  enable_check_tabs: bool,
282  fix: bool,
283  verbose: bool,
284  n_jobs: int = 1,
285 ) -> bool:
286  """
287  Check / fix the coding style of a list of files.
288 
289  @param paths List of paths to the files to check.
290  @param enable_check_include_prefixes Whether to enable checking #include headers from the same module with the "ns3/" prefix.
291  @param enable_check_formatting Whether to enable checking code formatting.
292  @param enable_check_whitespace Whether to enable checking trailing whitespace.
293  @param enable_check_tabs Whether to enable checking tabs.
294  @param fix Whether to fix (True) or just check (False) the file.
295  @param verbose Show the lines that are not compliant with the style.
296  @param n_jobs Number of parallel jobs.
297  @return Whether all files are compliant with all enabled style checks.
298  """
299 
300  (
301  files_to_check_include_prefixes,
302  files_to_check_formatting,
303  files_to_check_whitespace,
304  files_to_check_tabs,
305  ) = find_files_to_check_style(paths)
306 
307  check_include_prefixes_successful = True
308  check_formatting_successful = True
309  check_whitespace_successful = True
310  check_tabs_successful = True
311 
312  if enable_check_include_prefixes:
313  check_include_prefixes_successful = check_style_files(
314  '#include headers from the same module with the "ns3/" prefix',
315  check_manually_file,
316  files_to_check_include_prefixes,
317  fix,
318  verbose,
319  n_jobs,
320  respect_clang_format_guards=True,
321  check_style_line_function=check_include_prefixes_line,
322  )
323 
324  print("")
325 
326  if enable_check_formatting:
327  check_formatting_successful = check_style_files(
328  "bad code formatting",
329  check_formatting_file,
330  files_to_check_formatting,
331  fix,
332  verbose,
333  n_jobs,
334  clang_format_path=find_clang_format_path(),
335  )
336 
337  print("")
338 
339  if enable_check_whitespace:
340  check_whitespace_successful = check_style_files(
341  "trailing whitespace",
342  check_manually_file,
343  files_to_check_whitespace,
344  fix,
345  verbose,
346  n_jobs,
347  respect_clang_format_guards=False,
348  check_style_line_function=check_whitespace_line,
349  )
350 
351  print("")
352 
353  if enable_check_tabs:
354  check_tabs_successful = check_style_files(
355  "tabs",
356  check_manually_file,
357  files_to_check_tabs,
358  fix,
359  verbose,
360  n_jobs,
361  respect_clang_format_guards=True,
362  check_style_line_function=check_tabs_line,
363  )
364 
365  return all(
366  [
367  check_include_prefixes_successful,
368  check_formatting_successful,
369  check_whitespace_successful,
370  check_tabs_successful,
371  ]
372  )
373 
374 
376  style_check_str: str,
377  check_style_file_function: Callable[..., Tuple[str, bool, List[str]]],
378  filenames: List[str],
379  fix: bool,
380  verbose: bool,
381  n_jobs: int,
382  **kwargs,
383 ) -> bool:
384  """
385  Check / fix style of a list of files.
386 
387  @param style_check_str Description of the check to be performed.
388  @param check_style_file_function Function used to check the file.
389  @param filename Name of the file to be checked.
390  @param fix Whether to fix (True) or just check (False) the file (True).
391  @param verbose Show the lines that are not compliant with the style.
392  @param n_jobs Number of parallel jobs.
393  @param kwargs Additional keyword arguments to the check_style_file_function.
394  @return Whether all files are compliant with the style.
395  """
396 
397  # Check files
398  non_compliant_files: List[str] = []
399  files_verbose_infos: Dict[str, List[str]] = {}
400 
401  with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
402  non_compliant_files_results = executor.map(
403  check_style_file_function,
404  filenames,
405  itertools.repeat(fix),
406  itertools.repeat(verbose),
407  *[arg if isinstance(arg, list) else itertools.repeat(arg) for arg in kwargs.values()],
408  )
409 
410  for filename, is_file_compliant, verbose_infos in non_compliant_files_results:
411  if not is_file_compliant:
412  non_compliant_files.append(filename)
413 
414  if verbose:
415  files_verbose_infos[filename] = verbose_infos
416 
417  # Output results
418  if not non_compliant_files:
419  print(f"- No files detected with {style_check_str}")
420  return True
421 
422  else:
423  n_non_compliant_files = len(non_compliant_files)
424 
425  if fix:
426  print(f"- Fixed {style_check_str} in the files ({n_non_compliant_files}):")
427  else:
428  print(f"- Detected {style_check_str} in the files ({n_non_compliant_files}):")
429 
430  for f in non_compliant_files:
431  if verbose:
432  print(*[f" {l}" for l in files_verbose_infos[f]], sep="\n")
433  else:
434  print(f" - {f}")
435 
436  # If all files were fixed, there are no more non-compliant files
437  return fix
438 
439 
440 
444  filename: str,
445  fix: bool,
446  verbose: bool,
447  clang_format_path: str,
448 ) -> Tuple[str, bool, List[str]]:
449  """
450  Check / fix the coding style of a file with clang-format.
451 
452  @param filename Name of the file to be checked.
453  @param fix Whether to fix (True) or just check (False) the style of the file (True).
454  @param verbose Show the lines that are not compliant with the style.
455  @param clang_format_path Path to clang-format.
456  @return Tuple [Filename,
457  Whether the file is compliant with the style (before the check),
458  Verbose information].
459  """
460 
461  verbose_infos: List[str] = []
462 
463  # Check if the file is well formatted
464  process = subprocess.run(
465  [
466  clang_format_path,
467  filename,
468  "-style=file",
469  "--dry-run",
470  "--Werror",
471  # Optimization: In non-verbose mode, only one error is needed to check that the file is not compliant
472  f"--ferror-limit={0 if verbose else 1}",
473  ],
474  check=False,
475  capture_output=True,
476  text=True,
477  )
478 
479  is_file_compliant = process.returncode == 0
480 
481  if verbose:
482  verbose_infos = process.stderr.splitlines()
483 
484  # Fix file
485  if fix and not is_file_compliant:
486  process = subprocess.run(
487  [
488  clang_format_path,
489  filename,
490  "-style=file",
491  "-i",
492  ],
493  check=False,
494  stdout=subprocess.DEVNULL,
495  stderr=subprocess.DEVNULL,
496  )
497 
498  return (filename, is_file_compliant, verbose_infos)
499 
500 
502  filename: str,
503  fix: bool,
504  verbose: bool,
505  respect_clang_format_guards: bool,
506  check_style_line_function: Callable[[str, str, int], Tuple[bool, str, List[str]]],
507 ) -> Tuple[str, bool, List[str]]:
508  """
509  Check / fix a file manually using a function to check / fix each line.
510 
511  @param filename Name of the file to be checked.
512  @param fix Whether to fix (True) or just check (False) the style of the file (True).
513  @param verbose Show the lines that are not compliant with the style.
514  @param respect_clang_format_guards Whether to respect clang-format guards.
515  @param check_style_line_function Function used to check each line.
516  @return Tuple [Filename,
517  Whether the file is compliant with the style (before the check),
518  Verbose information].
519  """
520 
521  is_file_compliant = True
522  verbose_infos: List[str] = []
523  clang_format_enabled = True
524 
525  with open(filename, "r", encoding="utf-8") as f:
526  file_lines = f.readlines()
527 
528  for i, line in enumerate(file_lines):
529  # Check clang-format guards
530  if respect_clang_format_guards:
531  line_stripped = line.strip()
532 
533  if line_stripped == CLANG_FORMAT_GUARD_ON:
534  clang_format_enabled = True
535  elif line_stripped == CLANG_FORMAT_GUARD_OFF:
536  clang_format_enabled = False
537 
538  if not clang_format_enabled and line_stripped not in (
539  CLANG_FORMAT_GUARD_ON,
540  CLANG_FORMAT_GUARD_OFF,
541  ):
542  continue
543 
544  # Check if the line is compliant with the style and fix it
545  (is_line_compliant, line_fixed, line_verbose_infos) = check_style_line_function(
546  line, filename, i
547  )
548 
549  if not is_line_compliant:
550  is_file_compliant = False
551  file_lines[i] = line_fixed
552  verbose_infos.extend(line_verbose_infos)
553 
554  # Optimization: If running in non-verbose check mode, only one error is needed to check that the file is not compliant
555  if not fix and not verbose:
556  break
557 
558  # Update file with the fixed lines
559  if fix and not is_file_compliant:
560  with open(filename, "w", encoding="utf-8") as f:
561  f.writelines(file_lines)
562 
563  return (filename, is_file_compliant, verbose_infos)
564 
565 
567  line: str,
568  filename: str,
569  line_number: int,
570 ) -> Tuple[bool, str, List[str]]:
571  """
572  Check / fix #include headers from the same module with the "ns3/" prefix in a line.
573 
574  @param line The line to check.
575  @param filename Name of the file to be checked.
576  @param line_number The number of the line checked.
577  @return Tuple [Whether the line is compliant with the style (before the check),
578  Fixed line,
579  Verbose information].
580  """
581 
582  is_line_compliant = True
583  line_fixed = line
584  verbose_infos: List[str] = []
585 
586  # Check if the line is an #include and extract its header file
587  line_stripped = line.strip()
588  header_file = re.findall(r'^#include ["<]ns3/(.*\.h)[">]', line_stripped)
589 
590  if header_file:
591  # Check if the header file belongs to the same module and remove the "ns3/" prefix
592  header_file = header_file[0]
593  parent_path = os.path.split(filename)[0]
594 
595  if os.path.exists(os.path.join(parent_path, header_file)):
596  is_line_compliant = False
597  line_fixed = (
598  line_stripped.replace(f"ns3/{header_file}", header_file)
599  .replace("<", '"')
600  .replace(">", '"')
601  + "\n"
602  )
603 
604  header_index = len('#include "')
605 
606  verbose_infos.extend(
607  [
608  f'{filename}:{line_number + 1}:{header_index + 1}: error: #include headers from the same module with the "ns3/" prefix detected',
609  f" {line_stripped}",
610  f' {"":{header_index}}^',
611  ]
612  )
613 
614  return (is_line_compliant, line_fixed, verbose_infos)
615 
616 
618  line: str,
619  filename: str,
620  line_number: int,
621 ) -> Tuple[bool, str, List[str]]:
622  """
623  Check / fix whitespace in a line.
624 
625  @param line The line to check.
626  @param filename Name of the file to be checked.
627  @param line_number The number of the line checked.
628  @return Tuple [Whether the line is compliant with the style (before the check),
629  Fixed line,
630  Verbose information].
631  """
632 
633  is_line_compliant = True
634  line_fixed = line.rstrip() + "\n"
635  verbose_infos: List[str] = []
636 
637  if line_fixed != line:
638  is_line_compliant = False
639  line_fixed_stripped_expanded = line_fixed.rstrip().expandtabs(TAB_SIZE)
640 
641  verbose_infos = [
642  f"{filename}:{line_number + 1}:{len(line_fixed_stripped_expanded) + 1}: error: Trailing whitespace detected",
643  f" {line_fixed_stripped_expanded}",
644  f' {"":{len(line_fixed_stripped_expanded)}}^',
645  ]
646 
647  return (is_line_compliant, line_fixed, verbose_infos)
648 
649 
651  line: str,
652  filename: str,
653  line_number: int,
654 ) -> Tuple[bool, str, List[str]]:
655  """
656  Check / fix tabs in a line.
657 
658  @param line The line to check.
659  @param filename Name of the file to be checked.
660  @param line_number The number of the line checked.
661  @return Tuple [Whether the line is compliant with the style (before the check),
662  Fixed line,
663  Verbose information].
664  """
665 
666  is_line_compliant = True
667  line_fixed = line
668  verbose_infos: List[str] = []
669 
670  tab_index = line.find("\t")
671 
672  if tab_index != -1:
673  is_line_compliant = False
674  line_fixed = line.expandtabs(TAB_SIZE)
675 
676  verbose_infos = [
677  f"{filename}:{line_number + 1}:{tab_index + 1}: error: Tab detected",
678  f" {line.rstrip()}",
679  f' {"":{tab_index}}^',
680  ]
681 
682  return (is_line_compliant, line_fixed, verbose_infos)
683 
684 
685 
688 if __name__ == "__main__":
689  parser = argparse.ArgumentParser(
690  description="Check and apply the ns-3 coding style recursively to all files in the given PATHs. "
691  "The script checks the formatting of the file with clang-format. "
692  'Additionally, it checks #include headers from the same module with the "ns3/" prefix, '
693  "the presence of trailing whitespace and tabs. "
694  'Formatting, local #include "ns3/" prefixes and tabs checks respect clang-format guards. '
695  'When used in "check mode" (default), the script checks if all files are well '
696  "formatted and do not have trailing whitespace nor tabs. "
697  "If it detects non-formatted files, they will be printed and this process exits with a "
698  'non-zero code. When used in "fix mode", this script automatically fixes the files.'
699  )
700 
701  parser.add_argument(
702  "paths",
703  action="store",
704  type=str,
705  nargs="+",
706  help="List of paths to the files to check",
707  )
708 
709  parser.add_argument(
710  "--no-include-prefixes",
711  action="store_true",
712  help='Do not check / fix #include headers from the same module with the "ns3/" prefix',
713  )
714 
715  parser.add_argument(
716  "--no-formatting",
717  action="store_true",
718  help="Do not check / fix code formatting",
719  )
720 
721  parser.add_argument(
722  "--no-whitespace",
723  action="store_true",
724  help="Do not check / fix trailing whitespace",
725  )
726 
727  parser.add_argument(
728  "--no-tabs",
729  action="store_true",
730  help="Do not check / fix tabs",
731  )
732 
733  parser.add_argument(
734  "--fix",
735  action="store_true",
736  help="Fix coding style issues detected in the files",
737  )
738 
739  parser.add_argument(
740  "-v",
741  "--verbose",
742  action="store_true",
743  help="Show the lines that are not well-formatted",
744  )
745 
746  parser.add_argument(
747  "-j",
748  "--jobs",
749  type=int,
750  default=max(1, os.cpu_count() - 1),
751  help="Number of parallel jobs",
752  )
753 
754  args = parser.parse_args()
755 
756  try:
757  all_checks_successful = check_style_clang_format(
758  paths=args.paths,
759  enable_check_include_prefixes=(not args.no_include_prefixes),
760  enable_check_formatting=(not args.no_formatting),
761  enable_check_whitespace=(not args.no_whitespace),
762  enable_check_tabs=(not args.no_tabs),
763  fix=args.fix,
764  verbose=args.verbose,
765  n_jobs=args.jobs,
766  )
767 
768  except Exception as e:
769  print(e)
770  sys.exit(1)
771 
772  if all_checks_successful:
773  sys.exit(0)
774  else:
775  sys.exit(1)
#define max(a, b)
Definition: 80211b.c:42
Tuple[List[str], List[str], List[str], List[str]] find_files_to_check_style(List[str] paths)
Tuple[bool, str, List[str]] check_whitespace_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.
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.
bool check_style_clang_format(List[str] paths, bool enable_check_include_prefixes, bool enable_check_formatting, bool enable_check_whitespace, bool enable_check_tabs, bool fix, bool verbose, int n_jobs=1)
CHECK STYLE MAIN FUNCTIONS.