Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,425 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
# Note: this code should be cleaned up / refactored.
__all__ = (
"main",
)
import os
from os.path import (
dirname,
join,
normpath,
splitext,
)
from check_cmake_consistency_config import (
IGNORE_SOURCE,
IGNORE_SOURCE_MISSING,
IGNORE_CMAKE,
UTF8_CHECK,
SOURCE_DIR,
BUILD_DIR,
)
from collections.abc import (
Callable,
Iterator,
)
global_h = set()
global_c = set()
global_refs: dict[str, list[tuple[str, int]]] = {}
# Flatten `IGNORE_SOURCE_MISSING` to avoid nested looping.
IGNORE_SOURCE_MISSING_FLAT = [
(k, ignore_path) for k, ig_list in IGNORE_SOURCE_MISSING
for ignore_path in ig_list
]
# Ignore cmake file, path pairs.
global_ignore_source_missing: dict[str, list[str]] = {}
for k, v in IGNORE_SOURCE_MISSING_FLAT:
global_ignore_source_missing.setdefault(k, []).append(v)
del IGNORE_SOURCE_MISSING_FLAT
def replace_line(f: str, i: int, text: str) -> None:
file_handle = open(f, 'r')
data = file_handle.readlines()
file_handle.close()
l = data[i]
ws = l[:len(l) - len(l.lstrip())]
data[i] = "%s%s\n" % (ws, text)
file_handle = open(f, 'w')
file_handle.writelines(data)
file_handle.close()
def source_list(
path: str,
filename_check: Callable[[str], bool] | None = None,
) -> Iterator[str]:
for dirpath, dirnames, filenames in os.walk(path):
# skip '.git'
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
for filename in filenames:
if filename_check is None or filename_check(filename):
yield os.path.join(dirpath, filename)
# extension checking
def is_cmake(filename: str) -> bool:
ext = splitext(filename)[1]
return (ext == ".cmake") or (filename == "CMakeLists.txt")
def is_c_header(filename: str) -> bool:
ext = splitext(filename)[1]
return (ext in {".h", ".hpp", ".hxx", ".hh"})
def is_c(filename: str) -> bool:
ext = splitext(filename)[1]
return (ext in {".c", ".cpp", ".cxx", ".m", ".mm", ".rc", ".cc", ".inl", ".metal", ".msl"})
# def is_c_any(filename: str) -> bool:
# return is_c(filename) or is_c_header(filename)
def cmake_get_src(f: str) -> None:
# TODO: only partially implemented, needs work.
do_replace_text = False
sources_h = []
sources_c = []
filen = open(f, "r", encoding="utf8")
it: Iterator[str] | None = iter(filen)
found = False
i = 0
# print(f)
def is_definition(l: str, f: str, i: int, name: str) -> tuple[bool, int]:
"""
Return (is_definition, single_line_offset).
"""
if l.startswith("unset("):
return False, -1
single_line_offset = -1
name_test = 'set(%s' % name
single_line_offset = l.find(name_test)
if (single_line_offset != -1) or ('set(' in l and l.endswith(name)):
if single_line_offset != -1:
single_line_offset += len(name_test)
# if len(l.split()) > 1:
# raise Exception("strict formatting not kept 'set(%s*' %s:%d" % (name, f, i))
if l.endswith(")"):
pass
while single_line_offset < len(l) and l[single_line_offset] != " ":
single_line_offset += 1
else:
single_line_offset = -1
return True, single_line_offset
name_test = "list(APPEND %s" % name
single_line_offset = l.find(name_test)
if (single_line_offset != -1) or ('list(APPEND ' in l and l.endswith(name)):
if single_line_offset != -1:
single_line_offset += len(name_test)
if l.endswith(")"):
# raise Exception("strict formatting not kept 'list(APPEND %s...)' on 1 line %s:%d" % (name, f, i))
pass
while single_line_offset < len(l) and l[single_line_offset] != " ":
single_line_offset += 1
else:
single_line_offset = -1
return True, single_line_offset
return False, -1
while it is not None:
context_name = ""
while it is not None:
i += 1
try:
l = next(it)
except StopIteration:
it = None
break
l = l.strip()
if not l.startswith("#"):
for var in ("SRC", "INC"):
found, single_line_offset = is_definition(l, f, i, var)
if found:
context_name = var
break
if found:
break
if found:
tokens = []
if single_line_offset != -1:
end = False
for w in l[single_line_offset:].split():
if w.startswith("#"):
break
if w.endswith(")"):
w = w[:-1].rstrip()
end = True
tokens.append((w, i))
if end:
break
del end
if len(tokens) > 1:
print("Expect multi-variable to be split across multiple lines! '%s' %s:%d" % (l, f, i))
else:
while it is not None:
i += 1
try:
l = next(it)
except StopIteration:
it = None
break
l = l.strip()
if not l.startswith("#"):
# Remove in-line comments.
l = l.split(" # ")[0].rstrip()
if ")" in l:
if l.strip() != ")":
raise Exception("strict formatting not kept '*)' %s:%d" % (f, i))
break
tokens.append((l, i))
cmake_base = dirname(f)
cmake_base_bin = os.path.join(BUILD_DIR, os.path.relpath(cmake_base, SOURCE_DIR))
# Find known missing sources list (if we have one).
f_rel = os.path.relpath(f, SOURCE_DIR)
f_rel_key = f_rel
if os.sep != "/":
f_rel_key = f_rel_key.replace(os.sep, "/")
local_ignore_source_missing = global_ignore_source_missing.get(f_rel_key, [])
for l, line_number in tokens:
# Replace directories.
l = l.replace("${CMAKE_SOURCE_DIR}", SOURCE_DIR)
l = l.replace("${CMAKE_CURRENT_SOURCE_DIR}", cmake_base)
l = l.replace("${CMAKE_CURRENT_BINARY_DIR}", cmake_base_bin)
l = l.strip('"')
# For library lists.
for known_prefix in ("PUBLIC ", "PRIVATE "):
l = l.removeprefix(known_prefix).lstrip()
if not l:
pass
elif l in local_ignore_source_missing:
local_ignore_source_missing.remove(l)
elif l.startswith("$"):
if context_name == "SRC":
# assume if it ends with context_name we know about it
if not l.split("}")[0].endswith(context_name):
print("Can't use var '%s' %s:%d" % (l, f, line_number))
elif len(l.split()) > 1:
raise Exception("Multi-line define '%s' %s:%d" % (l, f, line_number))
else:
new_file = normpath(join(cmake_base, l))
if context_name == "SRC":
if is_c_header(new_file):
sources_h.append(new_file)
global_refs.setdefault(new_file, []).append((f, line_number))
elif is_c(new_file):
sources_c.append(new_file)
global_refs.setdefault(new_file, []).append((f, line_number))
elif l in {"PARENT_SCOPE", }:
# cmake var, ignore
pass
elif new_file.endswith(".list"):
pass
elif new_file.endswith(".def"):
pass
elif new_file.endswith(".cl"): # OPENCL.
pass
elif new_file.endswith(".cu"): # CUDA.
pass
elif new_file.endswith(".osl"): # open shading language.
pass
elif new_file.endswith(".glsl"):
pass
elif new_file.endswith(".natvis"):
pass
else:
raise Exception("unknown file type - not c or h %s -> %s" % (f, new_file))
elif context_name == "INC":
if new_file.startswith(BUILD_DIR):
# assume generated path
pass
elif os.path.isdir(new_file):
new_path_rel = os.path.relpath(new_file, cmake_base)
if new_path_rel != l:
print("overly relative path:\n %s:%d\n %s\n %s" % (f, line_number, l, new_path_rel))
# Save time. just replace the line.
if do_replace_text:
replace_line(f, line_number - 1, new_path_rel)
else:
raise Exception("non existent include %s:%d -> %s" % (f, line_number, new_file))
# print(new_file)
global_h.update(set(sources_h))
global_c.update(set(sources_c))
'''
if not sources_h and not sources_c:
raise Exception("No sources %s" % f)
sources_h_fs = list(source_list(cmake_base, is_c_header))
sources_c_fs = list(source_list(cmake_base, is_c))
'''
# find missing C files:
'''
for ff in sources_c_fs:
if ff not in sources_c:
print(" missing: " + ff)
'''
# reset
del sources_h[:]
del sources_c[:]
filen.close()
def is_ignore_source(f: str, ignore_used: list[bool]) -> bool:
for index, ignore_path in enumerate(IGNORE_SOURCE):
if ignore_path in f:
ignore_used[index] = True
return True
return False
def is_ignore_cmake(f: str, ignore_used: list[bool]) -> bool:
for index, ignore_path in enumerate(IGNORE_CMAKE):
if ignore_path in f:
ignore_used[index] = True
return True
return False
def main() -> None:
print("Scanning:", SOURCE_DIR)
ignore_used_source = [False] * len(IGNORE_SOURCE)
ignore_used_cmake = [False] * len(IGNORE_CMAKE)
for cmake in source_list(SOURCE_DIR, is_cmake):
if not is_ignore_cmake(cmake, ignore_used_cmake):
cmake_get_src(cmake)
# First do stupid check, do these files exist?
print("\nChecking for missing references:")
is_err = False
errs = []
for f in (global_h | global_c):
if f.startswith(BUILD_DIR):
continue
if not os.path.exists(f):
refs = global_refs[f]
if refs:
for cf, i in refs:
errs.append((cf, i))
else:
raise Exception("CMake references missing, internal error, aborting!")
is_err = True
errs.sort()
errs.reverse()
for cf, i in errs:
print("%s:%d" % (cf, i))
# Write a `sed` script, useful if we get a lot of theses:
# `print("sed '%dd' '%s' > '%s.tmp' ; mv '%s.tmp' '%s'" % (i, cf, cf, cf, cf))`
if is_err:
raise Exception("CMake references missing files, aborting!")
del is_err
del errs
# now check on files not accounted for.
print("\nC/C++ Files CMake does not know about...")
for cf in sorted(source_list(SOURCE_DIR, is_c)):
if not is_ignore_source(cf, ignore_used_source):
if cf not in global_c:
print("missing_c: ", cf)
# Check if `automake` builds a corresponding `.o` file.
'''
if cf in global_c:
out1 = os.path.splitext(cf)[0] + ".o"
out2 = os.path.splitext(cf)[0] + ".Po"
out2_dir, out2_file = out2 = os.path.split(out2)
out2 = os.path.join(out2_dir, ".deps", out2_file)
if not os.path.exists(out1) and not os.path.exists(out2):
print("bad_c: ", cf)
'''
print("\nC/C++ Headers CMake does not know about...")
for hf in sorted(source_list(SOURCE_DIR, is_c_header)):
if not is_ignore_source(hf, ignore_used_source):
if hf not in global_h:
print("missing_h: ", hf)
if UTF8_CHECK:
# test encoding
import traceback
for files in (global_c, global_h):
for f in sorted(files):
if os.path.exists(f):
# ignore outside of our source tree
if "extern" not in f:
i = 1
try:
for _ in open(f, "r", encoding="utf8"):
i += 1
except UnicodeDecodeError:
print("Non utf8: %s:%d" % (f, i))
if i > 1:
traceback.print_exc()
# Check ignores aren't stale
print("\nCheck for unused 'IGNORE_SOURCE' paths...")
for index, ignore_path in enumerate(IGNORE_SOURCE):
if not ignore_used_source[index]:
print("unused ignore: %r" % ignore_path)
# Check ignores aren't stale
print("\nCheck for unused 'IGNORE_SOURCE_MISSING' paths...")
for k, v in sorted(global_ignore_source_missing.items()):
for ignore_path in v:
print("unused ignore: %r -> %r" % (ignore_path, k))
# Check ignores aren't stale
print("\nCheck for unused 'IGNORE_CMAKE' paths...")
for index, ignore_path in enumerate(IGNORE_CMAKE):
if not ignore_used_cmake[index]:
print("unused ignore: %r" % ignore_path)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,142 @@
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
__all__ = (
"BUILD_DIR",
"IGNORE_CMAKE",
"IGNORE_SOURCE",
"IGNORE_SOURCE_MISSING",
"SOURCE_DIR",
"UTF8_CHECK",
)
import os
IGNORE_SOURCE = (
"/lib/",
"/test/",
"/tests/gtests/",
# Specific source files.
"extern/audaspace/",
"extern/quadriflow/3rd/",
"extern/mantaflow/",
"extern/Eigen3/",
# Use for `WIN32` only.
"source/creator/blender_launcher_win32.c",
# Pre-computed headers.
"source/blender/freestyle/FRS_precomp.h",
# Specific source files.
"extern/bullet2/src/BulletCollision/CollisionDispatch/btBox2dBox2dCollisionAlgorithm.cpp",
"extern/bullet2/src/BulletCollision/CollisionDispatch/btConvex2dConvex2dAlgorithm.cpp",
"extern/bullet2/src/BulletCollision/CollisionDispatch/btInternalEdgeUtility.cpp",
"extern/bullet2/src/BulletCollision/CollisionShapes/btBox2dShape.cpp",
"extern/bullet2/src/BulletCollision/CollisionShapes/btConvex2dShape.cpp",
"extern/bullet2/src/BulletDynamics/Character/btKinematicCharacterController.cpp",
"extern/bullet2/src/BulletDynamics/ConstraintSolver/btHinge2Constraint.cpp",
"extern/bullet2/src/BulletDynamics/ConstraintSolver/btUniversalConstraint.cpp",
# Specific source files.
"extern/bullet2/src/BulletCollision/BroadphaseCollision/btAxisSweep3Internal.h",
"extern/bullet2/src/BulletCollision/CollisionDispatch/btBox2dBox2dCollisionAlgorithm.h",
"extern/bullet2/src/BulletCollision/CollisionDispatch/btCollisionDispatcherMt.h",
"extern/bullet2/src/BulletCollision/CollisionDispatch/btConvex2dConvex2dAlgorithm.h",
"extern/bullet2/src/BulletCollision/CollisionDispatch/btInternalEdgeUtility.h",
"extern/bullet2/src/BulletCollision/CollisionShapes/btBox2dShape.h",
"extern/bullet2/src/BulletCollision/CollisionShapes/btConvex2dShape.h",
"extern/bullet2/src/BulletCollision/Gimpact/btContactProcessingStructs.h",
"extern/bullet2/src/BulletCollision/Gimpact/btGImpactBvhStructs.h",
"extern/bullet2/src/BulletCollision/Gimpact/btGImpactQuantizedBvhStructs.h",
"extern/bullet2/src/BulletCollision/Gimpact/gim_pair.h",
"extern/bullet2/src/BulletDynamics/Character/btKinematicCharacterController.h",
"extern/bullet2/src/BulletDynamics/ConstraintSolver/btBatchedConstraints.h",
"extern/bullet2/src/BulletDynamics/ConstraintSolver/btHinge2Constraint.h",
"extern/bullet2/src/BulletDynamics/ConstraintSolver/btSequentialImpulseConstraintSolverMt.h",
"extern/bullet2/src/BulletDynamics/ConstraintSolver/btUniversalConstraint.h",
"extern/bullet2/src/BulletDynamics/Dynamics/btDiscreteDynamicsWorldMt.h",
"extern/bullet2/src/BulletDynamics/Dynamics/btSimulationIslandManagerMt.h",
"extern/bullet2/src/BulletDynamics/Featherstone/btMultiBodyFixedConstraint.h",
"extern/bullet2/src/BulletDynamics/Featherstone/btMultiBodyGearConstraint.h",
"extern/bullet2/src/BulletDynamics/Featherstone/btMultiBodyInplaceSolverIslandCallback.h",
"extern/bullet2/src/BulletDynamics/Featherstone/btMultiBodyMLCPConstraintSolver.h",
"extern/bullet2/src/BulletDynamics/Featherstone/btMultiBodySliderConstraint.h",
"extern/bullet2/src/BulletDynamics/Featherstone/btMultiBodySphericalJointMotor.h",
"extern/bullet2/src/BulletSoftBody/DeformableBodyInplaceSolverIslandCallback.h",
"extern/bullet2/src/BulletSoftBody/btCGProjection.h",
"extern/bullet2/src/BulletSoftBody/btConjugateGradient.h",
"extern/bullet2/src/BulletSoftBody/btConjugateResidual.h",
"extern/bullet2/src/BulletSoftBody/btDeformableBackwardEulerObjective.h",
"extern/bullet2/src/BulletSoftBody/btDeformableBodySolver.h",
"extern/bullet2/src/BulletSoftBody/btDeformableContactConstraint.h",
"extern/bullet2/src/BulletSoftBody/btDeformableContactProjection.h",
"extern/bullet2/src/BulletSoftBody/btDeformableCorotatedForce.h",
"extern/bullet2/src/BulletSoftBody/btDeformableGravityForce.h",
"extern/bullet2/src/BulletSoftBody/btDeformableLagrangianForce.h",
"extern/bullet2/src/BulletSoftBody/btDeformableLinearElasticityForce.h",
"extern/bullet2/src/BulletSoftBody/btDeformableMassSpringForce.h",
"extern/bullet2/src/BulletSoftBody/btDeformableMousePickingForce.h",
"extern/bullet2/src/BulletSoftBody/btDeformableMultiBodyConstraintSolver.h",
"extern/bullet2/src/BulletSoftBody/btDeformableMultiBodyDynamicsWorld.h",
"extern/bullet2/src/BulletSoftBody/btDeformableNeoHookeanForce.h",
"extern/bullet2/src/BulletSoftBody/btKrylovSolver.h",
"extern/bullet2/src/BulletSoftBody/btPreconditioner.h",
"extern/bullet2/src/BulletSoftBody/btSoftMultiBodyDynamicsWorld.h",
"extern/bullet2/src/BulletSoftBody/poly34.h",
"extern/bullet2/src/LinearMath/TaskScheduler/btThreadSupportInterface.h",
"extern/bullet2/src/LinearMath/btImplicitQRSVD.h",
"extern/bullet2/src/LinearMath/btModifiedGramSchmidt.h",
"extern/bullet2/src/LinearMath/btReducedVector.h",
"extern/bullet2/src/LinearMath/btThreads.h",
"doc/doxygen/doxygen.extern.h",
"doc/doxygen/doxygen.intern.h",
"doc/doxygen/doxygen.main.h",
"doc/doxygen/doxygen.source.h",
"build_files/build_environment/patches/config_gmpxx.h",
# These could be included but are not part of Blender's core.
"intern/libmv/libmv/multiview/test_data_sets.h",
)
# Ignore cmake file, path pairs,
# NOTE: keep commented block to show the intended format (even when unused).
IGNORE_SOURCE_MISSING: tuple[tuple[str, tuple[str, ...]], ...] = (
( # Use for `WITH_NANOVDB`.
"intern/cycles/kernel/device/hiprt/CMakeLists.txt", (
"hiprt/impl/Aabb.h",
"hiprt/impl/BvhNode.h",
"hiprt/impl/Geometry.h",
"hiprt/impl/hiprt_device_impl.h",
"hiprt/impl/hiprt_kernels_bitcode.h",
"hiprt/impl/Instance.h",
"hiprt/impl/QrDecomposition.h",
"hiprt/impl/Quaternion.h",
"hiprt/impl/Scene.h",
"hiprt/impl/Transform.h",
"hiprt/impl/Triangle.h",
"hiprt/hiprt_common.h",
"hiprt/hiprt_device.h",
"hiprt/hiprt_math.h",
"hiprt/hiprt_types.h",
"hiprt/hiprt_vec.h",
),
),
)
IGNORE_CMAKE = (
"extern/audaspace/CMakeLists.txt",
)
UTF8_CHECK = True
SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
# doesn't have to exist, just use as reference
BUILD_DIR = os.path.normpath(os.path.abspath(os.path.normpath(os.path.join(SOURCE_DIR, "..", "build"))))

