Source code for cpp_linter.clang_tools
from concurrent.futures import ProcessPoolExecutor, as_completed
import json
from pathlib import Path
import re
import subprocess
from typing import Optional, List, Dict, Tuple, cast
import shutil
from ..common_fs import FileObj
from ..common_fs.file_filter import TidyFileFilter, FormatFileFilter
from ..loggers import start_log_group, end_log_group, worker_log_init, logger
from .clang_tidy import run_clang_tidy, TidyAdvice
from .clang_format import run_clang_format, FormatAdvice
from ..cli import Args
[docs]
def assemble_version_exec(tool_name: str, specified_version: str) -> Optional[str]:
"""Assembles the command to the executable of the given clang tool based on given
version information.
:param tool_name: The name of the clang tool to be executed.
:param specified_version: The version number or the installed path to a version of
the tool's executable.
"""
semver = specified_version.split(".")
exe_path = None
if semver and semver[0].isdigit(): # version info is not a path
# let's assume the exe is in the PATH env var
exe_path = shutil.which(f"{tool_name}-{specified_version}")
elif specified_version: # treat value as a path to binary executable
exe_path = shutil.which(tool_name, path=specified_version)
if exe_path is not None:
return exe_path
return shutil.which(tool_name)
def _run_on_single_file(
file: FileObj,
log_lvl: int,
tidy_cmd: Optional[str],
db_json: Optional[List[Dict[str, str]]],
format_cmd: Optional[str],
format_filter: Optional[FormatFileFilter],
tidy_filter: Optional[TidyFileFilter],
args: Args,
) -> Tuple[str, str, Optional[TidyAdvice], Optional[FormatAdvice]]:
log_stream = worker_log_init(log_lvl)
tidy_note = None
if tidy_cmd is not None and (
tidy_filter is None or tidy_filter.is_source_or_ignored(file.name)
):
tidy_note = run_clang_tidy(
command=tidy_cmd,
file_obj=file,
checks=args.tidy_checks,
lines_changed_only=args.lines_changed_only,
database=args.database,
extra_args=args.extra_arg,
db_json=db_json,
tidy_review=args.tidy_review,
style=args.style,
)
format_advice = None
if format_cmd is not None and (
format_filter is None or format_filter.is_source_or_ignored(file.name)
):
format_advice = run_clang_format(
command=format_cmd,
file_obj=file,
style=args.style,
lines_changed_only=args.lines_changed_only,
format_review=args.format_review,
)
return file.name, log_stream.getvalue(), tidy_note, format_advice
VERSION_PATTERN = re.compile(r"version\s(\d+\.\d+\.\d+)")
def _capture_tool_version(cmd: str) -> str:
"""Get version number from output for executable used."""
version_out = subprocess.run(
[cmd, "--version"], capture_output=True, check=True, text=True
)
matched = VERSION_PATTERN.search(version_out.stdout)
if matched is None: # pragma: no cover
raise RuntimeError(
f"Failed to get version numbers from `{cmd} --version` output"
)
ver = cast(str, matched.group(1))
logger.info("`%s --version`: %s", cmd, ver)
return ver
class ClangVersions:
def __init__(self) -> None:
self.tidy: Optional[str] = None
self.format: Optional[str] = None
[docs]
def capture_clang_tools_output(files: List[FileObj], args: Args) -> ClangVersions:
"""Execute and capture all output from clang-tidy and clang-format. This aggregates
results in the :attr:`~cpp_linter.Globals.OUTPUT`.
:param files: A list of files to analyze.
:param args: A namespace of parsed args from the :doc:`CLI <../cli_args>`.
"""
tidy_cmd, format_cmd = (None, None)
tidy_filter, format_filter = (None, None)
clang_versions = ClangVersions()
if args.style: # if style is an empty value, then clang-format is skipped
format_cmd = assemble_version_exec("clang-format", args.version)
if format_cmd is None: # pragma: no cover
raise FileNotFoundError("clang-format executable was not found")
clang_versions.format = _capture_tool_version(format_cmd)
format_filter = FormatFileFilter(
extensions=args.extensions,
ignore_value=args.ignore_format,
)
if args.tidy_checks != "-*":
# if all checks are disabled, then clang-tidy is skipped
tidy_cmd = assemble_version_exec("clang-tidy", args.version)
if tidy_cmd is None: # pragma: no cover
raise FileNotFoundError("clang-tidy executable was not found")
clang_versions.tidy = _capture_tool_version(tidy_cmd)
tidy_filter = TidyFileFilter(
extensions=args.extensions,
ignore_value=args.ignore_tidy,
)
db_json: Optional[List[Dict[str, str]]] = None
if args.database:
db = Path(args.database)
if not db.is_absolute():
args.database = str(db.resolve())
db_path = (db / "compile_commands.json").resolve()
if db_path.exists():
db_json = json.loads(db_path.read_text(encoding="utf-8"))
with ProcessPoolExecutor(args.jobs) as executor:
log_lvl = logger.getEffectiveLevel()
futures = [
executor.submit(
_run_on_single_file,
file,
log_lvl=log_lvl,
tidy_cmd=tidy_cmd,
db_json=db_json,
format_cmd=format_cmd,
format_filter=format_filter,
tidy_filter=tidy_filter,
args=args,
)
for file in files
]
# temporary cache of parsed notifications for use in log commands
for future in as_completed(futures):
file_name, logs, tidy_advice, format_advice = future.result()
start_log_group(f"Performing checkup on {file_name}")
print(logs, flush=True)
end_log_group()
if tidy_advice or format_advice:
for file in files:
if file.name == file_name:
if tidy_advice:
file.tidy_advice = tidy_advice
if format_advice:
file.format_advice = format_advice
break
else: # pragma: no cover
raise ValueError(f"Failed to find {file_name} in list of files.")
return clang_versions