diff --git a/utils/check-style-clang-format.py b/utils/check-style-clang-format.py new file mode 100755 index 000000000..7e019f92a --- /dev/null +++ b/utils/check-style-clang-format.py @@ -0,0 +1,626 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2022 Eduardo Nuno Almeida. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation; +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Author: Eduardo Nuno Almeida [INESC TEC and FEUP, Portugal] + +""" +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. + +This script can be applied to all text files in a given path or to individual files. + +NOTE: The formatting check requires clang-format (version >= 14) to be found on the path. +Trimming of trailing whitespace and conversion of tabs to spaces (via the "--no-formatting" +option) do not depend on clang-format. +""" + +import argparse +import concurrent.futures +import itertools +import os +import shutil +import subprocess +import sys +from typing import List, Tuple + + +########################################################### +# PARAMETERS +########################################################### +CLANG_FORMAT_VERSIONS = [ + 14, + 15, + 16, +] + +DIRECTORIES_TO_SKIP = [ + '__pycache__', + '.vscode', + 'bindings', + 'build', + 'cmake-cache', + 'testpy-output', +] + +FILE_EXTENSIONS_TO_CHECK_FORMATTING = [ + '.c', + '.cc', + '.h', +] + +FILE_EXTENSIONS_TO_CHECK_WHITESPACE = [ + '.c', + '.cc', + '.click', + '.conf', + '.css', + '.dot', + '.gnuplot', + '.gp', + '.h', + '.html', + '.js', + '.json', + '.m', + '.md', + '.mob', + '.ns_params', + '.ns_movements', + '.params', + '.pl', + '.plt', + '.py', + '.rst', + '.seqdiag', + '.sh', + '.txt', + '.yml', +] + +FILES_TO_CHECK_WHITESPACE = [ + 'Makefile', + 'ns3', +] + +FILE_EXTENSIONS_TO_CHECK_TABS = [ + '.c', + '.cc', + '.h', + '.md', + '.py', + '.rst', + '.sh', + '.yml', +] +TAB_SIZE = 4 + + +########################################################### +# AUXILIARY FUNCTIONS +########################################################### +def skip_directory(dirpath: str) -> bool: + """ + Check if a directory should be skipped. + + @param dirpath Directory path. + @return Whether the directory should be skipped or not. + """ + + _, directory = os.path.split(dirpath) + + return directory in DIRECTORIES_TO_SKIP or \ + (directory.startswith('.') and directory != '.') + + +def skip_file_formatting(filename: str) -> bool: + """ + Check if a file should be skipped from formatting analysis. + + @param filename Name of the file. + @return Whether the file should be skipped or not. + """ + + _, extension = os.path.splitext(os.path.split(filename)[1]) + + return extension not in FILE_EXTENSIONS_TO_CHECK_FORMATTING + + +def skip_file_whitespace(filename: str) -> bool: + """ + Check if a file should be skipped from trailing whitespace analysis. + + @param filename Name of the file. + @return Whether the file should be skipped or not. + """ + + basename, extension = os.path.splitext(os.path.split(filename)[1]) + + return basename not in FILES_TO_CHECK_WHITESPACE and \ + extension not in FILE_EXTENSIONS_TO_CHECK_WHITESPACE + + +def skip_file_tabs(filename: str) -> bool: + """ + Check if a file should be skipped from tabs analysis. + + @param filename Name of the file. + @return Whether the file should be skipped or not. + """ + + _, extension = os.path.splitext(os.path.split(filename)[1]) + + return extension not in FILE_EXTENSIONS_TO_CHECK_TABS + + +def find_files_to_check_style(path: str) -> Tuple[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, + List of files to check trailing whitespace, + List of files to check tabs]. + """ + + files_to_check_formatting: List[str] = [] + files_to_check_whitespace: List[str] = [] + files_to_check_tabs: List[str] = [] + + abs_path = os.path.normpath(os.path.abspath(os.path.expanduser(path))) + + if os.path.isfile(abs_path): + if not skip_file_formatting(path): + files_to_check_formatting.append(path) + + if not skip_file_whitespace(path): + files_to_check_whitespace.append(path) + + if not skip_file_tabs(path): + files_to_check_tabs.append(path) + + elif os.path.isdir(abs_path): + for dirpath, dirnames, filenames in os.walk(path, topdown=True): + if skip_directory(dirpath): + # Remove directory and its subdirectories + dirnames[:] = [] + continue + + filenames = [os.path.join(dirpath, f) for f in filenames] + + for f in filenames: + if not skip_file_formatting(f): + files_to_check_formatting.append(f) + + if not skip_file_whitespace(f): + files_to_check_whitespace.append(f) + + if not skip_file_tabs(f): + files_to_check_tabs.append(f) + + else: + raise ValueError(f'Error: {path} is not a file nor a directory') + + return ( + files_to_check_formatting, + files_to_check_whitespace, + files_to_check_tabs, + ) + + +def find_clang_format_path() -> str: + """ + Find the path to one of the supported versions of clang-format. + If no supported version of clang-format is found, raise an exception. + + @return Path to clang-format. + """ + + for version in CLANG_FORMAT_VERSIONS: + clang_format_path = shutil.which(f'clang-format-{version}') + + if clang_format_path: + return clang_format_path + + raise RuntimeError( + f'Could not find any supported version of clang-format installed on this system. ' + f'List of supported versions: {CLANG_FORMAT_VERSIONS}.' + ) + + +########################################################### +# CHECK STYLE +########################################################### +def check_style(path: str, + enable_check_formatting: bool, + enable_check_whitespace: bool, + enable_check_tabs: bool, + fix: bool, + n_jobs: int = 1, + ) -> None: + """ + Check / fix the coding style of a list of files, including formatting and + trailing whitespace. + + @param path Path to the files. + @param fix Whether to fix the style of the file (True) or + just check if the file is well-formatted (False). + @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 n_jobs Number of parallel jobs. + """ + + (files_to_check_formatting, + files_to_check_whitespace, + files_to_check_tabs) = find_files_to_check_style(path) + + check_formatting_successful = True + check_whitespace_successful = True + check_tabs_successful = True + + if enable_check_formatting: + check_formatting_successful = check_formatting( + files_to_check_formatting, fix, n_jobs) + + print('') + + if enable_check_whitespace: + check_whitespace_successful = check_trailing_whitespace( + files_to_check_whitespace, fix, n_jobs) + + print('') + + if enable_check_tabs: + check_whitespace_successful = check_tabs( + files_to_check_tabs, fix, n_jobs) + + if check_formatting_successful and \ + check_whitespace_successful and \ + check_tabs_successful: + sys.exit(0) + else: + sys.exit(1) + + +########################################################### +# CHECK FORMATTING +########################################################### +def check_formatting(filenames: List[str], fix: bool, n_jobs: int) -> bool: + """ + Check / fix the coding style of a list of files with clang-format. + + @param filenames List of filenames to be checked. + @param fix Whether to fix the formatting of the file (True) or + just check if the file is well-formatted (False). + @param n_jobs Number of parallel jobs. + @return True if all files are well formatted after the check process. + False if there are non-formatted files after the check process. + """ + + # Check files + clang_format_path = find_clang_format_path() + files_not_formatted: List[str] = [] + + with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor: + files_not_formatted_results = executor.map( + check_formatting_file, + filenames, + itertools.repeat(clang_format_path), + itertools.repeat(fix), + ) + + for (filename, formatted) in files_not_formatted_results: + if not formatted: + files_not_formatted.append(filename) + + files_not_formatted.sort() + + # Output results + if not files_not_formatted: + print('All files are well formatted') + return True + + else: + n_non_formatted_files = len(files_not_formatted) + + if fix: + print(f'Fixed formatting of the files ({n_non_formatted_files}):') + else: + print(f'Detected bad formatting in the files ({n_non_formatted_files}):') + + for f in files_not_formatted: + print(f'- {f}') + + # Return True if all files were fixed + return fix + + +def check_formatting_file(filename: str, + clang_format_path: str, + fix: bool, + ) -> Tuple[str, bool]: + """ + Check / fix the coding style of a file with clang-format. + + @param filename Name of the file to be checked. + @param clang_format_path Path to clang-format. + @param fix Whether to fix the style of the file (True) or + just check if the file is well-formatted (False). + @return Tuple [Filename, Whether the file is well-formatted]. + """ + + # Check if the file is well formatted + process = subprocess.run( + [ + clang_format_path, + filename, + '-style=file', + '--dry-run', + '--Werror', + # Optimization: Only 1 error is needed to check that the file is not formatted + '--ferror-limit=1', + ], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + file_formatted = (process.returncode == 0) + + # Fix file + if fix and not file_formatted: + process = subprocess.run( + [ + clang_format_path, + filename, + '-style=file', + '-i', + ], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + return (filename, file_formatted) + + +########################################################### +# CHECK TRAILING WHITESPACE +########################################################### +def check_trailing_whitespace(filenames: List[str], fix: bool, n_jobs: int) -> bool: + """ + Check / fix trailing whitespace in a list of files. + + @param filename Name of the file to be checked. + @param fix Whether to fix the file (True) or + just check if it has trailing whitespace (False). + @param n_jobs Number of parallel jobs. + @return True if no files have trailing whitespace after the check process. + False if there are trailing whitespace after the check process. + """ + + # Check files + files_with_whitespace: List[str] = [] + + with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor: + files_with_whitespace_results = executor.map( + check_trailing_whitespace_file, + filenames, + itertools.repeat(fix), + ) + + for (filename, has_whitespace) in files_with_whitespace_results: + if has_whitespace: + files_with_whitespace.append(filename) + + files_with_whitespace.sort() + + # Output results + if not files_with_whitespace: + print('No files detected with trailing whitespace') + return True + + else: + n_files_with_whitespace = len(files_with_whitespace) + + if fix: + print( + f'Fixed trailing whitespace in the files ({n_files_with_whitespace}):') + else: + print( + f'Detected trailing whitespace in the files ({n_files_with_whitespace}):') + + for f in files_with_whitespace: + print(f'- {f}') + + # If all files were fixed, there are no more trailing whitespace + return fix + + +def check_trailing_whitespace_file(filename: str, fix: bool) -> Tuple[str, bool]: + """ + Check / fix trailing whitespace in a file. + + @param filename Name of the file to be checked. + @param fix Whether to fix the file (True) or + just check if it has trailing whitespace (False). + @return Tuple [Filename, Whether the file has trailing whitespace]. + """ + + has_trailing_whitespace = False + + with open(filename, 'r', encoding='utf-8') as f: + file_lines = f.readlines() + + # Check if there are trailing whitespace and fix them + for (i, line) in enumerate(file_lines): + line_fixed = line.rstrip() + '\n' + + if line_fixed != line: + has_trailing_whitespace = True + + # Optimization: if only checking, skip the rest of the file + if not fix: + break + + file_lines[i] = line_fixed + + # Update file with the fixed lines + if fix and has_trailing_whitespace: + with open(filename, 'w', encoding='utf-8') as f: + f.writelines(file_lines) + + return (filename, has_trailing_whitespace) + + +########################################################### +# CHECK TABS +########################################################### +def check_tabs(filenames: List[str], fix: bool, n_jobs: int) -> bool: + """ + Check / fix tabs in a list of files. + + @param filename Name of the file to be checked. + @param fix Whether to fix the file (True) or just check if it has tabs (False). + @param n_jobs Number of parallel jobs. + @return True if no files have tabs after the check process. + False if there are tabs after the check process. + """ + + # Check files + files_with_tabs: List[str] = [] + + with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor: + files_with_tabs_results = executor.map( + check_tabs_file, + filenames, + itertools.repeat(fix), + ) + + for (filename, has_tabs) in files_with_tabs_results: + if has_tabs: + files_with_tabs.append(filename) + + files_with_tabs.sort() + + # Output results + if not files_with_tabs: + print('No files detected with tabs') + return True + + else: + n_files_with_tabs = len(files_with_tabs) + + if fix: + print( + f'Fixed tabs in the files ({n_files_with_tabs}):') + else: + print( + f'Detected tabs in the files ({n_files_with_tabs}):') + + for f in files_with_tabs: + print(f'- {f}') + + # If all files were fixed, there are no more trailing whitespace + return fix + + +def check_tabs_file(filename: str, fix: bool) -> Tuple[str, bool]: + """ + Check / fix tabs in a file. + + @param filename Name of the file to be checked. + @param fix Whether to fix the file (True) or just check if it has tabs (False). + @return Tuple [Filename, Whether the file has tabs]. + """ + + has_tabs = False + + with open(filename, 'r', encoding='utf-8') as f: + file_lines = f.readlines() + + # Check if there are tabs and fix them + for (i, line) in enumerate(file_lines): + if line.find('\t') != -1: + has_tabs = True + + # Optimization: if only checking, skip the rest of the file + if not fix: + break + + file_lines[i] = line.expandtabs(TAB_SIZE) + + # Update file with the fixed lines + if fix and has_tabs: + with open(filename, 'w', encoding='utf-8') as f: + f.writelines(file_lines) + + return (filename, has_tabs) + + +########################################################### +# MAIN +########################################################### +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. ' + '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 ' + 'non-zero code. When used in "fix mode", this script automatically fixes the files.') + + parser.add_argument('path', action='store', type=str, + help='Path to the files to check') + + parser.add_argument('--no-formatting', action='store_true', + help='Do not check / fix code formatting') + + parser.add_argument('--no-whitespace', action='store_true', + help='Do not check / fix trailing whitespace') + + parser.add_argument('--no-tabs', action='store_true', + help='Do not check / fix tabs') + + parser.add_argument('--fix', action='store_true', + help='Fix coding style issues detected in the files') + + parser.add_argument('-j', '--jobs', type=int, default=max(1, os.cpu_count() - 1), + help='Number of parallel jobs') + + args = parser.parse_args() + + try: + check_style( + path=args.path, + enable_check_formatting=(not args.no_formatting), + enable_check_whitespace=(not args.no_whitespace), + enable_check_tabs=(not args.no_tabs), + fix=args.fix, + n_jobs=args.jobs, + ) + + except Exception as e: + print(e) + sys.exit(1) diff --git a/utils/check-style.py b/utils/check-style.py deleted file mode 100755 index 2fa0d3f7c..000000000 --- a/utils/check-style.py +++ /dev/null @@ -1,568 +0,0 @@ -#!/usr/bin/env python3 - -import os -import subprocess -import tempfile -import sys -import filecmp -import optparse -import shutil -import difflib -import re - -def git_modified_files(): - files = os.popen('git diff --name-only') - process = subprocess.Popen(["git","rev-parse","--show-toplevel"], - stdout = subprocess.PIPE, - stderr = subprocess.PIPE) - root_dir, _ = process.communicate() - if isinstance(root_dir, bytes): - root_dir=root_dir.decode("utf-8") - files_changed = [item.strip() for item in files.readlines()] - files_changed = [item for item in files_changed if item.endswith('.h') or item.endswith('.cc')] - return [root_dir[: -1] + "/" + filename.strip () for filename in files_changed] - -def copy_file(filename): - _, pathname = tempfile.mkstemp() - with open(filename, 'r') as src, open(pathname, 'w') as dst: - for line in src: - dst.write(line) - return pathname - -# generate a temporary configuration file -def uncrustify_config_file(level): - level2 = """ -nl_if_brace=Add -nl_brace_else=Add -nl_elseif_brace=Add -nl_else_brace=Add -nl_while_brace=Add -nl_do_brace=Add -nl_for_brace=Add -nl_brace_while=Add -nl_switch_brace=Add -nl_after_case=True -nl_namespace_brace=ignore -nl_after_brace_open=True -nl_class_leave_one_liners=False -nl_enum_leave_one_liners=False -nl_func_leave_one_liners=False -nl_if_leave_one_liners=False -nl_class_colon=Ignore -nl_before_access_spec=2 -nl_after_access_spec=0 -indent_access_spec=-indent_columns -nl_after_semicolon=True -pos_class_colon=Lead -pos_class_comma=Trail -indent_constr_colon=true -pos_bool=Lead -nl_class_init_args=Add -nl_template_class=Add -nl_class_brace=Add -# does not work very well -nl_func_type_name=Ignore -nl_func_scope_name=Ignore -nl_func_type_name_class=Ignore -nl_func_proto_type_name=Ignore -# function\\n( -nl_func_paren=Remove -nl_fdef_brace=Add -nl_struct_brace=Add -nl_enum_brace=Add -nl_union_brace=Add -mod_full_brace_do=Add -mod_full_brace_for=Add -mod_full_brace_if=Add -mod_full_brace_while=Add -mod_full_brace_for=Add -mod_remove_extra_semicolon=True -# max code width -#code_width=128 -#ls_for_split_full=True -#ls_func_split_full=True -nl_cpp_lambda_leave_one_liners=True -""" - level1 = """ -# extra spaces here and there -sp_brace_typedef=Add -sp_enum_assign=Add -sp_before_sparen=Add -sp_after_semi_for=Add -sp_arith=Add -sp_assign=Add -sp_compare=Add -sp_func_class_paren=Add -sp_after_type=Add -sp_type_func=Add -sp_angle_paren=Add -""" - level0 = """ -sp_func_proto_paren=Add -sp_func_def_paren=Add -sp_func_call_paren=Add -sp_after_semi_for=Ignore -sp_before_sparen=Ignore -sp_before_ellipsis=Remove -sp_type_func=Ignore -sp_after_type=Ignore -nl_class_leave_one_liners=True -nl_enum_leave_one_liners=True -nl_func_leave_one_liners=True -nl_assign_leave_one_liners=True -nl_collapse_empty_body=True -nl_getset_leave_one_liners=True -nl_if_leave_one_liners=True -nl_fdef_brace=Ignore -# finally, indentation configuration -indent_with_tabs=0 -indent_namespace=false -indent_columns=2 -indent_brace=2 -indent_case_brace=indent_columns -indent_class=true -indent_class_colon=True -indent_switch_case=indent_columns -# alignment -indent_align_assign=False -align_left_shift=True -# comment reformating disabled -cmt_reflow_mode=1 # do not touch comments at all -cmt_indent_multi=False # really, do not touch them -disable_processing_cmt= " *NS_CHECK_STYLE_OFF*" -enable_processing_cmt= " *NS_CHECK_STYLE_ON*" -""" - _, pathname = tempfile.mkstemp() - with open(pathname, 'w') as dst: - dst.write(level0) - if level >= 1: - dst.write(level1) - if level >= 2: - dst.write(level2) - return pathname - -## PatchChunkLine class -class PatchChunkLine: - ## @var __type - # type - ## @var __line - # line - ## @var SRC - # Source - SRC = 1 - ## @var DST - # Destination - DST = 2 - ## @var BOTH - # Both - BOTH = 3 - def __init__(self): - """! Initializer - @param self The current class - """ - self.__type = 0 - self.__line = '' - def set_src(self,line): - """! Set source - @param self The current class - @param line source line - @return none - """ - self.__type = self.SRC - self.__line = line - def set_dst(self,line): - """! Set destination - @param self The current class - @param line destination line - @return none - """ - self.__type = self.DST - self.__line = line - def set_both(self,line): - """! Set both - @param self The current class - @param line - @return none - """ - self.__type = self.BOTH - self.__line = line - def append_to_line(self, s): - """! Append to line - @param self The current class - @param s line to append - @return none - """ - self.__line = self.__line + s - def line(self): - """! Get line - @param self The current class - @return line - """ - return self.__line - def is_src(self): - """! Is source - @param self The current class - @return true if type is source - """ - return self.__type == self.SRC or self.__type == self.BOTH - def is_dst(self): - """! Is destination - @param self The current class - @return true if type is destination - """ - return self.__type == self.DST or self.__type == self.BOTH - def write(self, f): - """! Write to file - @param self The current class - @param f file - @return exception if invalid type - """ - if self.__type == self.SRC: - f.write('-%s\n' % self.__line) - elif self.__type == self.DST: - f.write('+%s\n' % self.__line) - elif self.__type == self.BOTH: - f.write(' %s\n' % self.__line) - else: - raise Exception('invalid patch') - -## PatchChunk class -class PatchChunk: - ## @var __lines - # list of lines - ## @var __src_pos - # source position - ## @var __dst_pos - # destination position - ## @var src - # source - ## @var dst - # destination - def __init__(self, src_pos, dst_pos): - """! Initializer - @param self: this object - @param src_pos: source position - @param dst_pos: destination position - """ - self.__lines = [] - self.__src_pos = int(src_pos) - self.__dst_pos = int(dst_pos) - def src_start(self): - """! Source start function - @param self this object - @return source position - """ - return self.__src_pos - def add_line(self,line): - """! Add line function - @param self The current class - @param line line to add - @return none - """ - self.__lines.append(line) - def src(self): - """! Get source lines - @param self The current class - @return the source lines - """ - src = [] - for line in self.__lines: - if line.is_src(): - src.append(line) - return src - def dst(self): - """! Get destination lines - @param self The current class - @return the destination lines - """ - dst = [] - for line in self.__lines: - if line.is_dst(): - dst.append(line) - return dst - def src_len(self): - """! Get number of source lines - @param self The current class - @return number of source lines - """ - return len(self.src()) - def dst_len(self): - """! Get number of destinaton lines - @param self The current class - @return number of destination lines - """ - return len(self.dst()) - def write(self,f): - """! Write lines to file - @param self The current class - @param f: file to write to - @return none - """ - f.write('@@ -%d,%d +%d,%d @@\n' % (self.__src_pos, self.src_len(), - self.__dst_pos, self.dst_len())) - for line in self.__lines: - line.write(f) - -## Patch class -class Patch: - ## @var __src - # source - ## @var __dst - # destination - ## @var __chunks - # chunks - def __init__(self): - """! Initializer - @param self The current class - """ - self.__src = '' - self.__dst = '' - self.__chunks = [] - def add_chunk(self, chunk): - """! Add chunk - @param self this object - @param chunk chunk - @return none - """ - self.__chunks.append(chunk) - def chunks(self): - """! Get the chunks - @param self The current class - @return the chunks - """ - return self.__chunks - def set_src(self,src): - """! Set source - @param self this object - @param src source - @return none - """ - self.__src = src - def set_dst(self,dst): - """! Set destination - @param self this object - @param dst destintion - @return none - """ - self.__dst = dst - def apply(self,filename): - """! Apply function - @param self The current class - @param filename file name - @return none - """ - # XXX: not implemented - return - def write(self,f): - """! Write to file - @param self The current class - @param f the file - @return none - """ - f.write('--- %s\n' % self.__src ) - f.write('+++ %s\n' % self.__dst ) - for chunk in self.__chunks: - chunk.write(f) - -def parse_patchset(generator): - src_file = re.compile('^--- (.*)$') - dst_file = re.compile('^\+\+\+ (.*)$') - chunk_start = re.compile('^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@') - src = re.compile('^-(.*)$') - dst = re.compile('^\+(.*)$') - both = re.compile('^ (.*)$') - patchset = [] - current_patch = None - for line in generator: - m = src_file.search(line) - if m is not None: - current_patch = Patch() - patchset.append(current_patch) - current_patch.set_src(m.group(1)) - continue - m = dst_file.search(line) - if m is not None: - current_patch.set_dst(m.group(1)) - continue - m = chunk_start.search(line) - if m is not None: - current_chunk = PatchChunk(m.group(1), m.group(3)) - current_patch.add_chunk(current_chunk) - continue - m = src.search(line) - if m is not None: - l = PatchChunkLine() - l.set_src(m.group(1)) - current_chunk.add_line(l) - continue - m = dst.search(line) - if m is not None: - l = PatchChunkLine() - l.set_dst(m.group(1)) - current_chunk.add_line(l) - continue - m = both.search(line) - if m is not None: - l = PatchChunkLine() - l.set_both(m.group(1)) - current_chunk.add_line(l) - continue - raise Exception() - return patchset - -def remove_trailing_whitespace_changes(patch_generator): - whitespace = re.compile('^(.*)([ \t]+)$') - patchset = parse_patchset(patch_generator) - for patch in patchset: - for chunk in patch.chunks(): - src = chunk.src() - dst = chunk.dst() - try: - for i in range(0,len(src)): - s = src[i] - d = dst[i] - m = whitespace.search(s.line()) - if m is not None and m.group(1) == d.line(): - d.append_to_line(m.group(2)) - except: - return patchset - return patchset - - -def indent(source, debug, level): - output = tempfile.mkstemp()[1] - # apply uncrustify - cfg = uncrustify_config_file(level) - if debug: - sys.stderr.write('original file=' + source + '\n') - sys.stderr.write('uncrustify config file=' + cfg + '\n') - sys.stderr.write('temporary file=' + output + '\n') - try: - uncrust = subprocess.Popen(['uncrustify', '-c', cfg, '-f', source, '-o', output], - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - stderr = subprocess.PIPE, - universal_newlines = True) - (out, err) = uncrust.communicate('') - if debug: - sys.stderr.write(out) - sys.stderr.write(err) - except OSError: - raise Exception ('uncrustify not installed') - # generate a diff file - with open(source, 'r') as src, open(output, 'r') as dst: - diff = difflib.unified_diff(src.readlines(), dst.readlines(), - fromfile=source, tofile=output) - if debug: - initial_diff = tempfile.mkstemp()[1] - sys.stderr.write('initial diff file=' + initial_diff + '\n') - with open(initial_diff, 'w') as tmp: - tmp.writelines(diff) - final_diff = tempfile.mkstemp()[1] - if level < 3: - patchset = remove_trailing_whitespace_changes(diff) - if len(patchset) != 0: - with open(final_diff, 'w') as dst: - patchset[0].write(dst) - else: - with open(final_diff, 'w') as dst: - dst.writelines(diff) - - - # apply diff file - if debug: - sys.stderr.write('final diff file=' + final_diff + '\n') - shutil.copyfile(source,output) - patch = subprocess.Popen(['patch', '-p1', '-i', final_diff, output], - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - stderr = subprocess.PIPE, - universal_newlines = True) - (out, err) = patch.communicate('') - if debug: - sys.stderr.write(out) - sys.stderr.write(err) - return output - - - -def indent_files(files, diff=False, debug=False, level=0, inplace=False): - output = [] - for f in files: - dst = indent(f, debug=debug, level=level) - output.append([f,dst]) - - # First, copy to inplace - if inplace: - for src,dst in output: - shutil.copyfile(dst,src) - return True - - # now, compare - failed = [] - for src,dst in output: - if filecmp.cmp(src,dst) == 0: - failed.append([src, dst]) - if len(failed) > 0: - if not diff: - print('Found %u badly indented files:' % len(failed)) - for src,dst in failed: - print(' ' + src) - else: - for src,dst in failed: - with open(src, 'r') as f_src, open(dst, 'r') as f_dst: - s = f_src.readlines() - d = f_dst.readlines() - for line in difflib.unified_diff(s, d, fromfile=src, tofile=dst): - sys.stdout.write(line) - return False - return True - -def run_as_main(): - parser = optparse.OptionParser() - parser.add_option('--debug', action='store_true', dest='debug', default=False, - help='Output some debugging information') - parser.add_option('-l', '--level', type='int', dest='level', default=0, - help="Level of style conformance: higher levels include all lower levels. " - "level=0: re-indent only. level=1: add extra spaces. level=2: insert extra newlines and " - "extra braces around single-line statements. level=3: remove all trailing spaces") - parser.add_option('--check-git', action='store_true', dest='git', default=False, - help="Get the list of files to check from Git\'s list of modified and added files") - parser.add_option('-f', '--check-file', action='store', dest='file', default='', - help="Check a single file") - parser.add_option('--diff', action='store_true', dest='diff', default=False, - help="Generate a diff on stdout of the indented files") - parser.add_option('-i', '--in-place', action='store_true', dest='in_place', default=False, - help="Indent the input files in-place") - options, _ = parser.parse_args() - style_is_correct = False - - if options.git: - files = git_modified_files() - style_is_correct = indent_files(files, - diff=options.diff, - debug=options.debug, - level=options.level, - inplace=options.in_place) - elif options.file != '': - file = options.file - if not os.path.exists(file) or \ - not os.path.isfile(file): - print('file %s does not exist' % file) - sys.exit(1) - style_is_correct = indent_files([file], - diff=options.diff, - debug=options.debug, - level=options.level, - inplace=options.in_place) - - if not style_is_correct: - sys.exit(1) - sys.exit(0) - -if __name__ == '__main__': - try: - run_as_main() - except Exception as e: - sys.stderr.write(str(e) + '\n') - sys.exit(1) diff --git a/utils/trim-trailing-whitespace.py b/utils/trim-trailing-whitespace.py deleted file mode 100755 index a7df45bb7..000000000 --- a/utils/trim-trailing-whitespace.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (c) 2022 Eduardo Almeida. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 as -# published by the Free Software Foundation; -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Author: Eduardo Almeida [@edalm] [INESC TEC and FEUP] - -""" -Check and trim trailing whitespace in files indicated in the PATH argument. -This script can be applied to all text files in a given path or to -individual files. -""" - -import argparse -import os -import sys - -FILE_EXTENSIONS_TO_CHECK = [ - '.c', - '.cc', - '.click', - '.conf', - '.css', - '.dot', - '.gnuplot', - '.gp', - '.h', - '.html', - '.js', - '.json', - '.m', - '.md', - '.mob', - '.ns_params', - '.ns_movements', - '.params', - '.pl', - '.plt', - '.py', - '.rst', - '.seqdiag', - '.sh', - '.txt', - '.yml', -] - -FILES_TO_CHECK = [ - 'Makefile', - 'ns3', -] - -DIRECTORIES_TO_SKIP = [ - 'bindings', - 'build', - 'cmake-cache', -] - -def skip_file(filename: str) -> bool: - """ - Check if a file should be skipped. - - @param filename Name of the file. - @return Whether the directory should be skipped or not. - """ - - basename, extension = os.path.splitext(os.path.split(filename)[1]) - - return basename not in FILES_TO_CHECK and \ - extension not in FILE_EXTENSIONS_TO_CHECK - -def skip_directory(dirpath: str) -> bool: - """ - Check if a directory should be skipped. - - @param dirpath Directory path. - @return Whether the directory should be skipped or not. - """ - - _, directory = os.path.split(dirpath) - - return (directory.startswith('.') and directory != '.') or \ - directory in DIRECTORIES_TO_SKIP - -def trim_trailing_whitespace(path: str, trim: bool) -> None: - """ - Trim trailing whitespace in all text files in the given path. - - @param path Path to the files. - @param trim Whether to trim the file (True) or - just check if the file has trailing whitespace (False). - """ - - files_with_trailing_whitespace = [] - - abs_path = os.path.normpath(os.path.abspath(os.path.expanduser(path))) - - if os.path.isfile(abs_path): - - if not skip_file(abs_path): - file_had_whitespace = trim_file(abs_path, trim) - - if file_had_whitespace: - files_with_trailing_whitespace.append(path) - - elif os.path.isdir(abs_path): - - for dirpath, dirnames, filenames in os.walk(path, topdown=True): - - # Check if directory and its subdirectories should be skipped - if skip_directory(dirpath): - dirnames[:] = [] - continue - - # Process files with trailing whitespace - filenames = [os.path.join(dirpath, f) for f in filenames] - - for filename in filenames: - - # Skip files that should not be checked - if skip_file(filename): - continue - - file_had_whitespace = trim_file( - os.path.normpath(os.path.abspath(os.path.expanduser(filename))), - trim, - ) - - if file_had_whitespace: - files_with_trailing_whitespace.append(filename) - - else: - print(f'Error: {path} is not a file nor a directory') - sys.exit(1) - - # Output results - n_files_with_trailing_whitespace = len(files_with_trailing_whitespace) - - if files_with_trailing_whitespace: - if trim: - print('Trimmed files with trailing whitespace:\n') - else: - print('Detected files with trailing whitespace:\n') - - for f in files_with_trailing_whitespace: - print(f'- {f}') - - if trim: - print(f'\n' - f'Number of files trimmed: {n_files_with_trailing_whitespace}') - else: - print(f'\n' - f'Number of files with trailing whitespace: {n_files_with_trailing_whitespace}') - sys.exit(1) - - else: - print('No files detected with trailing whitespace') - -def trim_file(filename: str, trim: bool) -> bool: - """ - Trim trailing whitespace in a given file. - - @param filename Name of the file to be trimmed. - @param trim Whether to trim the file (True) or - just check if the file has trailing whitespace (False). - @return Whether the file had trailing whitespace (True) or not (False). - """ - - has_trailing_whitespace = False - - try: - with open(filename, 'r') as f: - file_lines = f.readlines() - - # Check if there are trailing whitespace and trim them - for (i, line) in enumerate(file_lines): - line_trimmed = line.rstrip() + '\n' - - if line_trimmed != line: - has_trailing_whitespace = True - - # Optimization: if only checking, skip the rest of the file, - # since it does have trailing whitespace - if not trim: - break - - file_lines[i] = line_trimmed - - # Update the file with the trimmed lines - if trim and has_trailing_whitespace: - with open(filename, 'w') as f: - f.writelines(file_lines) - - except Exception as e: - print(e) - sys.exit(1) - - return has_trailing_whitespace - -if __name__ == '__main__': - - parser = argparse.ArgumentParser(description= - 'Trim trailing whitespace in all text files in a given PATH.') - - parser.add_argument('path', action='store', type=str, help= - 'Path to the files.') - - parser.add_argument('--check', action='store_true', help= - 'Check if the files have trailing whitespace, without modifying them. ' - 'If they have, this process will exit with a non-zero exit code ' - 'and list them in the output.') - - args = parser.parse_args() - - trim_trailing_whitespace(args.path, trim=(not args.check)) diff --git a/utils/utils.h b/utils/utils.h deleted file mode 100644 index e6989e0ea..000000000 --- a/utils/utils.h +++ /dev/null @@ -1,80 +0,0 @@ -/** - * \ingroup utils - * \defgroup CheckStyle check-style.py - * - * The check-style.py script will test and reformat code according to the - * ns-3 coding style posted at https://www.nsnam.org/developers/contributing-code/coding-style/ - * It requires that you install 'uncrustify' - * - * It has multiple levels of conformance: - * - level=0: the default: merely checks indentation - * - level=1: checks also for missing spaces before parentheses - * - level=2: checks also for missing newlines and braces around single-line statements - * - level=3: checks also for missing trailing whitespaces - * - * Examples: - * - * check a single file (level 0 by default): -\verbatim -./check-style.py -f src/core/object.h -\endverbatim - * - * fix the style of a single file: -\verbatim -./check-style.py --level=2 --in-place -f src/core/object.h -\endverbatim - * - * look at the changes needed for a single file: -\verbatim -./check-style.py --diff --level=1 -f src/core/object.h | less -\endverbatim - * - * look at the status of all files modified in your mercurial repository: -\verbatim -./check-style.py --check-hg -\endverbatim - * - * look at the changes needed for all modified files in your mercurial - * repository: -\verbatim -./check-style.py --check-hg --diff |less -\endverbatim - * - * Enable this script to run as a 'commit' hook in your repository and - * disallow commits which contain files with invalid style: - * -\verbatim -cat hgrc (can be appended to .hg/hgrc or ~/.hg/hgrc or /etc/hg/hgrc -[hooks] -# uncomment below line to enable: works only with mercurial >= 1.3 -#pretxncommit.indent = path-to-binary/check-indent.py --check-hg-hook -# uncomment below line to enable: works with all (?) versions -# of mercurial but requires that PYTHONPATH is defined to point to -# the directory which contains check-indent.py -#pretxncommit.indent = python:check-indent.run_as_hg_hook -\endverbatim - * - * Usage: -\verbatim -Usage: check-style.py [options] - -Options: - -h, --help show this help message and exit - --debug Output some debugging information - -l LEVEL, --level=LEVEL - Level of style conformance: higher levels include all - lower levels. level=0: re-indent only. level=1: add - extra spaces. level=2: insert extra newlines and extra - braces around single-line statements. level=3: remove - all trailing spaces - --check-hg-hook Get the list of files to check from mercurial's list - of modified and added files and assume that the script - runs as a pretxncommit mercurial hook - --check-hg Get the list of files to check from mercurial's list - of modified and added files - -f FILE, --check-file=FILE - Check a single file - --diff Generate a diff on stdout of the indented files - -i, --in-place Indent the input files in-place -\endverbatim - */