View File

@@ -0,0 +1,179 @@
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
Utility for reporting deprecated code which should be removed,
noted by the date which must be included with the *DEPRECATED* comment.
Once this date is past, the code should be removed.
"""
__all__ = (
"main",
)
import os
import datetime
from os.path import splitext
from collections.abc import (
Callable,
Iterator,
)
SKIP_DIRS = (
"extern",
"lib",
"tests",
)
class term_colors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
def is_c_header(filename: str) -> bool:
ext = splitext(filename)[1]
return (ext in {".h", ".hh", ".hpp", ".hxx", ".hh"})
def is_c(filename: str) -> bool:
ext = splitext(filename)[1]
return (ext in {".c", ".cc", ".cpp", ".cxx", ".m", ".mm", ".rc", ".inl"})
def is_c_any(filename: str) -> bool:
return is_c(filename) or is_c_header(filename)
def is_py(filename: str) -> bool:
ext = splitext(filename)[1]
return (ext == ".py")
def is_source_any(filename: str) -> bool:
return is_c_any(filename) or is_py(filename)
def source_list(path: str, filename_check: Callable[[str], bool] | None = None) -> Iterator[str]:
for dirpath, dirnames, filenames in os.walk(path):
# skip '.git'
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
for filename in filenames:
if filename_check is None or filename_check(filename):
yield os.path.join(dirpath, filename)
def deprecations() -> list[tuple[datetime.datetime, tuple[str, int], str]]:
"""
Searches out source code for lines like
/* *DEPRECATED* 2011/7/17 ``bgl.Buffer.list`` info text. */
Or...
# *DEPRECATED* 2010/12/22 ``some.py.func`` more info.
"""
SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
SKIP_DIRS_ABS = [os.path.join(SOURCE_DIR, p) for p in SKIP_DIRS]
DEPRECATED_ID = "*DEPRECATED*"
depprecation_list = []
scan_count = 0
print("Scanning in %r for '%s YYYY/MM/DD info'" % (SOURCE_DIR, DEPRECATED_ID), end="...")
for fn in source_list(SOURCE_DIR, is_source_any):
if os.path.samefile(fn, __file__):
continue
skip = False
for p in SKIP_DIRS_ABS:
if fn.startswith(p):
skip = True
break
if skip:
continue
with open(fn, 'r', encoding="utf8") as fh:
fn = os.path.relpath(fn, SOURCE_DIR)
buf = fh.read()
index = 0
while True:
index = buf.find(DEPRECATED_ID, index)
if index == -1:
break
index_end = buf.find("\n", index)
if index_end == -1:
index_end = len(buf)
line_number = buf[:index].count("\n") + 1
l = buf[index + len(DEPRECATED_ID): index_end].strip()
try:
data = [w.strip() for w in l.split('/', 2)]
data[-1], info = data[-1].split(' ', 1)
info = info.split("*/", 1)[0].strip()
if len(data) != 3:
print(
" poorly formatting line:\n"
" %r:%d\n"
" %s" %
(fn, line_number, data)
)
else:
depprecation_list.append((
datetime.datetime(int(data[0]), int(data[1]), int(data[2])),
(fn, line_number),
info,
))
except:
print("Error file - %r:%d" % (fn, line_number))
import traceback
traceback.print_exc()
index = index_end
scan_count += 1
print(" {:d} files done, found {:d} deprecation(s)!".format(scan_count, len(depprecation_list)))
return depprecation_list
def main() -> None:
import datetime
now = datetime.datetime.now()
deps = deprecations()
for data, fileinfo, info in deps:
days_old = (now - data).days
info = term_colors.BOLD + info + term_colors.ENDC
if days_old > 0:
info = "[" + term_colors.FAIL + "REMOVE" + term_colors.ENDC + "] " + info
else:
info = "[" + term_colors.OKBLUE + "OK" + term_colors.ENDC + "] " + info
print("{:s}: days-old({:d}), {:s}:{:d} {:s}".format(
data.strftime("%Y/%m/%d"),
days_old,
fileinfo[0],
fileinfo[1],
info,
))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,140 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
this script updates XML themes once new settings are added
./blender.bin --background --python tools/check_source/check_descriptions.py
"""
__all__ = (
"main",
)
import bpy
# These are known duplicates which do not warn.
DUPLICATE_ACCEPT = (
# operators
('ACTION_OT_clean', 'GRAPH_OT_clean'),
('ACTION_OT_clickselect', 'GRAPH_OT_clickselect'),
('ACTION_OT_copy', 'GRAPH_OT_copy'),
('ACTION_OT_delete', 'GRAPH_OT_delete'),
('ACTION_OT_duplicate', 'GRAPH_OT_duplicate'),
('ACTION_OT_duplicate_move', 'GRAPH_OT_duplicate_move'),
('ACTION_OT_extrapolation_type', 'GRAPH_OT_extrapolation_type'),
('ACTION_OT_handle_type', 'GRAPH_OT_handle_type'),
('ACTION_OT_interpolation_type', 'GRAPH_OT_interpolation_type'),
('ACTION_OT_keyframe_insert', 'GRAPH_OT_keyframe_insert'),
('ACTION_OT_mirror', 'GRAPH_OT_mirror'),
('ACTION_OT_paste', 'GRAPH_OT_paste'),
('ACTION_OT_sample', 'GRAPH_OT_sample'),
('ACTION_OT_select_all', 'GRAPH_OT_select_all'),
('ACTION_OT_select_border', 'GRAPH_OT_select_border'),
('ACTION_OT_select_column', 'GRAPH_OT_select_column'),
('ACTION_OT_select_leftright', 'GRAPH_OT_select_leftright'),
('ACTION_OT_select_less', 'GRAPH_OT_select_less'),
('ACTION_OT_select_linked', 'GRAPH_OT_select_linked'),
('ACTION_OT_select_more', 'GRAPH_OT_select_more'),
('ACTION_OT_unlink', 'NLA_OT_action_unlink'),
('ACTION_OT_view_all', 'CLIP_OT_dopesheet_view_all', 'GRAPH_OT_view_all'),
('ACTION_OT_view_frame', 'GRAPH_OT_view_frame'),
('ANIM_OT_change_frame', 'CLIP_OT_change_frame', 'IMAGE_OT_change_frame'),
('ARMATURE_OT_autoside_names', 'POSE_OT_autoside_names'),
('ARMATURE_OT_bone_layers', 'POSE_OT_bone_layers'),
('ARMATURE_OT_extrude_forked', 'ARMATURE_OT_extrude_move'),
('ARMATURE_OT_flip_names', 'POSE_OT_flip_names'),
('ARMATURE_OT_select_all', 'POSE_OT_select_all'),
('ARMATURE_OT_select_hierarchy', 'POSE_OT_select_hierarchy'),
('ARMATURE_OT_select_linked', 'POSE_OT_select_linked'),
('ARMATURE_OT_select_mirror', 'POSE_OT_select_mirror'),
('CLIP_OT_cursor_set', 'UV_OT_cursor_set'),
('CLIP_OT_disable_markers', 'CLIP_OT_graph_disable_markers'),
('CLIP_OT_graph_select_border', 'MASK_OT_select_border'),
('CLIP_OT_view_ndof', 'IMAGE_OT_view_ndof', 'VIEW2D_OT_ndof'),
('CLIP_OT_view_pan', 'IMAGE_OT_view_pan', 'VIEW2D_OT_pan', 'VIEW3D_OT_view_pan'),
('CLIP_OT_view_zoom', 'VIEW2D_OT_zoom'),
('CLIP_OT_view_zoom_in', 'VIEW2D_OT_zoom_in'),
('CLIP_OT_view_zoom_out', 'VIEW2D_OT_zoom_out'),
('CONSOLE_OT_copy', 'FONT_OT_text_copy', 'TEXT_OT_copy'),
('CONSOLE_OT_delete', 'FONT_OT_delete', 'TEXT_OT_delete'),
('CONSOLE_OT_insert', 'FONT_OT_text_insert', 'TEXT_OT_insert'),
('CONSOLE_OT_paste', 'FONT_OT_text_paste', 'TEXT_OT_paste'),
('CURVE_OT_handle_type_set', 'MASK_OT_handle_type_set'),
('CURVE_OT_shortest_path_pick', 'MESH_OT_shortest_path_pick'),
('CURVE_OT_switch_direction', 'MASK_OT_switch_direction'),
('FONT_OT_line_break', 'TEXT_OT_line_break'),
('FONT_OT_move', 'TEXT_OT_move'),
('FONT_OT_move_select', 'TEXT_OT_move_select'),
('FONT_OT_select_all', 'TEXT_OT_select_all'),
('FONT_OT_text_cut', 'TEXT_OT_cut'),
('GRAPH_OT_previewrange_set', 'NLA_OT_previewrange_set'),
('GRAPH_OT_properties', 'IMAGE_OT_properties', 'LOGIC_OT_properties', 'NLA_OT_properties'),
('IMAGE_OT_clear_render_border', 'VIEW3D_OT_clear_render_border'),
('IMAGE_OT_render_border', 'VIEW3D_OT_render_border'),
('IMAGE_OT_toolshelf', 'NODE_OT_toolbar', 'VIEW3D_OT_toolshelf'),
('LATTICE_OT_select_ungrouped', 'MESH_OT_select_ungrouped', 'PAINT_OT_vert_select_ungrouped'),
('MESH_OT_extrude_region_move', 'MESH_OT_extrude_region_shrink_fatten'),
('NODE_OT_add_node', 'NODE_OT_add_search'),
('NODE_OT_move_detach_links', 'NODE_OT_move_detach_links_release'),
('NODE_OT_properties', 'VIEW3D_OT_properties'),
('OBJECT_OT_bake', 'OBJECT_OT_bake_image'),
('OBJECT_OT_duplicate_move', 'OBJECT_OT_duplicate_move_linked'),
('WM_OT_context_cycle_enum', 'WM_OT_context_toggle', 'WM_OT_context_toggle_enum'),
('WM_OT_context_set_boolean', 'WM_OT_context_set_enum', 'WM_OT_context_set_float',
'WM_OT_context_set_int', 'WM_OT_context_set_string', 'WM_OT_context_set_value'),
)
DUPLICATE_IGNORE = {
"",
}
def check_duplicates():
import _rna_info as rna_info
DUPLICATE_IGNORE_FOUND = set()
DUPLICATE_ACCEPT_FOUND = set()
structs, funcs, ops, props = rna_info.BuildRNAInfo()
# This is mainly useful for operators,
# other types have too many false positives
# for t in (structs, funcs, ops, props):
for t in (ops, ):
description_dict = {}
print("")
for k, v in t.items():
if v.description not in DUPLICATE_IGNORE:
id_str = ".".join([s if isinstance(s, str) else s.identifier for s in k if s])
description_dict.setdefault(v.description, []).append(id_str)
else:
DUPLICATE_IGNORE_FOUND.add(v.description)
# sort for easier viewing
sort_ls = [(tuple(sorted(v)), k) for k, v in description_dict.items()]
sort_ls.sort()
for v, k in sort_ls:
if len(v) > 1:
if v not in DUPLICATE_ACCEPT:
print("found %d: %r, \"%s\"" % (len(v), v, k))
# print("%r," % (v,))
else:
DUPLICATE_ACCEPT_FOUND.add(v)
test = (DUPLICATE_IGNORE - DUPLICATE_IGNORE_FOUND)
if test:
print("Invalid 'DUPLICATE_IGNORE': %r" % test)
test = (set(DUPLICATE_ACCEPT) - DUPLICATE_ACCEPT_FOUND)
if test:
print("Invalid 'DUPLICATE_ACCEPT': %r" % test)
def main():
check_duplicates()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,182 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
Run this script to check if headers are included multiple times.
python3 check_header_duplicate.py
Now build the code to find duplicate errors, resolve them manually.
Then restore the headers to their original state:
python3 check_header_duplicate.py --restore
"""
__all__ = (
"main",
)
import os
import sys
import argparse
from collections.abc import (
Callable,
Iterator,
)
# Use GCC's `__INCLUDE_LEVEL__` to find direct duplicate includes.
BASEDIR = os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
# TODO: make this an argument.
dirs_include = [
os.path.join(BASEDIR, "intern"),
os.path.join(BASEDIR, "source"),
]
files_exclude = {
os.path.join(BASEDIR, "extern", "curve_fit_nd", "intern", "generic_alloc_impl.h"),
os.path.join(BASEDIR, "source", "blender", "blenlib", "intern", "list_sort_impl.h"),
os.path.join(BASEDIR, "source", "blender", "makesdna", "intern", "dna_rename_defs.h"),
os.path.join(BASEDIR, "source", "blender", "makesrna", "RNA_enum_items.hh"),
}
HEADER_FMT = """\
#if __INCLUDE_LEVEL__ == 1
# ifdef _DOUBLEHEADERGUARD_{0:d}
# error "duplicate header!"
# endif
#endif
#if __INCLUDE_LEVEL__ == 1
# define _DOUBLEHEADERGUARD_{0:d}
#endif /* END! */
"""
HEADER_END = "#endif /* END! */\n"
UUID = 0
def source_filepath_guard_add(filepath: str) -> None:
global UUID
header = HEADER_FMT.format(UUID)
UUID += 1
with open(filepath, 'r', encoding='utf-8') as f:
data = f.read()
with open(filepath, 'w', encoding='utf-8') as f:
f.write(header)
f.write(data)
def source_filepath_guard_restore(filepath: str) -> None:
with open(filepath, 'r', encoding='utf-8') as f:
data = f.read()
index = data.index(HEADER_END)
if index == -1:
return
with open(filepath, 'w', encoding='utf-8') as f:
f.write(data[index + len(HEADER_END):])
def scan_source_recursive(dirpath: str, is_restore: bool) -> None:
from os.path import splitext
def source_list(
path: str,
filename_check: Callable[[str], bool] | None = None,
) -> Iterator[str]:
for dirpath, dirnames, filenames in os.walk(path):
# skip '.git'
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
for filename in filenames:
filepath = os.path.join(dirpath, filename)
if filename_check is None or filename_check(filepath):
yield filepath
def is_header_source(filename: str) -> bool:
ext = splitext(filename)[1]
return (ext in {".hpp", ".hxx", ".h", ".hh"})
for filepath in sorted(source_list(dirpath, is_header_source)):
if filepath in files_exclude:
continue
print("file:", filepath)
if is_restore:
source_filepath_guard_restore(filepath)
else:
source_filepath_guard_add(filepath)
def argparse_create() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Detect duplicate headers",
epilog=__doc__,
# Don't re-wrap text, keep newlines & indentation.
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--restore",
dest="restore",
default=False,
action='store_true',
help=(
"Restore the files to their original state"
"(default=False)"
),
required=False,
)
parser.add_argument(
"paths",
nargs=argparse.REMAINDER,
help="All trailing arguments are treated as paths.",
)
return parser
def main() -> int:
ok = True
args = argparse_create().parse_args()
if args.paths:
paths = [os.path.normpath(os.path.abspath(p)) for p in args.paths]
else:
paths = dirs_include
for p in paths:
if not p.startswith(BASEDIR + os.sep):
sys.stderr.write("Path \"{:s}\" outside \"{:s}\", aborting!\n".format(p, BASEDIR))
ok = False
if not os.path.exists(p):
sys.stderr.write("Path \"{:s}\" does not exist, aborting!\n".format(p))
ok = False
for p in files_exclude:
if not os.path.exists(p):
sys.stderr.write("Excluded path \"{:s}\" does not exist, aborting!\n".format(p))
ok = False
if not ok:
return 1
for dirpath in paths:
scan_source_recursive(dirpath, args.restore)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,605 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2022-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
Check license headers follow the SPDX spec
https://spdx.org/licenses/
This can be activated by calling "make check_licenses" from Blenders root directory.
"""
__all__ = (
"main",
)
import os
import argparse
import datetime
import re
from dataclasses import dataclass
from collections.abc import (
Callable,
Iterator,
)
# -----------------------------------------------------------------------------
# Constants
# Add one, maybe someone runs this on new-years in another timezone or so.
YEAR_MAX = datetime.date.today().year + 1
# Lets not worry about software written before this time.
YEAR_MIN = 1950
YEAR_RANGE = range(YEAR_MIN, YEAR_MAX + 1)
# Faster bug makes exceptions and errors more difficult to troubleshoot.
USE_MULTIPROCESS = False
EXPECT_SPDX_IN_FIRST_CHARS = 1024
# Show unique headers after modifying them.
# Useful when reviewing changes as there may be many duplicates.
REPORT_UNIQUE_HEADER_MAPPING = False
mapping: dict[str, list[str]] = {}
SOURCE_DIR = os.path.normpath(
os.path.abspath(
os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
)
)
SPDX_IDENTIFIER_FILE = os.path.join(
SOURCE_DIR, "doc", "license", "SPDX-license-identifiers.txt"
)
SPDX_IDENTIFIER_UNKNOWN = "*Unknown License*"
with open(SPDX_IDENTIFIER_FILE, "r", encoding="utf-8") as fh:
ACCEPTABLE_LICENSES = set(line.split()[0] for line in sorted(fh) if "https://spdx.org/licenses/" in line)
del fh
# -----------------------------------------------------------------------------
# Global Variables
# Count how many licenses are used.
SPDX_IDENTIFIER_STATS: dict[str, int] = {SPDX_IDENTIFIER_UNKNOWN: 0}
# -----------------------------------------------------------------------------
# File Type Checks
# Use `/* .. */` style comments.
def filename_is_c_compat(filename: str) -> bool:
return filename.endswith(
(
# C.
".c",
".h",
# C++
".cc",
".cxx",
".cpp",
".hh",
".hxx",
".hpp",
".inl",
# Objective-C/C++
".m",
".mm",
# OpenGL Shading Language.
".glsl",
# OPENCL.
".cl",
# CUDA.
".cu",
# Metal.
".metal",
# Metal Shading Language.
".msl",
# Open Shading Language.
".osl",
# Cycles uses this extension.
".tables",
)
)
def filename_is_cmake(filename: str) -> bool:
return filename.endswith(("CMakeLists.txt", ".cmake"))
# Use '#' style comments.
def filename_is_script_compat(filename: str) -> bool:
return filename.endswith((".py", ".sh", "GNUmakefile"))
# -----------------------------------------------------------------------------
# Cursor Motion
def txt_next_line_while_fn(text: str, index: int, fn: Callable[[str], bool]) -> int:
"""
Return the next line where ``fn`` fails.
"""
while index < len(text):
index_prev = index
index = text.find("\n", index)
if index == -1:
index = len(text)
if not fn(text[index_prev:index]):
index = index_prev
break
# Step over the newline.
index = index + 1
return index
def txt_next_eol(text: str, pos: int, limit: int, step_over: bool) -> int:
"""
Extend ``pos`` to just before the next EOL, otherwise EOF.
As this is intended for use as a range, ``text[pos]``
will either be ``\n`` or equal to out of range (equal to ``len(text)``).
"""
if pos + 1 >= len(text):
return pos
# Already at the bounds.
if text[pos] == "\n":
return pos + (1 if step_over else 0)
pos_next = text.find("\n", pos, limit)
if pos_next == -1:
return limit
return pos_next + (1 if step_over else 0)
def txt_prev_bol(text: str, pos: int, limit: int) -> int:
if pos == 0:
return pos
# Already at the bounds.
if text[pos - 1] == "\n":
return pos
pos_next = text.rfind("\n", limit, pos)
if pos_next == -1:
return limit
# We don't want to include the newline.
return pos_next + 1
def txt_anonymous_years(text: str) -> str:
"""
Replace year with text, since we don't want to consider them different when looking at unique headers.
"""
# Replace year ranges with `2005-2009`: `####`.
def key_replace_range(match: re.Match[str]) -> str:
values = match.groups()
if int(values[0]) in YEAR_RANGE and int(values[1]) in YEAR_RANGE:
return '#' * len(values[0])
return match.group()
text = re.sub(r'([0-9]+)-([0-9]+)', key_replace_range, text)
# Replace year ranges with `2005`: `####`.
def key_replace(match: re.Match[str]) -> str:
values = match.groups()
if int(values[0]) in YEAR_RANGE:
return '#' * len(values[0])
return match.group()
text = re.sub(r'([0-9]+)', key_replace, text)
return text
def txt_find_next_indented_block(text: str, find: str, pos: int, limit: int) -> tuple[int, int]:
"""
Support for finding an indented block of text.
Return the identifier index and the end of the block.
Where searching for ``SPDX-FileCopyrightText: ``
.. code-block::
# SPDX-FileCopyrightText: 2020 Name
^ begin ^ end.
With multiple lines supported:
.. code-block::
# SPDX-FileCopyrightText: 2020 Name
# 2021 Another Name
^ begin (one line up) ^ end.
"""
pos_found = text.find(find, pos, limit)
if pos_found == -1:
return (-1, -1)
pos_next = txt_next_eol(text, pos_found, limit - 1, False) + 1
if pos_next != limit:
pos_found_indent = pos_found - txt_prev_bol(text, pos_found, 0)
while True:
# Step over leading comment chars.
pos_next_test = pos_next + pos_found_indent
pos_next_step = pos_next_test + len(find)
# The next lines text is indented.
text_indent = text[pos_next_test:pos_next_step]
if (len(text_indent) == pos_next_step - pos_next_test) and (not text[pos_next_test:pos_next_step].strip()):
pos_next = txt_next_eol(text, pos_next_step, limit - 1, step_over=False) + 1
else:
break
return (pos_found, pos_next)
# -----------------------------------------------------------------------------
# License Checker
def check_contents(filepath: str, text: str) -> None:
"""
Check for license text, e.g: ``SPDX-License-Identifier: GPL-2.0-or-later``
Intentionally be strict here... no extra spaces, no trailing space at the end of line etc.
As there is no reason to be sloppy in this case.
"""
text_header = text[:EXPECT_SPDX_IN_FIRST_CHARS]
# Use the license to limit the copyright search,
# so code-generation that includes copyright headers don't cause false alarms.
license_id = " SPDX-License-Identifier: "
license_id_beg = text_header.find(license_id)
if license_id_beg == -1:
# Allow completely empty files (sometimes `__init__.py`).
if not text.rstrip():
return
# Empty file already accounted for.
print("Missing {:s}{:s}".format(license_id, filepath))
SPDX_IDENTIFIER_STATS[SPDX_IDENTIFIER_UNKNOWN] += 1
return
# Check copyright text, reading multiple (potentially multi-line indented) blocks.
copyright_id = " SPDX-FileCopyrightText: "
copyright_id_step = 0
copyright_id_beg = -1
copyright_id_end = -1
while ((copyright_id_item := txt_find_next_indented_block(
text_header,
copyright_id,
copyright_id_step,
license_id_beg,
)) != (-1, -1)):
if copyright_id_end == -1:
# Set once.
copyright_id_beg = copyright_id_item[0]
else:
lines = text_header[copyright_id_end:copyright_id_item[0]].count("\n")
if lines != 0:
print(
"Expected no blank lines, found {:d} between \"{:s}\": {:s}".format(
lines,
copyright_id,
filepath,
))
copyright_id_end = copyright_id_item[1]
copyright_id_step = copyright_id_end
del copyright_id_item, copyright_id_step
if copyright_id_beg == -1:
print("Missing {:s}{:s}".format(copyright_id, filepath))
# Maintain statistics.
SPDX_IDENTIFIER_STATS[SPDX_IDENTIFIER_UNKNOWN] += 1
return
# Check for blank lines:
blank_lines = text[:copyright_id_beg].count("\n")
if filename_is_script_compat(filepath):
if blank_lines > 0 and text.startswith("#!/"):
blank_lines -= 1
if blank_lines > 0:
print("SPDX \"{:s}\" not on first line: {:s}".format(copyright_id, filepath))
# Leading char.
leading_char = text_header[txt_prev_bol(text_header, license_id_beg, 0):license_id_beg].strip()
text_blank_line = text_header[copyright_id_end:license_id_beg]
if (text_blank_line.count("\n") != 1) or (text_blank_line.replace(leading_char, "").strip() != ""):
print("Expected blank line between \"{:s}\" & \"{:s}\": {:s}".format(copyright_id, license_id, filepath))
del text_blank_line, leading_char
license_id_end = license_id_beg + len(license_id)
line_end = txt_next_eol(text, license_id_end, len(text), step_over=False)
license_text = text[license_id_end:line_end]
# For C/C++ comments.
license_text = license_text.rstrip("*/")
for license_id in license_text.split():
if license_id in {"AND", "OR"}:
continue
if license_id not in ACCEPTABLE_LICENSES:
print(
"Unexpected:",
"{:s}:{:d}".format(filepath, text[:license_id_beg].count("\n") + 1),
"contains license",
repr(license_text),
"not in",
SPDX_IDENTIFIER_FILE,
)
try:
SPDX_IDENTIFIER_STATS[license_id] += 1
except KeyError:
SPDX_IDENTIFIER_STATS[license_id] = 1
if REPORT_UNIQUE_HEADER_MAPPING:
if filename_is_c_compat(filepath):
comment_beg = text.rfind("/*", 0, license_id_beg)
if comment_beg == -1:
print("Comment Block:", filepath, "failed to find comment start")
return
comment_end = text.find("*/", license_id_end, len(text))
if comment_end == -1:
print("Comment Block:", filepath, "failed to find comment end")
return
comment_end += 2
comment_block = text[comment_beg + 2: comment_end - 2]
comment_block = "\n".join(
[line.removeprefix(" *") for line in comment_block.split("\n")]
)
elif filename_is_script_compat(filepath) or filename_is_cmake(filepath):
comment_beg = txt_prev_bol(text, license_id_beg, 0)
comment_end = txt_next_eol(text, license_id_beg, len(text), step_over=False)
comment_beg = txt_next_line_while_fn(
text,
comment_beg,
lambda line: line.startswith("#") and not line.startswith("#!/"),
)
comment_end = txt_next_line_while_fn(
text,
comment_end,
lambda line: line.startswith("#"),
)
comment_block = text[comment_beg:comment_end].rstrip()
comment_block = "\n".join(
[line.removeprefix("# ") for line in comment_block.split("\n")]
)
else:
raise Exception("Unknown file type: {:s}".format(filepath))
mapping.setdefault(txt_anonymous_years(comment_block), []).append(filepath)
def report_statistics() -> None:
"""
Report some final statistics of license usage.
"""
print("")
files_total = sum(SPDX_IDENTIFIER_STATS.values())
files_unknown = SPDX_IDENTIFIER_STATS[SPDX_IDENTIFIER_UNKNOWN]
files_percent = (1.0 - (files_unknown / files_total)) * 100.0
files_percent_str = "{:.2f}".format(files_percent)
# Never show 100.00% if it's not complete.
if files_percent_str == "100.00" and files_percent != 100.0:
files_percent_str = "{:.8f}".format(files_percent).rstrip("0")
title = "License Statistics in {:,d} Files, {:s}% Complete".format(files_total, files_percent_str)
print("#" * len(title))
print(title)
print("#" * len(title))
print("")
max_length = max(len(k) for k in SPDX_IDENTIFIER_STATS.keys())
print(" License:" + (" " * (max_length - 7)) + "Files:")
print("")
items = [(k, "{:,d}".format(v)) for k, v in sorted(SPDX_IDENTIFIER_STATS.items())]
v_max = max([len(v) for _, v in items])
for k, v in items:
if v == "0":
continue
print("-", k + " " * (max_length - len(k)), (" " * (v_max - len(v))) + v)
print("")
# -----------------------------------------------------------------------------
# Main Function & Source Listing
operation = check_contents
def source_files(
path: str,
paths_exclude: tuple[str, ...],
filename_test: Callable[[str], bool],
) -> Iterator[str]:
# Split paths into directories & files.
dirs_exclude_list = []
files_exclude_list = []
for f in paths_exclude:
if not os.path.exists(f):
raise Exception("File {!r} doesn't exist!".format(f))
if os.path.isdir(f):
dirs_exclude_list.append(f)
else:
files_exclude_list.append(f)
del paths_exclude
dirs_exclude_set = set(p.rstrip("/") for p in dirs_exclude_list)
dirs_exclude = tuple(p.rstrip("/") + "/" for p in dirs_exclude_list)
files_exclude_set = set(p.rstrip("/") for p in files_exclude_list)
del dirs_exclude_list, files_exclude_list
for dirpath, dirnames, filenames in os.walk(path):
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
if dirpath in dirs_exclude_set or dirpath.startswith(dirs_exclude):
continue
for filename in filenames:
if filename.startswith("."):
continue
filepath = os.path.join(dirpath, filename)
if filepath in files_exclude_set:
files_exclude_set.remove(filepath)
continue
if filename_test(filename):
yield filepath
if files_exclude_set:
raise Exception("Excluded paths not found: {!r}".format(repr(tuple(sorted(files_exclude_set)))))
def operation_wrap(filepath: str) -> None:
with open(filepath, "r", encoding="utf-8") as f:
try:
text = f.read()
except Exception as ex:
print("Failed to read", filepath, "with", repr(ex))
return
operation(filepath, text)
def argparse_create() -> argparse.ArgumentParser:
# When --help or no args are given, print this help
description = __doc__
parser = argparse.ArgumentParser(description=description)
parser.add_argument(
"--show-headers",
dest="show_headers",
type=bool,
default=False,
required=False,
help="Show unique headers (useful for spotting irregularities).",
)
return parser
def main() -> None:
global REPORT_UNIQUE_HEADER_MAPPING
args = argparse_create().parse_args()
REPORT_UNIQUE_HEADER_MAPPING = args.show_headers
# Ensure paths are relative to the root, no matter where this script runs from.
os.chdir(SOURCE_DIR)
@dataclass
class Pass:
filename_test: Callable[[str], bool]
source_paths_include: tuple[str, ...]
source_paths_exclude: tuple[str, ...]
passes = (
Pass(
filename_test=filename_is_c_compat,
source_paths_include=(".",),
source_paths_exclude=(
# Directories:
"./extern",
"./scripts/templates_osl",
"./tools",
# Exclude library sources (GIT-LFS).
"./lib",
# Needs manual handling as it mixes two licenses.
"./intern/atomic",
# Practically an `./extern` within an `./intern` module, leave as-is.
"./intern/itasc/kdl",
# TODO: Files in these directories should be handled but the files have valid licenses.
"./intern/libmv",
# Files:
# This file is generated by a configure script (no point in manually setting the license).
"./build_files/build_environment/patches/config_gmpxx.h",
# A modified `Apache-2.0` license.
"./intern/opensubdiv/internal/evaluator/shaders/osd_eval_patches_comp.glsl",
"./intern/opensubdiv/internal/evaluator/shaders/osd_eval_stencils_comp.glsl",
),
),
Pass(
filename_test=filename_is_cmake,
source_paths_include=(".",),
source_paths_exclude=(
# Directories:
# This is an exception, it has its own CMake files we do not maintain.
"./extern/audaspace",
"./extern/quadriflow/3rd/lemon-1.3.1",
# Exclude library sources (GIT-LFS).
"./lib",
),
),
Pass(
filename_test=filename_is_script_compat,
source_paths_include=(".",),
source_paths_exclude=(
# Directories:
# This is an exception, it has its own CMake files we do not maintain.
"./extern",
# Exclude library sources (GIT-LFS).
"./lib",
# Just data.
"./doc/python_api/examples",
"./scripts/presets",
"./scripts/templates_py",
),
),
)
for pass_data in passes:
if USE_MULTIPROCESS:
filepath_args = [
filepath
for dirpath in pass_data.source_paths_include
for filepath in source_files(
dirpath,
pass_data.source_paths_exclude,
pass_data.filename_test,
)
]
import multiprocessing
job_total = multiprocessing.cpu_count()
pool = multiprocessing.Pool(processes=job_total)
pool.map(operation_wrap, filepath_args)
else:
for filepath in [
filepath
for dirpath in pass_data.source_paths_include
for filepath in source_files(
dirpath,
pass_data.source_paths_exclude,
pass_data.filename_test,
)
]:
operation_wrap(filepath)
if REPORT_UNIQUE_HEADER_MAPPING:
print("#####################")
print("Unique Header Listing")
print("#####################")
print("")
for k, v in sorted(mapping.items()):
print("=" * 79)
print(k)
print("-" * 79)
v.sort()
for filepath in v:
print("-", filepath)
print("")
report_statistics()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
__all__ = (
"main",
)
import os
from os.path import join
from check_mypy_config import PATHS, PATHS_EXCLUDE
from typing import (
Any,
)
from collections.abc import (
Callable,
Iterator,
)
FileAndArgs = tuple[str, tuple[Any, ...], dict[str, str]]
# print(PATHS)
SOURCE_EXT = (
# Python
".py",
)
def is_source(filename: str) -> bool:
return filename.endswith(SOURCE_EXT)
def path_iter(
path: str,
filename_check: Callable[[str], bool] | None = None,
) -> Iterator[str]:
for dirpath, dirnames, filenames in os.walk(path):
# skip ".git"
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
for filename in filenames:
if filename.startswith("."):
continue
filepath = join(dirpath, filename)
if filename_check is None or filename_check(filepath):
yield filepath
def path_expand_with_args(
paths_and_args: tuple[FileAndArgs, ...],
filename_check: Callable[[str], bool] | None = None,
) -> Iterator[FileAndArgs]:
for f_and_args in paths_and_args:
f, f_args = f_and_args[0], f_and_args[1:]
if not os.path.exists(f):
print("Missing:", f)
elif os.path.isdir(f):
for f_iter in path_iter(f, filename_check):
yield (f_iter, *f_args)
else:
yield (f, *f_args)
def main() -> None:
import sys
# import subprocess
import shlex
# Fixed location, so change the current working directory doesn't create cache everywhere.
cache_dir = os.path.join(os.getcwd(), ".mypy_cache")
# Allow files which are listed explicitly to override files that are included as part of a directory.
# Needed when files need their own arguments and/or environment.
files_explicitly_listed: set[str] = {f for f, _extra_args, _extra_env in PATHS}
if os.path.samefile(sys.argv[-1], __file__):
paths = path_expand_with_args(PATHS, is_source)
else:
paths = path_expand_with_args(
tuple((p, (), {}) for p in sys.argv[1:]),
is_source,
)
for f, extra_args, extra_env in paths:
if f in PATHS_EXCLUDE:
continue
if f in files_explicitly_listed:
continue
if not extra_args:
extra_args = ()
if not extra_env:
extra_env = {}
print(f)
cmd = (
"mypy",
"--strict",
"--cache-dir=" + cache_dir,
"--color-output",
f,
*extra_args,
)
# `p = subprocess.Popen(cmd, env=extra_env, stdout=sys.stdout, stderr=sys.stderr)`
if extra_env:
for k, v in extra_env.items():
os.environ[k] = v
os.chdir(os.path.dirname(f))
os.system(" ".join([shlex.quote(arg) for arg in cmd]))
if extra_env:
for k in extra_env.keys():
del os.environ[k]
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,106 @@
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
__all__ = (
"PATHS",
"PATHS_EXCLUDE",
"SOURCE_DIR",
)
import os
from typing import (
Any,
)
# Notes:
# - Most tests in `tests/python` use `bpy` enough that it's simpler to list the scripts that *are* type checked.
# - References individual files which are also included in a directory are supported
# without checking those files twice. This is needed to allow those files to use their own settings.
PATHS: tuple[tuple[str, tuple[Any, ...], dict[str, str]], ...] = (
("build_files/cmake/", (), {'MYPYPATH': "modules"}),
("build_files/utils/", (), {'MYPYPATH': "modules"}),
("doc/manpage/blender.1.py", (), {}),
("release/datafiles/", (), {}),
("release/release_notes/", (), {}),
("scripts/modules/_bpy_internal/extensions/junction_module.py", (), {}),
("scripts/modules/_bpy_internal/extensions/wheel_manager.py", (), {}),
("scripts/modules/_bpy_internal/platform/freedesktop.py", (), {}),
("source/blender/nodes/intern/discover_nodes.py", (), {}),
("tests/python/bl_keymap_validate.py", (), {}),
("tests/python/bl_pyapi_bpy_app_tempdir.py", (), {}),
("tests/utils/blender_headless.py", (), {}),
("tools/check_blender_release/", (), {}),
("tools/check_docs/", (), {}),
("tools/check_source/", (), {'MYPYPATH': "modules"}),
("tools/check_source/check_unused_defines.py", (), {'MYPYPATH': "../utils_maintenance/modules"}),
("tools/check_source/static_check_size_comments.py", (), {'MYPYPATH': "../utils_maintenance/modules"}),
("tools/config/", (), {}),
("tools/triage/", (), {}),
("tools/utils/", (), {}),
("tools/utils_api/", (), {}),
("tools/utils_build/", (), {}),
("tools/utils_doc/", (), {}),
("tools/utils_ide/", (), {}),
("tools/utils_maintenance/", (), {'MYPYPATH': "modules"}),
)
SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.normpath(
os.path.join(os.path.dirname(__file__), "..", ".."))))
PATHS_EXCLUDE = set(
os.path.join(SOURCE_DIR, p.replace("/", os.sep))
for p in
(
"release/datafiles/blender_icons_geom.py", # Uses `bpy` too much.
"tests/utils/bl_run_operators.py", # Uses `bpy` too much.
"tests/utils/bl_run_operators_event_simulate.py", # Uses `bpy` too much.
"tools/check_blender_release/check_module_enabled.py",
"tools/check_blender_release/check_module_numpy.py",
"tools/check_blender_release/check_module_requests.py",
"tools/check_blender_release/check_release.py",
"tools/check_blender_release/check_static_binaries.py",
"tools/check_blender_release/check_utils.py",
"tools/check_blender_release/scripts/modules_enabled.py",
"tools/check_blender_release/scripts/requests_basic_access.py",
"tools/check_blender_release/scripts/requests_import.py",
"tools/check_source/check_descriptions.py",
"tools/check_source/clang_array_check.py",
"tools/utils/blend2json.py",
"tools/utils/blender_keyconfig_export_permutations.py",
"tools/utils/blender_merge_format_changes.py",
"tools/utils/blender_theme_as_c.py",
"tools/utils/cycles_timeit.py",
"tools/utils/gdb_struct_repr_c99.py",
"tools/utils/git_log_review_commits.py",
"tools/utils/git_log_review_commits_advanced.py",
"tools/utils/make_cursor_gui.py",
"tools/utils/make_gl_stipple_from_xpm.py",
"tools/utils/make_shape_2d_from_blend.py",
"tools/utils_api/bpy_introspect_ui.py", # Uses `bpy`.
"tools/utils_doc/code_layout_diagram.py", # Uses `bpy`.
"tools/utils_doc/rna_manual_reference_updater.py",
"tools/utils_ide/qtcreator/externaltools/qtc_assembler_preview.py",
"tools/utils_ide/qtcreator/externaltools/qtc_blender_diffusion.py",
"tools/utils_ide/qtcreator/externaltools/qtc_cpp_to_c_comments.py",
"tools/utils_ide/qtcreator/externaltools/qtc_doxy_file.py",
"tools/utils_ide/qtcreator/externaltools/qtc_project_update.py",
"tools/utils_ide/qtcreator/externaltools/qtc_sort_paths.py",
"tools/utils_maintenance/blender_menu_search_coverage.py", # Uses `bpy`.
"tools/utils_maintenance/blender_update_themes.py", # Uses `bpy`.
)
)
PATHS = tuple(
(os.path.join(SOURCE_DIR, p_items[0].replace("/", os.sep)), *p_items[1:])
for p_items in PATHS
)
# Validate:
for p_items in PATHS:
if not os.path.exists(os.path.join(SOURCE_DIR, p_items[0])):
print("PATH:", p_items[0], "doesn't exist")
for p in PATHS_EXCLUDE:
if not os.path.exists(os.path.join(SOURCE_DIR, p)):
print("PATHS_EXCLUDE:", p, "doesn't exist")

