From e8320b17987a9dc0ae8eb69f67dfa0f9bf26259f Mon Sep 17 00:00:00 2001 From: Zhengshou Lai Date: Fri, 17 Apr 2026 00:02:50 +0800 Subject: [PATCH] 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. --- scripts/rename_funcs_to_snake.py | 413 +++++++++++++++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 scripts/rename_funcs_to_snake.py diff --git a/scripts/rename_funcs_to_snake.py b/scripts/rename_funcs_to_snake.py new file mode 100644 index 0000000..b56167d --- /dev/null +++ b/scripts/rename_funcs_to_snake.py @@ -0,0 +1,413 @@ +#!/usr/bin/env python3 +"""批量将 src 中的函数名重命名为 snake_case(Python 用 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"(?)?\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())