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.
This commit is contained in:
@@ -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"(?<![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())
|
||||
Reference in New Issue
Block a user