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 to all files in the PATH argument.
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 / apply clang-format.
26 - Check / trim trailing whitespace.
27 - Check / replace tabs with spaces.
28 
29 The clang-format and tabs checks respect clang-format guards, which mark code blocks
30 that should not be checked. Trailing whitespace is always checked regardless of
31 clang-format guards.
32 
33 This script can be applied to all text files in a given path or to individual files.
34 
35 NOTE: The formatting check requires clang-format (version >= 14) to be found on the path.
36 Trimming of trailing whitespace and conversion of tabs to spaces (via the "--no-formatting"
37 option) do not depend on clang-format.
38 """
39 
40 import argparse
41 import concurrent.futures
42 import itertools
43 import os
44 import shutil
45 import subprocess
46 import sys
47 from typing import List, Tuple
48 
49 
50 
53 CLANG_FORMAT_VERSIONS = [
54  16,
55  15,
56  14,
57 ]
58 
59 CLANG_FORMAT_GUARD_ON = '// clang-format on'
60 CLANG_FORMAT_GUARD_OFF = '// clang-format off'
61 
62 DIRECTORIES_TO_SKIP = [
63  '__pycache__',
64  '.vscode',
65  'bindings',
66  'build',
67  'cmake-cache',
68  'testpy-output',
69 ]
70 
71 # List of files entirely copied from elsewhere that should not be checked,
72 # in order to optimize the performance of this script
73 FILES_TO_SKIP = [
74  'valgrind.h',
75 ]
76 
77 FILE_EXTENSIONS_TO_CHECK_FORMATTING = [
78  '.c',
79  '.cc',
80  '.h',
81 ]
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 skip_directory(dirpath: str) -> bool:
135  """
136  Check if a directory should be skipped.
137 
138  @param dirpath Directory path.
139  @return Whether the directory should be skipped or not.
140  """
141 
142  _, directory = os.path.split(dirpath)
143 
144  return (directory in DIRECTORIES_TO_SKIP or
145  (directory.startswith('.') and directory != '.'))
146 
147 
148 def skip_file_formatting(path: str) -> bool:
149  """
150  Check if a file should be skipped from formatting analysis.
151 
152  @param path Path to the file.
153  @return Whether the file should be skipped or not.
154  """
155 
156  filename = os.path.split(path)[1]
157 
158  if filename in FILES_TO_SKIP:
159  return True
160 
161  _, extension = os.path.splitext(filename)
162 
163  return extension not in FILE_EXTENSIONS_TO_CHECK_FORMATTING
164 
165 
166 def skip_file_whitespace(path: str) -> bool:
167  """
168  Check if a file should be skipped from trailing whitespace analysis.
169 
170  @param path Path to the file.
171  @return Whether the file should be skipped or not.
172  """
173 
174  filename = os.path.split(path)[1]
175 
176  if filename in FILES_TO_SKIP:
177  return True
178 
179  basename, extension = os.path.splitext(filename)
180 
181  return (basename not in FILES_TO_CHECK_WHITESPACE and
182  extension not in FILE_EXTENSIONS_TO_CHECK_WHITESPACE)
183 
184 
185 def skip_file_tabs(path: str) -> bool:
186  """
187  Check if a file should be skipped from tabs analysis.
188 
189  @param path Path to the file.
190  @return Whether the file should be skipped or not.
191  """
192 
193  filename = os.path.split(path)[1]
194 
195  if filename in FILES_TO_SKIP:
196  return True
197 
198  _, extension = os.path.splitext(filename)
199 
200  return extension not in FILE_EXTENSIONS_TO_CHECK_TABS
201 
202 
203 def find_files_to_check_style(path: str) -> Tuple[List[str], List[str], List[str]]:
204  """
205  Find all files to be checked in a given path.
206 
207  @param path Path to check.
208  @return Tuple [List of files to check formatting,
209  List of files to check trailing whitespace,
210  List of files to check tabs].
211  """
212 
213  files_to_check_formatting: List[str] = []
214  files_to_check_whitespace: List[str] = []
215  files_to_check_tabs: List[str] = []
216 
217  abs_path = os.path.normpath(os.path.abspath(os.path.expanduser(path)))
218 
219  if os.path.isfile(abs_path):
220  if not skip_file_formatting(path):
221  files_to_check_formatting.append(path)
222 
223  if not skip_file_whitespace(path):
224  files_to_check_whitespace.append(path)
225 
226  if not skip_file_tabs(path):
227  files_to_check_tabs.append(path)
228 
229  elif os.path.isdir(abs_path):
230  for dirpath, dirnames, filenames in os.walk(path, topdown=True):
231  if skip_directory(dirpath):
232  # Remove directory and its subdirectories
233  dirnames[:] = []
234  continue
235 
236  filenames = [os.path.join(dirpath, f) for f in filenames]
237 
238  for f in filenames:
239  if not skip_file_formatting(f):
240  files_to_check_formatting.append(f)
241 
242  if not skip_file_whitespace(f):
243  files_to_check_whitespace.append(f)
244 
245  if not skip_file_tabs(f):
246  files_to_check_tabs.append(f)
247 
248  else:
249  raise ValueError(f'Error: {path} is not a file nor a directory')
250 
251  return (
252  files_to_check_formatting,
253  files_to_check_whitespace,
254  files_to_check_tabs,
255  )
256 
257 
259  """
260  Find the path to one of the supported versions of clang-format.
261  If no supported version of clang-format is found, raise an exception.
262 
263  @return Path to clang-format.
264  """
265 
266  # Find exact version
267  for version in CLANG_FORMAT_VERSIONS:
268  clang_format_path = shutil.which(f'clang-format-{version}')
269 
270  if clang_format_path:
271  return clang_format_path
272 
273  # Find default version and check if it is supported
274  clang_format_path = shutil.which('clang-format')
275 
276  if clang_format_path:
277  process = subprocess.run(
278  [clang_format_path, '--version'],
279  capture_output=True,
280  text=True,
281  check=True,
282  )
283 
284  version = process.stdout.strip().split(' ')[-1]
285  major_version = int(version.split('.')[0])
286 
287  if major_version in CLANG_FORMAT_VERSIONS:
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_VERSIONS}.'
294  )
295 
296 
297 
300 def check_style(path: str,
301  enable_check_formatting: bool,
302  enable_check_whitespace: bool,
303  enable_check_tabs: bool,
304  fix: bool,
305  n_jobs: int = 1,
306  ) -> None:
307  """
308  Check / fix the coding style of a list of files, including formatting and
309  trailing whitespace.
310 
311  @param path Path to the files.
312  @param fix Whether to fix the style of the file (True) or
313  just check if the file is well-formatted (False).
314  @param enable_check_formatting Whether to enable code formatting checking.
315  @param enable_check_whitespace Whether to enable trailing whitespace checking.
316  @param enable_check_tabs Whether to enable tabs checking.
317  @param n_jobs Number of parallel jobs.
318  """
319 
320  (files_to_check_formatting,
321  files_to_check_whitespace,
322  files_to_check_tabs) = find_files_to_check_style(path)
323 
324  check_formatting_successful = True
325  check_whitespace_successful = True
326  check_tabs_successful = True
327 
328  if enable_check_formatting:
329  check_formatting_successful = check_formatting(
330  files_to_check_formatting, fix, n_jobs)
331 
332  print('')
333 
334  if enable_check_whitespace:
335  check_whitespace_successful = check_trailing_whitespace(
336  files_to_check_whitespace, fix, n_jobs)
337 
338  print('')
339 
340  if enable_check_tabs:
341  check_tabs_successful = check_tabs(
342  files_to_check_tabs, fix, n_jobs)
343 
344  if check_formatting_successful and \
345  check_whitespace_successful and \
346  check_tabs_successful:
347  sys.exit(0)
348  else:
349  sys.exit(1)
350 
351 
352 
355 def check_formatting(filenames: List[str], fix: bool, n_jobs: int) -> bool:
356  """
357  Check / fix the coding style of a list of files with clang-format.
358 
359  @param filenames List of filenames to be checked.
360  @param fix Whether to fix the formatting of the file (True) or
361  just check if the file is well-formatted (False).
362  @param n_jobs Number of parallel jobs.
363  @return True if all files are well formatted after the check process.
364  False if there are non-formatted files after the check process.
365  """
366 
367  # Check files
368  clang_format_path = find_clang_format_path()
369  files_not_formatted: List[str] = []
370 
371  with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
372  files_not_formatted_results = executor.map(
373  check_formatting_file,
374  filenames,
375  itertools.repeat(clang_format_path),
376  itertools.repeat(fix),
377  )
378 
379  for (filename, formatted) in files_not_formatted_results:
380  if not formatted:
381  files_not_formatted.append(filename)
382 
383  files_not_formatted.sort()
384 
385  # Output results
386  if not files_not_formatted:
387  print('All files are well formatted')
388  return True
389 
390  else:
391  n_non_formatted_files = len(files_not_formatted)
392 
393  if fix:
394  print(f'Fixed formatting of the files ({n_non_formatted_files}):')
395  else:
396  print(f'Detected bad formatting in the files ({n_non_formatted_files}):')
397 
398  for f in files_not_formatted:
399  print(f'- {f}')
400 
401  # Return True if all files were fixed
402  return fix
403 
404 
405 def check_formatting_file(filename: str,
406  clang_format_path: str,
407  fix: bool,
408  ) -> Tuple[str, bool]:
409  """
410  Check / fix the coding style of a file with clang-format.
411 
412  @param filename Name of the file to be checked.
413  @param clang_format_path Path to clang-format.
414  @param fix Whether to fix the style of the file (True) or
415  just check if the file is well-formatted (False).
416  @return Tuple [Filename, Whether the file is well-formatted].
417  """
418 
419  # Check if the file is well formatted
420  process = subprocess.run(
421  [
422  clang_format_path,
423  filename,
424  '-style=file',
425  '--dry-run',
426  '--Werror',
427  # Optimization: Only 1 error is needed to check that the file is not formatted
428  '--ferror-limit=1',
429  ],
430  check=False,
431  stdout=subprocess.DEVNULL,
432  stderr=subprocess.DEVNULL,
433  )
434 
435  file_formatted = (process.returncode == 0)
436 
437  # Fix file
438  if fix and not file_formatted:
439  process = subprocess.run(
440  [
441  clang_format_path,
442  filename,
443  '-style=file',
444  '-i',
445  ],
446  check=False,
447  stdout=subprocess.DEVNULL,
448  stderr=subprocess.DEVNULL,
449  )
450 
451  return (filename, file_formatted)
452 
453 
454 
457 def check_trailing_whitespace(filenames: List[str], fix: bool, n_jobs: int) -> bool:
458  """
459  Check / fix trailing whitespace in a list of files.
460 
461  @param filename Name of the file to be checked.
462  @param fix Whether to fix the file (True) or
463  just check if it has trailing whitespace (False).
464  @param n_jobs Number of parallel jobs.
465  @return True if no files have trailing whitespace after the check process.
466  False if there are trailing whitespace after the check process.
467  """
468 
469  # Check files
470  files_with_whitespace: List[str] = []
471 
472  with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
473  files_with_whitespace_results = executor.map(
474  check_trailing_whitespace_file,
475  filenames,
476  itertools.repeat(fix),
477  )
478 
479  for (filename, has_whitespace) in files_with_whitespace_results:
480  if has_whitespace:
481  files_with_whitespace.append(filename)
482 
483  files_with_whitespace.sort()
484 
485  # Output results
486  if not files_with_whitespace:
487  print('No files detected with trailing whitespace')
488  return True
489 
490  else:
491  n_files_with_whitespace = len(files_with_whitespace)
492 
493  if fix:
494  print(
495  f'Fixed trailing whitespace in the files ({n_files_with_whitespace}):')
496  else:
497  print(
498  f'Detected trailing whitespace in the files ({n_files_with_whitespace}):')
499 
500  for f in files_with_whitespace:
501  print(f'- {f}')
502 
503  # If all files were fixed, there are no more trailing whitespace
504  return fix
505 
506 
507 def check_trailing_whitespace_file(filename: str, fix: bool) -> Tuple[str, bool]:
508  """
509  Check / fix trailing whitespace in a file.
510 
511  @param filename Name of the file to be checked.
512  @param fix Whether to fix the file (True) or
513  just check if it has trailing whitespace (False).
514  @return Tuple [Filename, Whether the file has trailing whitespace].
515  """
516 
517  has_trailing_whitespace = False
518 
519  with open(filename, 'r', encoding='utf-8') as f:
520  file_lines = f.readlines()
521 
522  # Check if there are trailing whitespace and fix them
523  for (i, line) in enumerate(file_lines):
524  line_fixed = line.rstrip() + '\n'
525 
526  if line_fixed != line:
527  has_trailing_whitespace = True
528 
529  # Optimization: if only checking, skip the rest of the file
530  if not fix:
531  break
532 
533  file_lines[i] = line_fixed
534 
535  # Update file with the fixed lines
536  if fix and has_trailing_whitespace:
537  with open(filename, 'w', encoding='utf-8') as f:
538  f.writelines(file_lines)
539 
540  return (filename, has_trailing_whitespace)
541 
542 
543 
546 def check_tabs(filenames: List[str], fix: bool, n_jobs: int) -> bool:
547  """
548  Check / fix tabs in a list of files.
549 
550  @param filename Name of the file to be checked.
551  @param fix Whether to fix the file (True) or just check if it has tabs (False).
552  @param n_jobs Number of parallel jobs.
553  @return True if no files have tabs after the check process.
554  False if there are tabs after the check process.
555  """
556 
557  # Check files
558  files_with_tabs: List[str] = []
559 
560  with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
561  files_with_tabs_results = executor.map(
562  check_tabs_file,
563  filenames,
564  itertools.repeat(fix),
565  )
566 
567  for (filename, has_tabs) in files_with_tabs_results:
568  if has_tabs:
569  files_with_tabs.append(filename)
570 
571  files_with_tabs.sort()
572 
573  # Output results
574  if not files_with_tabs:
575  print('No files detected with tabs')
576  return True
577 
578  else:
579  n_files_with_tabs = len(files_with_tabs)
580 
581  if fix:
582  print(
583  f'Fixed tabs in the files ({n_files_with_tabs}):')
584  else:
585  print(
586  f'Detected tabs in the files ({n_files_with_tabs}):')
587 
588  for f in files_with_tabs:
589  print(f'- {f}')
590 
591  # If all files were fixed, there are no more trailing whitespace
592  return fix
593 
594 
595 def check_tabs_file(filename: str, fix: bool) -> Tuple[str, bool]:
596  """
597  Check / fix tabs in a file.
598 
599  @param filename Name of the file to be checked.
600  @param fix Whether to fix the file (True) or just check if it has tabs (False).
601  @return Tuple [Filename, Whether the file has tabs].
602  """
603 
604  has_tabs = False
605  clang_format_enabled = True
606 
607  with open(filename, 'r', encoding='utf-8') as f:
608  file_lines = f.readlines()
609 
610  for (i, line) in enumerate(file_lines):
611 
612  # Check clang-format guards
613  line_stripped = line.strip()
614 
615  if line_stripped == CLANG_FORMAT_GUARD_ON:
616  clang_format_enabled = True
617  elif line_stripped == CLANG_FORMAT_GUARD_OFF:
618  clang_format_enabled = False
619 
620  if (not clang_format_enabled and
621  line_stripped not in (CLANG_FORMAT_GUARD_ON, CLANG_FORMAT_GUARD_OFF)):
622  continue
623 
624  # Check if there are tabs and fix them
625  if line.find('\t') != -1:
626  has_tabs = True
627 
628  # Optimization: if only checking, skip the rest of the file
629  if not fix:
630  break
631 
632  file_lines[i] = line.expandtabs(TAB_SIZE)
633 
634  # Update file with the fixed lines
635  if fix and has_tabs:
636  with open(filename, 'w', encoding='utf-8') as f:
637  f.writelines(file_lines)
638 
639  return (filename, has_tabs)
640 
641 
642 
645 if __name__ == '__main__':
646 
647  parser = argparse.ArgumentParser(
648  description='Check and apply the ns-3 coding style to all files in a given PATH. '
649  'The script checks the formatting of the file with clang-format. '
650  'Additionally, it checks the presence of trailing whitespace and tabs. '
651  'Formatting and tabs checks respect clang-format guards. '
652  'When used in "check mode" (default), the script checks if all files are well '
653  'formatted and do not have trailing whitespace nor tabs. '
654  'If it detects non-formatted files, they will be printed and this process exits with a '
655  'non-zero code. When used in "fix mode", this script automatically fixes the files.')
656 
657  parser.add_argument('path', action='store', type=str,
658  help='Path to the files to check')
659 
660  parser.add_argument('--no-formatting', action='store_true',
661  help='Do not check / fix code formatting')
662 
663  parser.add_argument('--no-whitespace', action='store_true',
664  help='Do not check / fix trailing whitespace')
665 
666  parser.add_argument('--no-tabs', action='store_true',
667  help='Do not check / fix tabs')
668 
669  parser.add_argument('--fix', action='store_true',
670  help='Fix coding style issues detected in the files')
671 
672  parser.add_argument('-j', '--jobs', type=int, default=max(1, os.cpu_count() - 1),
673  help='Number of parallel jobs')
674 
675  args = parser.parse_args()
676 
677  try:
678  check_style(
679  path=args.path,
680  enable_check_formatting=(not args.no_formatting),
681  enable_check_whitespace=(not args.no_whitespace),
682  enable_check_tabs=(not args.no_tabs),
683  fix=args.fix,
684  n_jobs=args.jobs,
685  )
686 
687  except Exception as e:
688  print(e)
689  sys.exit(1)
#define max(a, b)
Definition: 80211b.c:43
Tuple[str, bool] check_tabs_file(str filename, bool fix)
bool check_trailing_whitespace(List[str] filenames, bool fix, int n_jobs)
CHECK TRAILING WHITESPACE.
Tuple[List[str], List[str], List[str]] find_files_to_check_style(str path)
None check_style(str path, bool enable_check_formatting, bool enable_check_whitespace, bool enable_check_tabs, bool fix, int n_jobs=1)
CHECK STYLE.
Tuple[str, bool] check_trailing_whitespace_file(str filename, bool fix)
bool check_formatting(List[str] filenames, bool fix, int n_jobs)
CHECK FORMATTING.
Tuple[str, bool] check_formatting_file(str filename, str clang_format_path, bool fix)
bool skip_directory(str dirpath)
AUXILIARY FUNCTIONS.
bool check_tabs(List[str] filenames, bool fix, int n_jobs)
CHECK TABS.