package xtc.lang;
import java.io.CharArrayWriter;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.File;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.TreeSet;
/**
* A class to extract a line number mapping for a source-to-source
* transformation. The "input source" and the "output source" are input and
* output of the source-to-source transformation. For instance, Main.jni and
* Main.java are input source and output source files in the Jeannie framework.
* This source map extractor assumes that the output source file contains the
* input source line directives in a "//#line ${lineno} ${source file} form, and
* this form is similiar to the C line number directive.
*
* The source map extractor takes an output source file (Main.java), and
* produces a mapping from the input source files (Main.jni) to output source
* file (Main.java) in JSR45 SMAP format. For more information, look at JSR45
* [http://jcp.org/en/jsr/detail?id=45].
*
* @author Byeongcheol Lee
*/
public class SourceMapExtractor {
/**
* A regular expression to recognize line number directive like the following.
*
* //#line ${lineno} ${source file}
*/
private static final Pattern lineDirectivePattern = Pattern
.compile("^\\s*//#line\\s+([0-9]+)\\s+\"(\\S+)\"$");
/**
* Print a command line usage, and exit with an error code.
*
* @param msg A user message to explain what was wrong.
*/
private static void usage(String msg) {
String umsg = "usage: SourceMapExtractor [source file]";
System.err.println(umsg + "\n" + msg);
System.exit(-1);
}
/**
* Drives printing the source remapping infomation for the given
* #line-decorated source file.
*
* @param args A command line arguments.
*/
public static void main(String[] args) {
String outputSourceFileName = null;
// parse command line argument
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if (outputSourceFileName == null) {
outputSourceFileName = arg;
} else {
usage("can not recognize command line option:" + arg);
}
}
// check outputSourceFileName is readable
if (outputSourceFileName == null) {
usage("please, specify [java source file]");
}
File javaSourceFile = new File(outputSourceFileName);
if (!javaSourceFile.isFile()) {
usage("can not find " + outputSourceFileName);
}
if (!javaSourceFile.canRead()) {
usage("check permission for reading " + outputSourceFileName);
}
try {
// read output source file to get source-to-source mapping
SourceMapExtractor smapgen = new SourceMapExtractor(outputSourceFileName);
smapgen.genSMAP();
// print the source-to-source map to standard output.
System.out.println(smapgen.toStringInSMAPFormat());
} catch (IOException ioe) {
System.err.println("failed in extracting SMAP from the "
+ outputSourceFileName);
}
}
/**
* The input source file.
*/
final protected String sourceFile;
/**
* The total number of lines in the source file.
*/
protected int numSourceLines = 0;
/**
* The line-to-line mapping from the input source file to the output source
* file.
*/
final protected TreeSet<SMAPLineEntry> smapLineEntries = new TreeSet<SMAPLineEntry>();
/**
* The mapping from an inputSourceFile to its unique id. Here, the input
* source files appear in the line directive.
*/
final protected HashMap<String, Integer> inputSourceFile2id = new HashMap<String, Integer>();
/**
* The number of input source files so far. This is to generate unique id for
* new input file.
*/
protected int numInputSourceFiles = 0;
/**
* A reverse mapping from an output source line to an input source line in
* case of a single input source file.
*/
private int[] outputSourceLine2inputSourceLine;
/**
* @param s An input source file.
*/
public SourceMapExtractor(String s) {
this.sourceFile = s;
}
/**
* @return A number of input source files in the source-to-source mapping.
*/
public int getNumberOfInputSourceFiles() {
return numInputSourceFiles;
}
/**
* Search for line number directive (//#line ...) to generate a
* source-to-source mapping information.
*/
public void genSMAP() throws IOException {
FileReader freader = new FileReader(sourceFile);
LineNumberReader lnr = new LineNumberReader(freader);
LineNumberDirective prevJNILineDirective = null;
while (true) {
String line = lnr.readLine();
if (line == null) {
numSourceLines = lnr.getLineNumber();
// EOF
if (prevJNILineDirective != null) {
addJNISourceLine(prevJNILineDirective, lnr.getLineNumber());
}
break;
}
// check the line is of the form: //#line ${line number} ${filename}
LineNumberDirective curJNILineDirective = checkJNILineNumberDirective(
line, lnr.getLineNumber());
if (curJNILineDirective == null) {
continue;
}
// handle line directive: //#line ${line number} ${filename}
if (prevJNILineDirective != null) {
// flush the previous line number directive.
addJNISourceLine(prevJNILineDirective, lnr.getLineNumber() - 1);
}
prevJNILineDirective = curJNILineDirective;
}
}
/**
* Check if the current line is line number directive. If this line is not a
* line number directive, this method returns null. Otherwise, this method
* returns a line number directive object.
*
* @param line A line to be checked.
* @param lineNumber The current java line where the line appears.
* @return An line number directive object.
*/
private LineNumberDirective checkJNILineNumberDirective(String line,
int lineNumber) {
// try paring : //#line ${file name} ${line number}
final Matcher m = lineDirectivePattern.matcher(line);
if (!m.matches()) {
return null;
}
assert (m.group(1).length() > 0) && (m.group(2).length() > 0);
int jniLineNumber = Integer.parseInt(m.group(1));
String fileName = m.group(2);
int fileID = ensureSourceFileID(fileName);
LineNumberDirective directive = new LineNumberDirective(fileID,
jniLineNumber, lineNumber);
return directive;
}
/**
* Add a line-to-line mapping entry to the smapLineEntries table.
*
* @param lineDirective A line number directive.
* @param lineEnd The end line number that the lineDirective affects.
*/
private void addJNISourceLine(final LineNumberDirective lineDirective,
final int lineEnd) {
final int javaBegin = lineDirective.outputSourceLineNumber + 1;
final int count = lineEnd - javaBegin + 1;
// non-empty java line
assert (javaBegin > 0) && (lineEnd > 0) && (count >= 1);
final int jniLineStart = lineDirective.inputSurceLineNumber;
final int jniFileID = lineDirective.sourceFileID;
SMAPLineEntry entry = new SMAPLineEntry(jniLineStart, jniFileID, javaBegin,
count);
smapLineEntries.add(entry);
}
/**
* Ensure the input source file id is present in the inputSourceFile2id table.
*
* @param sourceFile A source file name.
* @return A file identification number.
*/
private int ensureSourceFileID(final String sourceFile) {
int id;
if (inputSourceFile2id.containsKey(sourceFile)) {
id = inputSourceFile2id.get(sourceFile);
} else {
numInputSourceFiles++;
id = numInputSourceFiles;
inputSourceFile2id.put(sourceFile, id);
}
assert inputSourceFile2id.containsKey(sourceFile);
return id;
}
/**
* This routine returns a source file name that appeared in all the line
* number directive. This routine is only for the -fatten option in the
* ClassSOurceRemapper.
*
* @return A source file name.
*/
protected String getSingleSourceFileName() {
assert numInputSourceFiles == 1;
String singleJNIFile = null;
for (final String jniFile : inputSourceFile2id.keySet()) {
singleJNIFile = jniFile;
break;
}
return singleJNIFile;
}
/**
* Given output source line number (e.g. in Main.java), this routine returns a
* corresponding input source line number (e.g. in Main.jni). This routine is
* only for the -fatten option in the ClassSOurceRemapper.
*
* @param sourceLineNumber An output source line number.
* @return An input source line number.
*/
protected int getSingleSourceLine(int sourceLineNumber) {
assert numInputSourceFiles == 1;
if (outputSourceLine2inputSourceLine == null) {
outputSourceLine2inputSourceLine = new int[numSourceLines + 1];
for (final SMAPLineEntry entry : smapLineEntries) {
int begin = entry.getOutputLineBegin();
int end = entry.getOutputLineEnd();
assert (begin >= 1) && (end >= begin);
for (int outputLine = begin; outputLine <= end; outputLine++) {
int inputLine = entry.getInputSourceLine(outputLine);
int oldInputLine = outputSourceLine2inputSourceLine[outputLine];
if (oldInputLine >= 1) {
// take minium if conflict
if (oldInputLine < inputLine) {
outputSourceLine2inputSourceLine[outputLine] = inputLine;
}
} else {
// first time
outputSourceLine2inputSourceLine[outputLine] = inputLine;
}
}
}
}
assert outputSourceLine2inputSourceLine != null;
return outputSourceLine2inputSourceLine[sourceLineNumber];
}
/**
* Return the source-to-source information in SMAP (JSR45) format.
*
* @return A text in SMAP format.
*/
public String toStringInSMAPFormat() {
CharArrayWriter wa = new CharArrayWriter();
PrintWriter w = new PrintWriter(wa);
w.println("SMAP");
w.println(sourceFile);
w.println("JNI");
w.println("*S JNI");
// list of jni source file name and id
w.println("*F");
for (final String fname : inputSourceFile2id.keySet()) {
int fid = inputSourceFile2id.get(fname);
w.println("" + fid + " " + fname);
}
// list of JNI to java line number with the following format (JSR45)
w.println("*L");
for (final SMAPLineEntry jline : smapLineEntries) {
w.println(jline.toStringInSMAPFormat());
}
w.println("*E");
w.close();
return wa.toString();
}
/**
* A SMAP line mapping entry class.
*
*/
private static final class SMAPLineEntry implements Comparable<SMAPLineEntry> {
/**
* These fields represent a partial continuous line number mapping from the
* input source files to the output source file. For futher information,
* look at JSR45.
*/
private final int inputStartLine;
private final int inputFileID;
private final int repeat;
private final int outputStartline;
private final int ouputLineIncrement;
/**
* @param inputLine A beginning input source line number.
* @param inputFileID An intput source file id number.
* @param outputLineStart A beginning output source line number.
* @param count A number of lines in the inputr source.
*/
SMAPLineEntry(int inputLine, int inputFileID, int outputLineStart, int count) {
assert (inputLine >= 1) && (outputLineStart >= 1) && (count > 0);
this.inputStartLine = inputLine;
this.inputFileID = inputFileID;
this.repeat = count;
this.outputStartline = outputLineStart;
this.ouputLineIncrement = 1;
}
/**
* @return The beginning input source line number.
*/
private int getInputLineBegin() {
return inputStartLine;
}
/**
* @return The ending input source line number.
*/
private int getInputLineEnd() {
return inputStartLine + repeat - 1;
}
/**
* @return The beginning output source line number.
*/
private int getOutputLineBegin() {
return outputStartline;
}
/**
* @return The end of output source line number.
*/
private int getOutputLineEnd() {
return outputStartline + repeat * ouputLineIncrement - 1;
}
/**
* @param outputLine The line number of the output file.
* @return An line number of the input file.
*/
private int getInputSourceLine(int outputLine) {
assert (outputLine >= getOutputLineBegin())
&& (outputLine <= getOutputLineEnd());
int jniLine = (outputLine - outputStartline) / ouputLineIncrement
+ inputStartLine;
assert (jniLine >= inputStartLine) && (jniLine <= getInputLineEnd());
return jniLine;
}
/**
* @return A text in the SMAP format.
*/
private String toStringInSMAPFormat() {
StringBuffer sb = new StringBuffer();
sb.append(inputStartLine);
sb.append("#").append(inputFileID);
if (repeat != 1) {
sb.append(",").append(repeat);
}
sb.append(":").append(outputStartline);
if (ouputLineIncrement != 1) {
sb.append(",").append(ouputLineIncrement);
}
return sb.toString();
}
/**
* Compare and Order smap entry by output source line number.
*
* @param o2 A SMAP entry.
* @return An integer value to compare two SMAP entry.
*/
public int compareTo(SMAPLineEntry o2) {
if (inputFileID != o2.inputFileID) {
return inputFileID - o2.inputFileID;
} else {
if (outputStartline != o2.outputStartline) {
return outputStartline - o2.outputStartline;
} else {
return inputStartLine - o2.inputStartLine;
}
}
}
}
/**
* A class to represent a line number directive event.
*/
private static final class LineNumberDirective {
/**
* The output source file line number where this event happened.
*/
private final int outputSourceLineNumber;
/**
* The input source line number that appeared in the line number directive.
*/
private final int inputSurceLineNumber;
/**
* The id number for the input source file that appeared in the line number
* directive.
*/
private final int sourceFileID;
/**
* @param fid The input source file id number.
* @param inline The input source line number.
* @param outline The outoput source line number.
*/
public LineNumberDirective(int fid, int inline, int outline) {
assert (fid > 0) & (inline > 0) & (outline > 0);
sourceFileID = fid;
inputSurceLineNumber = inline;
outputSourceLineNumber = outline;
}
}
}