utils: Add new "check-style-clang-format.py"

- Remove deprecated scripts "trim-trailing-whitespace.py" and "check-style.py".
- Remove deprecated "utils.h".
This commit is contained in:
Eduardo Almeida
2022-09-07 18:38:22 +01:00
parent 731d12b355
commit abe2b2a388
4 changed files with 626 additions and 874 deletions

626
utils/check-style-clang-format.py Executable file
View File

@@ -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 <enmsa@outlook.pt> [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)

View File

@@ -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)

View File

@@ -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))

View File

@@ -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
*/