diff --git a/doc/contributing/source/coding-style.rst b/doc/contributing/source/coding-style.rst index d5550a4e6..8f005519c 100644 --- a/doc/contributing/source/coding-style.rst +++ b/doc/contributing/source/coding-style.rst @@ -121,13 +121,14 @@ We recommend running this script over your newly introduced C++ files prior to s as a Merge Request. The script has multiple modes of operation. By default, the script checks if -source code files are well formatted and text files do not have trailing whitespace +source code files are well formatted, if there are no #include headers from the same +module with the "ns3/" prefix, and text files do not have trailing whitespace nor tabs. The process returns a zero exit code if all files adhere to these rules. If there are files that do not comply with the rules, the process returns a non-zero exit code and lists the respective files. This mode is useful for developers editing their code and for the GitLab CI/CD pipeline to check if the codebase is well formatted. All checks are enabled by default. Users can disable specific checks using the corresponding -flags: ``--no-formatting``, ``--no-whitespace`` and ``--no-tabs``. +flags: ``--no-include-prefixes``, ``--no-formatting``, ``--no-whitespace`` and ``--no-tabs``. Additional information about the formatting issues detected by the script can be enabled by adding the ``-v, --verbose`` flag. diff --git a/utils/check-style-clang-format.py b/utils/check-style-clang-format.py index f082d878e..f78ba7703 100755 --- a/utils/check-style-clang-format.py +++ b/utils/check-style-clang-format.py @@ -22,13 +22,10 @@ Check and apply the ns-3 coding style to all files in the PATH argument. The coding style is defined with the clang-format tool, whose definitions are in the ".clang-format" file. This script performs the following checks / fixes: -- Check / apply clang-format. -- Check / trim trailing whitespace. -- Check / replace tabs with spaces. - -The clang-format and tabs checks respect clang-format guards, which mark code blocks -that should not be checked. Trailing whitespace is always checked regardless of -clang-format guards. +- Check / fix local #include headers with "ns3/" prefix. Respects clang-format guards. +- Check / apply clang-format. Respects clang-format guards. +- Check / trim trailing whitespace. Always checked. +- Check / replace tabs with spaces. Respects clang-format guards. This script can be applied to all text files in a given path or to individual files. @@ -41,6 +38,7 @@ import argparse import concurrent.futures import itertools import os +import re import shutil import subprocess import sys @@ -80,6 +78,8 @@ FILE_EXTENSIONS_TO_CHECK_FORMATTING = [ '.h', ] +FILE_EXTENSIONS_TO_CHECK_INCLUDE_PREFIXES = FILE_EXTENSIONS_TO_CHECK_FORMATTING + FILE_EXTENSIONS_TO_CHECK_WHITESPACE = [ '.c', '.cc', @@ -169,12 +169,13 @@ def should_analyze_file(path: str, extension in file_extensions_to_check) -def find_files_to_check_style(path: str) -> Tuple[List[str], List[str], List[str]]: +def find_files_to_check_style(path: str) -> Tuple[List[str], List[str], List[str], List[str]]: """ Find all files to be checked in a given path. @param path Path to check. - @return Tuple [List of files to check formatting, + @return Tuple [List of files to check include prefixes, + List of files to check formatting, List of files to check trailing whitespace, List of files to check tabs]. """ @@ -199,11 +200,15 @@ def find_files_to_check_style(path: str) -> Tuple[List[str], List[str], List[str files_to_check.sort() + files_to_check_include_prefixes: List[str] = [] files_to_check_formatting: List[str] = [] files_to_check_whitespace: List[str] = [] files_to_check_tabs: List[str] = [] for f in files_to_check: + if should_analyze_file(f, [], FILE_EXTENSIONS_TO_CHECK_INCLUDE_PREFIXES): + files_to_check_include_prefixes.append(f) + if should_analyze_file(f, [], FILE_EXTENSIONS_TO_CHECK_FORMATTING): files_to_check_formatting.append(f) @@ -214,6 +219,7 @@ def find_files_to_check_style(path: str) -> Tuple[List[str], List[str], List[str files_to_check_tabs.append(f) return ( + files_to_check_include_prefixes, files_to_check_formatting, files_to_check_whitespace, files_to_check_tabs, @@ -263,6 +269,7 @@ def find_clang_format_path() -> str: # CHECK STYLE MAIN FUNCTIONS ########################################################### def check_style_clang_format(path: str, + enable_check_include_prefixes: bool, enable_check_formatting: bool, enable_check_whitespace: bool, enable_check_tabs: bool, @@ -274,23 +281,38 @@ def check_style_clang_format(path: str, Check / fix the coding style of a list of files. @param path Path to the files. - @param enable_check_formatting Whether to enable code formatting checking. - @param enable_check_whitespace Whether to enable trailing whitespace checking. - @param enable_check_tabs Whether to enable tabs checking. + @param enable_check_include_prefixes Whether to enable checking #include headers from the same module with the "ns3/" prefix. + @param enable_check_formatting Whether to enable checking code formatting. + @param enable_check_whitespace Whether to enable checking trailing whitespace. + @param enable_check_tabs Whether to enable checking tabs. @param fix Whether to fix (True) or just check (False) the file. @param verbose Show the lines that are not compliant with the style. @param n_jobs Number of parallel jobs. @return Whether all files are compliant with all enabled style checks. """ - (files_to_check_formatting, + (files_to_check_include_prefixes, + files_to_check_formatting, files_to_check_whitespace, files_to_check_tabs) = find_files_to_check_style(path) + check_include_prefixes_successful = True check_formatting_successful = True check_whitespace_successful = True check_tabs_successful = True + if enable_check_include_prefixes: + check_include_prefixes_successful = check_style_file( + files_to_check_include_prefixes, + check_include_prefixes_file, + '#include headers from the same module with the "ns3/" prefix', + fix, + verbose, + n_jobs, + ) + + print('') + if enable_check_formatting: check_formatting_successful = check_style_file( files_to_check_formatting, @@ -327,6 +349,7 @@ def check_style_clang_format(path: str, ) return all([ + check_include_prefixes_successful, check_formatting_successful, check_whitespace_successful, check_tabs_successful, @@ -400,6 +423,81 @@ def check_style_file(filenames: List[str], ########################################################### # CHECK STYLE FUNCTIONS ########################################################### +def check_include_prefixes_file(filename: str, + fix: bool, + verbose: bool, + ) -> Tuple[str, bool, List[str]]: + """ + Check / fix #include headers from the same module with the "ns3/" prefix in a file. + + @param filename Name of the file to be checked. + @param fix Whether to fix (True) or just check (False) the style of the file (True). + @param verbose Show the lines that are not compliant with the style. + @return Tuple [Filename, + Whether the file is compliant with the style (before the check), + Verbose information]. + """ + + is_file_compliant = True + clang_format_enabled = True + + verbose_infos: List[str] = [] + + with open(filename, 'r', encoding='utf-8') as f: + file_lines = f.readlines() + + for (i, line) in enumerate(file_lines): + + # Check clang-format guards + line_stripped = line.strip() + + if line_stripped == CLANG_FORMAT_GUARD_ON: + clang_format_enabled = True + elif line_stripped == CLANG_FORMAT_GUARD_OFF: + clang_format_enabled = False + + if (not clang_format_enabled and + line_stripped not in (CLANG_FORMAT_GUARD_ON, CLANG_FORMAT_GUARD_OFF)): + continue + + # Check if the line is an #include and extract its header file + header_file = re.findall(r'^#include ["<]ns3/(.*\.h)[">]', line_stripped) + + if not header_file: + continue + + # Check if the header file belongs to the same module and remove the "ns3/" prefix + header_file = header_file[0] + parent_path = os.path.split(filename)[0] + + if not os.path.exists(os.path.join(parent_path, header_file)): + continue + + is_file_compliant = False + file_lines[i] = line_stripped.replace( + f'ns3/{header_file}', header_file).replace('<', '"').replace('>', '"') + '\n' + + if verbose: + header_index = len('#include "') + + verbose_infos.extend([ + f'{filename}:{i + 1}:{header_index + 1}: error: #include headers from the same module with the "ns3/" prefix detected', + f' {line_stripped}', + f' {"":{header_index}}^', + ]) + + # Optimization: If running in non-verbose check mode, only one error is needed to check that the file is not compliant + if not fix and not verbose: + break + + # Update file with the fixed lines + if fix and not is_file_compliant: + with open(filename, 'w', encoding='utf-8') as f: + f.writelines(file_lines) + + return (filename, is_file_compliant, verbose_infos) + + def check_formatting_file(filename: str, fix: bool, verbose: bool, @@ -582,8 +680,9 @@ if __name__ == '__main__': parser = argparse.ArgumentParser( description='Check and apply the ns-3 coding style to all files in a given PATH. ' 'The script checks the formatting of the file with clang-format. ' - 'Additionally, it checks the presence of trailing whitespace and tabs. ' - 'Formatting and tabs checks respect clang-format guards. ' + 'Additionally, it checks #include headers from the same module with the "ns3/" prefix, ' + 'the presence of trailing whitespace and tabs. ' + 'Formatting, local #include "ns3/" prefixes and tabs checks respect clang-format guards. ' 'When used in "check mode" (default), the script checks if all files are well ' 'formatted and do not have trailing whitespace nor tabs. ' 'If it detects non-formatted files, they will be printed and this process exits with a ' @@ -592,6 +691,9 @@ if __name__ == '__main__': parser.add_argument('path', action='store', type=str, help='Path to the files to check') + parser.add_argument('--no-include-prefixes', action='store_true', + help='Do not check / fix #include headers from the same module with the "ns3/" prefix') + parser.add_argument('--no-formatting', action='store_true', help='Do not check / fix code formatting') @@ -615,6 +717,7 @@ if __name__ == '__main__': try: all_checks_successful = check_style_clang_format( path=args.path, + enable_check_include_prefixes=(not args.no_include_prefixes), enable_check_formatting=(not args.no_formatting), enable_check_whitespace=(not args.no_whitespace), enable_check_tabs=(not args.no_tabs),