Добавить codemod дедупликации summary и уточнить бэклог
scripts/dedup_summary_inheritdoc.py (dry-run не нашёл простых дублей summary в продакшне — реализации уже на <inheritdoc/>); закрыт TD-SETTINGS-UI, DEFERRED с обоснованием TD-CARD-MERGE/TD-VIRT/TD-STORE-ATTACH/TD-SOURCE-CONTACTS; игнор __pycache__.
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Заменить дублирующий <summary> реализации на <inheritdoc/> (код-стайл Дейла).
|
||||
|
||||
Консервативно:
|
||||
* собираются тексты <summary> членов интерфейсов (блоки, состоящие только из <summary>);
|
||||
* в типах с интерфейсной базой такой же по тексту блок-док заменяется на `/// <inheritdoc />`;
|
||||
* блоки с <param>/<returns>/<remarks>/... и summary самих типов не трогаются.
|
||||
|
||||
Запуск: python scripts/dedup_summary_inheritdoc.py [--apply]
|
||||
Без --apply — dry-run со счётчиками.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
ROOTS = [
|
||||
REPO / "src" / "core",
|
||||
REPO / "src" / "telegram-service",
|
||||
REPO / "src" / "ai-service",
|
||||
REPO / "src" / "ml-service",
|
||||
REPO / "src" / "storage-service",
|
||||
REPO / "src" / "grpc-hosting",
|
||||
]
|
||||
|
||||
INTERFACE_RE = re.compile(
|
||||
r"^\s*(?:public|internal|protected|private)?\s*"
|
||||
r"(?:static\s+|sealed\s+|abstract\s+|partial\s+|unsafe\s+)*interface\s+\w+"
|
||||
)
|
||||
TYPE_BASE_RE = re.compile(
|
||||
r"^\s*(?:public|internal|protected|private)?\s*"
|
||||
r"(?:static\s+|sealed\s+|abstract\s+|partial\s+|unsafe\s+)*"
|
||||
r"(?:class|struct|record)\s+\w+[^{;]*:\s*(.+?)(?:\{|$)"
|
||||
)
|
||||
IFACE_NAME_RE = re.compile(r"\bI[A-Z]\w*")
|
||||
EXTRA_TAG_RE = re.compile(r"<(?:param|typeparam|returns|remarks|exception|example|value)\b", re.IGNORECASE)
|
||||
SUMMARY_OPEN_RE = re.compile(r"<summary>", re.IGNORECASE)
|
||||
SUMMARY_CLOSE_RE = re.compile(r"</summary>", re.IGNORECASE)
|
||||
|
||||
|
||||
def iter_files() -> list[Path]:
|
||||
files: list[Path] = []
|
||||
for root in ROOTS:
|
||||
if not root.exists():
|
||||
continue
|
||||
for path in root.rglob("*.cs"):
|
||||
parts = set(path.parts)
|
||||
if "obj" in parts or "bin" in parts:
|
||||
continue
|
||||
if "tests" in path.as_posix().lower():
|
||||
continue
|
||||
files.append(path)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def normalize(block: list[str]) -> str:
|
||||
text = " ".join(line.strip().lstrip("/").strip() for line in block)
|
||||
text = SUMMARY_CLOSE_RE.sub("", SUMMARY_OPEN_RE.sub("", text))
|
||||
return re.sub(r"\s+", " ", text).strip()
|
||||
|
||||
|
||||
def is_summary_only(block: list[str]) -> bool:
|
||||
joined = "\n".join(block)
|
||||
return (
|
||||
bool(SUMMARY_OPEN_RE.search(joined))
|
||||
and bool(SUMMARY_CLOSE_RE.search(joined))
|
||||
and not EXTRA_TAG_RE.search(joined)
|
||||
)
|
||||
|
||||
|
||||
def depth_snapshot(lines: list[str]) -> list[int]:
|
||||
"""Глубина фигурных скобок перед каждой строкой (приблизительно, без строк/комментариев)."""
|
||||
depths: list[int] = []
|
||||
depth = 0
|
||||
for line in lines:
|
||||
depths.append(depth)
|
||||
depth += line.count("{") - line.count("}")
|
||||
return depths
|
||||
|
||||
|
||||
def type_scopes(lines: list[str]):
|
||||
"""Список (start_line, end_line, kind) для интерфейсов и типов с интерфейсной базой.
|
||||
|
||||
kind: "interface" | "type". Границы — по фигурным скобкам объявления.
|
||||
"""
|
||||
depths = depth_snapshot(lines)
|
||||
scopes: list[tuple[int, int, str]] = []
|
||||
stack: list[tuple[int, str, int]] = [] # (depth, kind, start_line)
|
||||
for idx, line in enumerate(lines):
|
||||
depth_before = depths[idx]
|
||||
if INTERFACE_RE.match(line):
|
||||
stack.append((depth_before, "interface", idx))
|
||||
else:
|
||||
m = TYPE_BASE_RE.match(line)
|
||||
if m and IFACE_NAME_RE.search(m.group(1)):
|
||||
stack.append((depth_before, "type", idx))
|
||||
# закрыть блоки на строке закрывающей скобки (глубина после неё возвращается к уровню объявления)
|
||||
while (
|
||||
stack
|
||||
and depths[idx] <= stack[-1][0]
|
||||
and lines[idx].strip().startswith("}")
|
||||
and idx > stack[-1][2]
|
||||
):
|
||||
depth, kind, start = stack.pop()
|
||||
scopes.append((start, idx, kind))
|
||||
for depth, kind, start in stack:
|
||||
scopes.append((start, len(lines) - 1, kind))
|
||||
return scopes
|
||||
|
||||
|
||||
def containing_kinds(scope_kinds: list[tuple[int, int, str]], line_idx: int, depth: int) -> set[str]:
|
||||
kinds: set[str] = set()
|
||||
for start, end, kind in scope_kinds:
|
||||
if start <= line_idx <= end and start != line_idx:
|
||||
kinds.add(kind)
|
||||
return kinds
|
||||
|
||||
|
||||
def collect_interface_summaries(files: list[Path]) -> set[str]:
|
||||
summaries: set[str] = set()
|
||||
for path in files:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
depths = depth_snapshot(lines)
|
||||
scopes = type_scopes(lines)
|
||||
idx = 0
|
||||
while idx < len(lines):
|
||||
if not lines[idx].lstrip().startswith("///"):
|
||||
idx += 1
|
||||
continue
|
||||
start = idx
|
||||
while idx < len(lines) and lines[idx].lstrip().startswith("///"):
|
||||
idx += 1
|
||||
block = lines[start:idx]
|
||||
kinds = containing_kinds(scopes, start, depths[start])
|
||||
if "interface" in kinds and is_summary_only(block):
|
||||
summaries.add(normalize(block))
|
||||
return summaries
|
||||
|
||||
|
||||
def process_file(path: Path, iface_summaries: set[str]) -> tuple[int, list[str]]:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
depths = depth_snapshot(lines)
|
||||
scopes = type_scopes(lines)
|
||||
result: list[str] = []
|
||||
replaced = 0
|
||||
idx = 0
|
||||
while idx < len(lines):
|
||||
if not lines[idx].lstrip().startswith("///"):
|
||||
result.append(lines[idx])
|
||||
idx += 1
|
||||
continue
|
||||
start = idx
|
||||
while idx < len(lines) and lines[idx].lstrip().startswith("///"):
|
||||
idx += 1
|
||||
block = lines[start:idx]
|
||||
kinds = containing_kinds(scopes, start, depths[start])
|
||||
if "type" in kinds and is_summary_only(block) and normalize(block) in iface_summaries:
|
||||
indent = block[0][: len(block[0]) - len(block[0].lstrip())]
|
||||
result.append(f"{indent}/// <inheritdoc />")
|
||||
replaced += 1
|
||||
else:
|
||||
result.extend(block)
|
||||
return replaced, result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
apply = "--apply" in sys.argv
|
||||
files = iter_files()
|
||||
iface_summaries = collect_interface_summaries(files)
|
||||
total = 0
|
||||
changed = 0
|
||||
for path in files:
|
||||
replaced, new_lines = process_file(path, iface_summaries)
|
||||
if not replaced:
|
||||
continue
|
||||
total += replaced
|
||||
changed += 1
|
||||
if apply:
|
||||
original = path.read_text(encoding="utf-8")
|
||||
text = "\n".join(new_lines) + ("\n" if original.endswith("\n") else "")
|
||||
path.write_text(text, encoding="utf-8")
|
||||
mode = "применено" if apply else "dry-run"
|
||||
print(f"{mode}: замен {total} в {changed} файлах (интерфейсных summary: {len(iface_summaries)})")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user