package net.sf.eclipsefp.haskell.ghccompiler.core;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sf.eclipsefp.haskell.buildwrapper.types.Location;
import net.sf.eclipsefp.haskell.buildwrapper.types.Note;
/**
* Parser that processes the GHC compiler output as it arrives and sends it off
* in parsed form to an {@link IGhcOutputListener}.
*
* For parsing, the Parsec "grammar" from GHCOutputParser.hs is used.
*
* TODO TtC this should be an {@link ICompilerListener}, I think.
*
* @author Thomas ten Cate
*/
public class GhcOutputParser {
private final BufferedReader reader;
private String line;
private int pos; // position within the line; only valid sometimes
private final IGhcOutputListener listener;
private final Pattern compilingPattern = Pattern
.compile( "^(\\[(\\d+) of (\\d+)\\]\\s*)?Compiling\\s+\\S+\\s+\\(\\s*([^,]*),\\s+.*\\s*\\)$" ); //$NON-NLS-1$
private static Pattern parenthesizedLocPattern = Pattern
.compile( "^\\((\\d+):(\\d+)\\)-\\((\\d+):(\\d+)\\)" ); //$NON-NLS-1$
private static Pattern sepLocsPattern = Pattern
.compile( "^(\\d+):(\\d+)(-(\\d+))?" ); //$NON-NLS-1$
public GhcOutputParser( final Reader reader, final IGhcOutputListener listener ) {
this.reader = new BufferedReader( reader );
this.listener = listener;
}
/**
* Parses the input stream until EOF occurs.
*/
public void parse() throws IOException {
nextLine(); // initialize
messages(); // main parsing method
}
// ///////////////////////
// main parsing methods
private void messages() throws IOException {
while( !eof() ) {
// abusing short-circuit ||
if( !( compiling() || skipping() || ghcMessage() ) ) {
// don't know what this is; be lenient
nextLine();
}
}
}
/**
* Either parses the current "[x of y] Compiling SomeFile.hs" line and returns
* true, or consumes no input and returns false.
*/
private boolean compiling() throws IOException {
Matcher matcher = compilingPattern.matcher( line );
if( !matcher.matches() ) {
return false;
}
int number, total;
String fileName;
if( matcher.groupCount() == 4 ) {
number = Integer.parseInt( matcher.group( 2 ) );
total = Integer.parseInt( matcher.group( 3 ) );
fileName = matcher.group( 4 );
} else {
// just guessing...
number = 1;
total = 1;
fileName = matcher.group( 1 );
}
listener.compiling( fileName, number, total );
nextLine();
return true;
}
/**
* Either parses the current "Skipping SomeFile.hs" line and returns true, or
* consumes no input and returns false.
*/
private boolean skipping() throws IOException {
if( !line.startsWith( "Skipping " ) ) { //$NON-NLS-1$
return false;
}
nextLine();
return true;
}
/**
* Either parses a compiler message or warning from the current line and
* returns true, or consumes no input and returns false.
*/
private boolean ghcMessage() throws IOException {
pos = line.indexOf( ':' );
if( pos < 0 ) {
return false;
}
String fileName = line.substring( 0, pos );
++pos; // skip the colon
Location location = location( fileName );
if( location == null ) {
return false;
}
++pos; // skip the colon
String message = line.substring( pos ).trim();
if (message.length() == 0) {
// sometimes the message is on its own line
nextLine();
message = line.trim();
}
nextLine();
Note.Kind kind = message.startsWith( "Warning:" ) ? Note.Kind.WARNING : Note.Kind.ERROR; //$NON-NLS-1$
if( kind == Note.Kind.WARNING ) {
message = message.substring( 8 ).trim();
}
// Lines starting with four or more spaces form additional info
StringBuffer additionalInfo = new StringBuffer();
boolean first = true;
while( !eof() && line.startsWith( " " ) ) { //$NON-NLS-1$
if( !first ) {
additionalInfo.append( '\n' );
}
first = false;
additionalInfo.append( line );
nextLine();
}
Note note = new Note( kind, location, message, additionalInfo.toString() );
listener.message( note );
return true;
}
/**
* Returns the location represented by the start of the given string. Returns
* null if it could not be parsed.
*/
private Location location( final String fileName ) {
Location location = parenthesizedLoc( fileName );
if( location != null ) {
return location;
}
location = sepLocs( fileName );
if( location != null ) {
return location;
}
return null;
}
/**
* Parses a location of the form (12,5)-(13,20) and returns it. Assumes that
* {@link #pos} is valid. If parsing fails, returns null and leaves
* {@link #pos} alone.
*/
private Location parenthesizedLoc( final String fileName ) {
Matcher matcher = parenthesizedLocPattern.matcher( line.substring( pos ) );
if( matcher.lookingAt() ) {
pos += matcher.end();
try {
int startLine = Integer.parseInt( matcher.group( 1 ) ) - 1;
int startCol = Integer.parseInt( matcher.group( 2 ) );
int endLine = Integer.parseInt( matcher.group( 3 ) ) - 1;
int endCol = Integer.parseInt( matcher.group( 4 ) );
return new Location( fileName, startLine, startCol, endLine, endCol );
} catch( NumberFormatException ex ) {
return null;
}
}
return null;
}
/**
* Parses a location of the form 12:5 or 12:5-20 and returns it. Assumes that
* {@link #pos} is valid. If parsing fails, returns null and leaves
* {@link #pos} alone.
*/
private Location sepLocs( final String fileName ) {
Matcher matcher = sepLocsPattern.matcher( line.substring( pos ) );
if( matcher.lookingAt() ) {
pos += matcher.end();
if( matcher.group(4) == null ) {
// 12:5
try {
int line = Integer.parseInt( matcher.group( 1 ) ) - 1;
int col = Integer.parseInt( matcher.group( 2 ) );
return new Location( fileName, line, col, line, col );
} catch( NumberFormatException ex ) {
return null;
}
} else if( matcher.groupCount() == 4 ) {
// 12:5-20
try {
int line = Integer.parseInt( matcher.group( 1 ) ) - 1;
int startCol = Integer.parseInt( matcher.group( 2 ) );
int endCol = Integer.parseInt( matcher.group( 4 ) );
return new Location( fileName, line, startCol, line, endCol );
} catch( NumberFormatException ex ) {
return null;
}
}
}
return null;
}
// //////////////////
// parsing helpers
private void nextLine() throws IOException {
line = reader.readLine();
pos = 0;
}
private boolean eof() {
return line == null;
}
}