View File

@@ -0,0 +1,944 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
Script for checking source code spelling.
python3 tools/check_source/check_spelling.py some_source_file.py
- Pass in a directory for it to be checked recursively.
- Pass in '--extract=STRINGS' to check strings instead of comments.
Currently only python source is checked.
"""
__all__ = (
"main",
)
import argparse
import os
import re
import sys
from enum import Enum
from collections.abc import (
Callable,
Iterator,
)
# Report: word, line, column.
Report = tuple[str, int, int]
# Cache: {filepath: length, hash, reports}.
CacheData = dict[str, tuple[int, bytes, list[Report]]]
# Map word to suggestions.
SuggestMap = dict[str, str]
ONLY_ONCE = True
USE_COLOR = True
# Ignore: `/*identifier*/` as these are used in C++ for unused arguments or to denote struct members.
# These identifiers can be ignored in most cases.
USE_SKIP_SINGLE_IDENTIFIER_COMMENTS = True
_words_visited = set()
_files_visited = set()
# Lowercase word -> suggestion list.
_suggest_map: SuggestMap = {}
VERBOSE_CACHE = False
if USE_COLOR:
COLOR_WORD = "\033[92m"
COLOR_ENDC = "\033[0m"
else:
COLOR_WORD = ""
COLOR_ENDC = ""
from check_spelling_config import (
dict_custom,
dict_ignore,
dict_ignore_hyphenated_prefix,
dict_ignore_hyphenated_suffix,
files_ignore,
directories_ignore,
)
SOURCE_EXT = (
"c",
"cc",
"inl",
"cpp",
"cxx",
"hpp",
"hxx",
"h",
"hh",
"m",
"mm",
"metal",
"msl",
"glsl",
"osl",
"py",
"txt", # for `CMakeLists.txt`.
"cmake",
)
class TokenType(Enum):
COMMENT = 0
STRING = 1
DOCSTRING = 1
class LangType(Enum):
C = 0
CMAKE = 1
PYTHON = 2
LangTokenType = tuple[LangType, TokenType]
BASEDIR = os.path.abspath(os.path.dirname(__file__))
ROOTDIR = os.path.normpath(os.path.join(BASEDIR, "..", ".."))
ROOTDIR_WITH_SLASH = ROOTDIR + os.sep
# Ensure native slashes.
files_ignore = {
os.path.normpath(os.path.join(ROOTDIR, f.replace("/", os.sep)))
for f in files_ignore
}
directories_ignore = {
os.path.normpath(os.path.join(ROOTDIR, f.replace("/", os.sep)))
for f in directories_ignore
}
# -----------------------------------------------------------------------------
# Dictionary Utilities
def dictionary_create(): # type: ignore
import enchant # type: ignore
dict_spelling = enchant.Dict("en_US")
# Don't add ignore to the dictionary, since they will be suggested.
for w in dict_custom:
# Also, don't use `add(w)`, this will manipulate users personal dictionaries.
dict_spelling.add_to_session(w)
return dict_spelling
def dictionary_check(w: str, code_words: set[str]) -> bool:
w_lower = w.lower()
if w_lower in dict_ignore:
return True
is_correct: bool = _dict.check(w)
# Split by hyphenation and check.
if not is_correct:
if "-" in w:
is_correct = True
# Allow: `un-word`, `re-word`.
w_split = w.strip("-").split("-")
if len(w_split) > 1:
if w_split and w_split[0].lower() in dict_ignore_hyphenated_prefix:
del w_split[0]
# Allow: `word-ish`, `word-ness`.
if len(w_split) > 1:
if w_split and w_split[-1].lower() in dict_ignore_hyphenated_suffix:
del w_split[-1]
for w_sub in w_split:
if w_sub:
if w_sub in code_words:
continue
w_sub_lower = w_sub.lower()
if w_sub_lower in dict_ignore:
continue
if not _dict.check(w_sub):
is_correct = False
break
return is_correct
def dictionary_suggest(w: str) -> list[str]:
return _dict.suggest(w) # type: ignore
_dict = dictionary_create() # type: ignore
# -----------------------------------------------------------------------------
# General Utilities
def hash_of_file_and_len(fp: str) -> tuple[bytes, int]:
import hashlib
with open(fp, 'rb') as fh:
data = fh.read()
m = hashlib.sha512()
m.update(data)
return m.digest(), len(data)
re_vars = re.compile("[A-Za-z]+")
def re_compile_from_sequence(ls: tuple[str, ...]) -> re.Pattern[str]:
return re.compile(
"({:s})".format("|".join(ls)), re.MULTILINE | re.DOTALL,
)
# First remove this from comments, so we don't spell check example code, DOXYGEN commands, etc.
re_ignore_elems_generic_url_email_tags: tuple[str, ...] = (
# URL.
r'\b(https?|ftp)://\S+',
# Email address: <me@email.com>
# <someone@foo.bar-baz.com>
r"<\w+@[\w\.\-]+>",
# Convention for TODO/FIXME messages: TODO(my name) OR FIXME(name+name) OR XXX(some-name) OR NOTE(name/other-name):
r"\b(TODO|FIXME|XXX|NOTE|WARNING|WORKAROUND)\(@?[\w\s\+\-/]+\)",
)
re_ignore_elems_generic_expressions: tuple[str, ...] = (
# Words containing underscores: a_b
r'\S*\w+_\S+',
# Words containing arrows: a->b
r'\S*\w+\->\S+',
# Words containing dot notation: a.b (NOT ab... since this is used in English).
r'\w+\.\w+\S*',
)
re_ignore_elems_generic_single_backtick: tuple[str, ...] = (
# Single and back-tick quotes (often used to reference code).
# Allow white-space or any bracket prefix, e.g:
# (`expr a+b`)
r"[\s\(\[\{]\`[^\n`]+\`",
)
re_ignore_elems_generic_double_backtick: tuple[str, ...] = (
# Double back-ticks are used for docstrings for literals:
# (`expr a+b`)
r"[\s\(\[\{]\`\`[^\n`]+\`\`",
)
re_ignore_elems_lang_c_doxygen: tuple[str, ...] = (
# DOXYGEN style: `<pre> ... </pre>`
r"<pre>.+</pre>",
# DOXYGEN style: `\code ... \endcode`
r"\s+\\code\b.+\s\\endcode\b",
# DOXYGEN style `#SOME_CODE`.
r'#\S+',
# DOXYGEN commands: `\param foo`
r"\\(section|subsection|subsubsection|defgroup|ingroup|addtogroup|param|tparam|page|a|see)\s+\S+",
# DOXYGEN commands without any arguments after them: \command
r"\\(retval|todo|name)\b",
# DOXYGEN 'param' syntax used rarely: `\param foo[in,out]`
r"\\param\[[a-z,]+\]\S*",
)
re_ignore_map: dict[tuple[LangType, TokenType], re.Pattern[str]] = {
(LangType.C, TokenType.COMMENT): re_compile_from_sequence((
*re_ignore_elems_generic_url_email_tags,
*re_ignore_elems_lang_c_doxygen,
*re_ignore_elems_generic_expressions,
*re_ignore_elems_generic_single_backtick,
)),
(LangType.C, TokenType.STRING): re_compile_from_sequence((
*re_ignore_elems_generic_url_email_tags,
*re_ignore_elems_generic_expressions,
*re_ignore_elems_generic_single_backtick,
)),
(LangType.PYTHON, TokenType.COMMENT): re_compile_from_sequence((
*re_ignore_elems_generic_url_email_tags,
*re_ignore_elems_generic_expressions,
*re_ignore_elems_generic_single_backtick,
)),
(LangType.PYTHON, TokenType.STRING): re_compile_from_sequence((
*re_ignore_elems_generic_url_email_tags,
*re_ignore_elems_generic_expressions,
*re_ignore_elems_generic_single_backtick,
)),
# Only Python uses the docstring type.
(LangType.PYTHON, TokenType.DOCSTRING): re_compile_from_sequence((
*re_ignore_elems_generic_url_email_tags,
*re_ignore_elems_generic_expressions,
*re_ignore_elems_generic_double_backtick,
)),
(LangType.CMAKE, TokenType.COMMENT): re_compile_from_sequence((
*re_ignore_elems_generic_url_email_tags,
*re_ignore_elems_generic_expressions,
*re_ignore_elems_generic_single_backtick,
)),
(LangType.CMAKE, TokenType.STRING): re_compile_from_sequence((
*re_ignore_elems_generic_url_email_tags,
*re_ignore_elems_generic_expressions,
*re_ignore_elems_generic_single_backtick,
)),
}
del re_ignore_elems_generic_url_email_tags
del re_ignore_elems_generic_expressions
del re_ignore_elems_generic_double_backtick
del re_ignore_elems_lang_c_doxygen
# Then extract words.
re_words = re.compile(
r"\b("
# Capital words, with optional '-' and "'".
r"[A-Z]+[\-'A-Z]*[A-Z]|"
# Lowercase words, with optional '-' and "'".
r"[A-Za-z][\-'a-z]*[a-z]+"
r")\b"
)
re_not_newline = re.compile("[^\n]")
if USE_SKIP_SINGLE_IDENTIFIER_COMMENTS:
re_single_word_c_comments = re.compile(r"\/\*[\s]*[a-zA-Z_]+[a-zA-Z0-9_]*[\s]*\*\/")
def words_from_text(
text: str,
lang: LangType,
type: TokenType,
check_type: str,
) -> list[tuple[str, int]]:
""" Extract words to treat as English for spell checking.
"""
# Replace non-newlines with white-space, so all alignment is kept.
def replace_ignore(match: re.Match[str]) -> str:
start, end = match.span()
return re_not_newline.sub(" ", match.string[start:end])
# Handy for checking what we ignore, in case we ignore too much and miss real errors.
# for match in re_ignore.finditer(text):
# print(match.group(0))
# Strip out URL's, code-blocks, etc.
re_ignore = re_ignore_map[(lang, type)]
text = re_ignore.sub(replace_ignore, text)
words = []
if check_type == 'SPELLING':
for match in re_words.finditer(text):
words.append((match.group(0), match.start()))
def word_ok(w: str) -> bool:
# Ignore all uppercase words.
if w.isupper():
return False
return True
words[:] = [w for w in words if word_ok(w[0])]
elif check_type == 'DUPLICATES':
w_prev = ""
w_prev_start = 0
for match in re_words.finditer(text):
w = match.group(0)
w_start = match.start()
w_lower = w.lower()
if w_lower == w_prev:
text_ws = text[w_prev_start + len(w_prev): w_start]
if text_ws == " ":
words.append((w_lower, w_start))
w_prev = w_lower
w_prev_start = w_start
else:
assert False, "unreachable"
return words
class Comment:
__slots__ = (
"file",
"text",
"line",
"lang",
"type",
)
def __init__(self, file: str, text: str, line: int, lang: LangType, type: TokenType):
self.file = file
self.text = text
self.line = line
self.lang = lang
self.type = type
def parse(self, check_type: str) -> list[tuple[str, int]]:
return words_from_text(self.text, self.lang, self.type, check_type=check_type)
def line_and_column_from_comment_offset(self, pos: int) -> tuple[int, int]:
text = self.text
slineno = self.line + text.count("\n", 0, pos)
# Allow for -1 to be not found.
scol = text.rfind("\n", 0, pos) + 1
if scol == 0:
# Not found.
scol = pos
else:
scol = pos - scol
return slineno, scol
def extract_code_strings(filepath: str) -> tuple[list[Comment], set[str]]:
from pygments import lexers
from pygments.token import Token
comments = []
code_words = set()
# lex = lexers.find_lexer_class_for_filename(filepath)
# if lex is None:
# return comments, code_words
if filepath.endswith(".py"):
lex = lexers.get_lexer_by_name("python")
lang_type = LangType.PYTHON
elif filepath.endswith((".cmake", ".txt")):
lex = lexers.get_lexer_by_name("cmake")
lang_type = LangType.CMAKE
else:
lex = lexers.get_lexer_by_name("c")
lang_type = LangType.C
slineno = 0
with open(filepath, encoding='utf-8') as fh:
source = fh.read()
for ty, ttext in lex.get_tokens(source):
if ty in {
Token.Literal.String,
Token.Literal.String.Double,
Token.Literal.String.Single,
}:
comments.append(Comment(filepath, ttext, slineno, lang_type, TokenType.STRING))
else:
for match in re_vars.finditer(ttext):
code_words.add(match.group(0))
# Ugh - not nice or fast.
slineno += ttext.count("\n")
return comments, code_words
def extract_py_comments(filepath: str) -> tuple[list[Comment], set[str]]:
import token
import tokenize
source = open(filepath, encoding='utf-8')
comments = []
code_words = set()
prev_toktype = token.INDENT
tokgen = tokenize.generate_tokens(source.readline)
for toktype, ttext, (slineno, scol), (elineno, ecol), ltext in tokgen:
if toktype == token.STRING:
if prev_toktype == token.INDENT:
comments.append(Comment(filepath, ttext, slineno - 1, LangType.PYTHON, TokenType.DOCSTRING))
elif toktype == tokenize.COMMENT:
# non standard hint for commented CODE that we can ignore
if not ttext.startswith("#~"):
comments.append(Comment(filepath, ttext, slineno - 1, LangType.PYTHON, TokenType.COMMENT))
else:
for match in re_vars.finditer(ttext):
code_words.add(match.group(0))
prev_toktype = toktype
return comments, code_words
def extract_cmake_comments(filepath: str) -> tuple[list[Comment], set[str]]:
from pygments import lexers
from pygments.token import Token
lex = lexers.get_lexer_by_name("cmake")
with open(filepath, encoding='utf-8') as fh:
source = fh.read()
comments = []
code_words = set()
slineno = 0
for ty, ttext in lex.get_tokens(source):
if ty in {Token.Literal.String, Token.Literal.String.Double, Token.Literal.String.Single}:
# Disable because most CMake strings are references to paths/code."
if False:
comments.append(Comment(filepath, ttext, slineno, LangType.CMAKE, TokenType.STRING))
elif ty in {Token.Comment, Token.Comment.Single}:
comments.append(Comment(filepath, ttext, slineno, LangType.CMAKE, TokenType.COMMENT))
else:
for match in re_vars.finditer(ttext):
code_words.add(match.group(0))
# Ugh - not nice or fast.
slineno += ttext.count("\n")
return comments, code_words
def extract_c_comments(filepath: str) -> tuple[list[Comment], set[str]]:
"""
Extracts comments like this:
/*
* This is a multi-line comment, notice the '*'s are aligned.
*/
"""
text = open(filepath, encoding='utf-8').read()
BEGIN = "/*"
END = "*/"
# reverse these to find blocks we won't parse
PRINT_NON_ALIGNED = False
PRINT_SPELLING = True
comment_ranges = []
if USE_SKIP_SINGLE_IDENTIFIER_COMMENTS:
comment_ignore_offsets = set()
for match in re_single_word_c_comments.finditer(text):
comment_ignore_offsets.add(match.start(0))
i = 0
while i != -1:
i = text.find(BEGIN, i)
if i != -1:
i_next = text.find(END, i)
if i_next != -1:
do_comment_add = True
if USE_SKIP_SINGLE_IDENTIFIER_COMMENTS:
if i in comment_ignore_offsets:
do_comment_add = False
# Not essential but seek back to find beginning of line.
while i > 0 and text[i - 1] in {"\t", " "}:
i -= 1
i_next += len(END)
if do_comment_add:
comment_ranges.append((i, i_next))
i = i_next
else:
pass
if PRINT_NON_ALIGNED:
for i, i_next in comment_ranges:
# Seek i back to the line start.
i_bol = text.rfind("\n", 0, i) + 1
l_ofs_first = i - i_bol
star_offsets = set()
block = text[i_bol:i_next]
for line_index, l in enumerate(block.split("\n")):
star_offsets.add(l.find("*", l_ofs_first))
l_ofs_first = 0
if len(star_offsets) > 1:
print("{:s}:{:d}".format(filepath, line_index + text.count("\n", 0, i)))
break
if not PRINT_SPELLING:
return [], set()
# Collect variables from code, so we can reference variables from code blocks
# without this generating noise from the spell checker.
code_ranges = []
if not comment_ranges:
code_ranges.append((0, len(text)))
else:
for index in range(len(comment_ranges) + 1):
if index == 0:
i_prev = 0
else:
i_prev = comment_ranges[index - 1][1]
if index == len(comment_ranges):
i_next = len(text)
else:
i_next = comment_ranges[index][0]
code_ranges.append((i_prev, i_next))
code_words = set()
for i, i_next in code_ranges:
for match in re_vars.finditer(text[i:i_next]):
w = match.group(0)
code_words.add(w)
# Allow plurals of these variables too.
code_words.add(w + "'s")
# Allow `th` suffix, mainly for indices, e.g. the `i'th` element.
code_words.add(w + "'th")
comments = []
slineno = 0
i_prev = 0
for i, i_next in comment_ranges:
block = text[i:i_next]
# Add white-space in front of the block (for alignment test)
# allow for -1 being not found, which results as zero.
j = text.rfind("\n", 0, i) + 1
block = (" " * (i - j)) + block
slineno += text.count("\n", i_prev, i)
comments.append(Comment(filepath, block, slineno, LangType.C, TokenType.COMMENT))
i_prev = i
return comments, code_words
def spell_check_report(filepath: str, check_type: str, report: Report) -> None:
w, slineno, scol = report
if check_type == 'SPELLING':
w_lower = w.lower()
if ONLY_ONCE:
if w_lower in _words_visited:
return
else:
_words_visited.add(w_lower)
suggest = _suggest_map.get(w_lower)
if suggest is None:
_suggest_map[w_lower] = suggest = " ".join(dictionary_suggest(w))
print("{:s}:{:d}:{:d}: {:s}{:s}{:s}, suggest ({:s})".format(
filepath,
slineno + 1,
scol + 1,
COLOR_WORD,
w,
COLOR_ENDC,
suggest,
))
elif check_type == 'DUPLICATES':
print("{:s}:{:d}:{:d}: {:s}{:s}{:s}, duplicate".format(
filepath,
slineno + 1,
scol + 1,
COLOR_WORD,
w,
COLOR_ENDC,
))
def spell_check_file(
filepath: str,
check_type: str,
extract_type: str = 'COMMENTS',
) -> Iterator[Report]:
if extract_type == 'COMMENTS':
if filepath.endswith(".py"):
comment_list, code_words = extract_py_comments(filepath)
elif filepath.endswith((".cmake", ".txt")):
comment_list, code_words = extract_cmake_comments(filepath)
else:
comment_list, code_words = extract_c_comments(filepath)
elif extract_type == 'STRINGS':
comment_list, code_words = extract_code_strings(filepath)
if check_type == 'SPELLING':
for comment in comment_list:
words = comment.parse(check_type='SPELLING')
for w, pos in words:
w_lower = w.lower()
if w_lower in dict_ignore:
continue
is_good_spelling = dictionary_check(w, code_words)
if not is_good_spelling:
# Ignore literals that show up in code,
# gets rid of a lot of noise from comments that reference variables.
if w in code_words:
# print("Skipping", w)
continue
slineno, scol = comment.line_and_column_from_comment_offset(pos)
yield (w, slineno, scol)
elif check_type == 'DUPLICATES':
for comment in comment_list:
words = comment.parse(check_type='DUPLICATES')
for w, pos in words:
slineno, scol = comment.line_and_column_from_comment_offset(pos)
# print(filepath + ":" + str(slineno + 1) + ":" + str(scol), w, "(duplicates)")
yield (w, slineno, scol)
else:
assert False, "unreachable"
def spell_check_file_recursive(
dirpath: str,
check_type: str,
regex_list: list[re.Pattern[str]],
extract_type: str = 'COMMENTS',
cache_data: CacheData | None = None,
) -> None:
from os.path import join
def source_list(
path: str,
filename_check: Callable[[str], bool] | None = None,
) -> Iterator[str]:
for dirpath, dirnames, filenames in os.walk(path):
# Only needed so this can be matches with ignore paths.
dirpath = os.path.abspath(dirpath)
if dirpath in directories_ignore:
dirnames.clear()
continue
# skip '.git'
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
for filename in filenames:
if filename.startswith("."):
continue
filepath = join(dirpath, filename)
if not (filename_check is None or filename_check(filepath)):
continue
if filepath in files_ignore:
continue
yield filepath
def is_source(filename: str) -> bool:
from os.path import splitext
filename = filename.removeprefix(ROOTDIR_WITH_SLASH)
for regex in regex_list:
if regex.match(filename) is not None:
filename
ext = splitext(filename)[1].removeprefix(".")
if ext not in SOURCE_EXT:
raise Exception("Unknown extension \".{:s}\" aborting!".format(ext))
return True
return False
for filepath in source_list(dirpath, is_source):
for report in spell_check_file_with_cache_support(
filepath, check_type, extract_type=extract_type, cache_data=cache_data,
):
spell_check_report(filepath, check_type, report)
# -----------------------------------------------------------------------------
# Cache File Support
#
# Cache is formatted as follows:
# (
# # Store all misspelled words.
# {filepath: (size, sha512, [reports, ...])},
#
# # Store suggestions, as these are slow to re-calculate.
# {lowercase_words: suggestions},
# )
#
def spell_cache_read(cache_filepath: str) -> tuple[CacheData, SuggestMap]:
import pickle
cache_store: tuple[CacheData, SuggestMap] = {}, {}
if os.path.exists(cache_filepath):
with open(cache_filepath, 'rb') as fh:
cache_store = pickle.load(fh)
return cache_store
def spell_cache_write(cache_filepath: str, cache_store: tuple[CacheData, SuggestMap]) -> None:
import pickle
with open(cache_filepath, 'wb') as fh:
pickle.dump(cache_store, fh)
def spell_check_file_with_cache_support(
filepath: str,
check_type: str,
*,
extract_type: str = 'COMMENTS',
cache_data: CacheData | None = None,
) -> Iterator[Report]:
"""
Iterator each item is a report: (word, line_number, column_number)
"""
_files_visited.add(filepath)
if cache_data is None:
yield from spell_check_file(filepath, check_type, extract_type=extract_type)
return
cache_data_for_file = cache_data.get(filepath)
if cache_data_for_file and len(cache_data_for_file) != 3:
cache_data_for_file = None
cache_hash_test, cache_len_test = hash_of_file_and_len(filepath)
if cache_data_for_file is not None:
cache_len, cache_hash, cache_reports = cache_data_for_file
if cache_len_test == cache_len:
if cache_hash_test == cache_hash:
if VERBOSE_CACHE:
print("Using cache for:", filepath)
yield from cache_reports
return
cache_reports = []
for report in spell_check_file(filepath, check_type, extract_type=extract_type):
cache_reports.append(report)
cache_data[filepath] = (cache_len_test, cache_hash_test, cache_reports)
yield from cache_reports
# -----------------------------------------------------------------------------
# Extract Bad Spelling from a Source File
# -----------------------------------------------------------------------------
# Main & Argument Parsing
def argparse_create() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--match",
nargs='+',
default=(
r".*\.(" + "|".join(SOURCE_EXT) + ")$",
),
required=False,
metavar="REGEX",
help="Match file paths against this expression",
)
parser.add_argument(
'--extract',
dest='extract',
choices=('COMMENTS', 'STRINGS'),
default='COMMENTS',
required=False,
metavar='WHAT',
help=(
'Text to extract for checking.\n'
'\n'
'- ``COMMENTS`` extracts comments from source code.\n'
'- ``STRINGS`` extracts text.'
),
)
parser.add_argument(
'--check',
dest='check_type',
choices=('SPELLING', 'DUPLICATES'),
default='SPELLING',
required=False,
metavar='CHECK_TYPE',
help=(
'The check to perform.\n'
'\n'
'- ``SPELLING`` check spelling.\n'
'- ``DUPLICATES`` report repeated words.'
),
)
parser.add_argument(
"--cache-file",
dest="cache_file",
help=(
"Optional cache, for fast re-execution, "
"avoiding re-extracting spelling when files have not been modified."
),
required=False,
)
parser.add_argument(
"paths",
nargs='+',
help="Files or directories to walk recursively.",
)
return parser
def main() -> int:
global _suggest_map
args = argparse_create().parse_args()
regex_list = []
for expr in args.match:
try:
regex_list.append(re.compile(expr))
except Exception as ex:
print("Error in expression: {!r}\n {!r}".format(expr, ex))
return 1
extract_type = args.extract
cache_filepath = args.cache_file
check_type = args.check_type
cache_data: CacheData | None = None
if cache_filepath:
cache_data, _suggest_map = spell_cache_read(cache_filepath)
clear_stale_cache = True
# print(extract_type)
try:
for filepath in args.paths:
if os.path.isdir(filepath):
# recursive search
spell_check_file_recursive(
filepath,
check_type,
regex_list=regex_list,
extract_type=extract_type,
cache_data=cache_data,
)
else:
# single file
for report in spell_check_file_with_cache_support(
filepath,
check_type,
extract_type=extract_type,
cache_data=cache_data,
):
spell_check_report(filepath, check_type, report)
except KeyboardInterrupt:
clear_stale_cache = False
if cache_filepath:
assert cache_data is not None
if VERBOSE_CACHE:
print("Writing cache:", len(cache_data))
if clear_stale_cache:
# Don't keep suggestions for old misspellings.
_suggest_map = {w_lower: _suggest_map[w_lower] for w_lower in _words_visited}
for filepath in list(cache_data.keys()):
if filepath not in _files_visited:
del cache_data[filepath]
spell_cache_write(cache_filepath, (cache_data, _suggest_map))
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,936 @@
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
# these must be all lower case for comparisons
__all__ = (
"dict_custom",
"dict_ignore",
"dict_ignore_hyphenated_prefix",
"dict_ignore_hyphenated_suffix",
"directories_ignore",
"files_ignore",
)
dict_custom = {
# Added to newer versions of the dictionary,
# we can remove these when the updated word-lists have been applied to `aspell-en`.
"accessor",
"accessors",
"completer",
"completers",
"enqueue",
"enqueued",
"enqueues",
"intrinsics",
"iterable",
"parallelization",
"parallelized",
"pipelining",
"polygonization",
"prepend",
"prepends",
"rasterize",
"reachability",
"runtime",
"runtimes",
"serializable",
"unary",
"variadic",
# Correct spelling, update the dictionary, here:
# https://github.com/en-wl/wordlist
"accessor",
"accumulatively",
"additively",
"adjoint",
"adjugate",
"affectable",
"alignable",
"bakeable",
"bindable",
"branchless",
"allocatable",
"allocator",
"allocators",
"anisotropic",
"anisotropy",
"asymptote",
"atomicity",
"attachmentless",
"attenuations",
"backends",
"backlit",
"backpropagated",
"backpropagation",
"bindless",
"bitwise",
"blocky",
"boolean",
"borderless",
"breaked",
"browsable",
"callables",
"canonicalization",
"canonicalize",
"canonicalized",
"canonicalizing",
"catadioptric",
"checksums",
"chromaticity",
"chrominance",
"clearcoat",
"codecs",
"collapser",
"collinear",
"comparator",
"comparators",
"compilable",
"confusticate",
"confusticated",
"constructability",
"constructible",
"contextless",
"convolved",
"coplanarity",
"copyable",
"correctors",
"counterforce",
"criterium",
"crosshair",
"crosstalk",
"cumulate",
"cumulated",
"customizable",
"deallocate",
"deallocated",
"deallocating",
"deallocation",
"decompressor",
"decorrelated",
"decrement",
"decrementing",
"deduplicate",
"deduplicated",
"deduplicates",
"deduplicating",
"deduplication",
"defocus",
"defocusing",
"defragment",
"defragmented",
"defragmenting",
"degeneracies",
"deinitialize",
"deinitializes",
"deletable",
"deleter",
"demangle",
"demangled",
"denoised",
"denoiser",
"denoising",
"denormal",
"denormalized",
"denormals",
"dereference",
"dereferenced",
"dereferences",
"dereferencing",
"derivates",
"desaturate",
"descenders",
"designator",
"despeckle",
"despeckled",
"destructor",
"destructors",
"dialogs",
"digitizers",
"dihedral",
"dimensionality",
"directionality",
"disambiguated",
"disambiguates",
"discoverability",
"discretization",
"discretized",
"discretizes",
"distributable",
"downcasting",
"downloader",
"downsample",
"downsampled",
"downsampler",
"downsamples",
"downsampling",
"draggable",
"drawable",
"durations",
"eachother",
"editability",
"effector",
"effectors",
"elementwise",
"embedder",
"enablement",
"encodable",
"enqueueing",
"equiangular",
"evolute",
"extrema",
"fallbacks",
"finalizer",
"fisheye",
"flippable",
"flushable",
"formatter",
"formatters",
"foveation",
"generatrix",
"glitchy",
"handlings",
"haptics",
"headerless",
"highlightable",
"homogenous",
"ideographic",
"illuminant",
"imbricated",
"impactful",
"incrementation",
"indexable",
"inferencing",
"initializations",
"initializer",
"initializers",
"inlining",
"instancer",
"instancers",
"instantiable",
"instantiation",
"instantiations",
"interdependencies",
"interferences",
"interocular",
"interpolant",
"interpolator",
"invariance",
"invariant",
"invariants",
"invisibilities",
"invocated",
"irradiance",
"iteratively",
"jitteriness",
"keyable",
"keyless",
"linearize",
"linearized",
"linearizes",
"linearizing",
"linkable",
"lockless",
"looper",
"loopers",
"losslessly",
"luminances",
"mappable",
"memoryless",
"merchantability",
"mergeable",
"minimalistic",
"misconfiguration",
"misconfigured",
"modally",
"modifiability",
"monoscopy",
"monospaced",
"mutators",
"natively",
"notarizatiom",
"nullable",
"occludee",
"occluder",
"occluders",
"octant",
"octants",
"optionals",
"orthogonalize",
"orthogonally",
"orthonormalize",
"orthonormalized",
"overridable",
"oversample",
"oversampled",
"oversampler",
"oversamples",
"oversampling",
"paddings",
"paintable",
"pannable",
"parallelepiped",
"parallelize",
"parallelizing",
"parameterization",
"parameterless",
"parametrization",
"parentless",
"passepartout",
"passthrough",
"performant",
"piecewise",
"pixelate",
"pixelated",
"pixelation",
"pixelisation",
"planarity",
"planarize",
"polygonizer",
"polytope",
"postfix",
"postfixes",
"postprocess",
"postprocessed",
"pre-filtered",
"pre-multiplied",
"precalculate",
"precisions",
"precomputations",
"precompute",
"precomputed",
"precomputing",
"prefetch",
"prefetched",
"prefetching",
"prefilter",
"prefiltered",
"prefiltering",
"preloading",
"premutliplied",
"preorder",
"prepend",
"prepending",
"preprocess",
"preprocesses",
"preprocessing",
"preprocessor",
"preprocessors",
"preventively",
"probabilistically",
"procedurally",
"profiler",
"programmatically",
"projective",
"purgeability",
"quadratically",
"queryable",
"rasterizer",
"rasterizes",
"rasterizing",
"reallocations",
"realtime",
"rebalancing",
"rebase",
"rebased",
"recomputation",
"reconnection",
"recurse",
"recursed",
"recurses",
"recursing",
"recursivity",
"redefinitions",
"rederive",
"redisplay",
"redistributions",
"registerable",
"reimplement",
"reimplementation",
"reimplemented",
"reimplementing",
"reimport",
"relink",
"relinked",
"relinking",
"remappable",
"remapper",
"remappings",
"remesher",
"renderer",
"renderable",
"renderers",
"renormalize",
"renormalized",
"reparameterization",
"reparametrization",
"representable",
"reproject",
"reprojected",
"reprojecting",
"reprojection",
"reprojections",
"reprojects",
"repurpose",
"rescale",
"rescaled",
"respecialized",
"restorable",
"resynced",
"resyncing",
"retarget",
"retiming",
"reupload",
"reusability",
"rotationally",
"sanitization",
"saveable",
"schemas",
"scrollable",
"selectability",
"serializers",
"shadowless",
"sharpnesses",
"sidedness",
"simplices",
"situationally",
"skeletally",
"skinnable",
"skippable",
"sortable",
"stationarity",
"stepsize",
"stepwise",
"stitchable",
"strobing",
"subclass",
"subclassed",
"subclasses",
"subclassing",
"subdirectories",
"subdirectory",
"submenu",
"submenus",
"suboptimally",
"subprocess",
"subprocesses",
"subrange",
"subtractive",
"subtype",
"subtypes",
"supersample",
"supersampled",
"supersampler",
"supersamples",
"supersampling",
"superset",
"symmetrizable",
"symmetrize",
"symmetrized",
"targetless",
"tedrahedral",
"teleporting",
"templating",
"tertiarily",
"testability",
"thumbstick",
"tokenization",
"tokenize",
"tokenizing",
"toolchain",
"trackpad",
"transcode",
"transmissive",
"triaging",
"triangulations",
"triangulator",
"trilinear",
"tunable",
"uber",
"unadjusted",
"unalignable",
"unallocated",
"unanimated",
"unapplied",
"unapply",
"unassign",
"unassigning",
"unassigns",
"unassociated",
"unbake",
"unbuffered",
"uncached",
"uncategorized",
"unclaim",
"unclamped",
"unclipped",
"unclosed",
"uncollapsed",
"uncomment",
"uncommented",
"uncompacted",
"uncomputed",
"unconfigured",
"unconvert",
"uncorrupted",
"undefine",
"undefined",
"undeform",
"undeformed",
"undersample",
"undersampled",
"undersamples",
"undersampling",
"undisplaced",
"undistorted",
"undistorting",
"unduplicated",
"uneditable",
"unescaped",
"unflagged",
"unflip",
"unfoldable",
"unformatted",
"unfreed",
"ungrabbed",
"ungrabbing",
"ungroup",
"ungrouped",
"ungrouping",
"ungrown",
"unhandled",
"unhidden",
"unhide",
"unintuitive",
"unkeyed",
"unkeyframed",
"unlink",
"unlinkable",
"unlinked",
"unlinking",
"unlinks",
"unmap",
"unmapped",
"unmark",
"unmask",
"unmatching",
"unmaximized",
"unmeasurable",
"unminimize",
"unmodulated",
"unmute",
"unnormalize",
"unnormalized",
"unoccluded",
"unoptimized",
"unparameterized",
"unparsed",
"unpause",
"unpaused",
"unphysical",
"unpoison",
"unproject",
"unquantifiable",
"unreferenced",
"unregister",
"unregistering",
"unregisters",
"unreproducible",
"unscaled",
"unselect",
"unselected",
"unsetting",
"unshadowed",
"unshared",
"unsharing",
"unsharp",
"unshearing",
"unspecialized",
"unsqueezed",
"unstretch",
"unsubdivided",
"unsubdividing",
"unsubdivisions",
"unsynchronized",
"untag",
"untagging",
"unterminated",
"untracked",
"untransformed",
"untrusted",
"untyped",
"unusably",
"unvisited",
"unwritable",
"upsample",
"upsampled",
"upsampler",
"upsamples",
"upsampling",
"userless",
"vectorial",
"vectorization",
"vectorize",
"vectorized",
"versionable",
"videogrammetry",
"viewports",
"virtualized",
"visibilities",
"volumetrics",
"vortices",
"voxelize",
"workspaces",
"writeable",
"zoomable",
# C/C++/Python types (we could quote every instance but it's impractical).
"enum",
"enums",
"int",
"ints",
"nullptr", # C++ NULL-pointer.
"str",
"tuple",
"tuples",
# python functions
"func",
"repr",
# Accepted concatenations.
"addon",
"addons",
"autocomplete",
"bitmask",
"codegen",
"colospace",
"datablock",
"datablocks",
"keyframe",
"keyframing",
"lookup",
"lookups",
"multithreaded",
"multithreading",
"namespace",
"namespaces",
"reparent",
"tooltip",
"unparent",
# Accepted abbreviations.
# `"dir",` # direction/directory? Too ambiguous, don't use this.
"anim", # animation.
"attr",
"attrs",
"config", # configuration.
"coord",
"coords",
"ctrl", # control (modifier key).
"ie",
"init",
"iter", # iteration.
"multi",
"numpad", # numeric-pad.
"numpads", # numeric-pads.
"ortho",
"recalc",
"resync",
"struct",
"structs",
"subdir",
# General computer terms.
"app",
"ascii",
"autocomplete",
"autorepeat",
"bilinear",
"blit",
"blitting",
"boids",
"booleans",
"backface",
"codepage",
"contructor",
"decimator",
"decref",
"decrefed",
"decrefing",
"diff",
"diffs",
"docstring",
"docstrings",
"endian",
"endianness",
"env",
"euler",
"eulers",
"foo",
"hashable",
"http",
"incref",
"increfed",
"increfing",
"intellisense",
"jitter",
"jittered",
"jittering",
"keymap",
"lerp",
"metadata",
"mutex",
"opengl",
"quantized",
"searchable",
"segfault",
"stdin",
"stdout",
"sudo",
"threadsafe",
"touchpad",
"touchpads",
"trackpad",
"trackpads",
"trilinear",
"unicode",
"usr",
"vert",
"verts",
"voxel",
"voxels",
"wiki",
# specific computer terms/brands
"ack",
"amiga",
"cmake",
"ffmpeg",
"freebsd",
"linux",
"manpage",
"mozilla",
"nvidia",
"openexr",
"posix",
"qtcreator",
"unix",
"valgrind",
"wayland",
"xinerama",
# general computer graphics terms
"atomics",
"barycentric",
"bezier",
"bicubic",
"bitangent",
"centroid",
"colinear",
"compositing",
"coplanar",
"crypto",
"deinterlace",
"emissive",
"fresnel",
"gaussian",
"grayscale",
"kerning",
"lacunarity",
"lossless",
"lossy",
"luma",
"macronormal",
"macronormals",
"mesonormal",
"mesonormals",
"microfacet",
"microfacets",
"micronormal",
"micronormals",
"mipmap",
"mipmapped",
"mipmapping",
"mipmaps",
"musgrave",
"n-gon",
"n-gons",
"normals",
"nurbs",
"octree",
"quaternion",
"quaternions",
"radiosity",
"reflectance",
"shader",
"shaders",
"specular",
# Mathematical terms.
"eigenvalue",
"eigenvalues",
# Blender specific terms.
"animsys",
"animviz",
"bmain",
"bmesh",
"bpy",
"channelbag",
"channelbags",
"depsgraph",
"doctree",
"editmode",
"eekadoodle",
"fcurve",
"look-dev",
"mathutils",
"obdata",
"userpref",
"userprefs",
# Should have apostrophe but ignore for now unless we want to get really picky!
"indices",
"vertices",
}
# incorrect spelling but ignore anyway
dict_ignore = {
"a-z",
"animatable",
"arg",
"args",
"bool",
"constness",
"dirpath",
"dupli",
"eg",
"filename",
"filenames",
"filepath",
"filepaths",
"hardcoded",
"id-block",
"inlined",
"loc",
"namespace",
"node-trees",
"ok",
"ok-ish",
"param",
"polyline",
"polylines",
"premultiplied",
"premultiply",
"pylint",
"quad",
"readonly",
"submodule",
"submodules",
"tooltips",
"tri",
"ui",
"unfuzzy",
"utils",
"uv",
"vec",
"wireframe",
"x-axis",
"y-axis",
"z-axis",
# acronyms
"api",
"cpu",
"gl",
"gpl",
"gpu",
"gzip",
"hg",
"ik",
"lhs",
"nan",
"nla",
"ppc",
"rgb",
"rhs",
"rna",
"smpte",
"svn",
"utf",
# extensions
"py",
"rst",
"xml",
"xpm",
# tags
"fixme",
"todo",
# sphinx/rst
"rtype",
# slang
"automagically",
"hacky",
"hrmf",
# names
"campbell",
"jahka",
"mikkelsen",
"morten",
# Company names.
"Logitech",
"Qualcomm",
"Wacom",
# Project Names.
"Wayland",
# clang-tidy (for convenience).
"bugprone-suspicious-enum-usage",
"bugprone-use-after-move",
}
# Allow: `un-word`, `re-word` ... etc, in this case only check `word`.
dict_ignore_hyphenated_prefix = {
"de",
"mis",
"non",
"post",
"pre",
"re",
"un",
}
dict_ignore_hyphenated_suffix = {
"ify",
"ish",
"ness",
}
files_ignore = {
"scripts/modules/_bl_i18n_utils/utils_spell_check.py", # UI spelling, doesn't always match code spelling.
"tools/utils/git_data_canonical_authors.py", # Too many names which aren't in the dictionary.
"tools/utils_doc/rna_manual_reference_updater.py", # Contains language ID references.
# Maintained by 3rd parties.
"source/blender/blenlib/intern/fnmatch.c",
"source/blender/draw/intern/shaders/common_fxaa_lib.glsl",
"source/blender/gpu/shaders/common/gpu_shader_smaa_lib.glsl",
# Contains `Lorem Ipsum`.
"source/blender/blenlib/tests/BLI_resource_strings.h",
}
# These contain many typos that could be resolved, then removed from this list.
directories_ignore = {
"scripts/addons_core/io_scene_gltf2/",
"scripts/addons_core/rigify/",
}

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
# Checks for defines which aren't used anywhere.
__all__ = (
"main",
)
import os
import sys
PWD = os.path.dirname(__file__)
sys.path.append(os.path.join(PWD, "..", "utils_maintenance", "modules"))
from batch_edit_text import run
SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.normpath(os.path.join(PWD, "..", ".."))))
# TODO, move to config file
SOURCE_DIRS = (
"source",
)
SOURCE_EXT = (
# C/C++
".c", ".h", ".cpp", ".hpp", ".cc", ".hh", ".cxx", ".hxx", ".inl",
# Objective C
".m", ".mm",
# GLSL
".glsl",
)
words: set[str] = set()
words_multi: set[str] = set()
defines: dict[str, str] = {}
import re
re_words = re.compile("[A-Za-z_][A-Za-z_0-9]*")
re_defines = re.compile("^\\s*#define\\s+([A-Za-z_][A-Za-z_0-9]*)", re.MULTILINE)
# From
# https://stackoverflow.com/a/18381470/432509
def remove_comments(string: str) -> str:
pattern = r"(\".*?\"|\'.*?\')|(/\*.*?\*/|//[^\r\n]*$)"
# first group captures quoted strings (double or single)
# second group captures comments (//single-line or /* multi-line */)
regex = re.compile(pattern, re.MULTILINE | re.DOTALL)
def _replacer(m: re.Match[str]) -> str:
# If the 2nd group (capturing comments) is not None,
# It means we have captured a non-quoted (real) comment string.
if m.group(2) is not None:
# So we will return empty to remove the comment.
return ""
# Otherwise, we will return the 1st group.
return m.group(1) # capture
return regex.sub(_replacer, string)
def extract_terms(fn: str, data_src: str) -> None:
data_src_nocomments = remove_comments(data_src)
for m in re_words.finditer(data_src_nocomments):
words_len = len(words)
m_text = m.group()
words.add(m_text)
if words_len == len(words):
words_multi.add(m_text)
for m in re_defines.finditer(data_src_nocomments):
defines[m.group(1)] = fn
# Returning None indicates the file is not edited.
def main() -> int:
run(
directories=[os.path.join(SOURCE_DIR, d) for d in SOURCE_DIRS],
is_text=lambda fn: fn.endswith(SOURCE_EXT),
text_operation=extract_terms,
# Can't be used if we want to accumulate in a global variable.
use_multiprocess=False,
)
print("Found", len(defines), "defines, searching", len(words_multi), "terms...")
for fn, define in sorted([(fn, define) for define, fn in defines.items()]):
if define not in words_multi:
print(define, "->", fn)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,398 @@
# SPDX-FileCopyrightText: 2012 Blender Authors
#
# SPDX-License-Identifier: Apache-2.0
"""
Invocation:
export CLANG_BIND_DIR="/dsk/src/llvm/tools/clang/bindings/python"
export CLANG_LIB_DIR="/opt/llvm/lib"
python clang_array_check.py somefile.c -DSOME_DEFINE -I/some/include
... defines and includes are optional
"""
__all__ = (
"main",
)
import sys
# delay parsing functions until we need them
USE_LAZY_INIT = True
USE_EXACT_COMPARE = False
# -----------------------------------------------------------------------------
# predefined function/arg sizes, handy sometimes, but not complete...
defs_precalc = {
"glColor3bv": {0: 3},
"glColor4bv": {0: 4},
"glColor3ubv": {0: 3},
"glColor4ubv": {0: 4},
"glColor3usv": {0: 3},
"glColor4usv": {0: 4},
"glColor3fv": {0: 3},
"glColor4fv": {0: 4},
"glColor3dv": {0: 3},
"glColor4dv": {0: 4},
"glVertex2fv": {0: 2},
"glVertex3fv": {0: 3},
"glVertex4fv": {0: 4},
"glEvalCoord1fv": {0: 1},
"glEvalCoord1dv": {0: 1},
"glEvalCoord2fv": {0: 2},
"glEvalCoord2dv": {0: 2},
"glRasterPos2dv": {0: 2},
"glRasterPos3dv": {0: 3},
"glRasterPos4dv": {0: 4},
"glRasterPos2fv": {0: 2},
"glRasterPos3fv": {0: 3},
"glRasterPos4fv": {0: 4},
"glRasterPos2sv": {0: 2},
"glRasterPos3sv": {0: 3},
"glRasterPos4sv": {0: 4},
"glTexCoord2fv": {0: 2},
"glTexCoord3fv": {0: 3},
"glTexCoord4fv": {0: 4},
"glTexCoord2dv": {0: 2},
"glTexCoord3dv": {0: 3},
"glTexCoord4dv": {0: 4},
"glNormal3fv": {0: 3},
"glNormal3dv": {0: 3},
"glNormal3bv": {0: 3},
"glNormal3iv": {0: 3},
"glNormal3sv": {0: 3},
# GPU immediate mode.
"immVertex2iv": {1: 2},
"immVertex2fv": {1: 2},
"immVertex3fv": {1: 3},
"immAttr2fv": {1: 2},
"immAttr3fv": {1: 3},
"immAttr4fv": {1: 4},
"immAttr4ubv": {1: 4},
"immUniform2fv": {1: 2},
"immUniform3fv": {1: 3},
"immUniform4fv": {1: 4},
"immUniformColor3fv": {0: 3},
"immUniformColor4fv": {0: 4},
"immUniformColor3ubv": {1: 3},
"immUniformColor4ubv": {1: 4},
"immUniformColor3fvAlpha": {0: 3},
"immUniformColor4fvAlpha": {0: 4},
}
# -----------------------------------------------------------------------------
import sys
if 0:
# Examples with LLVM as the root directory: `/dsk/src/llvm`.
# Path containing `clang/__init__.py`.
CLANG_BIND_DIR = "/dsk/src/llvm/tools/clang/bindings/python"
# Path containing `libclang.so`.
CLANG_LIB_DIR = "/opt/llvm/lib"
else:
import os
CLANG_BIND_DIR = os.environ.get("CLANG_BIND_DIR")
CLANG_LIB_DIR = os.environ.get("CLANG_LIB_DIR")
if CLANG_BIND_DIR is None:
print("$CLANG_BIND_DIR python binding dir not set")
if CLANG_LIB_DIR is None:
print("$CLANG_LIB_DIR clang lib dir not set")
if CLANG_BIND_DIR:
sys.path.append(CLANG_BIND_DIR)
import clang
import clang.cindex
from clang.cindex import (CursorKind,
TypeKind,
TokenKind)
if CLANG_LIB_DIR:
clang.cindex.Config.set_library_path(CLANG_LIB_DIR)
index = clang.cindex.Index.create()
args = sys.argv[2:]
# print(args)
tu = index.parse(sys.argv[1], args)
# print('Translation unit: %s' % tu.spelling)
filepath = tu.spelling
# -----------------------------------------------------------------------------
def function_parm_wash_tokens(parm):
# print(parm.kind)
assert parm.kind in (CursorKind.PARM_DECL,
CursorKind.VAR_DECL, # XXX, double check this
CursorKind.FIELD_DECL,
)
"""
Return tokens without trailing commands and 'const'
"""
tokens = [t for t in parm.get_tokens()]
if not tokens:
return tokens
# if tokens[-1].kind == To
# remove trailing char
if tokens[-1].kind == TokenKind.PUNCTUATION:
if tokens[-1].spelling in {",", ")", ";"}:
tokens.pop()
# else:
# print(tokens[-1].spelling)
t_new = []
for t in tokens:
t_kind = t.kind
t_spelling = t.spelling
ok = True
if t_kind == TokenKind.KEYWORD:
if t_spelling in {"const", "restrict", "volatile"}:
ok = False
elif t_spelling.startswith("__"):
ok = False # __restrict
elif t_kind in (TokenKind.COMMENT, ):
ok = False
# Use these
elif t_kind in (TokenKind.LITERAL,
TokenKind.PUNCTUATION,
TokenKind.IDENTIFIER):
# use but ignore
pass
else:
print("Unknown!", t_kind, t_spelling)
# if its OK we will add
if ok:
t_new.append(t)
return t_new
def parm_size(node_child):
tokens = function_parm_wash_tokens(node_child)
# print(" ".join([t.spelling for t in tokens]))
# NOT PERFECT CODE, EXTRACT SIZE FROM TOKENS
if len(tokens) >= 3: # foo [ 1 ]
if ((tokens[-3].kind == TokenKind.PUNCTUATION and tokens[-3].spelling == "[") and
(tokens[-2].kind == TokenKind.LITERAL and tokens[-2].spelling.isdigit()) and
(tokens[-1].kind == TokenKind.PUNCTUATION and tokens[-1].spelling == "]")):
# ---
return int(tokens[-2].spelling)
return -1
def function_get_arg_sizes(node):
# Return a dict if (index: size) items
# {arg_indx: arg_array_size, ... ]
arg_sizes = {}
if 1: # node.spelling == "BM_vert_create", for debugging
node_parms = [node_child for node_child in node.get_children()
if node_child.kind == CursorKind.PARM_DECL]
for i, node_child in enumerate(node_parms):
# print(node_child.kind, node_child.spelling)
# print(node_child.type.kind, node_child.spelling)
if node_child.type.kind == TypeKind.CONSTANTARRAY:
pointee = node_child.type.get_pointee()
size = parm_size(node_child)
if size != -1:
arg_sizes[i] = size
return arg_sizes
# -----------------------------------------------------------------------------
_defs = {}
def lookup_function_size_def(func_id):
if USE_LAZY_INIT:
result = _defs.get(func_id, {})
if type(result) != dict:
result = _defs[func_id] = function_get_arg_sizes(result)
return result
else:
return _defs.get(func_id, {})
# -----------------------------------------------------------------------------
def file_check_arg_sizes(tu):
# main checking function
def validate_arg_size(node):
"""
Loop over args and validate sizes for args we KNOW the size of.
"""
assert node.kind == CursorKind.CALL_EXPR
if 0:
print("---",
" <~> ".join(
[" ".join([t.spelling for t in C.get_tokens()])
for C in node.get_children()]
))
# print(node.location)
# first child is the function call, skip that.
children = list(node.get_children())
if not children:
return # XXX, look into this, happens on C++
func = children[0]
# get the func declaration!
# works but we can better scan for functions ahead of time.
if 0:
func_dec = func.get_definition()
if func_dec:
print("FD", " ".join([t.spelling for t in func_dec.get_tokens()]))
else:
# HRMP'f - why does this fail?
print("AA", " ".join([t.spelling for t in node.get_tokens()]))
else:
args_size_definition = () # dummy
# get the key
tok = list(func.get_tokens())
if tok:
func_id = tok[0].spelling
args_size_definition = lookup_function_size_def(func_id)
if not args_size_definition:
return
children = children[1:]
for i, node_child in enumerate(children):
children = list(node_child.get_children())
# skip if we don't have an index...
size_def = args_size_definition.get(i, -1)
if size_def == -1:
continue
# print([c.kind for c in children])
# print(" ".join([t.spelling for t in node_child.get_tokens()]))
if len(children) == 1:
arg = children[0]
if arg.kind in (CursorKind.DECL_REF_EXPR,
CursorKind.UNEXPOSED_EXPR):
if arg.type.kind == TypeKind.CONSTANTARRAY:
dec = arg.get_definition()
if dec:
size = parm_size(dec)
# size == 0 is for 'float *a'
if size != -1 and size != 0:
# nice print!
if 0:
print("".join([t.spelling for t in func.get_tokens()]),
i,
" ".join([t.spelling for t in dec.get_tokens()]))
# testing
# size_def = 100
if size != 1:
if USE_EXACT_COMPARE:
# is_err = (size != size_def) and (size != 4 and size_def != 3)
is_err = (size != size_def)
else:
is_err = (size < size_def)
if is_err:
location = node.location
# if "math_color_inline.c" not in str(location.file):
if 1:
print("%s:%d:%d: argument %d is size %d, should be %d (from %s)" %
(location.file,
location.line,
location.column,
i + 1, size, size_def,
filepath # always the same but useful when running threaded
))
# we don't really care what we are looking at, just scan entire file for
# function calls.
def recursive_func_call_check(node):
if node.kind == CursorKind.CALL_EXPR:
validate_arg_size(node)
for c in node.get_children():
recursive_func_call_check(c)
recursive_func_call_check(tu.cursor)
# -- first pass, cache function definitions sizes
# PRINT FUNC DEFINES
def recursive_arg_sizes(node, ):
# print(node.kind, node.spelling)
if node.kind == CursorKind.FUNCTION_DECL:
if USE_LAZY_INIT:
args_sizes = node
else:
args_sizes = function_get_arg_sizes(node)
# if args_sizes:
# print(node.spelling, args_sizes)
_defs[node.spelling] = args_sizes
# print("adding", node.spelling)
for c in node.get_children():
recursive_arg_sizes(c)
def main() -> int:
# cache function sizes
recursive_arg_sizes(tu.cursor)
_defs.update(defs_precalc)
# --- second pass, check against def's
file_check_arg_sizes(tu)
return 0
if __name__ == "__main__":
sys.exit(main)

View File

@@ -0,0 +1,340 @@
# SPDX-FileCopyrightText: 2011-2022 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
__all__ = (
"cmake_dir_set",
"build_info",
"SOURCE_DIR",
"CMAKE_DIR",
)
import os
import shlex
import subprocess
import sys
from os.path import join, dirname, normpath, abspath
from typing import (
Any,
IO,
)
from collections.abc import (
Callable,
Iterator,
Sequence,
)
SOURCE_DIR = join(dirname(__file__), "..", "..")
SOURCE_DIR = normpath(SOURCE_DIR)
SOURCE_DIR = abspath(SOURCE_DIR)
# copied from project_info.py
CMAKE_DIR = "."
def cmake_dir_set(cmake_dir: str) -> None:
"""
Callers may not run this tool from the CWD, in this case,
allow the value to be set.
"""
# Use a method in case any other values need to be updated in the future.
global CMAKE_DIR
CMAKE_DIR = cmake_dir
def is_c_header(filename: str) -> bool:
ext = os.path.splitext(filename)[1]
return (ext in {".h", ".hpp", ".hxx", ".hh"})
def is_c(filename: str) -> bool:
ext = os.path.splitext(filename)[1]
return (ext in {".c", ".cpp", ".cxx", ".m", ".mm", ".rc", ".cc", ".inl", ".osl"})
def is_c_any(filename: str) -> bool:
return is_c(filename) or is_c_header(filename)
def cmake_cache_var_iter() -> Iterator[tuple[str, str, str]]:
import re
re_cache = re.compile(r'([A-Za-z0-9_\-]+)?:?([A-Za-z0-9_\-]+)?=(.*)$')
with open(join(CMAKE_DIR, "CMakeCache.txt"), 'r', encoding='utf-8') as cache_file:
for l in cache_file:
match = re_cache.match(l.strip())
if match is not None:
var, type_, val = match.groups()
yield (var, type_ or "", val)
def cmake_cache_var(var: str) -> str | None:
for var_iter, _type_iter, value_iter in cmake_cache_var_iter():
if var == var_iter:
return value_iter
return None
def cmake_cache_var_or_exit(var: str) -> str:
value = cmake_cache_var(var)
if value is None:
print("Unable to find %r exiting!" % value)
sys.exit(1)
return value
def do_ignore(filepath: str, ignore_prefix_list: Sequence[str] | None) -> bool:
if ignore_prefix_list is None:
return False
relpath = os.path.relpath(filepath, SOURCE_DIR)
return any([relpath.startswith(prefix) for prefix in ignore_prefix_list])
def makefile_log() -> list[str]:
# support both make and ninja
make_exe = cmake_cache_var_or_exit("CMAKE_MAKE_PROGRAM")
make_exe_basename = os.path.basename(make_exe)
if make_exe_basename.startswith(("make", "gmake")):
print("running 'make' with --dry-run ...")
with subprocess.Popen(
(
make_exe,
"-C", CMAKE_DIR,
"--always-make",
"--dry-run",
"--keep-going",
"VERBOSE=1",
),
stdout=subprocess.PIPE,
) as proc:
stdout_data, stderr_data = proc.communicate()
elif make_exe_basename.startswith("ninja"):
print("running 'ninja' with -t commands ...")
with subprocess.Popen(
(
make_exe,
"-C", CMAKE_DIR,
"-t", "commands",
),
stdout=subprocess.PIPE,
) as proc:
stdout_data, stderr_data = proc.communicate()
else:
print("CMAKE_MAKE_PROGRAM: \"{:s}\" is not known (make/gmake/ninja)")
sys.exit(1)
del stderr_data
print("done!", len(stdout_data), "bytes")
return stdout_data.decode("utf-8", errors="ignore").split("\n")
def build_info(
use_c: bool = True,
use_cxx: bool = True,
ignore_prefix_list: list[str] | None = None,
) -> list[tuple[str, list[str], list[str]]]:
makelog = makefile_log()
source = []
compilers = []
if use_c:
compilers.append(cmake_cache_var_or_exit("CMAKE_C_COMPILER"))
if use_cxx:
compilers.append(cmake_cache_var_or_exit("CMAKE_CXX_COMPILER"))
print("compilers:", " ".join(compilers))
fake_compiler = "%COMPILER%"
print("parsing make log ...")
for line in makelog:
args_orig: str | list[str] = line.split()
args = [fake_compiler if c in compilers else c for c in args_orig]
if args == args_orig:
# No compilers in the command, skip.
continue
del args_orig
# Join arguments in case they are not.
args_str = " ".join(args)
args_str = args_str.replace(" -isystem", " -I")
args_str = args_str.replace(" -D ", " -D")
args_str = args_str.replace(" -I ", " -I")
args = shlex.split(args_str)
del args_str
# end
# remove compiler
args[:args.index(fake_compiler) + 1] = []
c_files = [f for f in args if is_c(f)]
inc_dirs = [f[2:].strip() for f in args if f.startswith('-I')]
defs = [f[2:].strip() for f in args if f.startswith('-D')]
for c in sorted(c_files):
if do_ignore(c, ignore_prefix_list):
continue
source.append((c, inc_dirs, defs))
# make relative includes absolute
# not totally essential but useful
for i, f in enumerate(inc_dirs):
if not os.path.isabs(f):
inc_dirs[i] = os.path.abspath(os.path.join(CMAKE_DIR, f))
# safety check that our includes are ok
for f in inc_dirs:
if not os.path.exists(f):
raise Exception("%s missing" % f)
print("done!")
return source
def build_defines_as_source() -> str:
"""
Returns a string formatted as an include:
'#defines A=B\n#define....'
"""
# Works for both GCC and CLANG.
cmd = (cmake_cache_var_or_exit("CMAKE_C_COMPILER"), "-dM", "-E", "-")
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL,
)
# We know this is always true based on the input arguments to `Popen`.
assert process.stdout is not None
stdout: IO[bytes] = process.stdout
return stdout.read().strip().decode('ascii')
def build_defines_as_args() -> list[str]:
return [
("-D" + "=".join(l.split(maxsplit=2)[1:]))
for l in build_defines_as_source().split("\n")
if l.startswith('#define')
]
def process_make_non_blocking(proc: subprocess.Popen[Any]) -> subprocess.Popen[Any]:
import fcntl
for fh in (proc.stderr, proc.stdout):
if fh is None:
continue
fd = fh.fileno()
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
return proc
# Could be moved elsewhere!, this just happens to be used by scripts that also
# use this module.
def queue_processes(
process_funcs: Sequence[tuple[Callable[..., subprocess.Popen[Any]], tuple[Any, ...]]],
*,
job_total: int = -1,
sleep: float = 0.1,
process_finalize: Callable[[subprocess.Popen[Any], bytes, bytes], int | None] | None = None,
) -> None:
"""
Takes a list of function argument pairs, each function must return a process.
"""
if job_total == -1:
import multiprocessing
job_total = multiprocessing.cpu_count()
del multiprocessing
if job_total == 1:
for func, args in process_funcs:
sys.stdout.flush()
sys.stderr.flush()
process = func(*args)
if process_finalize is not None:
data = process.communicate()
process_finalize(process, *data)
else:
import time
if process_finalize is not None:
def poll_and_finalize(
p: subprocess.Popen[Any],
stdout: list[bytes],
stderr: list[bytes],
) -> int | None:
assert p.stdout is not None
if data := p.stdout.read():
stdout.append(data)
assert p.stderr is not None
if data := p.stderr.read():
stderr.append(data)
if (returncode := p.poll()) is not None:
data_stdout, data_stderr = p.communicate()
if data_stdout:
stdout.append(data_stdout)
if data_stderr:
stderr.append(data_stderr)
process_finalize(p, b"".join(stdout), b"".join(stderr))
return returncode
else:
def poll_and_finalize(
p: subprocess.Popen[Any],
stdout: list[bytes],
stderr: list[bytes],
) -> int | None:
return p.poll()
processes: list[tuple[subprocess.Popen[Any], list[bytes], list[bytes]]] = []
for func, args in process_funcs:
# wait until a thread is free
while 1:
processes[:] = [p_item for p_item in processes if poll_and_finalize(*p_item) is None]
if len(processes) <= job_total:
break
time.sleep(sleep)
sys.stdout.flush()
sys.stderr.flush()
processes.append((process_make_non_blocking(func(*args)), [], []))
# Don't return until all jobs have finished.
while 1:
processes[:] = [p_item for p_item in processes if poll_and_finalize(*p_item) is None]
if not processes:
break
time.sleep(sleep)
def main() -> None:
if not os.path.exists(join(CMAKE_DIR, "CMakeCache.txt")):
print("This script must run from the cmake build dir")
return
for s in build_info():
print(s)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,591 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
A command line utility to check Blender's source code with CLANG's Python module.
To call this directly:
export CLANG_LIB_DIR=/usr/lib64
cd {BUILD_DIR}
python ../blender/tools/check_source/static_check_clang.py --match=".*" --checks=struct_comments
"""
__all__ = (
"main",
)
import argparse
import os
import re
import sys
from typing import (
Any,
)
from collections.abc import (
Sequence,
)
import project_source_info
# pylint: disable-next=import-outside-toplevel
import clang # type: ignore
# pylint: disable-next=import-outside-toplevel
import clang.cindex # type: ignore
from clang.cindex import (
CursorKind,
)
# Only for readability.
ClangNode = Any
ClangTranslationUnit = Any
ClangSourceLocation = Any
USE_VERBOSE = os.environ.get("VERBOSE", None) is not None
CLANG_BIND_DIR = os.environ.get("CLANG_BIND_DIR")
CLANG_LIB_DIR = os.environ.get("CLANG_LIB_DIR")
if CLANG_BIND_DIR is None:
print("$CLANG_BIND_DIR python binding dir not set")
if CLANG_LIB_DIR is None:
print("$CLANG_LIB_DIR clang lib dir not set")
if CLANG_LIB_DIR:
clang.cindex.Config.set_library_path(CLANG_LIB_DIR)
if CLANG_BIND_DIR:
sys.path.append(CLANG_BIND_DIR)
CHECKER_IGNORE_PREFIX = [
"extern",
]
CHECKER_EXCLUDE_SOURCE_FILES = set(os.path.join(*f.split("/")) for f in (
# Skip parsing these large (mostly data files).
"source/blender/editors/space_text/text_format_pov.cc",
"source/blender/editors/space_text/text_format_pov_ini.cc",
))
# -----------------------------------------------------------------------------
# Utility Functions
def clang_source_location_as_str(source_location: ClangSourceLocation) -> str:
return "{:s}:{:d}:{:d}:".format(str(source_location.file), source_location.line, source_location.column)
# -----------------------------------------------------------------------------
# Checkers
class ClangChecker:
"""
Base class for checkers.
Notes:
- The function ``check_source`` takes file_data as bytes instead of a string
because the offsets provided by CLANG are byte offsets.
While the offsets could be converted into UNICODE offset's,
there doesn't seem to be an efficient & convenient way to do that.
"""
__slots__ = ()
def __new__(cls, *args: tuple[Any], **kwargs: dict[str, Any]) -> Any:
raise RuntimeError("%s should not be instantiated" % cls)
@staticmethod
def check_source(
_filepath: str,
_file_data: bytes,
_tu: ClangTranslationUnit,
_shared_check_data: Any,
) -> list[str]:
raise RuntimeError("This function must be overridden by it's subclass!")
return []
@staticmethod
def setup() -> Any:
return None
@staticmethod
def teardown(_shared_check_data: Any) -> None:
pass
class clang_checkers:
# fake module.
class struct_comments(ClangChecker):
"""
Ensure comments in struct declarations match the members of the struct, e.g:
SomeStruct var = {
/*name*/ "Text",
/*children*/ nullptr,
/*flag*/ 0,
};
Will generate a warning if any of the names in the prefix comments don't match the struct member names.
"""
_struct_comments_ignore = {
# `PyTypeObject` uses compile time members that vary (see: #PyVarObject_HEAD_INIT macro)
# While some clever comment syntax could be supported to signify multiple/optional members
# this is such a specific case that it's simpler to skip this warning.
"PyTypeObject": {"ob_base": {"ob_size"}},
}
@staticmethod
def _struct_check_comments_recursive(
# Static (unchanged for each recursion).
filepath: str,
file_data: bytes,
# Different for each recursion.
node: ClangNode,
node_parent: ClangNode,
level: int,
# Used to build data.
struct_decl_map: dict[str, ClangNode],
struct_type_map: dict[str, str],
output: list[str],
) -> None:
# Needed to read back the node.
if USE_VERBOSE:
print("TRY:", node.kind, node.spelling, len(list(node.get_tokens())), level, node.location)
# if node.kind == CursorKind.VAR_DECL and node.spelling == "Vector_NumMethods":
# import IPython
# IPython.embed()
if node.kind == CursorKind.STRUCT_DECL:
# Ignore forward declarations.
if next(node.get_children(), None) is not None:
struct_type = node.spelling.strip()
if not struct_type:
# The parent may be a `typedef [..] TypeID` where `[..]` is `struct { a; b; c; }`.
# Inspect the parent.
if node_parent is not None and (node_parent.kind == CursorKind.TYPEDEF_DECL):
tokens = list(node_parent.get_tokens())
if tokens[0].spelling == "typedef":
struct_type = tokens[-1].spelling
struct_decl_map[struct_type] = node
# Ignore declarations for anything defined outside this file.
if str(node.location.file) == filepath:
if node.kind == CursorKind.INIT_LIST_EXPR:
if USE_VERBOSE:
print(node.spelling, node.location)
# Split to avoid `const struct` .. and similar.
# NOTE: there may be an array size suffix, e.g. `[4]`.
# This could be supported.
struct_type = node.type.spelling.split()[-1]
struct = struct_decl_map.get(struct_type)
if struct is None:
if USE_VERBOSE:
print("NOT FOUND:", struct_type)
struct_type = struct_type_map.get(struct_type)
if struct_type is not None:
struct = struct_decl_map.get(struct_type)
if USE_VERBOSE:
print("INSPECTING STRUCT:", struct_type)
if struct is not None:
member_names = [
node_child.spelling for node_child in struct.get_children()
if node_child.kind == CursorKind.FIELD_DECL
]
# if struct_type == "PyMappingMethods":
# import IPython
# IPython.embed()
children = list(node.get_children())
comment_names = []
# Set to true when there is a comment directly before a value,
# this is needed because:
# - Comments on the previous line are rarely intended to be identifiers of the struct member.
# - Comments which _are_ intended to be identifiers can be wrapped onto new-lines
# so they should not be ignored.
#
# While it's possible every member is wrapped onto a new-line,
# this is highly unlikely.
comment_names_prefix_any = False
for node_child in children:
# Extract the content before the child
# (typically a C-style comment containing the struct member).
end = min(node_child.location.offset, len(file_data))
# It's possible this ID has a preceding "name::space::etc"
# which should be skipped.
while end > 0 and ((ch := bytes((file_data[end - 1],))).isalpha() or ch == b":"):
end -= 1
has_newline = False
while end > 0:
ch = bytes((file_data[end - 1],))
if ch in {b"\t", b" "}:
end -= 1
elif ch == b"\n":
end -= 1
has_newline = True
else:
break
beg = end - 1
while beg != 0 and bytes((file_data[beg],)) not in {
b"\n",
# Needed so declarations on a single line don't detect a comment
# from an outer comment, e.g.
# SomeStruct x = {
# /*list*/ {nullptr, nullptr},
# };
# Would start inside the first `nullptr` and walk backwards to find `/*list*/`.
b"{"
}:
beg -= 1
# Seek back until the comment end (in some cases this includes code).
# This occurs when the body of the declaration includes code, e.g.
# rcti x = {
# /*xmin*/ foo->bar.baz,
# ... snip ...
# };
# Where `"xmin*/ foo->bar."` would be extracted were it not for this check.
# There might be a more elegant way to handle this, for how snipping off the last
# comment characters is sufficient.
end_test = file_data.rfind(b"*/", end + 1, beg)
if end_test != -1:
end = end_test
text = file_data[beg:end]
if text.lstrip().startswith(b"/*"):
if not has_newline:
comment_names_prefix_any = True
else:
text = b""
comment_names.append(text.decode('utf-8'))
if USE_VERBOSE:
print(member_names)
print(comment_names)
total = min(len(member_names), len(comment_names))
if total != 0 and comment_names_prefix_any:
result = [""] * total
count_found = 0
count_invalid = 0
for i in range(total):
comment = comment_names[i]
if "/*" in comment and "*/" in comment:
comment = comment.strip().strip("/").strip("*")
if comment == member_names[i]:
count_found += 1
else:
suppress_warning = False
if (
skip_members_table :=
clang_checkers.struct_comments._struct_comments_ignore.get(
node_parent.type.spelling,
)
) is not None:
if (skip_members := skip_members_table.get(comment)) is not None:
if member_names[i] in skip_members:
suppress_warning = True
if not suppress_warning:
result[i] = "Incorrect! found \"{:s}\" expected \"{:s}\"".format(
comment, member_names[i])
count_invalid += 1
else:
result[i] = "No comment for \"{:s}\"".format(member_names[i])
if count_found == 0 and count_invalid == 0:
# No comments used, skip this as not all declaration use this comment style.
output.append(
"NONE: {:s} {:s}".format(
clang_source_location_as_str(node.location),
node.type.spelling,
)
)
elif count_found != total:
for i in range(total):
if result[i]:
output.append(
"FAIL: {:s} {:s}".format(
clang_source_location_as_str(children[i].location),
result[i],
)
)
else:
output.append(
"OK: {:s} {:s}".format(
clang_source_location_as_str(node.location),
node.type.spelling,
)
)
for node_child in node.get_children():
clang_checkers.struct_comments._struct_check_comments_recursive(
filepath, file_data,
node_child, node, level + 1,
struct_decl_map, struct_type_map, output,
)
@staticmethod
def check_source(
filepath: str,
file_data: bytes,
tu: ClangTranslationUnit,
_shared_check_data: Any) -> list[str]:
output: list[str] = []
struct_decl_map: dict[str, Any] = {}
struct_type_map: dict[str, str] = {}
clang_checkers.struct_comments._struct_check_comments_recursive(
filepath, file_data,
tu.cursor, None, 0,
struct_decl_map, struct_type_map, output,
)
return output
# -----------------------------------------------------------------------------
# Checker Class Access
def check_function_get_all() -> list[str]:
checkers = []
for name in dir(clang_checkers):
value = getattr(clang_checkers, name)
if isinstance(value, type) and issubclass(value, ClangChecker):
checkers.append(name)
checkers.sort()
return checkers
def check_class_from_id(name: str) -> type[ClangChecker]:
result = getattr(clang_checkers, name)
assert issubclass(result, ClangChecker)
# MYPY 0.812 doesn't recognize the assert above.
return result # type: ignore
def check_docstring_from_id(name: str) -> str:
from textwrap import dedent
result = getattr(clang_checkers, name).__doc__
return dedent(result or '').strip('\n') + '\n'
# -----------------------------------------------------------------------------
# Generic Clang Checker
def check_source_file(
filepath: str,
args: Sequence[str],
check_ids: Sequence[str],
shared_check_data_foreach_check: Sequence[Any],
) -> str:
index = clang.cindex.Index.create()
try:
tu = index.parse(filepath, args)
except clang.cindex.TranslationUnitLoadError as ex:
return "PARSE_ERROR: {:s} {!r}".format(filepath, ex)
with open(filepath, "rb") as fh:
file_data = fh.read()
output: list[str] = []
# we don't really care what we are looking at, just scan entire file for
# function calls.
for check, shared_check_data in zip(check_ids, shared_check_data_foreach_check):
cls = check_class_from_id(check)
output.extend(cls.check_source(filepath, file_data, tu, shared_check_data))
if not output:
return ""
return "\n".join(output)
def check_source_file_for_imap(args: tuple[str, Sequence[str], Sequence[str], Sequence[Any]]) -> str:
return check_source_file(*args)
def source_info_filter(
source_info: list[tuple[str, list[str], list[str]]],
regex_list: Sequence[re.Pattern[str]],
) -> list[tuple[str, list[str], list[str]]]:
source_dir = project_source_info.SOURCE_DIR
if not source_dir.endswith(os.sep):
source_dir += os.sep
source_info_result = []
for item in source_info:
filepath_source = item[0]
if filepath_source.startswith(source_dir):
filepath_source_relative = filepath_source[len(source_dir):]
if filepath_source_relative in CHECKER_EXCLUDE_SOURCE_FILES:
CHECKER_EXCLUDE_SOURCE_FILES.remove(filepath_source_relative)
continue
if filepath_source_relative.startswith("intern" + os.sep + "ghost"):
pass
elif filepath_source_relative.startswith("source" + os.sep):
pass
else:
continue
has_match = False
for regex in regex_list:
if regex.match(filepath_source_relative) is not None:
has_match = True
if not has_match:
continue
else:
# Skip files not in source (generated files from the build directory),
# these could be check but it's not all that useful (preview blend ... etc).
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 run_checks_on_project(
check_ids: Sequence[str],
regex_list: Sequence[re.Pattern[str]],
jobs: int,
) -> None:
source_info = project_source_info.build_info(ignore_prefix_list=CHECKER_IGNORE_PREFIX)
source_defines = project_source_info.build_defines_as_args()
# Apply exclusion.
source_info = source_info_filter(source_info, regex_list)
shared_check_data_foreach_check = [
check_class_from_id(check).setup() for check in check_ids
]
all_args = []
index = 0
for filepath_source, inc_dirs, defs in source_info[index:]:
args = (
[("-I" + i) for i in inc_dirs] +
[("-D" + d) for d in defs] +
source_defines
)
all_args.append((filepath_source, args, check_ids, shared_check_data_foreach_check))
import multiprocessing
if jobs <= 0:
jobs = multiprocessing.cpu_count()
if jobs > 1:
with multiprocessing.Pool(processes=jobs) as pool:
# No `istarmap`, use an intermediate function.
for result in pool.imap(check_source_file_for_imap, all_args):
if result:
print(result)
else:
for (filepath_source, args, _check_ids, shared_check_data_foreach_check) in all_args:
result = check_source_file(filepath_source, args, check_ids, shared_check_data_foreach_check)
if result:
print(result)
for (check, shared_check_data) in zip(check_ids, shared_check_data_foreach_check):
check_class_from_id(check).teardown(shared_check_data)
def create_parser(checkers_all: Sequence[str]) -> argparse.ArgumentParser:
from textwrap import indent
# Create docstring for checks.
checks_all_docs = []
for checker in checkers_all:
# `%` -> `%%` is needed for `--help` not to interpret these as formatting arguments.
checks_all_docs.append(
" %s\n%s" % (
checker,
indent(check_docstring_from_id(checker).replace("%", "%%"), ' '),
)
)
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--match",
nargs='+',
required=True,
metavar="REGEX",
help="Match file paths against this expression",
)
parser.add_argument(
"--checks",
dest="checks",
help=(
"Specify the check presets to run.\n\n" +
"\n".join(checks_all_docs) + "\n"
"Multiple checkers may be passed at once (comma separated, no spaces)."),
required=True,
)
parser.add_argument(
"--jobs",
dest="jobs",
type=int,
default=0,
help=(
"The number of processes to use. "
"Defaults to zero which detects the available cores, 1 is single threaded (useful for debugging)."
),
required=False,
)
return parser
# -----------------------------------------------------------------------------
# Main Function
def main() -> int:
checkers_all = check_function_get_all()
parser = create_parser(checkers_all)
args = parser.parse_args()
regex_list = []
for expr in args.match:
try:
regex_list.append(re.compile(expr))
except Exception as ex:
print("Error in expression: \"{:s}\"\n {!r}".format(expr, ex))
return 1
run_checks_on_project(
args.checks.split(','),
regex_list,
args.jobs,
)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2011-2022 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
__all__ = (
"main",
)
import project_source_info
import subprocess
import sys
import os
from typing import (
Any,
)
USE_QUIET = (os.environ.get("QUIET", None) is not None)
CHECKER_IGNORE_PREFIX = [
"extern",
"intern/moto",
]
CHECKER_BIN = "python3"
CHECKER_ARGS = [
os.path.join(os.path.dirname(__file__), "clang_array_check.py"),
# not sure why this is needed, but it is.
"-I" + os.path.join(project_source_info.SOURCE_DIR, "extern", "glew", "include"),
# stupid but needed
"-Dbool=char"
]
def main() -> None:
source_info = project_source_info.build_info(ignore_prefix_list=CHECKER_IGNORE_PREFIX)
check_commands = []
for c, inc_dirs, defs in source_info:
# ~if "source/blender" not in c:
# ~ continue
cmd = (
[CHECKER_BIN] +
CHECKER_ARGS +
[c] +
[("-I%s" % i) for i in inc_dirs] +
[("-D%s" % d) for d in defs]
)
check_commands.append((c, cmd))
process_functions = []
def my_process(i: int, c: str, cmd: str) -> subprocess.Popen[Any]:
if not USE_QUIET:
percent = 100.0 * (i / (len(check_commands) - 1))
percent_str = "[" + ("%.2f]" % percent).rjust(7) + " %:"
sys.stdout.flush()
sys.stdout.write("%s %s\n" % (percent_str, c))
return subprocess.Popen(cmd)
for i, (c, cmd) in enumerate(check_commands):
process_functions.append((my_process, (i, c, cmd)))
project_source_info.queue_processes(process_functions)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,490 @@
#!/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()

