Files
RustNotMC/rsx-client/tools/show_errors.py
T
sasheg dev bdf0ae866a Форк клиента собирается из исходников, модзагрузчик встроен в игру
Проект начинается с двух вещей: клиент, который мы действительно собрали, и
загрузчик, который является его частью, а не чем-то рядом.

Форк
- prepare-game.ps1 пересобирает клиент целиком: ванильный jar, SRG и MCP,
  SpecialSource, FernFlower, патчи, автофиксеры, javac. Идемпотентен.
- Свой код игры лежит в src-overlay, потому что src пересоздаётся при каждой
  сборке и всё написанное вручную там погибает.
- 116 патчей в patches/decompiler-fixes.txt. Каждый сверялся с оригинальным
  байткодом через javap: компиляция не ловит ошибки декомпиляции.
- struct_diff.py сравнивает исходный байткод с нашей сборкой по классам,
  интерфейсам, суперклассам, методам и полям, и отдельно проверяет, что
  пропавшие классы действительно никому не нужны.

Загрузчик
- Собирается и тестируется без игры: build.ps1 не использует classpath
  вообще, поэтому SelfTest из 128 проверок проходит без запущенного форка.
- Клиентский API не содержит ни одного игрового типа, реализацию даёт игра.
- Политика: мод меняет то, что клиент показывает, но никогда то, что истинно
  в мире. Мир, предметы, игроки и сеть принадлежат серверу.
- Capability приведены в соответствие с этой политикой: осталось шесть, и
  за каждым стоит реальная проверка. Прежние WORLD, PLAYER, NETWORK, TEXT,
  INPUT и COMMAND выдавались, но ничего не проверяли.
- Команды модов имеют область CLIENT, SERVER или BOTH. Серверные не
  выполняются на клиенте никогда.
- Свой jar загрузчика лежит в classpath игры и сборки форка.

Инструменты
- add_patch.py, чтобы каждое исправление переживало передекомпиляцию
- find_self_comparisons.py, find_remove_index_bugs.py, find_callers.py
- stage_run.py и fetch_missing.py для запуска клиента
2026-10-18 21:31:27 +03:00

103 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""Show compile errors next to the source line that caused them.
javac prints file:line: error: message, which is not enough to work from: the
interesting part is usually the statement, and hunting for it in a 3000 line
decompiled file by hand is what turns a 160 error job into an afternoon.
python show_errors.py <errors.txt> [<errors.txt> ...] [--top N] [--file NAME]
"""
import argparse
import collections
import os
import re
LINE = re.compile(r'^(?P<file>[A-Za-z]:.*?\.java):(?P<line>\d+): error: (?P<msg>.*)$')
DETAIL = re.compile(r'^ {2}(?P<key>symbol|location|where|required|found|reason):\s*(?P<rest>.*)$')
def load(paths):
"""Returns [(file, line, message, [detail lines])].
javac puts the useful part after the message: which symbol was missing, and
which types were expected. Those continuation lines are indented and carry
no filename, so they are attached to the error above them.
"""
found = []
for path in paths:
pending = None
with open(path, encoding='utf-8', errors='replace') as handle:
for raw in handle:
line = raw.rstrip('\n').rstrip('\r')
m = LINE.match(line)
if m:
if pending:
found.append(pending)
pending = (m.group('file'), int(m.group('line')),
m.group('msg'), [])
continue
if pending is not None:
# The quoted source line and its caret sit between the error
# and the symbol block, so anything that is not a detail line
# is skipped rather than treated as the end of the error.
d = DETAIL.match(line)
if d:
text = d.group('rest').strip()
pending[3].append('%s: %s' % (d.group('key'), text)
if text else d.group('key'))
if pending:
found.append(pending)
return found
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('errors', nargs='+')
parser.add_argument('--top', type=int, default=0, help='only the N worst files')
parser.add_argument('--file', help='show only errors in this file (substring match)')
parser.add_argument('--context', type=int, default=0, help='lines around the error')
args = parser.parse_args()
found = load(args.errors)
if not found:
print('no errors')
return 0
counts = collections.Counter(f for f, _, _, _ in found)
if args.file:
targets = [f for f in counts if args.file in f]
elif args.top:
targets = [f for f, _ in counts.most_common(args.top)]
else:
targets = sorted(counts)
shown = 0
for path in targets:
rows = [(ln, msg, det) for f, ln, msg, det in found if f == path]
print('%s (%d errors)' % (path, len(rows)))
cache = {}
for ln, msg, details in rows:
if path not in cache:
try:
with open(path, encoding='utf-8', errors='replace') as handle:
cache[path] = handle.readlines()
except OSError:
cache[path] = []
body = cache[path]
print(' %d: %s' % (ln, msg))
for extra in details:
print(' %s' % extra)
lo = max(0, ln - 1 - args.context)
hi = min(len(body), ln + args.context)
for i in range(lo, hi):
mark = '>' if i == ln - 1 else ' '
print(' %s %5d | %s' % (mark, i + 1, body[i].rstrip()))
shown += 1
print()
print('---- %d error(s) across %d file(s)' % (len(found), len(counts)))
return 0
if __name__ == '__main__':
raise SystemExit(main())