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 для запуска клиента
187 lines
7.3 KiB
Python
187 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Insert the missing cast where a generic method loses its type parameter.
|
|
|
|
Almost every remaining error in the fork has one shape: a value whose static
|
|
type came out erased is used where the enclosing method's type parameter is
|
|
expected.
|
|
|
|
Object cannot be converted to T
|
|
Comparable cannot be converted to T
|
|
Class<CAP#1> cannot be converted to Class<T>
|
|
|
|
The cast that is already there was written to the erased supertype, so the fix
|
|
is to retype it. This is only safe when the line contains exactly one candidate
|
|
cast, which is the condition checked here; anything ambiguous is reported and
|
|
left alone.
|
|
|
|
python fix_generic_casts.py <errors.txt> [--apply]
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
|
|
ERROR = re.compile(r'^(?P<file>[A-Za-z]:.*?\.java):(?P<line>\d+): error: (?P<msg>.*)$')
|
|
# javac prefixes most of these with "incompatible types: ", some not at all.
|
|
PREFIX = re.compile(r'^(?:incompatible types:\s*)?')
|
|
MISMATCH = re.compile(r'^(?P<from>[\w.]+(?:<.*?>)?(?:\[\])?) cannot be converted to (?P<to>\w+)$')
|
|
GENERIC_MISMATCH = re.compile(r'^Class<CAP#1> cannot be converted to Class<(?P<to>\w+)>$')
|
|
|
|
# A method head, used to confirm that a return really has the type we are about
|
|
# to cast it to. The type is whatever follows the last '>' of the pre-name
|
|
# segment, so a declaration like "public <T extends TileEntity> T getTile()"
|
|
# reports T and "private static <T extends C<T>> String getName()" reports
|
|
# String. Reading it any other way gets generics-bearing declarations wrong.
|
|
SIGNATURE = re.compile(r'^[ \t]{0,8}(?:(?:public|private|protected|static|final|abstract|'
|
|
r'synchronized|native|default|strictfp)\s+)*'
|
|
r'(?P<pre>[\w$.<>,\[\] ]+?)\s+(?P<name>\w+)\s*\([^;]*\)'
|
|
r'\s*(?:throws\s+[\w., ]+)?\s*\{\s*$')
|
|
|
|
# if / while / for and friends fit the shape above but are not method heads.
|
|
KEYWORDS = frozenset((
|
|
'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'try', 'catch',
|
|
'finally', 'return', 'new', 'synchronized', 'assert', 'throw', 'super',
|
|
'this', 'null', 'true', 'false',
|
|
))
|
|
|
|
|
|
def enclosing_return_type(lines, index):
|
|
# The window is generous on purpose: a method body can run long before the
|
|
# value that needs the cast, and missing the declaration would skip a fix
|
|
# that is perfectly safe.
|
|
for back in range(index, max(-1, index - 60), -1):
|
|
sig = SIGNATURE.match(lines[back])
|
|
if sig and sig.group('name') not in KEYWORDS:
|
|
return sig.group('pre').split('>')[-1].strip()
|
|
return None
|
|
|
|
# Bare single letters and short names are the type variables javac prints.
|
|
TYPE_VAR = re.compile(r'^[A-Z]\w{0,2}$')
|
|
|
|
|
|
def candidates(line, from_type):
|
|
"""Every `(Type)` cast on the line matching the type javac complained about."""
|
|
plain = from_type.split('<')[0]
|
|
found = []
|
|
for m in re.finditer(r'\(([^()]*)\)', line):
|
|
text = m.group(1).strip()
|
|
if text == plain or text == from_type:
|
|
found.append(m)
|
|
return found
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('errors')
|
|
parser.add_argument('--apply', action='store_true')
|
|
args = parser.parse_args()
|
|
|
|
per_file = {}
|
|
skipped = []
|
|
current = None
|
|
with open(args.errors, encoding='utf-8', errors='replace') as handle:
|
|
for raw in handle:
|
|
m = ERROR.match(raw.rstrip('\n').rstrip('\r'))
|
|
if not m:
|
|
continue
|
|
current = (m.group('file'), int(m.group('line')), m.group('msg'))
|
|
msg = PREFIX.sub('', current[2])
|
|
|
|
generic = GENERIC_MISMATCH.match(msg)
|
|
if generic:
|
|
to = generic.group('to')
|
|
per_file.setdefault(current[0], []).append(
|
|
(current[1], to, None, msg))
|
|
continue
|
|
|
|
mm = MISMATCH.match(msg)
|
|
if not mm:
|
|
continue
|
|
to = mm.group('to')
|
|
if not TYPE_VAR.match(to):
|
|
continue
|
|
per_file.setdefault(current[0], []).append(
|
|
(current[1], to, mm.group('from'), msg))
|
|
|
|
fixed = 0
|
|
files_changed = 0
|
|
|
|
for path, items in sorted(per_file.items()):
|
|
if not os.path.isfile(path):
|
|
continue
|
|
with open(path, encoding='utf-8', errors='replace') as handle:
|
|
lines = handle.read().split('\n')
|
|
|
|
touched = False
|
|
for item in items:
|
|
lineno, to, from_type, message = item
|
|
if lineno - 1 >= len(lines):
|
|
continue
|
|
line = lines[lineno - 1]
|
|
|
|
if from_type is None:
|
|
# Class<CAP#1> -> Class<T>: the capture comes out of a getter
|
|
# such as getRawType(), so the cast belongs on the right hand side.
|
|
spots = [m for m in re.finditer(r'=\s*([^=].*?);\s*$', line)]
|
|
if len(spots) != 1:
|
|
skipped.append('%s:%d %s (assignments=%d)'
|
|
% (os.path.basename(path), lineno, message, len(spots)))
|
|
continue
|
|
m = spots[0]
|
|
lines[lineno - 1] = (line[:m.start(1)]
|
|
+ '(Class<%s>) ' % to
|
|
+ line[m.start(1):])
|
|
touched = True
|
|
fixed += 1
|
|
continue
|
|
|
|
spots = candidates(line, from_type)
|
|
replacement = '(%s)' % to
|
|
|
|
if len(spots) == 1:
|
|
m = spots[0]
|
|
lines[lineno - 1] = line[:m.start()] + replacement + line[m.end():]
|
|
touched = True
|
|
fixed += 1
|
|
continue
|
|
|
|
# No cast on the line at all: the value was simply used raw. Insert
|
|
# one immediately after the assignment or the return, which is where
|
|
# the target type is known. A return is only safe when the method
|
|
# actually returns that type variable: casting a String to T would
|
|
# introduce a new error rather than fix the old one.
|
|
anchor = re.search(r'(?<=return )|(?<==\s)(?!=)', line)
|
|
if anchor is None:
|
|
skipped.append('%s:%d %s (no anchor)'
|
|
% (os.path.basename(path), lineno, message))
|
|
continue
|
|
cut = anchor.end()
|
|
if line[:cut].rstrip().endswith('return'):
|
|
enclosing = enclosing_return_type(lines, lineno - 1)
|
|
if enclosing != to:
|
|
skipped.append('%s:%d %s (method returns %s, not %s)'
|
|
% (os.path.basename(path), lineno, message,
|
|
enclosing or '?', to))
|
|
continue
|
|
lines[lineno - 1] = line[:cut] + replacement + ' ' + line[cut:]
|
|
touched = True
|
|
fixed += 1
|
|
|
|
if touched:
|
|
files_changed += 1
|
|
if args.apply:
|
|
with open(path, 'w', encoding='utf-8', newline='\n') as handle:
|
|
handle.write('\n'.join(lines))
|
|
|
|
print('casts rewritten: %d in %d files' % (fixed, files_changed))
|
|
if skipped:
|
|
print('left alone: %d' % len(skipped))
|
|
for line in skipped:
|
|
print(' %s' % line)
|
|
if not args.apply:
|
|
print('dry run, pass --apply to write')
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main()) |