mirror of
https://github.com/nasa/fprime-tools.git
synced 2025-12-10 09:55:16 -06:00
* add build to .gitignore * add format utility * black formatter * spelling * add whitelist feature * minor review changes * return runner status code
156 lines
5.8 KiB
Python
156 lines
5.8 KiB
Python
""" fprime.fbuild.code_formatter
|
|
|
|
Wrapper for clang-format utility.
|
|
|
|
@author thomas-bc
|
|
"""
|
|
|
|
from typing import Dict, List, Tuple
|
|
|
|
from fprime.fbuild.target import ExecutableAction, TargetScope
|
|
from pathlib import Path
|
|
|
|
import subprocess
|
|
import shutil
|
|
import re
|
|
|
|
|
|
# MARKER is needed to differentiate at postprocess between access specifiers
|
|
# that were previously an uppercase MACRO, and those that were originally lowercase.
|
|
# MARKER must be a comment for the formatting to behave - so might as well make it
|
|
# a meaningful warning in case it's not postprocessed correctly
|
|
MARKER = "// WARNING: fprime-util format mishap"
|
|
|
|
# POST pattern is different because the formatting will likely introduce whitespaces
|
|
PRIVATE_PRE_PATTERN = f"private:{MARKER}"
|
|
PRIVATE_POST_PATTERN = f"private:[\s]*{MARKER}"
|
|
PROTECTED_PRE_PATTERN = f"protected:{MARKER}"
|
|
PROTECTED_POST_PATTERN = f"protected:[\s]*{MARKER}"
|
|
STATIC_PRE_PATTERN = f"static:{MARKER}"
|
|
STATIC_POST_PATTERN = f"static:[\s]*{MARKER}"
|
|
|
|
# clang-format will try to format everything it is given - restrict for the time being
|
|
ALLOWED_EXTENSIONS = [
|
|
".cpp",
|
|
".c++",
|
|
".cxx",
|
|
".cc",
|
|
".c",
|
|
".hpp",
|
|
".h++",
|
|
".hxx",
|
|
".hh",
|
|
".h",
|
|
]
|
|
|
|
|
|
class ClangFormatter(ExecutableAction):
|
|
"""Class encapsulating the clang-format logic for fprime-util"""
|
|
|
|
def __init__(self, executable: str, style_file: "Path", options: Dict):
|
|
super().__init__(TargetScope.LOCAL)
|
|
self.executable = executable
|
|
self.style_file = style_file
|
|
self.backup = options["backup"]
|
|
self.verbose = options["verbose"]
|
|
self.validate_extensions = options["validate_extensions"]
|
|
self.allowed_extensions = ALLOWED_EXTENSIONS.copy()
|
|
self._files_to_format: List[Path] = []
|
|
|
|
def is_supported(self) -> bool:
|
|
return bool(shutil.which(self.executable))
|
|
|
|
def allow_extension(self, file_ext: str) -> None:
|
|
"""Add a file extension str to the list of allowed extension"""
|
|
self.allowed_extensions.append(file_ext)
|
|
|
|
def stage_file(self, filepath: Path) -> None:
|
|
"""Request ClangFormatter to consider the file for formatting.
|
|
If the file exists and its extension matches a known C/C++ format,
|
|
it will be passed to clang-format when the execute() function is called.
|
|
|
|
Args:
|
|
filepath (str): file path to file to be staged.
|
|
"""
|
|
if not filepath.is_file():
|
|
if self.verbose:
|
|
print(f"[INFO] Skipping {filepath} : is not a file.")
|
|
elif self.validate_extensions and (
|
|
filepath.suffix not in self.allowed_extensions
|
|
):
|
|
if self.verbose:
|
|
print(
|
|
f"[INFO] Skipping {filepath} : unrecognized C/C++ file extension "
|
|
f"('{filepath.suffix}'). Use --allow-extension or --force."
|
|
)
|
|
else:
|
|
self._files_to_format.append(filepath)
|
|
|
|
def _preprocess_files(self) -> None:
|
|
"""Preprocess a file to ensure that clang-format behaves.
|
|
This is because of the access specifier macros (e.g. PROTECTED)
|
|
that are defined in F', which clang-format does not recognize
|
|
"""
|
|
for filepath in self._files_to_format:
|
|
# It is unsafe to write to file while reading from it
|
|
# Better to read in memory, close the file, then re-open to write out from memory
|
|
with open(filepath, "r") as file:
|
|
content = file.read()
|
|
# Replace the strings in the file content
|
|
content = re.sub("PROTECTED:", PROTECTED_PRE_PATTERN, content)
|
|
content = re.sub("PRIVATE:", PRIVATE_PRE_PATTERN, content)
|
|
content = re.sub("STATIC:", STATIC_PRE_PATTERN, content)
|
|
# Write the file out to the same location, seemingly in-place
|
|
with open(filepath, "w") as file:
|
|
file.write(content)
|
|
|
|
def _postprocess_files(self) -> None:
|
|
"""Postprocess a file to restore the access specifier macros."""
|
|
for filepath in self._files_to_format:
|
|
# Same logic as _preprocess_files()
|
|
with open(filepath, "r") as file:
|
|
content = file.read()
|
|
content = re.sub(PROTECTED_POST_PATTERN, "PROTECTED:", content)
|
|
content = re.sub(PRIVATE_POST_PATTERN, "PRIVATE:", content)
|
|
content = re.sub(STATIC_POST_PATTERN, "STATIC:", content)
|
|
with open(filepath, "w") as file:
|
|
file.write(content)
|
|
|
|
def execute(
|
|
self, builder: "Build", context: "Path", args: Tuple[Dict[str, str], List[str]]
|
|
):
|
|
"""Execute clang-format on the files that were staged.
|
|
|
|
Args:
|
|
builder (Build): build object to run the utility with
|
|
context (Path): context path of module clang-format can run on if --module is provided
|
|
args (Tuple[Dict[str, str], List[str]]): extra arguments to supply to the utility
|
|
"""
|
|
|
|
if len(self._files_to_format) == 0:
|
|
print("[INFO] No files were formatted.")
|
|
return 0
|
|
if not self.style_file.is_file():
|
|
print(
|
|
f"[ERROR] No .clang-format file found in {self.style_file.parent}. "
|
|
"Override location with --pass-through --style=file:<path>."
|
|
)
|
|
return 1
|
|
# Backup files unless --no-backup is requested
|
|
if self.backup:
|
|
for file in self._files_to_format:
|
|
shutil.copy2(file, file.parent / (file.stem + ".bak" + file.suffix))
|
|
pass_through = args[1]
|
|
self._preprocess_files()
|
|
clang_args = [
|
|
self.executable,
|
|
"-i",
|
|
f"--style=file:{self.style_file}",
|
|
*(["--verbose"] if self.verbose else []),
|
|
*pass_through,
|
|
*self._files_to_format,
|
|
]
|
|
status = subprocess.run(clang_args)
|
|
self._postprocess_files()
|
|
return status.returncode
|