4jcraft/scripts/sweep_linuxgame_includes.py
2026-04-09 15:24:13 +10:00

109 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Conservative sweep: remove dead app/linux/LinuxGame.h includes from minecraft/.
LinuxGame.h is included by 155 files in targets/minecraft/ but most of
those are leftover from before the IGameServices refactor. The only real
reasons to include it are:
- to use the global `LinuxGame app;` extern
- to use the LinuxGame class type by name
- to use C4JStringTable (forward-declared in the header)
For each minecraft/ file that includes LinuxGame.h, this script checks
whether the file references any of: LinuxGame, C4JStringTable, or
contains `app.`/`app::`. If none of those appear, the include is dead
and removed.
Usage:
python3 scripts/sweep_linuxgame_includes.py [--apply]
"""
from __future__ import annotations
import argparse
import os
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
MINECRAFT_ROOT = REPO_ROOT / "targets" / "minecraft"
INCLUDE_RE = re.compile(
r'^[ \t]*#[ \t]*include[ \t]*"app/linux/LinuxGame\.h"[ \t]*\n',
re.MULTILINE,
)
# Patterns that mean the include is genuinely needed.
# LinuxGame.h transitively pulls in Game.h (LinuxGame : public Game), so any
# reference to the Game class also keeps the include alive.
USAGE_PATTERNS = [
re.compile(r'\bLinuxGame\b'),
re.compile(r'\bC4JStringTable\b'),
re.compile(r'\bapp\s*\.'),
re.compile(r'\bapp\s*::'),
re.compile(r'\bGame\s*::'),
re.compile(r'\bGame\s*\*'),
re.compile(r'\bGame\s*&'),
re.compile(r'class\s+\w+\s*:\s*(?:public|private|protected)?\s*Game\b'),
]
def strip_comments(text: str) -> str:
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
text = re.sub(r'//[^\n]*', '', text)
return text
def file_needs_linuxgame(text: str) -> bool:
# Strip the LinuxGame.h include line itself before checking, to avoid
# the include path matching `LinuxGame`. Also strip comments so
# references in commented-out code don't keep the include alive.
scan = INCLUDE_RE.sub("", text)
scan = strip_comments(scan)
for pat in USAGE_PATTERNS:
if pat.search(scan):
return True
return False
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--apply", action="store_true",
help="Actually remove the dead includes")
args = parser.parse_args()
candidates: list[Path] = []
for dirpath, _dirnames, filenames in os.walk(MINECRAFT_ROOT):
for name in filenames:
if not name.endswith((".cpp", ".c", ".h", ".hpp")):
continue
path = Path(dirpath) / name
try:
text = path.read_text(encoding="utf-8", errors="surrogateescape")
except OSError:
continue
if not INCLUDE_RE.search(text):
continue
if file_needs_linuxgame(text):
continue
candidates.append(path)
candidates.sort()
for path in candidates:
print(path.relative_to(REPO_ROOT))
print(f"\n{len(candidates)} files have a dead LinuxGame.h include")
if args.apply:
for path in candidates:
text = path.read_text(encoding="utf-8", errors="surrogateescape")
new_text = INCLUDE_RE.sub("", text)
path.write_text(new_text, encoding="utf-8", errors="surrogateescape")
print(f"Removed from {len(candidates)} files")
return 0
if __name__ == "__main__":
sys.exit(main())