View File

@@ -0,0 +1,280 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
r"""
Validates sizes in C/C++ sources written as: ``type name[/*MAX_NAME*/ 64]``
where ``MAX_NAME`` is expected to be a define equal to 64, otherwise a warning is reported.
"""
__all__ = (
"main",
)
import os
import sys
import re
THIS_DIR = os.path.dirname(__file__)
BASE_DIR = os.path.normpath(os.path.abspath(os.path.normpath(os.path.join(THIS_DIR, "..", ".."))))
sys.path.append(os.path.join(THIS_DIR, "..", "utils_maintenance", "modules"))
from batch_edit_text import run
import line_number_utils
# -----------------------------------------------------------------------------
# Utilities
# -----------------------------------------------------------------------------
# Local Settings
# TODO, move to config file
SOURCE_DIRS = (
"source",
)
SOURCE_EXT = (
# C/C++
".c", ".h", ".cpp", ".hpp", ".cc", ".hh", ".cxx", ".hxx", ".inl",
# Objective C
".m", ".mm",
# GLSL
".glsl",
)
# Mainly useful for development to check extraction & validation are working.
SHOW_SUCCESS = True
# -----------------------------------------------------------------------------
# Globals
# Map defines to a list of (filename-split, value) pairs.
global_defines: dict[
# The define ID.
str,
# Value(s), in case it's defined in multiple files.
list[
tuple[
# The `BASE_DIR` relative path (split by `os.sep`).
tuple[str, ...],
# The value of the define,
# a literal string with comments stripped out.
str,
],
],
] = {}
REGEX_ID_LITERAL = "[A-Za-z_][A-Za-z_0-9]*"
# Detect:
# `[/*ID*/ 64]`.
# `[/*ID - 2*/ 62]`.
REGEX_SIZE_COMMENT_IN_ARRAY = re.compile("\\[\\/\\*([^\\]]+)\\*\\/\\s*(\\d+)\\]")
# Detect: `#define ID 64`
REGEX_DEFINE_C_LIKE = re.compile("^\\s*#\\s*define\\s+(" + REGEX_ID_LITERAL + ")[ \t]+([^\n]+)", re.MULTILINE)
# Detect:
# `ID = 64,`
# `ID = 64`
REGEX_ENUM_C_LIKE = re.compile("^\\s*(" + REGEX_ID_LITERAL + ")\\s=\\s([^,\n]+)", re.MULTILINE)
# Detect ID's.
REGEX_ID_OR_NUMBER_C_LIKE = re.compile("[A-Za-z0-9_]+")
def extract_defines(filepath: str, data_src: str) -> None:
filepath_rel = os.path.relpath(filepath, BASE_DIR)
for regex_matcher in (REGEX_DEFINE_C_LIKE, REGEX_ENUM_C_LIKE):
for m in regex_matcher.finditer(data_src):
value_id = m.group(1)
value_literal = m.group(2)
# Weak comment stripping.
# This is (arguably) acceptable since the intent is to extract numbers,
# if developers feel the need to write lines such as:
# `#define VALUE_MAX /* Lets make some trouble! */ 64`
# Then they can consider if that's actually needed (sigh!)...
# Otherwise, we could replace this with a full parser such as CLANG,
# however this is a bit of a hassle to setup.
if "//" in value_literal:
value_literal = value_literal.split("//", 1)[0]
if "/*" in value_literal:
value_literal = value_literal.split("/*", 1)[0]
try:
global_defines[value_id].append((tuple(filepath_rel.split(os.sep)), value_literal))
except KeyError:
global_defines[value_id] = [(tuple(filepath_rel.split(os.sep)), value_literal)]
# Returning None indicates the file is not edited.
def path_score_distance(a: tuple[str, ...], b: tuple[str, ...]) -> tuple[int, int]:
"""
Compare two paths, to find which paths are "closer" to each-other.
This is used as a tie breaker when defines are found in multiple headers.
"""
count_shared = 0
range_min = min(len(a), len(b))
range_max = max(len(a), len(b))
for i in range(range_min):
if a[i] != b[i]:
break
count_shared += 1
count_nested = range_max - count_shared
# Negate shared so smaller is better.
# Less path nesting also gets priority.
return (-count_shared, count_nested)
def eval_define(
value_literal: str,
*,
default: str,
filepath_ref_split: tuple[str, ...],
) -> tuple[str, list[str]]:
failed: list[str] = []
def re_replace_fn(match: re.Match[str]) -> str:
value = match.group()
if value.isdigit():
return value
other_values = global_defines.get(value)
if other_values is None:
failed.append(value)
return value
if len(other_values) == 1:
other_filepath_split, other_literal = other_values[0]
else:
# Find the "closest" on the file system.
# In practice favor paths which are co-located works fairly well,
# needed as it's now known which headers ID's in a head *could* reference.
other_literal_best = ""
other_score_best = (0, 0)
other_filepath_split_best: tuple[str, ...] = ("",)
for other_filepath_split_test, other_literal_test in other_values:
other_score_test = path_score_distance(filepath_ref_split, other_filepath_split_test)
if (
# First time.
(not other_literal_best) or
# A lower score has been found (smaller is better).
(other_score_test < other_score_best)
):
other_literal_best = other_literal_test
other_score_best = other_score_test
other_filepath_split_best = other_filepath_split_test
del other_score_test
other_literal = other_literal_best
other_filepath_split = other_filepath_split_best
del other_literal_best, other_score_best, other_filepath_split_best
other_literal_eval, other_failed = eval_define(
other_literal,
default="",
filepath_ref_split=other_filepath_split,
)
if other_literal_eval:
return other_literal_eval
# `failed.append(value)` is also valid, report the gestured failure as its more likely to give insights
# into what went wrong.
failed.extend(other_failed)
return value
# Use integer division.
value_literal = value_literal.replace(r"/", r"//")
# Populates `failed`.
value_literal_eval = REGEX_ID_OR_NUMBER_C_LIKE.sub(re_replace_fn, value_literal)
if failed:
# One or more ID could not be found.
return default, failed
# This could use exception handling, don't unless it's needed though.
# pylint: disable-next=eval-used
return str(eval(value_literal_eval)), failed
def validate_sizes(filepath: str, data_src: str) -> None:
# Nicer for printing.
filepath_rel = os.path.relpath(filepath, BASE_DIR)
filepath_rel_split = tuple(filepath_rel.split(os.sep))
for m, line, (beg, end) in line_number_utils.finditer_with_line_numbers_and_bounds(
REGEX_SIZE_COMMENT_IN_ARRAY,
data_src,
):
del end
value_id = m.group(1)
value_literal = m.group(2)
value_eval, lookups_failed = eval_define(
value_id,
default="",
filepath_ref_split=filepath_rel_split,
)
data_line_column = "{:s}:{:d}:{:d}:".format(
filepath_rel,
line + 1,
# Place the cursor after the `[`.
(m.start(0) + 1) - beg,
)
if len(value_id.strip()) != len(value_id):
print("WARN:", data_line_column, "comment includes white-space")
continue
if lookups_failed:
print("WARN:", data_line_column, "[{:s}]".format(", ".join(lookups_failed)), "unknown")
continue
if value_literal != value_eval:
print("WARN:", data_line_column, value_id, "mismatch", "({:s} != {:s})".format(value_literal, value_eval))
continue
if SHOW_SUCCESS:
print("OK: ", data_line_column, "{:s}={:s},".format(value_id, value_literal))
# Returning None indicates the file is not edited.
def main() -> int:
# Extract defines.
run(
directories=[os.path.join(BASE_DIR, d) for d in SOURCE_DIRS],
is_text=lambda filepath: filepath.endswith(SOURCE_EXT),
text_operation=extract_defines,
# Can't be used if we want to accumulate in a global variable.
use_multiprocess=False,
)
# For predictable lookups on tie breakers.
# In practice it should almost never matter.
for values in global_defines.values():
if len(values) > 1:
values.sort()
# Validate sizes.
run(
directories=[os.path.join(BASE_DIR, d) for d in SOURCE_DIRS],
is_text=lambda filepath: filepath.endswith(SOURCE_EXT),
text_operation=validate_sizes,
# Can't be used if we want to accumulate in a global variable.
use_multiprocess=False,
)
return 0
if __name__ == "__main__":
sys.exit(main())