blender/tools/check_source/static_check_cppcheck.py
Hans Goudey 91803e130f Cleanup: Grammar: Fix uses of "for e.g."
e.g. stands for "exempli gratia" in Latin which means "for example".
The best way to make sure it makes sense when writing is to just expand
it to "for example". In these cases where the text was "for e.g.", that
leaves us with "for for example" which makes no sense. This commit fixes
all 110 cases, mostly just just replacing the words with "for example",
but also restructuring the text a bit more in a few cases, mostly by
moving "e.g." to the beginning of a list in parentheses.

Pull Request: https://projects.blender.org/blender/blender/pulls/139596
2025-05-29 21:21:18 +02:00

491 lines
16 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2011-2022 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
Run CPPCHECK on Blender's source files,
writing results to a log as well as a summary of all checks.
Existing logs are renamed to ``.old.log`` so they can be compared.
"""
__all__ = (
"main",
)
import argparse
import project_source_info
import subprocess
import sys
import os
import re
import tempfile
import time
from typing import (
Any,
IO,
)
USE_VERBOSE = (os.environ.get("VERBOSE", None) is not None)
# Could make configurable.
USE_VERBOSE_PROGRESS = True
CHECKER_BIN = "cppcheck"
CHECKER_IGNORE_PREFIX = [
"extern",
]
# Optionally use a separate build dir for each source code directory.
# According to CPPCHECK docs using one directory is a way to take advantage of "whole program" checks,
# although it looks as if there might be name-space issues - overwriting files with similar names across
# different parts of the source.
CHECKER_ISOLATE_BUILD_DIR = False
CHECKER_EXCLUDE_SOURCE_FILES_EXT = (
# Exclude generated shaders, harmless but also not very useful and are quite slow.
".glsl.c",
)
# To add files use a relative path.
CHECKER_EXCLUDE_SOURCE_FILES = set(os.path.join(*f.split("/")) for f in (
"source/blender/draw/engines/eevee/eevee_lut.cc",
# Hangs for hours CPPCHECK-2.14.0.
"intern/cycles/blender/output_driver.cpp",
))
CHECKER_EXCLUDE_SOURCE_DIRECTORIES_BUILD = set(os.path.join(*f.split("/")) + os.sep for f in (
# Exclude data-files, especially `datatoc` as the files can be large & are slow to scan.
"release/datafiles",
# Exclude generated RNA, harmless but also not very useful and are quite slow.
"source/blender/makesrna/intern",
# Exclude generated WAYLAND protocols.
"intern/ghost/libwayland"
))
CHECKER_ARGS = (
# Speed up execution.
# As Blender has many defines, the total number of configurations is large making execution unreasonably slow.
# This could be increased but do so with care.
"--max-configs=1",
# Enable this when includes are missing.
# `"--check-config",`
# May be interesting to check on increasing this for better results:
# `"--max-ctu-depth=2",`
# This is slower, for a comprehensive output it is needed.
"--check-level=exhaustive",
# Shows many pedantic issues, some are quite useful.
"--enable=all",
# Tends to give many false positives, could investigate if there are any ways to resolve, for now it's noisy.
"--disable=unusedFunction",
# Also shows useful messages, even if some are false-positives.
"--inconclusive",
# Generates many warnings, CPPCHECK known about system includes without resolving them.
# To get a list of these use:
# `cppcheck --errorlist | pcregrep --only-matching "error id=\"[a-zA-Z_0-9]+\""`
*("--suppress={:s}".format(s) for s in (
# Noisy, and we can't always avoid this.
"missingIncludeSystem",
# Typically these can't be made `const`.
"constParameterCallback",
# Overly noisy, we could consider resolving all of these at some point.
"cstyleCast",
# Calling `memset` of float may technically be a bug but works in practice.
"memsetClassFloat",
# There are various classes which don't have copy or equal constructors (GHOST windows for example)
"noCopyConstructor",
# Also noisy, looks like these are not issues to "solve".
"unusedFunction",
# There seems to be many false positives here.
"unusedPrivateFunction",
# May be interesting to handle but very noisy currently.
"variableScope",
# TODO: consider enabling this, more of a preference,
# not using STL algorithm's doesn't often hint at actual errors.
"useStlAlgorithm",
# TODO: consider enabling this, currently noisy and we are not likely to resolve them short term.
"functionStatic",
# These could be added back, currently there are so many warnings and they don't seem especially error-prone.
"missingMemberCopy",
"missingOverride",
"noExplicitConstructor",
"uninitDerivedMemberVar",
"uninitDerivedMemberVarPrivate",
"uninitMemberVar",
"useInitializationList",
)),
# Quiet output, otherwise all defines/includes are printed (overly verbose).
# Only enable this for troubleshooting (if defines are not set as expected for example).
*(() if USE_VERBOSE else ("--quiet",))
# NOTE: `--cppcheck-build-dir=<dir>` is added later as a temporary directory.
)
CHECKER_ARGS_C = (
"--std=c11",
)
CHECKER_ARGS_CXX = (
"--std=c++17",
)
# NOTE: it seems we can't exclude these from CPPCHECK directly (from what I can see)
# so exclude them from the summary.
CHECKER_EXCLUDE_FROM_SUMMARY = {
# Not considered an error.
"allocaCalled",
# Similar for `noCopyConstructor`.
"nonoOperatorEq",
}
def source_info_filter(
source_info: list[tuple[str, list[str], list[str]]],
source_dir: str,
cmake_dir: str,
) -> list[tuple[str, list[str], list[str]]]:
source_dir = source_dir.rstrip(os.sep) + os.sep
cmake_dir = cmake_dir.rstrip(os.sep) + os.sep
cmake_dir_prefix_tuple = tuple(CHECKER_EXCLUDE_SOURCE_DIRECTORIES_BUILD)
source_info_result = []
for i, item in enumerate(source_info):
c = item[0]
if c.endswith(*CHECKER_EXCLUDE_SOURCE_FILES_EXT):
continue
if c.startswith(source_dir):
c_relative = c[len(source_dir):]
if c_relative in CHECKER_EXCLUDE_SOURCE_FILES:
CHECKER_EXCLUDE_SOURCE_FILES.remove(c_relative)
continue
elif c.startswith(cmake_dir):
c_relative = c[len(cmake_dir):]
if c_relative.startswith(cmake_dir_prefix_tuple):
continue
# TODO: support filtering on filepath.
# if "/editors/mask" not in c:
# continue
source_info_result.append(item)
if CHECKER_EXCLUDE_SOURCE_FILES:
sys.stderr.write(
"Error: exclude file(s) are missing: {!r}\n".format(list(sorted(CHECKER_EXCLUDE_SOURCE_FILES)))
)
sys.exit(1)
return source_info_result
def cppcheck(cppcheck_dir: str, temp_dir: str, log_fh: IO[bytes]) -> None:
temp_source_dir = os.path.join(temp_dir, "source")
os.mkdir(temp_source_dir)
del temp_dir
source_dir = os.path.normpath(os.path.abspath(project_source_info.SOURCE_DIR))
cmake_dir = os.path.normpath(os.path.abspath(project_source_info.CMAKE_DIR))
cppcheck_build_dir = os.path.join(cppcheck_dir, "build")
os.makedirs(cppcheck_build_dir, exist_ok=True)
source_info = project_source_info.build_info(ignore_prefix_list=CHECKER_IGNORE_PREFIX)
cppcheck_compiler_h = os.path.join(temp_source_dir, "cppcheck_compiler.h")
with open(cppcheck_compiler_h, "w", encoding="utf-8") as fh:
fh.write(project_source_info.build_defines_as_source())
# Add additional defines.
fh.write("\n")
# Python's `pyport.h` errors without this.
fh.write("#define UCHAR_MAX 255\n")
# `intern/atomic/intern/atomic_ops_utils.h` errors with `Cannot find int size` without this.
fh.write("#define UINT_MAX 0xFFFFFFFF\n")
# Apply exclusion.
source_info = source_info_filter(source_info, source_dir, cmake_dir)
check_commands = []
for c, inc_dirs, defs in source_info:
if c.endswith(".c"):
checker_args_extra = CHECKER_ARGS_C
else:
checker_args_extra = CHECKER_ARGS_CXX
if CHECKER_ISOLATE_BUILD_DIR:
build_dir_for_source = os.path.relpath(os.path.dirname(os.path.normpath(os.path.abspath(c))), source_dir)
build_dir_for_source = os.sep + build_dir_for_source + os.sep
build_dir_for_source = build_dir_for_source.replace(
os.sep + ".." + os.sep,
os.sep + "__" + os.sep,
).strip(os.sep)
build_dir_for_source = os.path.join(cppcheck_build_dir, build_dir_for_source)
os.makedirs(build_dir_for_source, exist_ok=True)
else:
build_dir_for_source = cppcheck_build_dir
cmd = (
CHECKER_BIN,
*CHECKER_ARGS,
*checker_args_extra,
"--cppcheck-build-dir=" + build_dir_for_source,
"--include=" + cppcheck_compiler_h,
# NOTE: for some reason failing to include this crease a large number of syntax errors
# from `intern/guardedalloc/MEM_guardedalloc.h`. Include directly to resolve.
"--include={:s}".format(os.path.join(source_dir, "source", "blender", "blenlib", "BLI_compiler_attrs.h")),
c,
*[("-I{:s}".format(i)) for i in inc_dirs],
*[("-D{:s}".format(d)) for d in defs],
)
check_commands.append((c, cmd))
process_functions = []
def my_process(i: int, c: str, cmd: list[str]) -> subprocess.Popen[Any]:
del c
proc = subprocess.Popen(
cmd,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
)
# A bit dirty, but simplifies logic to read these back later.
proc.my_index = i # type: ignore
proc.my_time = time.time() # type: ignore
return proc
for i, (c, cmd) in enumerate(check_commands):
process_functions.append((my_process, (i, c, cmd)))
index_current = 0
index_count = 0
proc_results_by_index: dict[int, tuple[bytes, bytes]] = {}
def process_finalize(
proc: subprocess.Popen[Any],
stdout: bytes,
stderr: bytes,
) -> None:
nonlocal index_current, index_count
index_count += 1
assert hasattr(proc, "my_index")
index = proc.my_index
assert hasattr(proc, "my_time")
time_orig = proc.my_time
c = check_commands[index][0]
time_delta = time.time() - time_orig
if USE_VERBOSE_PROGRESS:
percent = 100.0 * (index_count / len(check_commands))
sys.stdout.flush()
sys.stdout.write("[{:s}] %: {:s} ({:.2f})\n".format(
("{:.2f}".format(percent)).rjust(6),
os.path.relpath(c, source_dir),
time_delta,
))
while index == index_current:
log_fh.write(stderr)
log_fh.write(b"\n")
log_fh.write(stdout)
log_fh.write(b"\n")
index_current += 1
test_data = proc_results_by_index.pop(index_current, None)
if test_data is not None:
stdout, stderr = test_data
index += 1
else:
proc_results_by_index[index] = stdout, stderr
project_source_info.queue_processes(
process_functions,
process_finalize=process_finalize,
# job_total=4,
)
print("Finished!")
def cppcheck_generate_summary(
log_fh: IO[str],
log_summary_fh: IO[str],
) -> None:
source_dir = project_source_info.SOURCE_DIR
source_dir_source = os.path.join(source_dir, "source") + os.sep
source_dir_intern = os.path.join(source_dir, "intern") + os.sep
filter_line_prefix = (source_dir_source, source_dir_intern)
source_dir_prefix_len = len(source_dir.rstrip(os.sep))
# Avoids many duplicate lines generated by headers.
lines_unique = set()
category: dict[str, list[str]] = {}
re_match = re.compile(".* \\[([a-zA-Z_]+)\\]$")
for line in log_fh:
if not line.startswith(filter_line_prefix):
continue
# Print a relative directory from `SOURCE_DIR`,
# less visual noise and makes it possible to compare reports from different systems.
line = "." + line[source_dir_prefix_len:]
if (m := re_match.match(line)) is None:
continue
g = m.group(1)
if g in CHECKER_EXCLUDE_FROM_SUMMARY:
continue
if line in lines_unique:
continue
lines_unique.add(line)
try:
category_list = category[g]
except KeyError:
category_list = category[g] = []
category_list.append(line)
for key, value in sorted(category.items()):
log_summary_fh.write("\n\n{:s}\n".format(key))
for line in value:
log_summary_fh.write(line)
def argparse_create() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--build-dir",
dest="build_dir",
metavar='BUILD_DIR',
type=str,
help=(
"The build directory (containing CMakeCache.txt).\n"
"\n"
"Defaults to the \".\"."
),
default=".",
required=False,
)
parser.add_argument(
"--output-dir",
dest="output_dir",
metavar='OUTPUT_DIR',
type=str,
help=(
"Specify the directory where CPPCHECK logs will be written to.\n"
"Using this may be preferred so the build directory can be cleared\n"
"without loosing the result of previous checks.\n"
"\n"
"Defaults to {BUILD_DIR}/cppcheck/"
),
default="",
required=False,
)
return parser
def main() -> None:
args = argparse_create().parse_args()
project_source_info.cmake_dir_set(args.build_dir)
cppcheck_dir = args.output_dir
if cppcheck_dir:
cppcheck_dir = os.path.normpath(os.path.abspath(cppcheck_dir))
else:
cppcheck_dir = os.path.join(os.path.normpath(os.path.abspath(project_source_info.CMAKE_DIR)), "cppcheck")
del args
filepath_output_log = os.path.join(cppcheck_dir, "cppcheck.part.log")
filepath_output_summary_log = os.path.join(cppcheck_dir, "cppcheck_summary.part.log")
try:
os.makedirs(cppcheck_dir, exist_ok=True)
files_old = {}
# Comparing logs is useful, keep the old ones (renamed).
with tempfile.TemporaryDirectory() as temp_dir:
with open(filepath_output_log, "wb") as log_fh:
cppcheck(cppcheck_dir, temp_dir, log_fh)
with (
open(filepath_output_log, "r", encoding="utf-8") as log_fh,
open(filepath_output_summary_log, "w", encoding="utf-8") as log_summary_fh,
):
cppcheck_generate_summary(log_fh, log_summary_fh)
except KeyboardInterrupt:
print("\nCanceling...")
for filepath_part in (
filepath_output_log,
filepath_output_summary_log,
):
if os.path.exists(filepath_part):
os.remove(filepath_part)
return
# The partial files have been written.
# - Move previous files -> `.old.log`.
# - Move `.log.part` -> `.log`
#
# Do this last so it's possible to cancel execution without breaking the old/new log comparison
# which is especially useful when comparing the old/new summary.
for filepath_part in (
filepath_output_log,
filepath_output_summary_log,
):
filepath = filepath_part.removesuffix(".part.log") + ".log"
if not os.path.exists(filepath):
os.rename(filepath_part, filepath)
continue
filepath_old = filepath.removesuffix(".log") + ".old.log"
if os.path.exists(filepath_old):
os.remove(filepath_old)
os.rename(filepath, filepath_old)
os.rename(filepath_part, filepath)
files_old[filepath] = filepath_old
print("Written:")
for filepath_part in (
filepath_output_log,
filepath_output_summary_log,
):
filepath = filepath_part.removesuffix(".part.log") + ".log"
print(" ", filepath, "<->", files_old.get(filepath, "<none>"))
if __name__ == "__main__":
main()