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# 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"""
21Check and apply the ns-3 coding style to all files in the PATH argument.
22
23The coding style is defined with the clang-format tool, whose definitions are in
24the ".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
30This script can be applied to all text files in a given path or to individual files.
31
32NOTE: The formatting check requires clang-format (version >= 14) to be found on the path.
33Trimming of trailing whitespace and conversion of tabs to spaces (via the "--no-formatting"
34option) do not depend on clang-format.
35"""
36
37import argparse
38import concurrent.futures
39import itertools
40import os
41import re
42import shutil
43import subprocess
44import sys
45
46from typing import Callable, Dict, List, Tuple
47
48
51CLANG_FORMAT_VERSIONS = [
52 16,
53 15,
54 14,
55]
56
57CLANG_FORMAT_GUARD_ON = '// clang-format on'
58CLANG_FORMAT_GUARD_OFF = '// clang-format off'
59
60DIRECTORIES_TO_SKIP = [
61 '__pycache__',
62 '.vscode',
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
71FILES_TO_SKIP = [
72 'valgrind.h',
73]
74
75FILE_EXTENSIONS_TO_CHECK_FORMATTING = [
76 '.c',
77 '.cc',
78 '.h',
79]
80
81FILE_EXTENSIONS_TO_CHECK_INCLUDE_PREFIXES = FILE_EXTENSIONS_TO_CHECK_FORMATTING
82
83FILE_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
113FILES_TO_CHECK_WHITESPACE = [
114 'Makefile',
115 'ns3',
116]
117
118FILE_EXTENSIONS_TO_CHECK_TABS = [
119 '.c',
120 '.cc',
121 '.h',
122 '.md',
123 '.py',
124 '.rst',
125 '.sh',
126 '.yml',
127]
128TAB_SIZE = 4
129
130
131
134def 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 (directory in DIRECTORIES_TO_SKIP or
145 (directory.startswith('.') and directory != '.'))
146
147
149 files_to_check: List[str],
150 file_extensions_to_check: List[str],
151 ) -> bool:
152 """
153 Check whether a file should be analyzed.
154
155 @param path Path to the file.
156 @param files_to_check List of files that shall be checked.
157 @param file_extensions_to_check List of file extensions that shall be checked.
158 @return Whether the file should be analyzed.
159 """
160
161 filename = os.path.split(path)[1]
162
163 if filename in FILES_TO_SKIP:
164 return False
165
166 basename, extension = os.path.splitext(filename)
167
168 return (basename in files_to_check or
169 extension in file_extensions_to_check)
170
171
172def find_files_to_check_style(path: str) -> Tuple[List[str], List[str], List[str], List[str]]:
173 """
174 Find all files to be checked in a given path.
175
176 @param path Path to check.
177 @return Tuple [List of files to check include prefixes,
178 List of files to check formatting,
179 List of files to check trailing whitespace,
180 List of files to check tabs].
181 """
182
183 files_to_check: List[str] = []
184 abs_path = os.path.abspath(os.path.expanduser(path))
185
186 if os.path.isfile(abs_path):
187 files_to_check = [path]
188
189 elif os.path.isdir(abs_path):
190 for dirpath, dirnames, filenames in os.walk(path, topdown=True):
191 if not should_analyze_directory(dirpath):
192 # Remove directory and its subdirectories
193 dirnames[:] = []
194 continue
195
196 files_to_check.extend([os.path.join(dirpath, f) for f in filenames])
197
198 else:
199 raise ValueError(f'Error: {path} is not a file nor a directory')
200
201 files_to_check.sort()
202
203 files_to_check_include_prefixes: List[str] = []
204 files_to_check_formatting: List[str] = []
205 files_to_check_whitespace: List[str] = []
206 files_to_check_tabs: List[str] = []
207
208 for f in files_to_check:
209 if should_analyze_file(f, [], FILE_EXTENSIONS_TO_CHECK_INCLUDE_PREFIXES):
210 files_to_check_include_prefixes.append(f)
211
212 if should_analyze_file(f, [], FILE_EXTENSIONS_TO_CHECK_FORMATTING):
213 files_to_check_formatting.append(f)
214
215 if should_analyze_file(f, FILES_TO_CHECK_WHITESPACE, FILE_EXTENSIONS_TO_CHECK_WHITESPACE):
216 files_to_check_whitespace.append(f)
217
218 if should_analyze_file(f, [], FILE_EXTENSIONS_TO_CHECK_TABS):
219 files_to_check_tabs.append(f)
220
221 return (
222 files_to_check_include_prefixes,
223 files_to_check_formatting,
224 files_to_check_whitespace,
225 files_to_check_tabs,
226 )
227
228
230 """
231 Find the path to one of the supported versions of clang-format.
232 If no supported version of clang-format is found, raise an exception.
233
234 @return Path to clang-format.
235 """
236
237 # Find exact version
238 for version in CLANG_FORMAT_VERSIONS:
239 clang_format_path = shutil.which(f'clang-format-{version}')
240
241 if clang_format_path:
242 return clang_format_path
243
244 # Find default version and check if it is supported
245 clang_format_path = shutil.which('clang-format')
246
247 if clang_format_path:
248 process = subprocess.run(
249 [clang_format_path, '--version'],
250 capture_output=True,
251 text=True,
252 check=True,
253 )
254
255 version = process.stdout.strip().split(' ')[-1]
256 major_version = int(version.split('.')[0])
257
258 if major_version in CLANG_FORMAT_VERSIONS:
259 return clang_format_path
260
261 # No supported version of clang-format found
262 raise RuntimeError(
263 f'Could not find any supported version of clang-format installed on this system. '
264 f'List of supported versions: {CLANG_FORMAT_VERSIONS}.'
265 )
266
267
268
272 enable_check_include_prefixes: bool,
273 enable_check_formatting: bool,
274 enable_check_whitespace: bool,
275 enable_check_tabs: bool,
276 fix: bool,
277 verbose: bool,
278 n_jobs: int = 1,
279 ) -> bool:
280 """
281 Check / fix the coding style of a list of files.
282
283 @param path Path to the files.
284 @param enable_check_include_prefixes Whether to enable checking #include headers from the same module with the "ns3/" prefix.
285 @param enable_check_formatting Whether to enable checking code formatting.
286 @param enable_check_whitespace Whether to enable checking trailing whitespace.
287 @param enable_check_tabs Whether to enable checking tabs.
288 @param fix Whether to fix (True) or just check (False) the file.
289 @param verbose Show the lines that are not compliant with the style.
290 @param n_jobs Number of parallel jobs.
291 @return Whether all files are compliant with all enabled style checks.
292 """
293
294 (files_to_check_include_prefixes,
295 files_to_check_formatting,
296 files_to_check_whitespace,
297 files_to_check_tabs) = find_files_to_check_style(path)
298
299 check_include_prefixes_successful = True
300 check_formatting_successful = True
301 check_whitespace_successful = True
302 check_tabs_successful = True
303
304 if enable_check_include_prefixes:
305 check_include_prefixes_successful = check_style_file(
306 files_to_check_include_prefixes,
307 check_include_prefixes_file,
308 '#include headers from the same module with the "ns3/" prefix',
309 fix,
310 verbose,
311 n_jobs,
312 )
313
314 print('')
315
316 if enable_check_formatting:
317 check_formatting_successful = check_style_file(
318 files_to_check_formatting,
319 check_formatting_file,
320 'bad code formatting',
321 fix,
322 verbose,
323 n_jobs,
324 clang_format_path=find_clang_format_path(),
325 )
326
327 print('')
328
329 if enable_check_whitespace:
330 check_whitespace_successful = check_style_file(
331 files_to_check_whitespace,
332 check_trailing_whitespace_file,
333 'trailing whitespace',
334 fix,
335 verbose,
336 n_jobs,
337 )
338
339 print('')
340
341 if enable_check_tabs:
342 check_tabs_successful = check_style_file(
343 files_to_check_tabs,
344 check_tabs_file,
345 'tabs',
346 fix,
347 verbose,
348 n_jobs,
349 )
350
351 return all([
352 check_include_prefixes_successful,
353 check_formatting_successful,
354 check_whitespace_successful,
355 check_tabs_successful,
356 ])
357
358
359def check_style_file(filenames: List[str],
360 check_style_file_function: Callable,
361 style_check_str: str,
362 fix: bool,
363 verbose: bool,
364 n_jobs: int,
365 **kwargs,
366 ) -> bool:
367 """
368 Check / fix style of a list of files.
369
370 @param filename Name of the file to be checked.
371 @param check_style_file_function Function used to check the file.
372 @param style_check_str Description of the check to be performed.
373 @param fix Whether to fix (True) or just check (False) the file (True).
374 @param verbose Show the lines that are not compliant with the style.
375 @param n_jobs Number of parallel jobs.
376 @param kwargs Additional keyword arguments to the check_style_file_function.
377 @return Whether all files are compliant with the style.
378 """
379
380 # Check files
381 non_compliant_files: List[str] = []
382 files_verbose_infos: Dict[str, List[str]] = {}
383
384 with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
385 non_compliant_files_results = executor.map(
386 check_style_file_function,
387 filenames,
388 itertools.repeat(fix),
389 itertools.repeat(verbose),
390 *[arg if isinstance(arg, list) else itertools.repeat(arg) for arg in kwargs.values()],
391 )
392
393 for (filename, is_file_compliant, verbose_infos) in non_compliant_files_results:
394 if not is_file_compliant:
395 non_compliant_files.append(filename)
396
397 if verbose:
398 files_verbose_infos[filename] = verbose_infos
399
400 # Output results
401 if not non_compliant_files:
402 print(f'- No files detected with {style_check_str}')
403 return True
404
405 else:
406 n_non_compliant_files = len(non_compliant_files)
407
408 if fix:
409 print(f'- Fixed {style_check_str} in the files ({n_non_compliant_files}):')
410 else:
411 print(f'- Detected {style_check_str} in the files ({n_non_compliant_files}):')
412
413 for f in non_compliant_files:
414 if verbose:
415 print(*[f' {l}' for l in files_verbose_infos[f]], sep='\n')
416 else:
417 print(f' - {f}')
418
419 # If all files were fixed, there are no more non-compliant files
420 return fix
421
422
423
427 fix: bool,
428 verbose: bool,
429 ) -> Tuple[str, bool, List[str]]:
430 """
431 Check / fix #include headers from the same module with the "ns3/" prefix in a file.
432
433 @param filename Name of the file to be checked.
434 @param fix Whether to fix (True) or just check (False) the style of the file (True).
435 @param verbose Show the lines that are not compliant with the style.
436 @return Tuple [Filename,
437 Whether the file is compliant with the style (before the check),
438 Verbose information].
439 """
440
441 is_file_compliant = True
442 clang_format_enabled = True
443
444 verbose_infos: List[str] = []
445
446 with open(filename, 'r', encoding='utf-8') as f:
447 file_lines = f.readlines()
448
449 for (i, line) in enumerate(file_lines):
450
451 # Check clang-format guards
452 line_stripped = line.strip()
453
454 if line_stripped == CLANG_FORMAT_GUARD_ON:
455 clang_format_enabled = True
456 elif line_stripped == CLANG_FORMAT_GUARD_OFF:
457 clang_format_enabled = False
458
459 if (not clang_format_enabled and
460 line_stripped not in (CLANG_FORMAT_GUARD_ON, CLANG_FORMAT_GUARD_OFF)):
461 continue
462
463 # Check if the line is an #include and extract its header file
464 header_file = re.findall(r'^#include ["<]ns3/(.*\.h)[">]', line_stripped)
465
466 if not header_file:
467 continue
468
469 # Check if the header file belongs to the same module and remove the "ns3/" prefix
470 header_file = header_file[0]
471 parent_path = os.path.split(filename)[0]
472
473 if not os.path.exists(os.path.join(parent_path, header_file)):
474 continue
475
476 is_file_compliant = False
477 file_lines[i] = line_stripped.replace(
478 f'ns3/{header_file}', header_file).replace('<', '"').replace('>', '"') + '\n'
479
480 if verbose:
481 header_index = len('#include "')
482
483 verbose_infos.extend([
484 f'{filename}:{i + 1}:{header_index + 1}: error: #include headers from the same module with the "ns3/" prefix detected',
485 f' {line_stripped}',
486 f' {"":{header_index}}^',
487 ])
488
489 # Optimization: If running in non-verbose check mode, only one error is needed to check that the file is not compliant
490 if not fix and not verbose:
491 break
492
493 # Update file with the fixed lines
494 if fix and not is_file_compliant:
495 with open(filename, 'w', encoding='utf-8') as f:
496 f.writelines(file_lines)
497
498 return (filename, is_file_compliant, verbose_infos)
499
500
501def check_formatting_file(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 (True).
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 fix: bool,
560 verbose: bool,
561 ) -> Tuple[str, bool, List[str]]:
562 """
563 Check / fix trailing whitespace in a file.
564
565 @param filename Name of the file to be checked.
566 @param fix Whether to fix (True) or just check (False) the style of the file (True).
567 @param verbose Show the lines that are not compliant with the style.
568 @return Tuple [Filename,
569 Whether the file is compliant with the style (before the check),
570 Verbose information].
571 """
572
573 is_file_compliant = True
574 verbose_infos: List[str] = []
575
576 with open(filename, 'r', encoding='utf-8') as f:
577 file_lines = f.readlines()
578
579 # Check if there are trailing whitespace and fix them
580 for (i, line) in enumerate(file_lines):
581 line_fixed = line.rstrip() + '\n'
582
583 if line_fixed == line:
584 continue
585
586 is_file_compliant = False
587 file_lines[i] = line_fixed
588
589 if verbose:
590 line_fixed_stripped_expanded = line_fixed.rstrip().expandtabs(TAB_SIZE)
591
592 verbose_infos.extend([
593 f'{filename}:{i + 1}:{len(line_fixed_stripped_expanded) + 1}: error: Trailing whitespace detected',
594 f' {line_fixed_stripped_expanded}',
595 f' {"":{len(line_fixed_stripped_expanded)}}^',
596 ])
597
598 # Optimization: If running in non-verbose check mode, only one error is needed to check that the file is not compliant
599 if not fix and not verbose:
600 break
601
602 # Update file with the fixed lines
603 if fix and not is_file_compliant:
604 with open(filename, 'w', encoding='utf-8') as f:
605 f.writelines(file_lines)
606
607 return (filename, is_file_compliant, verbose_infos)
608
609
610def check_tabs_file(filename: str,
611 fix: bool,
612 verbose: bool,
613 ) -> Tuple[str, bool, List[str]]:
614 """
615 Check / fix tabs in a file.
616
617 @param filename Name of the file to be checked.
618 @param fix Whether to fix (True) or just check (False) the style of the file (True).
619 @param verbose Show the lines that are not compliant with the style.
620 @return Tuple [Filename,
621 Whether the file is compliant with the style (before the check),
622 Verbose information].
623 """
624
625 is_file_compliant = True
626 clang_format_enabled = True
627
628 verbose_infos: List[str] = []
629
630 with open(filename, 'r', encoding='utf-8') as f:
631 file_lines = f.readlines()
632
633 for (i, line) in enumerate(file_lines):
634
635 # Check clang-format guards
636 line_stripped = line.strip()
637
638 if line_stripped == CLANG_FORMAT_GUARD_ON:
639 clang_format_enabled = True
640 elif line_stripped == CLANG_FORMAT_GUARD_OFF:
641 clang_format_enabled = False
642
643 if (not clang_format_enabled and
644 line_stripped not in (CLANG_FORMAT_GUARD_ON, CLANG_FORMAT_GUARD_OFF)):
645 continue
646
647 # Check if there are tabs and fix them
648 tab_index = line.find('\t')
649
650 if tab_index == -1:
651 continue
652
653 is_file_compliant = False
654 file_lines[i] = line.expandtabs(TAB_SIZE)
655
656 if verbose:
657 verbose_infos.extend([
658 f'{filename}:{i + 1}:{tab_index + 1}: error: Tab detected',
659 f' {line.rstrip()}',
660 f' {"":{tab_index}}^',
661 ])
662
663 # Optimization: If running in non-verbose check mode, only one error is needed to check that the file is not compliant
664 if not fix and not verbose:
665 break
666
667 # Update file with the fixed lines
668 if fix and not is_file_compliant:
669 with open(filename, 'w', encoding='utf-8') as f:
670 f.writelines(file_lines)
671
672 return (filename, is_file_compliant, verbose_infos)
673
674
675
678if __name__ == '__main__':
679
680 parser = argparse.ArgumentParser(
681 description='Check and apply the ns-3 coding style to all files in a given PATH. '
682 'The script checks the formatting of the file with clang-format. '
683 'Additionally, it checks #include headers from the same module with the "ns3/" prefix, '
684 'the presence of trailing whitespace and tabs. '
685 'Formatting, local #include "ns3/" prefixes and tabs checks respect clang-format guards. '
686 'When used in "check mode" (default), the script checks if all files are well '
687 'formatted and do not have trailing whitespace nor tabs. '
688 'If it detects non-formatted files, they will be printed and this process exits with a '
689 'non-zero code. When used in "fix mode", this script automatically fixes the files.')
690
691 parser.add_argument('path', action='store', type=str,
692 help='Path to the files to check')
693
694 parser.add_argument('--no-include-prefixes', action='store_true',
695 help='Do not check / fix #include headers from the same module with the "ns3/" prefix')
696
697 parser.add_argument('--no-formatting', action='store_true',
698 help='Do not check / fix code formatting')
699
700 parser.add_argument('--no-whitespace', action='store_true',
701 help='Do not check / fix trailing whitespace')
702
703 parser.add_argument('--no-tabs', action='store_true',
704 help='Do not check / fix tabs')
705
706 parser.add_argument('--fix', action='store_true',
707 help='Fix coding style issues detected in the files')
708
709 parser.add_argument('-v', '--verbose', action='store_true',
710 help='Show the lines that are not well-formatted')
711
712 parser.add_argument('-j', '--jobs', type=int, default=max(1, os.cpu_count() - 1),
713 help='Number of parallel jobs')
714
715 args = parser.parse_args()
716
717 try:
718 all_checks_successful = check_style_clang_format(
719 path=args.path,
720 enable_check_include_prefixes=(not args.no_include_prefixes),
721 enable_check_formatting=(not args.no_formatting),
722 enable_check_whitespace=(not args.no_whitespace),
723 enable_check_tabs=(not args.no_tabs),
724 fix=args.fix,
725 verbose=args.verbose,
726 n_jobs=args.jobs,
727 )
728
729 except Exception as e:
730 print(e)
731 sys.exit(1)
732
733 if all_checks_successful:
734 sys.exit(0)
735 else:
736 sys.exit(1)
#define max(a, b)
Definition: 80211b.c:42
Tuple[str, bool, List[str]] check_trailing_whitespace_file(str filename, bool fix, bool verbose)
Tuple[str, bool, List[str]] check_tabs_file(str filename, bool fix, bool verbose)
Tuple[str, bool, List[str]] check_formatting_file(str filename, bool fix, bool verbose, str clang_format_path)
Tuple[str, bool, List[str]] check_include_prefixes_file(str filename, bool fix, bool verbose)
CHECK STYLE FUNCTIONS.
bool should_analyze_file(str path, List[str] files_to_check, List[str] file_extensions_to_check)
bool check_style_file(List[str] filenames, Callable check_style_file_function, str style_check_str, bool fix, bool verbose, int n_jobs, **kwargs)
bool should_analyze_directory(str dirpath)
AUXILIARY FUNCTIONS.
Tuple[List[str], List[str], List[str], List[str]] find_files_to_check_style(str path)
bool check_style_clang_format(str path, 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.