Files
unison/utils/check-style-clang-format.py
2025-09-30 20:11:28 +01:00

1130 lines
35 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (c) 2022 Eduardo Nuno Almeida.
#
# SPDX-License-Identifier: GPL-2.0-only
#
# Author: Eduardo Nuno Almeida <enmsa@outlook.pt> [INESC TEC and FEUP, Portugal]
"""
Check and apply the ns-3 coding style recursively to all files in the PATH arguments.
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. Respects clang-format guards.
- Check / fix local #include headers with "ns3/" prefix. Respects clang-format guards.
- Check / fix ns-3 #include headers using angle brackets <> rather than quotes "". Respects clang-format guards.
- Check / fix Doxygen tags using @ rather than \\. Respects clang-format guards.
- Check / fix SPDX licenses rather than GPL text. Respects clang-format guards.
- Check / fix emacs file style comments. Respects clang-format guards.
- Check / trim trailing whitespace. Always checked.
- Check / replace tabs with spaces. Respects clang-format guards.
- Check file encoding. Always checked.
This script can be applied to all text files in a given path or to individual files.
NOTE: The formatting check requires clang-format to be found on the path (see the supported versions below).
The remaining checks do not depend on clang-format and can be executed by disabling clang-format
checking with the "--no-formatting" option.
"""
import argparse
import concurrent.futures
import itertools
import os
import re
import shutil
import subprocess
import sys
from typing import Callable, Dict, List, Tuple
###########################################################
# PARAMETERS
###########################################################
CLANG_FORMAT_MAX_VERSION = 20
CLANG_FORMAT_MIN_VERSION = 20
FORMAT_GUARD_ON = [
"// clang-format on",
"# cmake-format: on",
"# fmt: on",
]
FORMAT_GUARD_OFF = [
"// clang-format off",
"# cmake-format: off",
"# fmt: off",
]
DIRECTORIES_TO_SKIP = [
"__pycache__",
".git",
".venv",
"bindings",
"build",
"cmake-cache",
"testpy-output",
"venv",
]
# List of files entirely copied from elsewhere that should not be checked,
# in order to optimize the performance of this script
FILES_TO_SKIP = [
"valgrind.h",
]
# List of checks
CHECKS = [
"include_prefixes",
"include_quotes",
"doxygen_tags",
"license",
"emacs",
"whitespace",
"tabs",
"formatting",
"encoding",
]
# Files to check
FILES_TO_CHECK: Dict[str, List[str]] = {c: [] for c in CHECKS}
FILES_TO_CHECK["tabs"] = [
".clang-format",
".clang-tidy",
".codespellrc",
"CMakeLists.txt",
"codespell-ignored-lines",
"codespell-ignored-words",
"ns3",
]
FILES_TO_CHECK["whitespace"] = FILES_TO_CHECK["tabs"] + [
"Makefile",
]
# File extensions to check
FILE_EXTENSIONS_TO_CHECK: Dict[str, List[str]] = {c: [] for c in CHECKS}
FILE_EXTENSIONS_TO_CHECK["formatting"] = [
".c",
".cc",
".h",
]
FILE_EXTENSIONS_TO_CHECK["include_prefixes"] = FILE_EXTENSIONS_TO_CHECK["formatting"]
FILE_EXTENSIONS_TO_CHECK["include_quotes"] = FILE_EXTENSIONS_TO_CHECK["formatting"]
FILE_EXTENSIONS_TO_CHECK["doxygen_tags"] = FILE_EXTENSIONS_TO_CHECK["formatting"]
FILE_EXTENSIONS_TO_CHECK["encoding"] = FILE_EXTENSIONS_TO_CHECK["formatting"]
FILE_EXTENSIONS_TO_CHECK["license"] = [
".c",
".cc",
".cmake",
".h",
".py",
]
FILE_EXTENSIONS_TO_CHECK["emacs"] = [
".c",
".cc",
".h",
".py",
".rst",
]
FILE_EXTENSIONS_TO_CHECK["tabs"] = [
".c",
".cc",
".cmake",
".css",
".h",
".html",
".js",
".json",
".m",
".md",
".pl",
".py",
".rst",
".sh",
".toml",
".yml",
]
FILE_EXTENSIONS_TO_CHECK["whitespace"] = FILE_EXTENSIONS_TO_CHECK["tabs"] + [
".click",
".cfg",
".conf",
".dot",
".gnuplot",
".gp",
".mob",
".ns_params",
".ns_movements",
".params",
".plt",
".seqdiag",
".txt",
]
# Other check parameters
TAB_SIZE = 4
FILE_ENCODING = "UTF-8"
###########################################################
# AUXILIARY FUNCTIONS
###########################################################
def should_analyze_directory(dirpath: str) -> bool:
"""
Check whether a directory should be analyzed.
@param dirpath Directory path.
@return Whether the directory should be analyzed.
"""
_, directory = os.path.split(dirpath)
return directory not in DIRECTORIES_TO_SKIP
def should_analyze_file(
path: str,
files_to_check: List[str],
file_extensions_to_check: List[str],
) -> bool:
"""
Check whether a file should be analyzed.
@param path Path to the file.
@param files_to_check List of files that shall be checked.
@param file_extensions_to_check List of file extensions that shall be checked.
@return Whether the file should be analyzed.
"""
filename = os.path.split(path)[1]
if filename in FILES_TO_SKIP:
return False
extension = os.path.splitext(filename)[1]
return filename in files_to_check or extension in file_extensions_to_check
def find_files_to_check_style(
paths: List[str],
) -> Dict[str, List[str]]:
"""
Find all files to be checked in a given list of paths.
@param paths List of paths to the files to check.
@return Dictionary of checks and corresponding list of files to check.
Example: {
"formatting": list_of_files_to_check_formatting,
...,
}
"""
# Get list of files found in the given path
files_found: List[str] = []
for path in paths:
abs_path = os.path.abspath(os.path.expanduser(path))
if os.path.isfile(abs_path):
files_found.append(path)
elif os.path.isdir(abs_path):
for dirpath, dirnames, filenames in os.walk(path, topdown=True):
if not should_analyze_directory(dirpath):
# Remove directory and its subdirectories
dirnames[:] = []
continue
files_found.extend([os.path.join(dirpath, f) for f in filenames])
else:
raise ValueError(f"{path} is not a valid file nor a directory")
files_found.sort()
# Check which files should be checked
files_to_check: Dict[str, List[str]] = {c: [] for c in CHECKS}
for f in files_found:
for check in CHECKS:
if should_analyze_file(f, FILES_TO_CHECK[check], FILE_EXTENSIONS_TO_CHECK[check]):
files_to_check[check].append(f)
return files_to_check
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.
"""
# Find exact version, starting from the most recent one
for version in range(CLANG_FORMAT_MAX_VERSION, CLANG_FORMAT_MIN_VERSION - 1, -1):
clang_format_path = shutil.which(f"clang-format-{version}")
if clang_format_path:
return clang_format_path
# Find default version and check if it is supported
clang_format_path = shutil.which("clang-format")
major_version = None
if clang_format_path:
process = subprocess.run(
[clang_format_path, "--version"],
capture_output=True,
text=True,
check=True,
)
clang_format_version = process.stdout.strip()
version_regex = re.findall(r"\b(\d+)(\.\d+){0,2}\b", clang_format_version)
if version_regex:
major_version = int(version_regex[0][0])
if CLANG_FORMAT_MIN_VERSION <= major_version <= CLANG_FORMAT_MAX_VERSION:
return clang_format_path
# No supported version of clang-format found
raise RuntimeError(
f"Could not find any supported version of clang-format installed on this system. "
f"List of supported versions: [{CLANG_FORMAT_MAX_VERSION}-{CLANG_FORMAT_MIN_VERSION}]. "
+ (f"Found clang-format {major_version}." if major_version else "")
)
###########################################################
# CHECK STYLE MAIN FUNCTIONS
###########################################################
def check_style_clang_format(
paths: List[str],
checks_enabled: Dict[str, bool],
fix: bool,
verbose: bool,
n_jobs: int = 1,
) -> bool:
"""
Check / fix the coding style of a list of files.
@param paths List of paths to the files to check.
@param checks_enabled Dictionary of checks indicating whether to enable each of them.
@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 = find_files_to_check_style(paths)
checks_successful = {c: True for c in CHECKS}
style_check_strs = {
"include_prefixes": '#include headers from the same module with the "ns3/" prefix',
"include_quotes": 'ns-3 #include headers using angle brackets <> rather than quotes ""',
"doxygen_tags": "Doxygen tags using \\ rather than @",
"license": "GPL license text instead of SPDX license",
"emacs": "emacs file style comments",
"whitespace": "trailing whitespace",
"tabs": "tabs",
"formatting": "bad code formatting",
"encoding": f"bad file encoding ({FILE_ENCODING})",
}
check_style_file_functions_kwargs = {
"include_prefixes": {
"function": check_manually_file,
"kwargs": {
"respect_clang_format_guards": True,
"check_style_line_function": check_include_prefixes_line,
},
},
"include_quotes": {
"function": check_manually_file,
"kwargs": {
"respect_clang_format_guards": True,
"check_style_line_function": check_include_quotes_line,
},
},
"doxygen_tags": {
"function": check_manually_file,
"kwargs": {
"respect_clang_format_guards": True,
"check_style_line_function": check_doxygen_tags_line,
},
},
"license": {
"function": check_manually_file,
"kwargs": {
"respect_clang_format_guards": True,
"check_style_line_function": check_licenses_line,
},
},
"emacs": {
"function": check_manually_file,
"kwargs": {
"respect_clang_format_guards": True,
"check_style_line_function": check_emacs_line,
},
},
"whitespace": {
"function": check_manually_file,
"kwargs": {
"respect_clang_format_guards": False,
"check_style_line_function": check_whitespace_line,
},
},
"tabs": {
"function": check_manually_file,
"kwargs": {
"respect_clang_format_guards": True,
"check_style_line_function": check_tabs_line,
},
},
"formatting": {
"function": check_formatting_file,
"kwargs": {}, # The formatting keywords are added below
},
"encoding": {
"function": check_encoding_file,
"kwargs": {},
},
}
if checks_enabled["formatting"]:
check_style_file_functions_kwargs["formatting"]["kwargs"] = {
"clang_format_path": find_clang_format_path(),
}
n_checks_enabled = sum(checks_enabled.values())
n_check = 0
for check in CHECKS:
if checks_enabled[check]:
checks_successful[check] = check_style_files(
style_check_strs[check],
check_style_file_functions_kwargs[check]["function"],
files_to_check[check],
fix,
verbose,
n_jobs,
**check_style_file_functions_kwargs[check]["kwargs"],
)
n_check += 1
if n_check < n_checks_enabled:
print("")
return all(checks_successful.values())
def check_style_files(
style_check_str: str,
check_style_file_function: Callable[..., Tuple[str, bool, List[str]]],
filenames: List[str],
fix: bool,
verbose: bool,
n_jobs: int,
**kwargs,
) -> bool:
"""
Check / fix style of a list of files.
@param style_check_str Description of the check to be performed.
@param check_style_file_function Function used to check the file.
@param filename Name of the file to be checked.
@param fix Whether to fix (True) or just check (False) the file (True).
@param verbose Show the lines that are not compliant with the style.
@param n_jobs Number of parallel jobs.
@param kwargs Additional keyword arguments to the check_style_file_function.
@return Whether all files are compliant with the style.
"""
# Check files
non_compliant_files: List[str] = []
files_verbose_infos: Dict[str, List[str]] = {}
with concurrent.futures.ProcessPoolExecutor(n_jobs) as executor:
non_compliant_files_results = executor.map(
check_style_file_function,
filenames,
itertools.repeat(fix),
itertools.repeat(verbose),
*[arg if isinstance(arg, list) else itertools.repeat(arg) for arg in kwargs.values()],
)
for filename, is_file_compliant, verbose_infos in non_compliant_files_results:
if not is_file_compliant:
non_compliant_files.append(filename)
if verbose:
files_verbose_infos[filename] = verbose_infos
# Output results
if not non_compliant_files:
print(f"- No files detected with {style_check_str}")
return True
else:
n_non_compliant_files = len(non_compliant_files)
if fix:
print(f"- Fixed {style_check_str} in the files ({n_non_compliant_files}):")
else:
print(f"- Detected {style_check_str} in the files ({n_non_compliant_files}):")
for f in non_compliant_files:
if verbose:
print(*[f" {l}" for l in files_verbose_infos[f]], sep="\n")
else:
print(f" - {f}")
# If all files were fixed, there are no more non-compliant files
return fix
###########################################################
# CHECK STYLE FUNCTIONS
###########################################################
def check_formatting_file(
filename: str,
fix: bool,
verbose: bool,
clang_format_path: str,
) -> Tuple[str, bool, List[str]]:
"""
Check / fix the coding style of a file with clang-format.
@param filename Name of the file to be checked.
@param fix Whether to fix (True) or just check (False) the style of the file.
@param verbose Show the lines that are not compliant with the style.
@param clang_format_path Path to clang-format.
@return Tuple [Filename,
Whether the file is compliant with the style (before the check),
Verbose information].
"""
verbose_infos: List[str] = []
# Check if the file is well formatted
process = subprocess.run(
[
clang_format_path,
filename,
"-style=file",
"--dry-run",
"--Werror",
# Optimization: In non-verbose mode, only one error is needed to check that the file is not compliant
f"--ferror-limit={0 if verbose else 1}",
],
check=False,
capture_output=True,
text=True,
)
is_file_compliant = process.returncode == 0
if verbose:
verbose_infos = process.stderr.splitlines()
# Fix file
if fix and not is_file_compliant:
process = subprocess.run(
[
clang_format_path,
filename,
"-style=file",
"-i",
],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return (filename, is_file_compliant, verbose_infos)
def check_encoding_file(
filename: str,
fix: bool,
verbose: bool,
) -> Tuple[str, bool, List[str]]:
"""
Check / fix the encoding of a file.
@param filename Name of the file to be checked.
@param fix Whether to fix (True) or just check (False) the encoding of the file.
@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].
"""
verbose_infos: List[str] = []
is_file_compliant = True
with open(filename, "rb") as f:
file_data = f.read()
file_lines = file_data.decode(FILE_ENCODING, errors="replace").splitlines(keepends=True)
# Check if file has correct encoding
try:
file_data.decode(FILE_ENCODING)
except UnicodeDecodeError as e:
is_file_compliant = False
if verbose:
# Find line and column with bad encoding
bad_char_start_index = e.start
n_chars_file_read = 0
for line_number, line in enumerate(file_lines):
n_chars_line = len(line)
if bad_char_start_index < n_chars_file_read + n_chars_line:
bad_char_column = bad_char_start_index - n_chars_file_read
verbose_infos.extend(
[
f"{filename}:{line_number + 1}:{bad_char_column + 1}: error: bad {FILE_ENCODING} encoding",
f" {line.rstrip()}",
f" {'':>{bad_char_column}}^",
]
)
break
n_chars_file_read += n_chars_line
# Fix file encoding
if fix and not is_file_compliant:
with open(filename, "w", encoding=FILE_ENCODING) as f:
f.writelines(file_lines)
return (filename, is_file_compliant, verbose_infos)
def check_manually_file(
filename: str,
fix: bool,
verbose: bool,
respect_clang_format_guards: bool,
check_style_line_function: Callable[[str, str, int], Tuple[bool, str, List[str]]],
) -> Tuple[str, bool, List[str]]:
"""
Check / fix a file manually using a function to check / fix each line.
@param filename Name of the file to be checked.
@param fix Whether to fix (True) or just check (False) the style of the file.
@param verbose Show the lines that are not compliant with the style.
@param respect_clang_format_guards Whether to respect clang-format guards.
@param check_style_line_function Function used to check each line.
@return Tuple [Filename,
Whether the file is compliant with the style (before the check),
Verbose information].
"""
is_file_compliant = True
verbose_infos: List[str] = []
clang_format_enabled = True
with open(filename, "r", encoding=FILE_ENCODING) as f:
file_lines = f.readlines()
for i, line in enumerate(file_lines):
# Check clang-format guards
if respect_clang_format_guards:
line_stripped = line.strip()
if line_stripped in FORMAT_GUARD_ON:
clang_format_enabled = True
elif line_stripped in FORMAT_GUARD_OFF:
clang_format_enabled = False
if not clang_format_enabled and line_stripped not in (
FORMAT_GUARD_ON + FORMAT_GUARD_OFF
):
continue
# Check if the line is compliant with the style and fix it
(is_line_compliant, line_fixed, line_verbose_infos) = check_style_line_function(
line, filename, i
)
if not is_line_compliant:
is_file_compliant = False
file_lines[i] = line_fixed
verbose_infos.extend(line_verbose_infos)
# 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=FILE_ENCODING) as f:
f.writelines(file_lines)
return (filename, is_file_compliant, verbose_infos)
def check_include_prefixes_line(
line: str,
filename: str,
line_number: int,
) -> Tuple[bool, str, List[str]]:
"""
Check / fix #include headers from the same module with the "ns3/" prefix in a line.
@param line The line to check.
@param filename Name of the file to be checked.
@param line_number The number of the line checked.
@return Tuple [Whether the line is compliant with the style (before the check),
Fixed line,
Verbose information].
"""
is_line_compliant = True
line_fixed = line
verbose_infos: List[str] = []
# Check if the line is an #include and extract its header file
line_stripped = line.strip()
header_file = re.findall(r'^#include ["<]ns3/(.*\.h)[">]', line_stripped)
if header_file:
# 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 os.path.exists(os.path.join(parent_path, header_file)):
is_line_compliant = False
line_fixed = (
line_stripped.replace(f"ns3/{header_file}", header_file)
.replace("<", '"')
.replace(">", '"')
+ "\n"
)
header_index = len('#include "')
verbose_infos.extend(
[
f'{filename}:{line_number + 1}:{header_index + 1}: error: #include headers from the same module with the "ns3/" prefix detected',
f" {line_stripped}",
f" {'':>{header_index}}^",
]
)
return (is_line_compliant, line_fixed, verbose_infos)
def check_include_quotes_line(
line: str,
filename: str,
line_number: int,
) -> Tuple[bool, str, List[str]]:
"""
Check / fix ns-3 #include headers using angle brackets <> rather than quotes "" in a line.
@param line The line to check.
@param filename Name of the file to be checked.
@param line_number The number of the line checked.
@return Tuple [Whether the line is compliant with the style (before the check),
Fixed line,
Verbose information].
"""
is_line_compliant = True
line_fixed = line
verbose_infos: List[str] = []
# Check if the line is an #include <ns3/...>
header_file = re.findall(r"^#include <ns3/.*\.h>", line)
if header_file:
is_line_compliant = False
line_fixed = line.replace("<", '"').replace(">", '"')
header_index = len("#include ")
verbose_infos = [
f"{filename}:{line_number + 1}:{header_index + 1}: error: ns-3 #include headers with angle brackets detected",
f" {line}",
f" {'':{header_index}}^",
]
return (is_line_compliant, line_fixed, verbose_infos)
def check_doxygen_tags_line(
line: str,
filename: str,
line_number: int,
) -> Tuple[bool, str, List[str]]:
"""
Check / fix Doxygen tags using \\ rather than @ in a line.
@param line The line to check.
@param filename Name of the file to be checked.
@param line_number The number of the line checked.
@return Tuple [Whether the line is compliant with the style (before the check),
Fixed line,
Verbose information].
"""
IGNORED_WORDS = [
"\\dots",
"\\langle",
"\\quad",
]
is_line_compliant = True
line_fixed = line
verbose_infos: List[str] = []
# Match Doxygen tags at the start of the line (e.g., "* \param arg Description")
line_stripped = line.rstrip()
regex_findings = re.findall(r"^\s*(?:\*|\/\*\*|\/\/\/)\s*(\\\w{3,})(?=(?:\s|$))", line_stripped)
if regex_findings:
doxygen_tag = regex_findings[0]
if doxygen_tag not in IGNORED_WORDS:
is_line_compliant = False
doxygen_tag_index = line_fixed.find(doxygen_tag)
line_fixed = line.replace(doxygen_tag, f"@{doxygen_tag[1:]}")
verbose_infos.extend(
[
f"{filename}:{line_number + 1}:{doxygen_tag_index + 1}: error: detected Doxygen tags using \\ rather than @",
f" {line_stripped}",
f" {'':{doxygen_tag_index}}^",
]
)
return (is_line_compliant, line_fixed, verbose_infos)
def check_licenses_line(
line: str,
filename: str,
line_number: int,
) -> Tuple[bool, str, List[str]]:
"""
Check / fix SPDX licenses rather than GPL text in a line.
@param line The line to check.
@param filename Name of the file to be checked.
@param line_number The number of the line checked.
@return Tuple [Whether the line is compliant with the style (before the check),
Fixed line,
Verbose information].
"""
# fmt: off
GPL_LICENSE_LINES = [
"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",
]
# fmt: on
SPDX_LICENSE = "SPDX-License-Identifier: GPL-2.0-only"
is_line_compliant = True
line_fixed = line
verbose_infos: List[str] = []
# Check if the line is a GPL license text
line_stripped = line.strip()
line_stripped_no_leading_comments = line_stripped.strip("*#/").strip()
if line_stripped_no_leading_comments in GPL_LICENSE_LINES:
is_line_compliant = False
col_index = 0
# Replace GPL text with SPDX license.
# Replace the first line of the GPL text with SPDX.
# Delete the remaining GPL text lines.
if line_stripped_no_leading_comments == GPL_LICENSE_LINES[0]:
line_fixed = line.replace(line_stripped_no_leading_comments, SPDX_LICENSE)
else:
line_fixed = ""
verbose_infos.extend(
[
f"{filename}:{line_number + 1}:{col_index}: error: GPL license text detected instead of SPDX license",
f" {line_stripped}",
f" {'':>{col_index}}^",
]
)
return (is_line_compliant, line_fixed, verbose_infos)
def check_emacs_line(
line: str,
filename: str,
line_number: int,
) -> Tuple[bool, str, List[str]]:
"""
Check / fix emacs file style comment in a line.
@param line The line to check.
@param filename Name of the file to be checked.
@param line_number The number of the line checked.
@return Tuple [Whether the line is compliant with the style (before the check),
Fixed line,
Verbose information].
"""
is_line_compliant = True
line_fixed = line
verbose_infos: List[str] = []
# Check if line is an emacs file style comment
line_stripped = line.strip()
# fmt: off
emacs_line = re.search(r"c-file-style:|py-indent-offset:", line_stripped)
# fmt: on
if emacs_line:
is_line_compliant = False
line_fixed = ""
col_index = emacs_line.start()
verbose_infos = [
f"{filename}:{line_number + 1}:{col_index}: error: emacs file style comment detected",
f" {line_stripped}",
f" {'':{col_index}}^",
]
return (is_line_compliant, line_fixed, verbose_infos)
def check_whitespace_line(
line: str,
filename: str,
line_number: int,
) -> Tuple[bool, str, List[str]]:
"""
Check / fix whitespace in a line.
@param line The line to check.
@param filename Name of the file to be checked.
@param line_number The number of the line checked.
@return Tuple [Whether the line is compliant with the style (before the check),
Fixed line,
Verbose information].
"""
is_line_compliant = True
line_fixed = line.rstrip() + "\n"
verbose_infos: List[str] = []
if line_fixed != line:
is_line_compliant = False
line_fixed_stripped_expanded = line_fixed.rstrip().expandtabs(TAB_SIZE)
verbose_infos = [
f"{filename}:{line_number + 1}:{len(line_fixed_stripped_expanded) + 1}: error: Trailing whitespace detected",
f" {line_fixed_stripped_expanded}",
f" {'':>{len(line_fixed_stripped_expanded)}}^",
]
return (is_line_compliant, line_fixed, verbose_infos)
def check_tabs_line(
line: str,
filename: str,
line_number: int,
) -> Tuple[bool, str, List[str]]:
"""
Check / fix tabs in a line.
@param line The line to check.
@param filename Name of the file to be checked.
@param line_number The number of the line checked.
@return Tuple [Whether the line is compliant with the style (before the check),
Fixed line,
Verbose information].
"""
is_line_compliant = True
line_fixed = line
verbose_infos: List[str] = []
tab_index = line.find("\t")
if tab_index != -1:
is_line_compliant = False
line_fixed = line.expandtabs(TAB_SIZE)
verbose_infos = [
f"{filename}:{line_number + 1}:{tab_index + 1}: error: Tab detected",
f" {line.rstrip()}",
f" {'':>{tab_index}}^",
]
return (is_line_compliant, line_fixed, verbose_infos)
###########################################################
# MAIN
###########################################################
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Check and apply the ns-3 coding style recursively to all files in the given PATHs. "
"The script checks the formatting of the files using clang-format and"
" other coding style rules manually (see script arguments). "
"All checks respect clang-format guards, except trailing whitespace and file encoding,"
" which are always checked. "
'When used in "check mode" (default), the script runs all checks in all files. '
"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 and exits with 0 code.'
)
parser.add_argument(
"paths",
action="store",
type=str,
nargs="+",
help="List of paths 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 (respects clang-format guards)',
)
parser.add_argument(
"--no-include-quotes",
action="store_true",
help='Do not check / fix ns-3 #include headers using angle brackets <> rather than quotes "" (respects clang-format guards)',
)
parser.add_argument(
"--no-doxygen-tags",
action="store_true",
help="Do not check / fix Doxygen tags using @ rather than \\ (respects clang-format guards)",
)
parser.add_argument(
"--no-licenses",
action="store_true",
help="Do not check / fix SPDX licenses rather than GPL text (respects clang-format guards)",
)
parser.add_argument(
"--no-emacs",
action="store_true",
help="Do not check / fix emacs file style comments (respects clang-format guards)",
)
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 (respects clang-format guards)",
)
parser.add_argument(
"--no-formatting",
action="store_true",
help="Do not check / fix code formatting (respects clang-format guards)",
)
parser.add_argument(
"--no-encoding",
action="store_true",
help=f"Do not check / fix file encoding ({FILE_ENCODING})",
)
parser.add_argument(
"--fix",
action="store_true",
help="Fix coding style issues detected in the files",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Show the lines that are not well-formatted",
)
parser.add_argument(
"-j",
"--jobs",
type=int,
default=max(1, os.cpu_count() - 1),
help="Number of parallel jobs",
)
args = parser.parse_args()
try:
all_checks_successful = check_style_clang_format(
paths=args.paths,
checks_enabled={
"include_prefixes": not args.no_include_prefixes,
"include_quotes": not args.no_include_quotes,
"doxygen_tags": not args.no_doxygen_tags,
"license": not args.no_licenses,
"emacs": not args.no_emacs,
"whitespace": not args.no_whitespace,
"tabs": not args.no_tabs,
"formatting": not args.no_formatting,
"encoding": not args.no_encoding,
},
fix=args.fix,
verbose=args.verbose,
n_jobs=args.jobs,
)
except Exception as ex:
print("ERROR:", ex)
sys.exit(1)
if not all_checks_successful:
if args.verbose:
print(
"",
"Notes to fix the above formatting issues:",
' - To fix the formatting of specific files, run this script with the flag "--fix":',
" $ ./utils/check-style-clang-format.py --fix path [path ...]",
" - To fix the formatting of all files modified by this branch, run this script in the following way:",
" $ git diff --name-only master | xargs ./utils/check-style-clang-format.py --fix",
sep="\n",
)
sys.exit(1)