Add Chromium-only Blender WebEngine parity work
This commit is contained in:
425
blender-5.2.0/tools/check_source/check_cmake_consistency.py
Executable file
425
blender-5.2.0/tools/check_source/check_cmake_consistency.py
Executable 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()
|
||||
@@ -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"))))
|
||||
179
blender-5.2.0/tools/check_source/check_deprecated.py
Normal file
179
blender-5.2.0/tools/check_source/check_deprecated.py
Normal 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()
|
||||
140
blender-5.2.0/tools/check_source/check_descriptions.py
Normal file
140
blender-5.2.0/tools/check_source/check_descriptions.py
Normal 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()
|
||||
182
blender-5.2.0/tools/check_source/check_header_duplicate.py
Executable file
182
blender-5.2.0/tools/check_source/check_header_duplicate.py
Executable 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())
|
||||
605
blender-5.2.0/tools/check_source/check_licenses.py
Normal file
605
blender-5.2.0/tools/check_source/check_licenses.py
Normal 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()
|
||||
123
blender-5.2.0/tools/check_source/check_mypy.py
Executable file
123
blender-5.2.0/tools/check_source/check_mypy.py
Executable 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()
|
||||
106
blender-5.2.0/tools/check_source/check_mypy_config.py
Normal file
106
blender-5.2.0/tools/check_source/check_mypy_config.py
Normal 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")
|
||||
944
blender-5.2.0/tools/check_source/check_spelling.py
Executable file
944
blender-5.2.0/tools/check_source/check_spelling.py
Executable 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())
|
||||
936
blender-5.2.0/tools/check_source/check_spelling_config.py
Normal file
936
blender-5.2.0/tools/check_source/check_spelling_config.py
Normal 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/",
|
||||
}
|
||||
98
blender-5.2.0/tools/check_source/check_unused_defines.py
Executable file
98
blender-5.2.0/tools/check_source/check_unused_defines.py
Executable 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())
|
||||
398
blender-5.2.0/tools/check_source/clang_array_check.py
Normal file
398
blender-5.2.0/tools/check_source/clang_array_check.py
Normal 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)
|
||||
340
blender-5.2.0/tools/check_source/project_source_info.py
Normal file
340
blender-5.2.0/tools/check_source/project_source_info.py
Normal 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()
|
||||
591
blender-5.2.0/tools/check_source/static_check_clang.py
Normal file
591
blender-5.2.0/tools/check_source/static_check_clang.py
Normal 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())
|
||||
76
blender-5.2.0/tools/check_source/static_check_clang_array.py
Normal file
76
blender-5.2.0/tools/check_source/static_check_clang_array.py
Normal 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()
|
||||
490
blender-5.2.0/tools/check_source/static_check_cppcheck.py
Executable file
490
blender-5.2.0/tools/check_source/static_check_cppcheck.py
Executable 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()
|
||||
280
blender-5.2.0/tools/check_source/static_check_size_comments.py
Executable file
280
blender-5.2.0/tools/check_source/static_check_size_comments.py
Executable 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())
|
||||
Reference in New Issue
Block a user