"""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+(?(?:public\s+|final\s+|abstract\s+)*)record\s+(?P[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"(?= 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()