#!/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 [ ...] [--top N] [--file NAME] """ import argparse import collections import os import re LINE = re.compile(r'^(?P[A-Za-z]:.*?\.java):(?P\d+): error: (?P.*)$') DETAIL = re.compile(r'^ {2}(?Psymbol|location|where|required|found|reason):\s*(?P.*)$') 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())