Files
myagents/scripts/rename_funcs_to_snake.py
T
Zhengshou Lai e8320b1798 feat(scripts): add rename_funcs_to_snake helper
Utility script to bulk-rename Python and C++ functions to
snake_case using AST analysis and regex heuristics.
2026-04-17 00:02:50 +08:00

414 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""批量将 src 中的函数名重命名为 snake_casePython 用 AST+正则,C++ 用正则启发式)."""
import argparse
import ast
import re
import sys
from pathlib import Path
def camel_to_snake(name: str) -> str:
"""将驼峰/帕斯卡命名转为 snake_case(保留连续大写)."""
s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
s2 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1)
return s2.lower()
def needs_rename(name: str) -> bool:
if not name or (name.startswith("__") and name.endswith("__")):
return False
return camel_to_snake(name) != name
# ============================================================================
# Python 处理(AST 收集 + 正则替换)
# ============================================================================
def _collect_python_funcs(source: str) -> set[str]:
"""用 AST 收集文件中所有需要重命名的函数定义名."""
try:
tree = ast.parse(source)
except SyntaxError:
return set()
found: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
name = node.name
if needs_rename(name):
found.add(name)
return found
def rename_python_file(path: Path, dry_run: bool = True) -> dict[str, str]:
source = path.read_text(encoding="utf-8")
funcs = _collect_python_funcs(source)
if not funcs:
return {}
mapping = {name: camel_to_snake(name) for name in funcs}
# 安全整词替换(按长度降序)
for old, new in sorted(mapping.items(), key=lambda x: -len(x[0])):
source = re.sub(rf"\b{re.escape(old)}\b", new, source)
if not dry_run:
path.write_text(encoding="utf-8", data=source)
return mapping
# ============================================================================
# C++ 处理(正则启发式)
# ============================================================================
# 这些名称明确属于外部库/API,不应重命名
CPP_SKIP_NAMES = {
# C++ 关键字 / STL
"if", "for", "while", "switch", "catch", "return", "sizeof", "alignof",
"decltype", "static_cast", "dynamic_cast", "const_cast", "reinterpret_cast",
"new", "delete", "throw", "try", "namespace", "class", "struct", "enum",
"union", "template", "typename", "using", "typedef", "const", "volatile",
"inline", "virtual", "static", "explicit", "override", "final", "constexpr",
"noexcept", "mutable", "register", "extern", "thread_local", "friend",
"public", "protected", "private", "default", "nullptr", "true", "false",
"this", "auto", "void", "bool", "char", "short", "int", "long", "float",
"double", "signed", "unsigned", "wchar_t", "char8_t", "char16_t", "char32_t",
"size_t", "ssize_t", "ptrdiff_t", "intptr_t", "uintptr_t",
"int8_t", "int16_t", "int32_t", "int64_t",
"uint8_t", "uint16_t", "uint32_t", "uint64_t",
"string", "vector", "map", "set", "list", "array", "pair", "tuple",
"optional", "variant", "unique_ptr", "shared_ptr", "weak_ptr",
"make_unique", "make_shared", "move", "forward", "swap", "min", "max",
"abs", "sqrt", "pow", "sin", "cos", "tan", "asin", "acos", "atan", "atan2",
"exp", "log", "log10", "floor", "ceil", "round", "trunc", "fmod", "modf",
"frexp", "ldexp", "isnan", "isinf", "isfinite",
"printf", "sprintf", "fprintf", "scanf", "sscanf", "fopen", "fclose",
"fread", "fwrite", "malloc", "calloc", "realloc", "free", "memcpy",
"memmove", "memset", "strcpy", "strncpy", "strcmp", "strncmp", "strlen",
"strcat", "strncat", "strchr", "strstr", "atoi", "atol", "atof", "getenv",
"setenv", "system", "exit", "abort", "assert", "static_assert",
"emplace_back", "push_back", "pop_back", "insert", "erase", "find",
"count", "begin", "end", "cbegin", "cend", "rbegin", "rend", "crbegin",
"crend", "empty", "size", "length", "clear", "resize", "reserve",
"shrink_to_fit", "capacity", "front", "back", "at", "top", "data",
"c_str", "substr", "compare", "append", "replace", "assign", "copy",
"getline", "stoi", "stol", "stoll", "stoul", "stoull", "stof", "stod",
"stold", "to_string", "to_wstring", "get", "put", "read", "write",
"seekg", "seekp", "tellg", "tellp", "flush", "good", "eof", "fail",
"bad", "open", "close", "is_open", "precision", "width", "fill",
"setf", "unsetf", "flags", "sync_with_stdio", "tie",
"lock", "unlock", "try_lock", "wait", "notify_one", "notify_all",
"join", "detach", "yield", "sleep_for", "sleep_until", "get_id",
"hardware_concurrency", "exchange", "compare_exchange_strong",
"compare_exchange_weak", "fetch_add", "fetch_sub", "fetch_and",
"fetch_or", "fetch_xor", "load", "store",
# OpenFOAM
"runTime", "mesh", "U", "p", "rho", "nu", "mu", "k", "epsilon",
"omega", "phi", "turbulence", "transport", "fvSchemes", "fvSolution",
"controlDict", "blockMeshDict", "snappyHexMeshDict", "decomposeParDict",
"fvOptions", "turbulentIntensity", "turbulentLengthScale", "inletOutlet",
"zeroGradient", "noSlip", "slip", "wallFunction", "kqrWallFunction",
"epsilonWallFunction", "omegaWallFunction", "nutWallFunction",
"correctBoundaryConditions", "oldTime", "subOrEmptyDict", "nCells",
"IOdictionary", "IOobject", "system", "timeName", "boundaryMesh",
# MPI
"MPI_Init", "MPI_Finalize", "MPI_Comm_rank", "MPI_Comm_size",
"MPI_Send", "MPI_Recv", "MPI_Bcast", "MPI_Reduce", "MPI_Allreduce",
"MPI_Gather", "MPI_Allgather", "MPI_Scatter", "MPI_Barrier",
"MPI_Wtime", "MPI_Datatype", "MPI_Status", "MPI_Request",
# VTK
"vtkSmartPointer", "vtkXMLImageDataWriter", "vtkXMLImageDataReader",
"vtkImageData", "vtkPoints", "vtkCellArray", "vtkPolyData",
"vtkXMLUnstructuredGridWriter", "vtkUnstructuredGrid",
"vtkXMLPolyDataWriter", "vtkXMLPolyDataReader",
# pybind11
"pybind11", "module_", "class_", "def", "def_readwrite",
"def_property", "def_property_readonly", "def_static",
"export_values", "value", "doc", "arg", "arg_v", "noconvert",
"keep_alive", "base", "multiple_inheritance", "buffer_protocol",
"metaclass", "module_local", "is_operator", "pos_only", "kw_only",
"PYBIND11_MODULE", "init", "pickle", "copy", "deepcopy",
"make_tuple", "make_iterator", "cast", "object", "handle",
"str", "bytes", "dict", "list", "tuple", "slice", "none",
"bool_", "int_", "float_", "str_attr", "delattr", "hasattr",
"getattr", "setattr", "isinstance", "len", "iter", "next",
# Eigen
"Matrix", "Vector", "Array", "Quaternion", "Transform",
"AngleAxis", "Translation", "Scaling", "Identity", "Zero",
"Ones", "Random", "LinSpaced", "Constant", "Map", "Ref",
"Block", "Segment", "Head", "Tail", "Col", "Row", "setZero",
"setOnes", "setIdentity", "setConstant", "setRandom", "setLinSpaced",
"normalize", "normalized", "squaredNorm", "norm", "dot", "cross",
"transpose", "conjugate", "inverse", "determinant", "trace",
"diagonal", "adjoint", "reshaped", "reverse", "replicate",
"cwise", "redux", "all", "any", "prod", "sum", "mean",
"minCoeff", "maxCoeff", "visit", "allFinite", "hasNaN", "isMuchSmallerThan",
"isApprox", "isApproxToConstant", "isOnes", "isZero", "isIdentity",
# CGAL
"CGAL", "Alpha_shape_3", "Delaunay_triangulation_3", "Triangulation_3",
"Periodic_3_triangulation_3", "Exact_predicates_inexact_constructions_kernel",
"alpha_complex", "persistence", "filtration", "simplex", "vertex",
"edge", "facet", "cell", "neighbor", "incident", "finite", "infinite",
"circulator", "iterator", "handle", "index",
# OpenMP
"omp_get_max_threads", "omp_get_thread_num", "omp_set_num_threads",
"omp_get_num_threads", "omp_get_num_procs", "omp_in_parallel",
# Common short words
"is", "as", "to", "by", "on", "in", "at", "of", "up", "do",
"go", "no", "ok", "id", "ip", "io", "os", "it", "or", "and",
"not", "eq", "ne", "lt", "le", "gt", "ge", "add", "sub", "mul",
"div", "mod", "log", "exp", "sin", "cos", "tan", "abs",
# Project types / aliases that should not be treated as functions
"Vec2d", "Vec3d", "Vec2i", "Vec3i", "VecXT", "VecNd",
"Mat2d", "Mat3d", "Mat2i", "Mat3i", "MatXT",
"Quatd", "Quaterniond",
"Point_2", "Point_3", "Vector_2", "Vector_3",
"STLModel", "STLReader",
"TetMesh", "TriMesh",
"Shape", "Sphere", "Plane", "Cylinder", "Ellipsoid",
"Particle", "Wall", "ContactPP", "ContactPW", "BondEntry", "CollisionEntry",
"Domain", "Cell", "Scene", "Simulation", "DEMSolver", "Modifier",
"LevelSetFunction", "WSCVTSampler", "Voronoi", "CorkWrapper",
"PeriDigmSimulator", "FEMSimulator", "MFEMMembraneSolver", "MFEMSolidSolver",
"DeformableParticle", "MembraneWall", "Membrane",
"RegressionNet", "GeneralNet", "MLPNet",
"DragModel", "DragCoefModel", "DragForceModel",
"LinearSpring", "HertzMindlin", "ParallelBond", "VolumeBased",
"GJKSimplex", "ContactSolverFactory", "ContactModelFactory",
"CollisionSolverPP", "CollisionSolverPW", "BondSolverPP", "BondSolverPW",
"SolverGJKPP", "SolverGJKPW", "SolverSDFPP", "SolverSDFPW",
"SolverBooleanPP", "SolverBooleanPW", "SolverSpherePlane", "SolverSphereSphere",
"SolverSphereTriangle",
}
def _collect_cpp_classes(text: str) -> set[str]:
"""从 C++ 源码中提取 class/struct 定义名,加入跳过列表."""
classes: set[str] = set()
class_re = re.compile(
r"(?m)^\s*"
r"(?:template\s*<[^>]+>\s*)?"
r"(?:class|struct)\s+"
r"(?:[A-Za-z_][\w:]*\s*)?"
r"([A-Za-z_][A-Za-z0-9_]*)"
r"(?:\s*:\s*[^{;]+)?\s*[;{]"
)
for m in class_re.finditer(text):
classes.add(m.group(1))
return classes
def _cpp_needs_rename(name: str) -> bool:
"""C++ 专用的重命名判断,更保守."""
if not name or len(name) <= 1:
return False
if name.isupper():
return False
if name.startswith("__") and name.endswith("__"):
return False
return camel_to_snake(name) != name
def _find_cpp_candidates_in_file(path: Path, skip_names: set[str]) -> set[str]:
"""扫描单个 C++ 文件,找出可能是函数名的 camelCase/PascalCase 标识符."""
text = path.read_text(errors="replace")
found: set[str] = set()
# 把本文件定义的 class/struct 也加入跳过列表
local_skip = skip_names | _collect_cpp_classes(text)
# Pattern 1: 强信号 — 类作用域函数定义 ClassName::FuncName(
scope_func_re = re.compile(
r"(?<![A-Za-z0-9_])"
r"[A-Za-z_][A-Za-z0-9_]*\s*::\s*"
r"([A-Za-z_][A-Za-z0-9_]*)"
r"(?=\s*\()"
)
# Pattern 2: 普通函数定义/声明 — return_type funcName(args) {/;
# 使用负向前瞻,避免把 return/if/for/while 等关键字后的内容误判为函数
plain_func_re = re.compile(
r"(?m)"
r"(?:^|(?<=[;{}]))\s*"
r"(?:"
r"(?:inline|virtual|static|const|explicit|override|constexpr|friend|unsigned|signed|template|typename|struct|class|enum|union)\s+"
r")*"
r"(?!(?:return|if|for|while|switch|catch|sizeof|alignof|decltype|new|delete|throw|using|namespace|typedef)\b)"
r"(?:[A-Za-z_][\w:]*(?:\s*<[^;{]*>)?\s+)"
r"([A-Za-z_][A-Za-z0-9_]*)"
r"\s*\([^)]*\)\s*"
r"(?:const\s*|override\s*|final\s*|noexcept\s*|default\s*|delete\s*|->\s*[A-Za-z_][\w:]*\s*)*"
r"[;{]"
)
# Pattern 3: pybind .def("Name", ... 中暴露的 C++ 函数引用
pybind_ref_re = re.compile(
r'\.def\s*\(\s*"[^"]+"\s*,\s*'
r'(?:[^\(]*\()?'
r'\s*&?\s*'
r'(?:[A-Za-z_][A-Za-z0-9_]*\s*::\s*)*'
r'([A-Za-z_][A-Za-z0-9_]*)'
r"(?=\s*(?:\)|,))"
)
for line in text.split("\n"):
stripped = line.strip()
if not stripped or stripped.startswith("//") or stripped.startswith("#"):
continue
if stripped.startswith("*") or stripped.startswith("/*"):
continue
# --- Pattern 1: 类作用域函数 ---
for m in scope_func_re.finditer(line):
name = m.group(1)
if name in local_skip:
continue
# 跳过构造函数/析构函数(名字与类名相同)
class_part = line[m.start():m.start(1)].rstrip(": ")
class_name = class_part.split("::")[-1].strip() if "::" in class_part else class_part.strip()
if name == class_name or (name.startswith("~") and name[1:] == class_name):
continue
if _cpp_needs_rename(name):
found.add(name)
# --- Pattern 2: 普通函数定义/声明 ---
for m in plain_func_re.finditer(line):
name = m.group(1)
if name in local_skip:
continue
if _cpp_needs_rename(name):
found.add(name)
# --- Pattern 3: pybind 引用 ---
for m in pybind_ref_re.finditer(line):
name = m.group(1)
if name in local_skip:
continue
if _cpp_needs_rename(name):
found.add(name)
return found
def _rename_in_cpp_text(text: str, mapping: dict[str, str]) -> str:
"""在 C++ 文本中执行安全的整词替换."""
# 按长度降序,避免短名影响长名
for old, new in sorted(mapping.items(), key=lambda x: -len(x[0])):
text = re.sub(rf"\b{re.escape(old)}\b", new, text)
return text
def scan_cpp_candidates(root: Path) -> dict[str, str]:
"""扫描整个目录,生成 C++ 函数重命名映射."""
candidates: set[str] = set()
for path in root.rglob("*"):
if path.suffix in {".cpp", ".hpp", ".h", ".cxx", ".cc", ".hh"}:
candidates.update(_find_cpp_candidates_in_file(path, CPP_SKIP_NAMES))
return {name: camel_to_snake(name) for name in candidates}
def rename_cpp_files(root: Path, mapping: dict[str, str], dry_run: bool = True) -> int:
"""对目录下所有 C++ 文件应用重命名,返回改动文件数."""
if not mapping:
return 0
changed_files = 0
for path in root.rglob("*"):
if path.suffix in {".cpp", ".hpp", ".h", ".cxx", ".cc", ".hh"}:
text = path.read_text(errors="replace")
new_text = _rename_in_cpp_text(text, mapping)
if new_text != text:
changed_files += 1
action = "[DRY-RUN] Would update" if dry_run else "Updated"
print(f" {action} {path.relative_to(root.parent)}")
if not dry_run:
path.write_text(encoding="utf-8", data=new_text)
return changed_files
# ============================================================================
# 主程序
# ============================================================================
def main() -> int:
parser = argparse.ArgumentParser(
description="Rename functions in src/ to snake_case (Python + C++)"
)
parser.add_argument("src", help="Source directory to scan")
parser.add_argument("--dry-run", action="store_true", default=True, help="Preview changes")
parser.add_argument("--apply", action="store_true", help="Apply changes")
args = parser.parse_args()
root = Path(args.src).resolve()
if not root.is_dir():
print(f"Error: {root} is not a directory", file=sys.stderr)
return 1
dry_run = not args.apply
# ------------------------------------------------------------------
# Python
# ------------------------------------------------------------------
py_files = sorted(root.rglob("*.py"))
py_total_changes = 0
py_file_changes = 0
if py_files:
print("=" * 60)
print("Python files")
print("=" * 60)
for pyfile in py_files:
mapping = rename_python_file(pyfile, dry_run=dry_run)
if mapping:
py_total_changes += len(mapping)
py_file_changes += 1
action = "[DRY-RUN] Would rename" if dry_run else "Renamed"
print(f"{action} in {pyfile.relative_to(root.parent)}:")
for old, new in mapping.items():
print(f" {old} -> {new}")
# ------------------------------------------------------------------
# C++
# ------------------------------------------------------------------
cpp_mapping = scan_cpp_candidates(root)
cpp_file_changes = 0
if cpp_mapping:
print("=" * 60)
print("C++ files")
print("=" * 60)
print("Candidate functions:")
for old, new in sorted(cpp_mapping.items()):
print(f" {old} -> {new}")
print()
cpp_file_changes = rename_cpp_files(root, cpp_mapping, dry_run=dry_run)
# ------------------------------------------------------------------
# 汇总
# ------------------------------------------------------------------
print("=" * 60)
print("Summary")
print("=" * 60)
print(f"Python files changed: {py_file_changes}")
print(f"Python functions renamed: {py_total_changes}")
print(f"C++ files changed: {cpp_file_changes}")
print(f"C++ functions renamed: {len(cpp_mapping)}")
if dry_run and (py_total_changes or cpp_mapping):
print("\nRun with --apply to execute.")
if cpp_mapping:
print("\nNOTE: C++ renaming is regex-based heuristic. Please review changes carefully,")
print("especially for constructors, templates, macros, and pybind string literals.")
return 0
if __name__ == "__main__":
raise SystemExit(main())