package com.twitter.common.tools; import java.io.IOException; import java.io.PrintWriter; import java.util.Arrays; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.tools.Diagnostic; import javax.tools.FileObject; import org.fusesource.jansi.Ansi; import org.fusesource.jansi.Ansi.Color; import org.fusesource.jansi.AnsiConsole; /** * A DiagnosticListener that supports colorized console output and warning filters. * * <p>Users should make sure to call {@link #prepareConsole(boolean)} prior to reporting on any * diagnostics. * * @param <T> The type of FileObject this listener can handle. */ class AnsiColorDiagnosticListener<T extends FileObject> extends FilteredDiagnosticListener<T> { private static final Pattern EOL = Pattern.compile("$", Pattern.MULTILINE); private final PrintWriter outWriter; private final PrintWriter errWriter; private boolean colorOutput; /** * Creates a diagnostic listener that outputs notes to {@link System#out} and warnings and errors * to {@link System#err}. */ AnsiColorDiagnosticListener() { this(new PrintWriter(System.out), new PrintWriter(System.err)); } /** * Creates a diagnostic listener that outputs notes to the given {@code outWriter} and warnings * and errors to the given {@code errWriter}. */ AnsiColorDiagnosticListener(PrintWriter outWriter, PrintWriter errWriter) { this.outWriter = outWriter; this.errWriter = errWriter; } private static String getMessage(Diagnostic<? extends FileObject> diagnostic) { return diagnostic.getMessage(Locale.getDefault()); } /** * Prepares the console for either plain or color output. Caller's should make sure to call * {@link #releaseConsole()} when they are done reporting diagnostics. * * @param color {@code true} to output diagnostics in color. */ void prepareConsole(boolean color) { colorOutput = color; if (color) { AnsiConsole.systemInstall(); } else { System.setProperty(Ansi.DISABLE, "true"); } } /** * Returns the console to its state prior to the last call of {@link #prepareConsole(boolean)}. */ void releaseConsole() { if (colorOutput) { AnsiConsole.systemUninstall(); } else { System.setProperty(Ansi.DISABLE, "false"); } } @Override protected void reportOn(Diagnostic<? extends T> diagnostic) { switch (diagnostic.getKind()) { case NOTE: logDiagnostic(outWriter, Ansi.ansi().fg(Color.GREEN), diagnostic); break; case WARNING: case MANDATORY_WARNING: logDiagnostic(errWriter, Ansi.ansi().fg(Color.YELLOW), diagnostic); break; case ERROR: logDiagnostic(errWriter, Ansi.ansi().fg(Color.RED), diagnostic); break; case OTHER: default: outWriter.println(getMessage(diagnostic)); } } private void logDiagnostic(PrintWriter out, Ansi ansi, Diagnostic<? extends FileObject> diagnostic) { String message = getMessage(diagnostic); CharSequence sourceCode = extractSource(diagnostic); if (sourceCode == null) { out.println(ansi.format("%s", message)); } else { out.println(ansi.format( "%s\n%s\n%s^", message, sourceCode, spaces((int) diagnostic.getColumnNumber() - 1))); } out.print(Ansi.ansi().reset()); out.flush(); } private CharSequence extractSource(Diagnostic<? extends FileObject> diagnostic) { FileObject source = diagnostic.getSource(); int startPosition = (int) diagnostic.getStartPosition(); if ((source == null) || (Diagnostic.NOPOS == startPosition) || (startPosition < 1)) { return null; } try { CharSequence content = source.getCharContent(true); // Start and end positions can both be in the middle of a line, here we expand out and grab // The whole line for useful error context. Matcher matcher = EOL.matcher(content); int endPosition = (int) diagnostic.getEndPosition(); if (matcher.find(startPosition)) { endPosition = matcher.end(); } // Handle the error being at eol - back up a step so we can scan back to the prior eol and // capture the line of program text preceding the error point at eol. if (startPosition == endPosition && startPosition > 0) { startPosition--; } for (; startPosition > 0; startPosition--) { if (content.charAt(startPosition) == '\n' || content.charAt(startPosition) == '\r') { // Start just after the beginning newline startPosition++; break; } } return content.subSequence(startPosition, endPosition); } catch (IOException e) { return null; } } private String spaces(int count) { char[] spaces = new char[count]; Arrays.fill(spaces, ' '); return new String(spaces); } }