mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
refactor(scripts): 将 C# 命名验证脚本从 Python 重写为 Bash
- 将验证逻辑从 Python 代码转换为 Bash 脚本实现 - 使用 grep 和正则表达式替换 Python 的字符串匹配功能 - 实现 PascalCase 验证的正则表达式模式 - 添加目录路径和命名空间段的验证逻辑 - 保持原有的排除规则和错误报告格式 - 移除对 Python3 的依赖,仅使用系统内置命令
This commit is contained in:
parent
cb0d0682b0
commit
10640f1c73
@ -6,137 +6,143 @@ if ! command -v git >/dev/null 2>&1; then
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "python3 is required to validate C# naming conventions." >&2
|
||||
if ! command -v grep >/dev/null 2>&1; then
|
||||
echo "grep is required to validate C# naming conventions." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel)"
|
||||
cd "$repo_root"
|
||||
|
||||
python3 - <<'PY'
|
||||
from __future__ import annotations
|
||||
readonly PASCAL_CASE_REGEX='^(?:[A-Z](?=[A-Z][a-z0-9])|[A-Z]{2}(?=$|[A-Z][a-z0-9])|[A-Z][a-z0-9]+)+$'
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
files_checked=0
|
||||
declare -a namespace_violations=()
|
||||
declare -a directory_violations=()
|
||||
declare -A seen_directories=()
|
||||
declare -A seen_directory_violations=()
|
||||
|
||||
ROOT = Path.cwd()
|
||||
EXCLUDED_PREFIXES = (
|
||||
"Godot/script_templates/",
|
||||
)
|
||||
is_excluded() {
|
||||
local path="$1"
|
||||
case "$path" in
|
||||
Godot/script_templates|Godot/script_templates/*)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
NAMESPACE_PATTERN = re.compile(r"^\s*namespace\s+([A-Za-z][A-Za-z0-9_.]*)\s*(?:[;{]|$)")
|
||||
SEGMENT_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9]*$")
|
||||
PASCAL_CASE_PATTERN = re.compile(
|
||||
r"^(?:[A-Z](?=[A-Z][a-z0-9])|[A-Z]{2}(?=$|[A-Z][a-z0-9])|[A-Z][a-z0-9]+)+$"
|
||||
)
|
||||
validate_segment() {
|
||||
local segment="$1"
|
||||
|
||||
if [[ ! "$segment" =~ ^[A-Za-z][A-Za-z0-9]*$ ]]; then
|
||||
printf '%s' "must start with a letter and contain only letters or digits"
|
||||
return 1
|
||||
fi
|
||||
|
||||
def is_excluded(path: str) -> bool:
|
||||
normalized = path.strip("./")
|
||||
return any(
|
||||
normalized == prefix.rstrip("/") or normalized.startswith(prefix)
|
||||
for prefix in EXCLUDED_PREFIXES
|
||||
if [[ ! "$segment" =~ ^[A-Z] ]]; then
|
||||
printf '%s' "must start with an uppercase letter"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$segment" =~ ^[A-Z]+$ ]]; then
|
||||
if (( ${#segment} <= 2 )); then
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s' "acronyms longer than 2 letters must use PascalCase"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! printf '%s\n' "$segment" | grep -Pq "$PASCAL_CASE_REGEX"; then
|
||||
printf '%s' "must use PascalCase; only 2-letter acronyms may stay fully uppercase"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
check_directory_path() {
|
||||
local relative_dir="$1"
|
||||
local raw_segment=""
|
||||
local segment=""
|
||||
local reason=""
|
||||
local key=""
|
||||
|
||||
IFS='/' read -r -a raw_segments <<< "$relative_dir"
|
||||
for raw_segment in "${raw_segments[@]}"; do
|
||||
IFS='.' read -r -a segments <<< "$raw_segment"
|
||||
for segment in "${segments[@]}"; do
|
||||
if ! reason="$(validate_segment "$segment")"; then
|
||||
key="$relative_dir|$segment|$reason"
|
||||
if [[ -z "${seen_directory_violations[$key]:-}" ]]; then
|
||||
seen_directory_violations["$key"]=1
|
||||
directory_violations+=("- $relative_dir -> \"$segment\": $reason")
|
||||
fi
|
||||
|
||||
return
|
||||
fi
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
while IFS= read -r relative_file; do
|
||||
if [[ -z "$relative_file" ]] || is_excluded "$relative_file"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
((files_checked += 1))
|
||||
|
||||
while IFS=: read -r line_number namespace; do
|
||||
[[ -z "$line_number" ]] && continue
|
||||
|
||||
IFS='.' read -r -a segments <<< "$namespace"
|
||||
errors=()
|
||||
for segment in "${segments[@]}"; do
|
||||
if ! reason="$(validate_segment "$segment")"; then
|
||||
errors+=(" * $segment: $reason")
|
||||
fi
|
||||
done
|
||||
|
||||
if (( ${#errors[@]} > 0 )); then
|
||||
namespace_violations+=("- $relative_file:$line_number -> $namespace")
|
||||
namespace_violations+=("${errors[@]}")
|
||||
fi
|
||||
done < <(
|
||||
sed '1s/^\xEF\xBB\xBF//' "$relative_file" |
|
||||
grep -nE '^[[:space:]]*namespace[[:space:]]+[A-Za-z][A-Za-z0-9_.]*[[:space:]]*([;{]|$)' |
|
||||
sed -E 's/^([0-9]+):[[:space:]]*namespace[[:space:]]+([^[:space:];{]+).*/\1:\2/'
|
||||
)
|
||||
|
||||
current_dir="$(dirname "$relative_file")"
|
||||
while [[ "$current_dir" != "." ]]; do
|
||||
if [[ -z "${seen_directories[$current_dir]:-}" ]]; then
|
||||
seen_directories["$current_dir"]=1
|
||||
check_directory_path "$current_dir"
|
||||
fi
|
||||
|
||||
def validate_segment(segment: str) -> str | None:
|
||||
if not SEGMENT_PATTERN.fullmatch(segment):
|
||||
return "must start with a letter and contain only letters or digits"
|
||||
current_dir="$(dirname "$current_dir")"
|
||||
done
|
||||
done < <(git ls-files -- '*.cs')
|
||||
|
||||
if not segment[0].isupper():
|
||||
return "must start with an uppercase letter"
|
||||
if (( ${#namespace_violations[@]} > 0 || ${#directory_violations[@]} > 0 )); then
|
||||
echo "C# naming validation failed."
|
||||
|
||||
if segment.isupper():
|
||||
if len(segment) <= 2:
|
||||
return None
|
||||
return "acronyms longer than 2 letters must use PascalCase"
|
||||
if (( ${#namespace_violations[@]} > 0 )); then
|
||||
echo
|
||||
echo "Namespace violations:"
|
||||
printf '%s\n' "${namespace_violations[@]}"
|
||||
fi
|
||||
|
||||
if not PASCAL_CASE_PATTERN.fullmatch(segment):
|
||||
return "must use PascalCase; only 2-letter acronyms may stay fully uppercase"
|
||||
if (( ${#directory_violations[@]} > 0 )); then
|
||||
echo
|
||||
echo "Directory violations:"
|
||||
printf '%s\n' "${directory_violations[@]}"
|
||||
fi
|
||||
|
||||
return None
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
def tracked_csharp_files() -> list[str]:
|
||||
result = subprocess.run(
|
||||
["git", "ls-files", "--", "*.cs"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return [
|
||||
line
|
||||
for line in result.stdout.splitlines()
|
||||
if line and not is_excluded(line)
|
||||
]
|
||||
|
||||
|
||||
def read_text(path: Path) -> str:
|
||||
try:
|
||||
return path.read_text(encoding="utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
files = tracked_csharp_files()
|
||||
namespace_violations: list[tuple[str, int, str, list[str]]] = []
|
||||
directory_violations: list[tuple[str, str, str]] = []
|
||||
seen_directories: set[str] = set()
|
||||
|
||||
for relative_file in files:
|
||||
file_path = ROOT / relative_file
|
||||
for line_number, line in enumerate(read_text(file_path).splitlines(), start=1):
|
||||
match = NAMESPACE_PATTERN.match(line)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
namespace = match.group(1)
|
||||
segment_errors = []
|
||||
for segment in namespace.split("."):
|
||||
reason = validate_segment(segment)
|
||||
if reason is not None:
|
||||
segment_errors.append(f'{segment}: {reason}')
|
||||
|
||||
if segment_errors:
|
||||
namespace_violations.append((relative_file, line_number, namespace, segment_errors))
|
||||
|
||||
parent = Path(relative_file).parent
|
||||
while parent != Path("."):
|
||||
relative_dir = parent.as_posix()
|
||||
if relative_dir not in seen_directories:
|
||||
seen_directories.add(relative_dir)
|
||||
for raw_segment in relative_dir.split("/"):
|
||||
segments = raw_segment.split(".")
|
||||
for segment in segments:
|
||||
reason = validate_segment(segment)
|
||||
if reason is not None:
|
||||
directory_violations.append((relative_dir, segment, reason))
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
|
||||
parent = parent.parent
|
||||
|
||||
if namespace_violations or directory_violations:
|
||||
print("C# naming validation failed.")
|
||||
|
||||
if namespace_violations:
|
||||
print("\nNamespace violations:")
|
||||
for file_path, line_number, namespace, errors in namespace_violations:
|
||||
print(f"- {file_path}:{line_number} -> {namespace}")
|
||||
for error in errors:
|
||||
print(f" * {error}")
|
||||
|
||||
if directory_violations:
|
||||
print("\nDirectory violations:")
|
||||
for directory_path, segment, reason in sorted(set(directory_violations)):
|
||||
print(f'- {directory_path} -> "{segment}": {reason}')
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
print(f"C# naming validation passed for {len(files)} tracked C# files.")
|
||||
PY
|
||||
echo "C# naming validation passed for $files_checked tracked C# files."
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user