236 lines
8.0 KiB
Python
236 lines
8.0 KiB
Python
"""
|
|
命名规范提取器
|
|
|
|
使用 AST 解析 Python 代码,分析类名、函数名、变量名的命名模式。
|
|
"""
|
|
import os
|
|
import re
|
|
import ast
|
|
from typing import Dict, List, Any, Optional
|
|
from pathlib import Path
|
|
from collections import Counter
|
|
|
|
|
|
class NamingExtractor:
|
|
"""命名规范提取器"""
|
|
|
|
def extract(self, project_path: str) -> Dict[str, Any]:
|
|
"""
|
|
提取项目的命名规范
|
|
|
|
Args:
|
|
project_path: 项目根目录路径
|
|
|
|
Returns:
|
|
命名规范信息字典
|
|
"""
|
|
# 收集所有 Python 文件中的命名
|
|
names = self._collect_names(project_path)
|
|
|
|
return {
|
|
"files": self._analyze_file_naming(project_path),
|
|
"classes": self._analyze_pattern(names["classes"], "classes"),
|
|
"functions": self._analyze_pattern(names["functions"], "functions"),
|
|
"variables": self._analyze_pattern(names["variables"], "variables"),
|
|
"constants": self._analyze_pattern(names["constants"], "constants"),
|
|
"summary": self._generate_summary(names)
|
|
}
|
|
|
|
def _find_python_files(self, project_path: str) -> List[str]:
|
|
"""查找所有 Python 文件"""
|
|
python_files = []
|
|
ignore_dirs = {'__pycache__', '.git', '.venv', 'venv', 'node_modules', '.pytest_cache'}
|
|
|
|
for root, dirs, files in os.walk(project_path):
|
|
# 过滤忽略的目录
|
|
dirs[:] = [d for d in dirs if d not in ignore_dirs]
|
|
|
|
for f in files:
|
|
if f.endswith('.py'):
|
|
python_files.append(os.path.join(root, f))
|
|
|
|
return python_files
|
|
|
|
def _collect_names(self, project_path: str) -> Dict[str, List[str]]:
|
|
"""使用 AST 收集代码中的命名"""
|
|
names = {
|
|
"classes": [],
|
|
"functions": [],
|
|
"variables": [],
|
|
"constants": []
|
|
}
|
|
|
|
for py_file in self._find_python_files(project_path):
|
|
try:
|
|
with open(py_file, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
tree = ast.parse(content)
|
|
|
|
for node in ast.walk(tree):
|
|
# 类名
|
|
if isinstance(node, ast.ClassDef):
|
|
names["classes"].append(node.name)
|
|
|
|
# 函数名(包括方法)
|
|
elif isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
|
|
# 排除魔术方法
|
|
if not (node.name.startswith('__') and node.name.endswith('__')):
|
|
names["functions"].append(node.name)
|
|
|
|
# 变量和常量(模块级别的赋值)
|
|
elif isinstance(node, ast.Assign):
|
|
for target in node.targets:
|
|
if isinstance(target, ast.Name):
|
|
name = target.id
|
|
# 排除私有变量
|
|
if not name.startswith('_'):
|
|
# 全大写视为常量
|
|
if name.isupper() or (name.upper() == name and '_' in name):
|
|
names["constants"].append(name)
|
|
else:
|
|
names["variables"].append(name)
|
|
except Exception:
|
|
continue
|
|
|
|
return names
|
|
|
|
def _detect_naming_pattern(self, name: str) -> Optional[str]:
|
|
"""检测单个名称的命名模式"""
|
|
if not name:
|
|
return None
|
|
|
|
# UPPER_SNAKE_CASE (常量)
|
|
if re.match(r'^[A-Z][A-Z0-9_]*$', name):
|
|
return "UPPER_SNAKE_CASE"
|
|
|
|
# PascalCase (类名)
|
|
if re.match(r'^[A-Z][a-zA-Z0-9]*$', name):
|
|
return "PascalCase"
|
|
|
|
# snake_case
|
|
if re.match(r'^[a-z][a-z0-9_]*$', name):
|
|
return "snake_case"
|
|
|
|
# camelCase
|
|
if re.match(r'^[a-z][a-zA-Z0-9]*$', name):
|
|
return "camelCase"
|
|
|
|
# kebab-case (通常用于文件名)
|
|
if re.match(r'^[a-z][a-z0-9-]*$', name):
|
|
return "kebab-case"
|
|
|
|
return "mixed"
|
|
|
|
def _analyze_pattern(self, names: List[str], category: str) -> Dict[str, Any]:
|
|
"""分析命名模式"""
|
|
if not names:
|
|
return {
|
|
"dominant_pattern": None,
|
|
"distribution": {},
|
|
"examples": [],
|
|
"consistency": 0.0
|
|
}
|
|
|
|
# 统计各模式的数量
|
|
patterns = Counter()
|
|
for name in names:
|
|
pattern = self._detect_naming_pattern(name)
|
|
if pattern:
|
|
patterns[pattern] += 1
|
|
|
|
if not patterns:
|
|
return {
|
|
"dominant_pattern": None,
|
|
"distribution": {},
|
|
"examples": names[:5],
|
|
"consistency": 0.0
|
|
}
|
|
|
|
# 找出主导模式
|
|
dominant = patterns.most_common(1)[0][0]
|
|
total = sum(patterns.values())
|
|
consistency = patterns[dominant] / total if total > 0 else 0.0
|
|
|
|
# 获取示例
|
|
examples = []
|
|
for name in names:
|
|
if self._detect_naming_pattern(name) == dominant and name not in examples:
|
|
examples.append(name)
|
|
if len(examples) >= 5:
|
|
break
|
|
|
|
return {
|
|
"dominant_pattern": dominant,
|
|
"distribution": dict(patterns),
|
|
"examples": examples,
|
|
"consistency": round(consistency, 2)
|
|
}
|
|
|
|
def _analyze_file_naming(self, project_path: str) -> Dict[str, Any]:
|
|
"""分析文件命名规范"""
|
|
python_files = []
|
|
ignore_dirs = {'__pycache__', '.git', '.venv', 'venv', 'node_modules'}
|
|
|
|
for root, dirs, files in os.walk(project_path):
|
|
dirs[:] = [d for d in dirs if d not in ignore_dirs]
|
|
|
|
for f in files:
|
|
if f.endswith('.py') and not f.startswith('__'):
|
|
# 去掉扩展名
|
|
name = f[:-3]
|
|
python_files.append(name)
|
|
|
|
if not python_files:
|
|
return {
|
|
"dominant_pattern": None,
|
|
"examples": [],
|
|
"consistency": 0.0
|
|
}
|
|
|
|
# 分析文件名模式
|
|
patterns = Counter()
|
|
for name in python_files:
|
|
pattern = self._detect_naming_pattern(name)
|
|
if pattern:
|
|
patterns[pattern] += 1
|
|
|
|
if not patterns:
|
|
return {
|
|
"dominant_pattern": None,
|
|
"examples": python_files[:5],
|
|
"consistency": 0.0
|
|
}
|
|
|
|
dominant = patterns.most_common(1)[0][0]
|
|
total = sum(patterns.values())
|
|
consistency = patterns[dominant] / total if total > 0 else 0.0
|
|
|
|
# 获取示例(加回 .py 扩展名)
|
|
examples = [f"{name}.py" for name in python_files[:5]]
|
|
|
|
return {
|
|
"dominant_pattern": dominant,
|
|
"examples": examples,
|
|
"consistency": round(consistency, 2)
|
|
}
|
|
|
|
def _generate_summary(self, names: Dict[str, List[str]]) -> Dict[str, str]:
|
|
"""生成命名规范摘要"""
|
|
summary = {}
|
|
|
|
# 分析各类别的主导模式
|
|
for category, name_list in names.items():
|
|
if name_list:
|
|
patterns = Counter()
|
|
for name in name_list:
|
|
pattern = self._detect_naming_pattern(name)
|
|
if pattern:
|
|
patterns[pattern] += 1
|
|
|
|
if patterns:
|
|
dominant = patterns.most_common(1)[0][0]
|
|
summary[category] = dominant
|
|
|
|
return summary
|