/
ump-team
/
ump-infra
Обзор
Документация
Войти
/
ump-team
/
ump-infra
Код
Запросы
1
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
dev/test
scripts/clean_audit_comments.py
266 строк
12 KB
Dmitry Kochenov
init v0.0.2
19 июл 2026, 19:11
19 июл 2026, 19:11
13d6d0b
Код
Авторство
О чём код?
#!/usr/bin/env python3 """ clean_audit_comments.py — безопасная очистка audit-комментариев. Принципы (после нескольких итераций): 1. НИКОГДА не удалять префикс `# ` у комментариев — иначе остаётся голый текст 2. Для комментариев: либо удаляем ВСЮ строку (с \n), либо заменяем audit-часть на пустоту 3. Для inline: удаляем только `# ...` часть, код остаётся 4. Для markdown blockquote: удаляем всю строку 5. Для parenthetical: удаляем скобки целиком 6. Safety check: компилируем .py перед записью, откатываем при SyntaxError Запуск: python3 scripts/clean_audit_comments.py # очистка python3 scripts/clean_audit_comments.py --check # только подсчёт остатков """ from __future__ import annotations import argparse import re import sys from pathlib import Path # ─── Паттерны (БЕЗОПАСНЫЕ) ─────────────────────────────────────────────────── # # Каждый паттерн либо: # (a) удаляет ВСЮ строку целиком (с \n) — для комментариев/blockquotes # (b) удаляет только inline-часть — для скобок, FIX-V токенов # (c) заменяет audit-фразу на пустоту, сохраняя синтаксис # # КРИТИЧНО: паттерны типа `^(\s*#\s*)[Аа]удит v\d+:\s*` ЗАПРЕЩЕНЫ — они удаляют # `# ` и оставляют голый текст. Вместо этого используем: # - `^\s*#\s*[Аа]удит v\d+[^\n]*\n` (вся строка) # - ИЛИ `(?<=# )Аудит v\d+:\s*` (сохраняем #, удаляем только audit-фразу) PATTERNS = [ # ─── (a) Полные строки-комментарии: удаляем целиком с \n ─── # Python/YAML комментарий: "# Аудит v35: ..." или "# аудит v36: ..." (вся строка) re.compile(r'^[ \t]*#[ \t]*[Аа]удит v\d+[^\n]*\n', re.MULTILINE), re.compile(r'^[ \t]*#[ \t]*[Aa]udit v\d+[^\n]*\n', re.MULTILINE), # Нумерованные комментарии: "# 1.5 Аудит v21: ..." или "# 2. Аудит v22: ..." re.compile(r'^[ \t]*#[ \t]*\d+\.?\d*[ \t]+[Аа]удит v\d+[^\n]*\n', re.MULTILINE), # Markdown blockquote: "> **Аудит v35:** ..." или "> Аудит v36: ..." re.compile(r'^>[ \t]*\*\*[Аа]удит v\d+[^\n]*\n', re.MULTILINE), re.compile(r'^>[ \t]*[Аа]удит v\d+[^\n]*\n', re.MULTILINE), # "> **Основание:** Аудит v41 — ..." re.compile(r'^>[ \t]*\*\*Основание:\*\*[ \t]*[Аа]удит v\d+[^\n]*\n', re.MULTILINE), # Markdown строка, начинающаяся с "Аудит vXX": "Аудит v35: ..." или "Аудит v36 (date): ..." re.compile(r'^[Аа]удит v\d+[^\n]*\n', re.MULTILINE), # ─── (b) Inline-части: удаляем только audit-фрагмент ─── # Inline YAML/Python комментарий в конце строки: " # Аудит v35: текст" # Удаляем весь комментарий (от # до конца строки), сохраняя код re.compile(r'[ \t]+#[ \t]*[Аа]удит v\d+[^\n]*', re.MULTILINE), re.compile(r'[ \t]+#[ \t]*[Aa]udit v\d+[^\n]*', re.MULTILINE), # Parenthetical: "(аудит v35, требование №5)" или "(audit v36)" — удаляем скобки целиком re.compile(r'[ \t]*\([Аа]удит v\d+[^\)]*\)', re.IGNORECASE), re.compile(r'[ \t]*\([Aa]udit v\d+[^\)]*\)', re.IGNORECASE), # FIX-V токены: "FIX-V25-3.2a", "FIX-V24-3.11" — удаляем токен re.compile(r'FIX-V\d+[-.]?\d*[-.]?\d*[a-z]?', re.IGNORECASE), # ─── (c) Замена audit-фразы на пустоту (сохраняя структуру) ─── # " — Аудит v35: текст" в конце строки (em-dash + аудит) re.compile(r'[ \t]+—[ \t]+[Аа]удит v\d+[^\n]*', re.MULTILINE), re.compile(r'[ \t]+-[ \t]+[Аа]удит v\d+[^\n]*', re.MULTILINE), # "Аудит v35+v36:" комбинированные версии re.compile(r'[Аа]удит v\d+\+v\d+:[ \t]*', re.MULTILINE), re.compile(r'[Аа]удит v\d+\+v\d+[ \t]*', re.MULTILINE), # "Аудит v36 (Fix A):" — удаляем audit-метку, оставляем текст после re.compile(r'[Аа]удит v\d+[ \t]*\([^)]+\):[ \t]*', re.MULTILINE), # ", аудит v35" в конце строки перед точкой/запятой re.compile(r',[ \t]*[аa]удит v\d+(?=[\n.,;)])', re.IGNORECASE), # Python help='Аудит v21: текст' → help='' re.compile(r"help='[Аа]удит v\d+:[^']*'", re.IGNORECASE), re.compile(r'help="[Аа]удит v\d+:[^"]*"', re.IGNORECASE), # "(rev v26.1, аудит v30 §3.2):" → "" re.compile(r'\(rev v\d+\.?\d*,[ \t]*[аa]удит v\d+[^\)]*\)', re.IGNORECASE), re.compile(r'\(rev v\d+\.?\d*\)', re.IGNORECASE), # "(v36+v37 термины)" → "" re.compile(r'\(v\d+\+v\d+[^\)]*\)', re.IGNORECASE), # "См. audit v7." → "" re.compile(r'См\.[ \t]*[аa]udit v\d+\.', re.IGNORECASE), # "аудит v29 применён" — фраза целиком re.compile(r'[аa]удит v\d+[ \t]+применён[^\n]*', re.IGNORECASE), ] # ─── Файлы для обработки ───────────────────────────────────────────────────── EXTENSIONS = {'.py', '.md', '.yaml', '.yml', '.sh'} SKIP_DIRS = {'_design', '.git', 'node_modules', '__pycache__', '.venv', '.pytest_cache'} SKIP_FILES = { 'worklog.md', 'clean_audit_comments.py', 'CHANGELOG.md', # содержит историю с упоминаниями audit } def clean_text(text: str) -> tuple[str, int]: """Применить все паттерны. Возвращает (new_text, total_replacements).""" total = 0 for pattern in PATTERNS: new_text, n = pattern.subn('', text) if n: total += n text = new_text # Пост-обработка: удалить пустые строки, появившиеся после удаления комментариев # (только если перед нами была строка-комментарий) if total > 0: lines = text.split('\n') cleaned_lines = [] prev_was_blank = False for line in lines: is_blank = line.strip() == '' # Не допускаем 3+ подряд пустых строк if is_blank and prev_was_blank: continue cleaned_lines.append(line) prev_was_blank = is_blank text = '\n'.join(cleaned_lines) return text, total def process_file(path: Path, *, dry_run: bool = False) -> tuple[int, int]: """Обработать один файл. Возвращает (replacements, remaining_markers).""" if path.name in SKIP_FILES: return 0, 0 try: original = path.read_text(encoding='utf-8') except Exception as e: print(f' ⚠ {path}: {e}') return 0, 0 new_text, count = clean_text(original) if count == 0 or new_text == original: # Подсчитаем остатки remaining = sum(len(p.findall(original)) for p in PATTERNS) return 0, remaining # Safety check для .py файлов if path.suffix == '.py' and not dry_run: try: compile(new_text, str(path), 'exec') except SyntaxError as e: print(f' ⚠ {path}: SYNTAX ERROR после очистки (line {e.lineno}): {e.msg}') print(' Откат. Файл не изменён.') remaining = sum(len(p.findall(original)) for p in PATTERNS) return 0, remaining if not dry_run: path.write_text(new_text, encoding='utf-8') # Подсчитаем остатки в новом тексте remaining = sum(len(p.findall(new_text)) for p in PATTERNS) return count, remaining def count_all_markers(base: Path) -> int: """Подсчитать все audit-маркеры в проекте.""" total = 0 for path in base.rglob('*'): if not path.is_file() or path.suffix not in EXTENSIONS: continue if any(part in SKIP_DIRS for part in path.parts): continue if path.name in SKIP_FILES: continue try: text = path.read_text(encoding='utf-8') for pattern in PATTERNS: total += len(pattern.findall(text)) except (OSError, UnicodeDecodeError): # P1-15 fix: точечный except вместо Exception — ловит только # файловые ошибки (FileNotFoundError, PermissionError) и # проблемы с кодировкой. pass return total def main() -> int: parser = argparse.ArgumentParser(description='Очистка audit-комментариев') parser.add_argument('--check', action='store_true', help='Только подсчёт остатков') parser.add_argument('--dry-run', action='store_true', help='Не записывать изменения') args = parser.parse_args() base = Path(__file__).resolve().parent.parent if args.check: total = count_all_markers(base) print(f'Остаток audit-маркеров: {total}') return 0 if total == 0 else 1 print(f'Очистка audit-комментариев в {base}...\n') total_files = 0 total_replacements = 0 total_remaining = 0 for path in sorted(base.rglob('*')): if not path.is_file() or path.suffix not in EXTENSIONS: continue if any(part in SKIP_DIRS for part in path.parts): continue if path.name in SKIP_FILES: continue replacements, remaining = process_file(path, dry_run=args.dry_run) if replacements > 0: total_files += 1 total_replacements += replacements print(f' ✓ {path}: {replacements} замен' + (f' ({remaining} осталось)' if remaining else '')) elif remaining > 0: total_remaining += remaining print(f'\nИтого: {total_replacements} замен в {total_files} файлах') if total_remaining > 0: print(f'Осталось неочищенными: {total_remaining} маркеров (требуют ручной правки)') # Финальная проверка final = count_all_markers(base) if final > 0: print(f'\nФинальный подсчёт: {final} маркеров осталось') # Показать где for path in sorted(base.rglob('*')): if not path.is_file() or path.suffix not in EXTENSIONS: continue if any(part in SKIP_DIRS for part in path.parts): continue if path.name in SKIP_FILES: continue try: text = path.read_text(encoding='utf-8') count = sum(len(p.findall(text)) for p in PATTERNS) if count > 0: print(f' {path}: {count}') except (OSError, UnicodeDecodeError): # P1-15 fix: точечный except вместо Exception. pass return 1 print('\n✅ Все audit-маркеры удалены') return 0 if __name__ == '__main__': sys.exit(main())