/** * This file is part of the OpenJML plugin project. * Copyright (c) 2005-2013 David R. Cok * @author David R. Cok */ package org.jmlspecs.openjml.eclipse; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.StringReader; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.compiler.IProblem; import org.eclipse.jdt.internal.compiler.problem.DefaultProblem; import org.eclipse.jdt.internal.compiler.problem.ProblemSeverities; import org.jmlspecs.annotation.Nullable; import com.sun.tools.javac.code.Symbol; // FIXME - perhaps should be using DefaultProblem.errorReportSource // instead of carting the lineStart and end positions around // FIXME - should we inherit from CategorizedProblem directly? but then we need to copy errorReportSource // FIXME - clean up this file; severities, file position, error range info /** * This is a class that implements the eclipse IProblem interface * tailored for the JmlParser. It is mostly present to provide an * interface between the OpenJML plugin and the internal class that this class * extends, but it also provides some utility functions to produce * and report errors and warnings. It also adds some additional * information to its parent class to hold information relevant to * line and print oriented problem reporting. */ public class JmlEclipseProblem extends DefaultProblem { /** This holds a reference to the source text as known * at the time the problem was created, or null if * the source text is not available. */ public @Nullable String sourceText; /** Holds the symbol, such as a MethodSymbol, to which the problem relates */ public @Nullable Symbol sourceSymbol; /** * Holds the 1-based character position of the beginning of the line indicated * by the line argument (and on which startPosition resides); the * value is -1 if the lineStart information is not available. */ public int lineStart; /** * Holds the 1-based character position of the end of the line indicated * by the line argument (and on which startPosition resides); this is * the character position of the line termination or end of file; * the value is -1 if the line end information is not available. */ public int lineEnd; /** The severity of the problem. This duplicates a private field in the * super class, but we have to because isError() just returns the opposite of * isWarning(), and we cannot get at other severities. */ final public int severity; // These are copied here for reference from the internal superinterface ProblemSeverities // final int Ignore = -1; // during handling only // final int Warning = 0; // during handling only // // final int Error = 1; // when bit is set: problem is error, if not it is a warning // final int AbortCompilation = 2; // final int AbortCompilationUnit = 4; // final int AbortType = 8; // final int AbortMethod = 16; // final int Abort = 30; // 2r11110 // final int SecondaryError = 64; /** * Constructor for a new problem. * @param ifile The workspace IFile in which the offending source text is located, if any (otherwise null) * @param message The problem message * @param id A numerical id for the problem * @param severity The severity of the problem, using one of the constants listed above (e.g. Error, Warning) * @param startPosition The 1-based character position within the line that begins the offending region * @param endPosition The 1-based character position within the line that ends (not one after) the offending region * @param line The 1-based line within the source text containing the offending source text * @param source A reference to the source text, or null * @param lineStart The 1-based character position of the beginning of the stated line in the IFile or source text, or -1 if not known * @param lineEnd The 1-based character position of the end of the stated line (that is, where the line termination character is) in the IFile or source text, or -1 if not known */ public JmlEclipseProblem(IFile ifile, String message, int id, int severity, int startPosition, int endPosition, int line, String source, int lineStart, int lineEnd) { super(ifile == null ? null : ifile.getFullPath().toString().toCharArray(), message, id, null, severity, startPosition, endPosition, line, startPosition - lineStart); // The last argument is the column number of the beginning of the problem; // it does not appear to be used in anything the OpenJML plug-in does. // The null argument between the id and the severity is for arguments to // the problem message. We don't use those in CompilerProblems. this.sourceText = source; this.lineStart = lineStart; this.lineEnd = lineEnd; this.severity = severity; } /** An identifier for the JML category of problems. This is a randomly chosen * number that needs to be different than the numbers defined in * org.eclipse.jdt.core.compiler.CategorizedProblem. */ private final int JML_CAT = 15423; /** An identifier for the JML category of problems. This is a randomly chosen * number that needs to be different than the numbers defined in * org.eclipse.jdt.core.compiler.CategorizedProblem. */ public int getCategoryID() { return JML_CAT; } // /** // * A utility method to obtain a reference to the source text for // * an ICompilationUnit, or null if it could not be obtained. // * @param icu The ICompilationUnit // * @return The source text for the ICompilationUnit // */ // private static /*@Nullable*/ String getSource(ICompilationUnit icu) { // if (icu == null) return null; // try { // return icu.getSource(); // } catch (Exception e) { // return null; // } // } // /** // * Converts the given IProblem into an instance of JmlEclipseProblem, // * using additional information from the given ICompilationUnit. // * The line start and end information is not available. // * @param p The base problem // * @param icu The ICompilationUnit from which to get position information // */ // public JmlEclipseProblem(IProblem p, ICompilationUnit icu) { // this((IFile)icu.getResource(), p.getMessage(), p.getID(), // p.isError() ? ProblemSeverities.Error : p.isWarning() ? ProblemSeverities.Warning : -1, // p.getSourceStart(), p.getSourceEnd(), p.getSourceLineNumber(), // getSource(icu), -1, -1); // } /** * A utility method that prints the given problem on the given PrintStream * @param out the stream on which to write the problem * @param p The IProblem to print */ static public void printProblem(PrintStream out, IProblem p) { char[] filenameAsChars = p.getOriginatingFileName(); // The above is the workspace-relative filename String filename = null; IResource resource = null; // translate the workspace-relative filename into a file-system // filename if (filenameAsChars != null) { filename = String.valueOf(p.getOriginatingFileName()); IWorkspace workspace = ResourcesPlugin.getWorkspace(); IWorkspaceRoot root = workspace.getRoot(); resource = root.findMember(filename); if (resource != null) { filename = resource.getLocation().toOSString(); // FIXME make relative to user directory? } } // Print the filename, line number and message int line = p.getSourceLineNumber(); String lineString = line <= 0 ? "" : String.valueOf(line); if (Env.testingMode) { // This format has been historically used for test outputs. // If you change it you will need to regenerate and recheck the // test output files. out.println((filename == null ? "" : "FILE") + " " + lineString + ": [" + (p.getID() & IProblem.IgnoreCategoriesMask)+ "] " + p.getMessage()); } else { // This format generates links to the source code in the Eclipse console String ff = filename == null ? null : filename.replaceAll("\\\\","/"); out.println((filename == null ? lineString : ("("+ff+":"+lineString+")")) + " [" + (p.getID() & IProblem.IgnoreCategoriesMask)+ "] " + p.getMessage()); } if (p.getSourceStart() < 0) return; // Get the line of text, if possible String sline = null; int lineStart = -1; if (p instanceof JmlEclipseProblem) { // We're here if the problem is a JmlEclipseProblem. We have source text. // If we have line start and end information, we can get the offending // line immediately. JmlEclipseProblem cp = (JmlEclipseProblem)p; String stext = cp.sourceText; lineStart = cp.lineStart; if (lineStart >= 0 && stext != null) { // Caution: cp.lineEnd is the characterPosition of the last // line-termination character, if any. int k = cp.lineEnd + 1;// since the end index in substring is one beyond the end sline = stext.substring(lineStart,k > 0 && k <= stext.length() ? k : stext.length()); } } BufferedReader r = null; try { if (sline == null) try { if (p instanceof JmlEclipseProblem) { // We're here if we have JmlEclipseProblem without line start and end // information. This happens if we have a non-JmlEclipseProblem that // was converted to a JmlEclipseProblem. It typically then has source // text but not line start information. String stext = ((JmlEclipseProblem)p).sourceText; if (stext != null) r = new BufferedReader(new StringReader(stext)); } if (r == null && resource != null && (resource instanceof IFile)) { // We're here if we have a non-JmlEclipseProblem, and consequently we // have neither source text nor line start information. So we have to // open the resource and read the requisite number of lines. // This is a bit expensive, but we only do it once for each // error message. r = new BufferedReader(new InputStreamReader(((IFile)resource).getContents())); } if (r == null) return; // Skip (line-1) lines of text, including line terminations, // and count the number of characters read. int count = 0; // Number of chars read int c = 0; --line; if (line > 0) { // TODO: This could perhaps be more efficient, but it is // only used once per error message. c = r.read(); count++; while (line > 0) { if (c == -1) return; // SHould not end before getting // to the beginning of the desired line // If we do, we just pretend that // no text is available if (c == '\r') { do { --line; c = r.read(); count++; if (c == '\n') { // Ignore this following NL c = r.read(); count++; } } while (c == '\r'); } else if (c == '\n') { --line; c = r.read(); count++; } else { c = r.read(); count++; } } } // Have read the first character beyond the end of the requisite // number of lines to be skipped. if (c == '\r' || c == '\n') { // The next line is empty sline = Env.eol; } else if (c == -1) { // Still not enough data return; } else { sline = (char)c + r.readLine() + Env.eol; } if (lineStart == -1) lineStart = count; // lineStart is 1-based r.close(); } catch (Exception e) { Log.errorlog("INTERNAL ERROR - Exception occurred while printing a Problem: " + e,e); return; // OK to quit since we just leave out some detail of the error } if (sline != null) { // Print the line of source text on which the error lies out.print(sline); // line termination usually included, except perhaps if // this is the last line of a file or if the source was // provided directly as a string by a test or if the line was too // long and has been truncated. { int last = sline.length() -1; int c = sline.charAt(last); if (!(c == '\n' || c == '\r')) out.println(); } // Now mark the error itself with ^^ characters from // getSourceStart() through getSourceEnd() // st is the beginning of the error section to mark int st = p.getSourceStart(); int length = p.getSourceEnd() - st + 1; st -= lineStart; // Now st is the amount of white space to put before the first ^ // Do some adjustment in case st points into the line termination characters // This is needed because when problems are marked by highlighting // in the Eclipse editors, no highlighting occurs if the error // is completely in the line termination characters. int extra = 0; if (st >= sline.length()) { extra = st - sline.length(); st = sline.length(); } // The end of the error region can be on the next line, in which // case we still just print one line and put ellipsis at the end // of the string of ^^s boolean ellipsis = false; if (st + extra + length > sline.length() + 2) { length = sline.length() + 2 - st - extra; if (length <= 0) length = 1; ellipsis = true; } int i = 0; while (--st >= 0) out.print(sline.charAt(i++) == '\t'?"\t":" "); while (--extra >= 0) out.print(" "); while (--length >= 0) out.print("^"); if (ellipsis) out.print("..."); out.println(); } } finally { if (r != null) { try { r.close(); } catch (java.io.IOException e) { Log.errorlog("Failed to close a BufferedReader",e); } } } } }