evo/tools/fixwaves.py

670 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Error-driven фиксы по логу javac: волна boolean-кастов и private→package-private."""
import re, sys, os, collections
LOG = sys.argv[1]
WAVES = sys.argv[2] if len(sys.argv) > 2 else "ab"
HDR = re.compile(r"^(\S+\.java):(\d+): error: (.+)$")
def load_errors(path):
errs = []
lines = open(path, encoding="utf-8", errors="replace").read().splitlines()
i = 0
while i < len(lines):
m = HDR.match(lines[i])
if m:
col = None
srcline = None
if i + 2 < len(lines) and lines[i + 2].lstrip().startswith("^"):
srcline = lines[i + 1]
col = len(lines[i + 2]) - len(lines[i + 2].lstrip()) + 1
extra = []
j = i + 1
while j < len(lines) and j < i + 8 and lines[j].strip():
if (
lines[j]
.lstrip()
.startswith(("symbol:", "location:", "required:", "found:"))
):
extra.append(lines[j].strip())
j += 1
errs.append(
(
m.group(1),
int(m.group(2)),
col,
srcline,
m.group(3) + (" | " + " | ".join(extra) if extra else ""),
)
)
i += 3
else:
i += 1
return errs
OPS_STOP = set("+-*/%&|^<>!=?:,")
def scan_end(line, start):
"""Конец primary-выражения, начиная с start (0-based). None = не найден."""
depth = 0
i = start
n = len(line)
first = True
while i < n:
c = line[i]
if c in "([{":
depth += 1
elif c in ")]}":
if depth == 0:
break
depth -= 1
elif depth == 0 and not first and (c in OPS_STOP or c == ";"):
break
elif depth == 0 and c == ")":
break
first = False
i += 1
return i
def scan_multiline(lines, ln, start):
"""Возвращает (li, idx, ok): конец operand, начиная с lines[ln-1][start]."""
depth = 0
first = True
li = ln - 1
i = start
guard = 0
while li < len(lines) and guard < 400:
guard += 1
line = lines[li]
while i < len(line):
c = line[i]
if c in "([{":
depth += 1
elif c in ")]}":
if depth == 0:
return li, i, True
depth -= 1
elif depth == 0 and not first and (c in OPS_STOP or c == ";"):
return li, i, True
first = False
i += 1
li += 1
i = 0
return li, i, False
def wave_a(errs, stats, i2b=True, b2i=True):
per_file = collections.defaultdict(list)
for f, ln, col, srcline, msg in errs:
if i2b and "int cannot be converted to boolean" in msg and srcline and col:
per_file[f].append((ln, col, srcline, "i2b"))
elif b2i and "boolean cannot be converted to int" in msg and srcline and col:
per_file[f].append((ln, col, srcline, "b2i"))
for f, items in per_file.items():
lines = open(f, encoding="utf-8").read().splitlines(keepends=True)
by_line = collections.defaultdict(list)
for ln, col, srcline, kind in items:
by_line[ln].append((col, kind))
touched = False
for ln, cols in sorted(by_line.items(), reverse=True):
if ln - 1 >= len(lines):
continue
i2b_done = False
for col, kind in sorted(cols, reverse=True):
if kind != "i2b":
continue
if i2b_done:
stats["i2b_dup_line"] += 1
continue
pos = col - 1
# найти (boolean): на этой строке перед колонкой, иначе выше до 3 строк
cast_line, cast_pos = None, None
for back in range(0, 4):
bl = ln - 1 - back
if bl < 0:
break
hay = lines[bl]
limit = pos + 1 if back == 0 else len(hay)
cp = hay.rfind("(boolean)", 0, limit)
if cp != -1:
cast_line, cast_pos = bl, cp
break
if cast_line is None:
stats["i2b_no_cast"] += 1
continue
if cast_line == ln - 1:
op_start = cast_pos + len("(boolean)")
while (
op_start < len(lines[cast_line])
and lines[cast_line][op_start] == " "
):
op_start += 1
else:
# cast на предыдущей строке: operand начинается в начале строки ln
op_start = 0
cast_line = ln - 1
while (
op_start < len(lines[cast_line])
and lines[cast_line][op_start] == " "
):
op_start += 1
end_li, end_i, ok = scan_multiline(lines, ln, op_start)
if not ok:
stats["i2b_no_end"] += 1
continue
# собрать span
if end_li == ln - 1:
span = lines[ln - 1][op_start:end_i]
new_line = (
lines[ln - 1][:op_start]
+ "(("
+ span
+ ") != 0)"
+ lines[ln - 1][end_i:]
)
lines[ln - 1] = new_line
else:
span = (
lines[ln - 1][op_start:]
+ "".join(lines[ln:end_li])
+ lines[end_li][:end_i]
)
tail = lines[end_li][end_i:]
head = lines[ln - 1][:op_start]
merged = head + "((" + span + ") != 0)" + tail
if not merged.endswith("\n"):
merged += "\n"
del lines[ln - 1 : end_li + 1]
lines.insert(ln - 1, merged)
stats["i2b"] += 1
touched = True
i2b_done = True
if touched:
open(f, "w", encoding="utf-8").write("".join(lines))
# b2i оставлен на потом
if b2i:
stats["b2i_skipped"] = sum(
1 for x in errs if "boolean cannot be converted to int" in x[4]
)
def _scan_stmt_end(lines, li, start):
"""Конец statement: ';' на depth 0, начиная с lines[li][start]. (end_li, end_i, ok)."""
depth = 0
i = start
guard = 0
while li < len(lines) and guard < 500:
guard += 1
line = lines[li]
while i < len(line):
c = line[i]
if c in "([{":
depth += 1
elif c in ")]}":
if depth == 0 and c == "}":
return li, i, False
if depth > 0:
depth -= 1
elif c == ";" and depth == 0:
return li, i, True
i += 1
li += 1
i = 0
return li, i, False
def wave_b2i(errs, stats):
"""boolean-выражение в int-контексте: обернуть RHS в (EXPR) ? 1 : 0."""
per_file = collections.defaultdict(list)
for f, ln, col, srcline, msg in errs:
if "boolean cannot be converted to int" in msg and srcline and col:
per_file[f].append((ln, col))
for f, items in per_file.items():
lines = open(f, encoding="utf-8").read().splitlines(keepends=True)
for ln, col in sorted(set(items), reverse=True):
if ln - 1 >= len(lines):
continue
eq = None
for back in range(0, 8):
li = ln - 1 - back
if li < 0:
break
hay = lines[li]
limit = col if back == 0 else len(hay)
for m in re.finditer(r"(?<![=!<>+\-*/&|^])=(?!=)", hay[:limit]):
idx = m.start()
e_li, e_i, ok = _scan_stmt_end(lines, li, idx + 1)
if not ok:
continue
if (e_li, e_i) >= (ln - 1, col):
eq = (li, idx)
break
if eq:
break
if not eq:
stats["b2i_no_eq"] += 1
continue
li, idx = eq
rs = idx + 1
while rs < len(lines[li]) and lines[li][rs] in " \t":
rs += 1
end_li, end_i, ok = _scan_stmt_end(lines, li, rs)
if not ok:
stats["b2i_no_end"] += 1
continue
if end_li == li:
span = lines[li][rs:end_i]
else:
span = (
lines[li][rs:]
+ "".join(lines[li + 1 : end_li])
+ lines[end_li][:end_i]
)
if span.rstrip().endswith("? 1 : 0"):
stats["b2i_dup"] += 1
continue
if end_li == li:
lines[li] = (
lines[li][:rs] + "(" + span + ") ? 1 : 0" + lines[li][end_i:]
)
else:
head = lines[li][:rs]
tail = lines[end_li][end_i:]
merged = head + "(" + span + ") ? 1 : 0" + tail
if not merged.endswith("\n"):
merged += "\n"
del lines[li : end_li + 1]
lines.insert(li, merged)
stats["b2i"] += 1
open(f, "w", encoding="utf-8").write("".join(lines))
PRIV = re.compile(r"has private access in (\w+)$")
def wave_b(errs, stats):
members = collections.defaultdict(set)
for f, ln, col, srcline, msg in errs:
m = PRIV.search(msg)
if not m:
continue
owner = m.group(1)
sig = msg[: m.start()].strip()
name = sig.split("(")[0].strip()
# убрать квалификаторы вида SomeClass.member -> member
name = name.split(".")[-1]
if re.fullmatch(r"[A-Za-z_$][\w$]*", name):
members[owner].add(name)
for owner, names in members.items():
path = None
for cand in (
"/storage/aboba/evovis-client/src/main/java/defpackage/%s.java" % owner,
"/storage/aboba/evovis-client/src/main/java/a/%s.java" % owner,
"/storage/aboba/evovis-client/src/main/java/b/%s.java" % owner,
"/storage/aboba/evovis-client/src/main/java/evo/%s.java" % owner,
):
import os
if os.path.exists(cand):
path = cand
break
if not path:
stats["b_noowner"] += 1
continue
text = open(path, encoding="utf-8").read()
orig = text
for name in names:
rx = re.compile(
r"^(\s*)private\s+((?:static\s+|final\s+|volatile\s+|transient\s+|abstract\s+|native\s+|default\s+|synchronized\s+)*"
r"[\w.$<>\[\], ?]+\s+(?<![\w$])"
+ re.escape(name)
+ r"(?![\w$])\s*[;=(])",
re.M,
)
text, n = rx.subn(r"\1\2", text)
stats["b_priv"] += n
if text != orig:
open(path, "w", encoding="utf-8").write(text)
RECORD_RX = re.compile(
r"^(?P<mods>(?:public\s+|final\s+|abstract\s+)*)record\s+(?P<name>[A-Za-z_$][\w$]*)\s*\(",
re.M,
)
def _split_top(s, sep=","):
out, depth, cur = [], 0, ""
for ch in s:
if ch in "<([":
depth += 1
elif ch in ">)]":
depth -= 1
if ch == sep and depth == 0:
out.append(cur)
cur = ""
else:
cur += ch
if cur.strip():
out.append(cur)
return out
def wave_records(errs, stats):
owners = set()
for f, ln, col, srcline, msg in errs:
m = PRIV.search(msg.split(" | ")[0])
if m:
owners.add(m.group(1))
base = "/storage/aboba/evovis-client/src/main/java/"
for owner in sorted(owners):
path = None
for pkg in ("defpackage/", "a/", "b/", "evo/"):
cand = base + pkg + owner + ".java"
if os.path.exists(cand):
path = cand
break
if not path:
continue
text = open(path, encoding="utf-8").read()
head = text[:2000]
m = RECORD_RX.search(text)
if not m or m.group("name") != owner:
continue
# найти компоненты: от '(' до matching ')' с балансом
start = m.end() - 1
depth = 0
i = start
while i < len(text):
if text[i] == "(":
depth += 1
elif text[i] == ")":
depth -= 1
if depth == 0:
break
i += 1
comps_raw = text[start + 1 : i]
comps = []
for piece in _split_top(comps_raw):
piece = " ".join(piece.split())
if not piece:
continue
parts = piece.rsplit(None, 1)
if len(parts) != 2:
continue
typ, name = parts
typ = re.sub(r"\s*\bfinal\b\s*", " ", typ).strip()
comps.append((typ, name))
if not comps:
stats["rec_skip"] += 1
continue
# тело: от ')' до конца класса — сохранить как есть
rest = text[i + 1 :]
brace = rest.find("{")
if brace == -1:
stats["rec_skip"] += 1
continue
body = rest[brace + 1 :]
header = text[: m.start()]
mods = (m.group("mods") or "").replace("record", "").strip()
is_public = "public" in mods
lines_cls = []
vis = "public " if is_public else ""
lines_cls.append(f"{vis}final class {owner} {{")
for typ, name in comps:
lines_cls.append(f" {typ} {name};")
lines_cls.append("")
ctor_args = ", ".join(f"{typ} {name}" for typ, name in comps)
lines_cls.append(f" {vis}{owner}({ctor_args}) {{")
for typ, name in comps:
lines_cls.append(f" this.{name} = {name};")
lines_cls.append(" }")
lines_cls.append("")
for typ, name in comps:
lines_cls.append(f" public {typ} {name}() {{")
lines_cls.append(f" return this.{name};")
lines_cls.append(" }")
lines_cls.append("")
# equals
lines_cls.append(" @Override")
lines_cls.append(" public boolean equals(Object o) {")
lines_cls.append(f" if (!(o instanceof {owner} that)) {{")
lines_cls.append(" return false;")
lines_cls.append(" }")
for typ, name in comps:
if "[]" in typ:
if typ.endswith("[]") and typ[:-2] in (
"int",
"long",
"double",
"float",
"boolean",
"byte",
"short",
"char",
):
lines_cls.append(
f" if (!java.util.Arrays.equals(this.{name}, that.{name})) {{ return false; }}"
)
else:
lines_cls.append(
f" if (!java.util.Objects.deepEquals(this.{name}, that.{name})) {{ return false; }}"
)
elif typ in (
"int",
"long",
"double",
"float",
"boolean",
"byte",
"short",
"char",
):
if typ == "float":
lines_cls.append(
f" if (Float.compare(this.{name}, that.{name}) != 0) {{ return false; }}"
)
elif typ == "double":
lines_cls.append(
f" if (Double.compare(this.{name}, that.{name}) != 0) {{ return false; }}"
)
else:
lines_cls.append(
f" if (this.{name} != that.{name}) {{ return false; }}"
)
else:
lines_cls.append(
f" if (!java.util.Objects.equals(this.{name}, that.{name})) {{ return false; }}"
)
lines_cls.append(" return true;")
lines_cls.append(" }")
lines_cls.append("")
lines_cls.append(" @Override")
lines_cls.append(" public int hashCode() {")
args = ", ".join(
(
"java.util.Arrays.deepHashCode(this.%s)" % n
if "[]" in ty
else "this.%s" % n
)
for ty, n in comps
)
lines_cls.append(f" return java.util.Objects.hash({args});")
lines_cls.append(" }")
lines_cls.append("")
lines_cls.append(" @Override")
lines_cls.append(" public String toString() {")
sb = 'return "%s[" + ' % owner
parts = []
for ty, n in comps:
parts.append('"%s=" + this.%s' % (n, n))
lines_cls.append(" " + sb + ' + ", " + '.join(parts) + ' + "]";')
lines_cls.append(" }")
lines_cls.append(" ")
new_text = header + "\n".join(lines_cls) + body
open(path, "w", encoding="utf-8").write(new_text)
stats["records"] += 1
def wave_ctors(errs, stats):
pairs = set()
for f, ln, col, srcline, msg in errs:
m = PRIV.search(msg.split(" | ")[0])
if not m:
continue
owner = m.group(1)
sig = msg.split(" | ")[0][: m.start()].strip()
if sig.startswith(owner + "("):
pairs.add(owner)
base = "/storage/aboba/evovis-client/src/main/java/"
for owner in pairs:
path = None
for pkg in ("defpackage/", "a/", "b/", "evo/"):
cand = base + pkg + owner + ".java"
if os.path.exists(cand):
path = cand
break
if not path:
continue
text = open(path, encoding="utf-8").read()
new = re.sub(
r"(?m)^(\s*)private(\s+)" + re.escape(owner) + r"\s*\(",
r"\1\2" + owner + "(",
text,
)
if new != text:
open(path, "w", encoding="utf-8").write(new)
stats["ctors"] += 1
def wave_pkgqual(errs, stats):
base = "/storage/aboba/evovis-client/src/main/java/"
per_file = collections.defaultdict(list)
for f, ln, col, srcline, msg in errs:
if not f.startswith(base + "b/"):
continue
if "cannot find symbol" not in msg:
continue
parts = msg.split(" | ")
symbol = location = None
for p in parts:
if p.startswith("symbol:"):
symbol = p[len("symbol:") :].strip()
elif p.startswith("location:"):
location = p[len("location:") :].strip()
if not location:
continue
lm = re.match(r"(?:class|interface|enum|record)\s+([\w$]+)", location)
if not lm:
continue
loc = lm.group(1)
if symbol and symbol.startswith("method "):
member = symbol[len("method ") :].split("(")[0]
elif symbol and symbol.startswith("class "):
member = None
else:
member = None
per_file[f].append((ln, col, loc, member))
for f, items in per_file.items():
lines = open(f, encoding="utf-8").read().splitlines(keepends=True)
for ln, col, loc, member in sorted(set(items), reverse=True):
if ln - 1 >= len(lines):
continue
line = lines[ln - 1]
if member:
needle = loc + "." + member
idx = line.find(needle)
if idx == -1:
stats["qual_miss"] += 1
continue
lines[ln - 1] = (
line[:idx] + "defpackage." + needle + line[idx + len(needle) :]
)
stats["qual"] += 1
else:
# класс: заменить идентификатор на позиции колонки
pos = col - 1
m = re.match(r"[A-Za-z_$][\w$]*", line[pos:])
if not m or m.group() != loc:
# найти loc как целый идентификатор
m2 = re.search(r"(?<![\w.$])" + re.escape(loc) + r"(?![\w$])", line)
if not m2:
stats["qual_miss"] += 1
continue
idx = m2.start()
else:
idx = pos
lines[ln - 1] = (
line[:idx] + "defpackage." + loc + line[idx + len(loc) :]
)
stats["qual"] += 1
open(f, "w", encoding="utf-8").write("".join(lines))
def wave_vec(errs, stats):
per_file = collections.defaultdict(list)
for f, ln, col, srcline, msg in errs:
if "cannot find symbol" not in msg or not col:
continue
parts = msg.split(" | ")
symbol = location = None
for p in parts:
if p.startswith("symbol:"):
symbol = p[len("symbol:") :].strip()
elif p.startswith("location:"):
location = p[len("location:") :].strip()
if not symbol or not location or "Vector3fc" not in location:
continue
vm = re.match(r"variable\s+([\w$]+)", location)
sm = re.match(r"variable\s+([\w$]+)", symbol)
if not vm or not sm:
continue
per_file[f].append((ln, col, vm.group(1), sm.group(1)))
for f, items in per_file.items():
lines = open(f, encoding="utf-8").read().splitlines(keepends=True)
for ln, col, var, field in sorted(items, reverse=True):
if ln - 1 >= len(lines):
continue
line = lines[ln - 1]
needle = var + "." + field
idx = line.rfind(needle, 0, col + 10)
if idx == -1:
idx = line.find(needle)
if idx == -1:
stats["vec_miss"] += 1
continue
lines[ln - 1] = (
line[:idx] + var + "." + field + "()" + line[idx + len(needle) :]
)
stats["vec"] += 1
open(f, "w", encoding="utf-8").write("".join(lines))
def main():
errs = load_errors(LOG)
stats = collections.Counter()
if "a" in WAVES:
wave_a(errs, stats, i2b=("1" in WAVES or WAVES == "a"), b2i=False)
elif "1" in WAVES or "2" in WAVES:
wave_a(errs, stats, i2b="1" in WAVES, b2i=False)
if "2" in WAVES:
wave_b2i(errs, stats)
if "b" in WAVES:
wave_b(errs, stats)
if "c" in WAVES:
wave_records(errs, stats)
wave_ctors(errs, stats)
if "e" in WAVES:
wave_pkgqual(errs, stats)
if "f" in WAVES:
wave_vec(errs, stats)
print(dict(stats))
if __name__ == "__main__":
main()