/*
* This file is part of the OpenJML project.
* Author: David R. Cok
*/
package org.jmlspecs.openjml;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.processing.Processor;
import javax.tools.DiagnosticListener;
import javax.tools.JavaFileObject;
import org.jmlspecs.annotation.NonNull;
import org.jmlspecs.annotation.Nullable;
import org.jmlspecs.annotation.Pure;
import org.jmlspecs.openjml.esc.JmlEsc;
import com.sun.tools.javac.code.JmlTypes;
import com.sun.tools.javac.comp.JmlAttr;
import com.sun.tools.javac.comp.JmlCheck;
import com.sun.tools.javac.comp.JmlDeferredAttr;
import com.sun.tools.javac.comp.JmlEnter;
import com.sun.tools.javac.comp.JmlFlow;
import com.sun.tools.javac.comp.JmlMemberEnter;
import com.sun.tools.javac.comp.JmlResolve;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.main.CommandLine;
import com.sun.tools.javac.main.JavaCompiler;
import com.sun.tools.javac.main.Option;
import com.sun.tools.javac.main.Main.Result;
import com.sun.tools.javac.parser.ExpressionExtension;
import com.sun.tools.javac.parser.JmlFactory;
import com.sun.tools.javac.parser.JmlScanner;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCAssign;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCLiteral;
import com.sun.tools.javac.tree.JCTree.JCModifiers;
import com.sun.tools.javac.tree.JCTree.JCNewArray;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.JavacMessages;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Log;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;
import com.sun.tools.javac.util.Log.WriterKind;
import com.sun.tools.javac.util.Options;
/**
* This class is the main entry point for the JML tool. It uses the javac
* compiler, but overrides some of the functionality here in order to register
* tools for the compiler to use that are extensions with JML functionality of
* the usual tools. Also the handling of JML-specific options is initialized
* here. For programmatic access to the facilities of openjml, use the methods
* of the API class.
*
* <P>
* NOTE ON OUTPUT:
* <P>
* Output is produced by this program in a variety of ways, through mechanisms
* that enable it to be captured for various means of display by the wrapping
* UI.
* <P>
* Log - there is one Log instance per context, retrieved by Log.instance(context).
* It has a number of writers:
* <UL>
* <LI> log.noticeWriter - general information, often guarded by tests of the
* verbose flag
* <LI> log.warnWriter - for compiler/JML/ESC warnings
* <LI> log.errWriter - for compiler/JML/ESC errors
*
* <LI> The IProgressReporter - this is used for messages that report progress back
* to the UI (textual or graphic).
*
* <LI> Diagnostic listener - this listens for warnings and errors, as written to
* log.warnWriter and log.errWriter.
* </UL>
* <P>
* The output of the log goes to the registered instance of Log [ by
* context.put(logKey, new Log(...)) ]. If no log is registered, a default log
* is used, which sends all channels of information to the PrintWriter registered
* under context.put(outKey,...). There is a constructor for a Log that allows
* the notice, warning and error output to be directed to different PrintWriters.
* <P>
* If there is no DiagnosticListener registered, the warning and error outputs
* are treated as described above. If there is a DiagnosticListener, then the
* diagnostic output is sent to that listener instead.
* <P>
* For OpenJML:
* <UL><LI>command-line tool: no diagnostic listener is defined; Log output can be
* directed to either System.out or System.err by a command-line option
* <LI>Eclipse: a diagnostic listener is defined that converts warnings and errors
* into Eclipse markers; Log output is captured by a PrintWriter that directs
* the output to the Eclipse console.
* </UL><P>
* Progress Reporting: Various tools within OpenJML report progress (e.g. as
* individual files are parsed). The progress messages are tiered:
* <UL>
* <LI> level 1: low volume, appropriate for text and normal output
* <LI> level 2: transient progress messages
* <LI> level 3: verbose only
* </UL>
* A tool can receive these progress reports by
* registering as a delegate via
*
* TODO - check and complete the documentation above
*/
public class Main extends com.sun.tools.javac.main.Main {
public static final int EXIT_CANCELED = -1;
/** The compilation unit context associated with this instance of Main
* (for the programmatic API); for the command-line API it is simply
* the most recent value of the context, and is used that way in testing. */
protected Context context;
public boolean canceled = false;
/** The diagListener provided when an instance of Main is constructed.
* The listener will be notified when any diagnostic is generated.
*/
@Nullable
protected DiagnosticListener<? extends JavaFileObject> diagListener;
/** Instances of this class are used to abruptly terminate long-running
* JML operations.
* @author David R. Cok
*/
public static class JmlCanceledException extends RuntimeException {
private static final long serialVersionUID = 1L;
public JmlCanceledException(String message) {
super(message);
}
}
// TODO - review use of and document these progress reporters; perhaps move them
public DelegatingProgressListener progressDelegate = new DelegatingProgressListener();
/** An interface for progress information; the implementation reports progress
* by calling report(...); clients will receive notification of progress
* events by implementing this interface and registering the listener with
* progressDelegate.setDelegate(IProgressReporter).
*
*/
public static interface IProgressListener {
void setContext(Context context);
boolean report(int ticks, int level, String message);
}
/** The compilation Context only allows one instance to be registered for
* a given class and that instance cannot be changed.
* So we register an instance of DelegatingProgressListener;
* then we can change the delegate registered in DelegatingProgressListener
* to our heart's content.
* <P>
* This class is a Listener that simply passes on reports to its own listeners.
* It currently only allows one delegate.
*/
static public class DelegatingProgressListener implements IProgressListener {
/** The delegate to which this class passes reports. */
private IProgressListener delegate;
/** The callback that is called when the application has progress to report */
public boolean report(int ticks, int level, String message) {
if (delegate != null) return delegate.report(ticks,level,message);
return false;
}
/** Returns true if there is a listener. */
@Pure
public boolean hasDelegate() { return delegate != null; }
/** Sets a listener; may be null to erase a listener. */
public void setDelegate(@Nullable IProgressListener d) {
delegate = d;
}
/** Sets the compilation context (passed on to listeners) */
@Override
public void setContext(Context context) { if (delegate!= null) delegate.setContext(context); }
}
/** This class is a progress listener that prints the progress messages to
* a given OutputStream.
*/
public static class PrintProgressReporter implements IProgressListener {
protected PrintWriter pw;
protected Context context;
public PrintProgressReporter(Context context, OutputStream out) {
pw = new PrintWriter(out);
this.context = context;
}
public PrintProgressReporter(Context context, PrintWriter w) {
pw = w;
this.context = context;
}
@Override
public boolean report(int ticks, int level, String message) {
if (level <= 1 || (context != null && Utils.instance(context).jmlverbose >= Utils.JMLVERBOSE)) {
pw.println(message);
pw.flush();
}
return false;
}
@Override
public void setContext(Context context) { this.context = context; }
}
/**
* Construct a compiler instance; all options are set to values read from
* the environment.
* Errors go to stderr.
*/
// @edu.umd.cs.findbugs.annotations.SuppressWarnings("NM_SAME_SIMPLE_NAME_AS_SUPERCLASS")
public Main() throws java.io.IOException {
this(Strings.applicationName, new PrintWriter(System.err, true), null, null);
}
/**
* Construct an initialized compiler instance; all options are set
* according to values from the options and args arguments.
* @param applicationName the name to use for the application
* @param out the PrintWriter to which to send information and error messages
* @param diagListener the listener to receive problem and warning reports
* @param options the default set of options (adjusted by the command-line
* args); if null, then default values are read from the environment
* @param args command-line options
*/
public Main(/*@ non_null */String applicationName,
/*@ non_null */PrintWriter out,
@Nullable DiagnosticListener<? extends JavaFileObject> diagListener,
@Nullable Options options,
String... args)
throws java.io.IOException {
super(applicationName,out);
initialize(out,diagListener,options,args);
}
protected void initialize(
/*@ non_null */PrintWriter out,
@Nullable DiagnosticListener<? extends JavaFileObject> diagListener,
@Nullable Options options,
String... args) {
check(); // Aborts if the environment does not support OpenJML
this.out = out;
this.diagListener = diagListener;
context = new Context();
log = uninitializedLog();
JavacFileManager.preRegister(context); // can't create it until Log has been set up
register(context);
if (args == null) args = emptyArgs;
initializeOptions(options,args); // Creates a new Options instance and initializes it per the arguments
}
public PrintWriter out() {
return out;
}
/** Checks that the tool is being run with an adequate JVM - and exits abruptly if not */
public void check() {
javax.lang.model.element.ElementKind[] kinds = javax.lang.model.element.ElementKind.values();
if (kinds[kinds.length-1] == javax.lang.model.element.ElementKind.OTHER) {
System.out.println("OpenJML is being run with a Java 6 or earlier VM, which is not supported.");
System.exit(99);
}
}
/** The external entry point - simply calls execute(args) and exits with the
* exit code returned.
* @param args the command-line arguments
*/
//@ requires args != null && \nonnullelements(args);
public static void main(String[] args) throws Exception {
if (args.length > 0 && args[0].equals("-Xjdb")) {
// Note: Copied directly from com.sun.tools.javac.Main and not tested
String[] newargs = new String[args.length + 2];
Class<?> c = Class.forName("com.sun.tools.example.debug.tty.TTY");
Method method = c.getDeclaredMethod ("main", new Class[] {args.getClass()});
method.setAccessible(true);
System.arraycopy(args, 1, newargs, 3, args.length - 1);
newargs[0] = "-connect";
newargs[1] = "com.sun.jdi.CommandLineLaunch:options=-esa -ea:com.sun.tools...";
newargs[2] = "org.jmlspecs.openjml.Main";
method.invoke(null, new Object[] { newargs });
} else {
System.exit(execute(args));
}
}
// TODO: Move these? Get them from Option?
/** The option string for requesting help information */
@NonNull
final public static String helpOption = "-help";
/** The option string for running jmldoc */
@NonNull
final public static String jmldocOption = "-doc";
/** Invokes the compiler on the given command-line arguments; errors go to
* stdout.
* @param args the command-line arguments
* @return the exit code, as returned to the shell - 0 is success
*/
//@ requires args != null && \nonnullelements(args);
public static int execute(String[] args) {
if (args != null) {
for (String a: args) {
if (jmldocOption.equals(a)) {
return 4; // FIXME - org.jmlspecs.openjml.jmldoc.Main.execute(args);
}
}
}
return execute(args,false); // The boolean: true - errors to stdErr, false - errors to stdOut
}
/** A programmatic interface to the compiler that returns the exit code, but
* does not itself call System.exit. [This is called execute rather than
* compile as in com.sun.tools.javac.Main because we also have to override
* com.sun.tools.javac.main.Main.compile ]
* @param args the command-line arguments
* @param useStdErr if true, errors go to stderr; if false they go to stdout
* @return the exit code as sent to the shell (0 is success)
*/
//@ requires args != null && \nonnullelements(args);
public static int execute(String[] args, boolean useStdErr) {
return execute(new PrintWriter(useStdErr ? System.err : System.out, true), null, null, args);
}
/** Static method to do the work of Main.
*
* @param writer where to write output that is not sent to the diagnosticListener
* @param diagListener a listener to hear any compiler diagnostics produced
* @param options the default set of options to use (including system properties)
* @param args the command-line arguments
* @return the exit code
*/
public static int execute(@NonNull PrintWriter writer, @Nullable DiagnosticListener<? extends JavaFileObject> diagListener, @Nullable Options options, @NonNull String[] args) {
int errorcode = com.sun.tools.javac.main.Main.Result.ERROR.exitCode; // 1
try {
if (args == null) {
uninitializedLog().error("jml.main.null.args","org.jmlspecs.openjml.Main.main");
errorcode = com.sun.tools.javac.main.Main.Result.CMDERR.exitCode; // 2
} else {
// We have to interpret the -java option before we start
// the compiler (which does the normal option processing).
// Since this is rare, we'll require that it be the first
// option.
boolean useJavaCompiler = args.length > 0 &&
args[0].equals(JmlOption.USEJAVACOMPILER.optionName());
if (useJavaCompiler) {
String[] newargs = new String[args.length-1];
System.arraycopy(args,1,newargs,0,newargs.length);
if (options != null) {
uninitializedLog().warning("jml.ignoring.options");
}
errorcode = com.sun.tools.javac.Main.compile(newargs);
} else {
// We create an instance of main through which to call the
// actual compile method. Note though that the compile method
// does its own initialization (in the super class). Thus the
// context and any option processing in the constructor call
// are thrown away. That is also why we do the hack of saving
// the options to a private variable, just to be able to
// apply them in the compile() call below.
Main compiler = new Main(Strings.applicationName, writer, diagListener, options, emptyArgs);
savedOptions = Options.instance(compiler.context());
// The following line does an end-to-end compile, in a fresh context
errorcode = compiler.compile(args).exitCode; // context and new options are created in here
if (//errorcode > Result.CMDERR.exitCode ||
Utils.instance(compiler.context()).jmlverbose >= Utils.JMLVERBOSE) {
writer.println("ENDING with exit code " + errorcode);
}
}
}
} catch (JmlCanceledException e) {
// Error message already issued
errorcode = Result.CMDERR.exitCode;
} catch (Exception e) {
// Most exceptions are caught prior to this, so this will happen only for the
// most catastrophic kinds of failure such as failures to initialize
// properly. (You can test this by programmatically throwing an exception in the try
// block above.)
uninitializedLog().error("jml.toplevel.exception",e);
e.printStackTrace(System.err);
errorcode = com.sun.tools.javac.main.Main.Result.SYSERR.exitCode; // 3
}
return errorcode;
}
/** Executes the given command-line, but within the same instance of Main;
* the instance of Main is completely reinitialized, with a new context.
* @param writer
* @param diagListener
* @param options
* @param args
* @return
*/
public int executeNS(@NonNull PrintWriter writer, @Nullable DiagnosticListener<? extends JavaFileObject> diagListener, @Nullable Options options, @NonNull String[] args) {
int errorcode = com.sun.tools.javac.main.Main.Result.ERROR.exitCode; // 1
try {
if (args == null) {
uninitializedLog().error("jml.main.null.args","org.jmlspecs.openjml.Main.main");
errorcode = com.sun.tools.javac.main.Main.Result.CMDERR.exitCode; // 2
} else {
// We create an instance of main through which to call the
// actual compile method. Note though that the compile method
// does its own initialization (in the super class). Thus the
// context and any option processing in the constructor call
// are thrown away. That is also why we do the hack of saving
// the options to a private variable, just to be able to
// apply them in the compile() call below.
savedOptions = Options.instance(context());
initialize(writer, diagListener, options, emptyArgs);
// The following line does an end-to-end compile, in a fresh context
errorcode = compile(args).exitCode; // context and new options are created in here
if (errorcode > Result.CMDERR.exitCode ||
Utils.instance(context()).jmlverbose > Utils.PROGRESS) {
writer.println("ENDING with exit code " + errorcode); // TODO - not sure we want this - but we'll need to change the tests
}
}
} catch (JmlCanceledException e) {
// Error message already issued
errorcode = EXIT_CANCELED; // Indicates being cancelled
} catch (Exception e) {
// Most exceptions are caught prior to this, so this will happen only for the
// most catastrophic kinds of failure such as failures to initialize
// properly. (You can test this by programmatically throwing an exception in the try
// block above.)
uninitializedLog().error("jml.toplevel.exception",e);
e.printStackTrace(System.err);
errorcode = com.sun.tools.javac.main.Main.Result.SYSERR.exitCode; // 3
}
return errorcode;
}
// FIXME - this is a hack to communicate a parameter to where it can be used
public IAPI.IProofResultListener proofResultListener;
public int executeNS(@NonNull PrintWriter writer, @Nullable DiagnosticListener<? extends JavaFileObject> diagListener, IAPI.IProofResultListener prListener, @Nullable Options options, @NonNull String[] args) {
int errorcode = com.sun.tools.javac.main.Main.Result.ERROR.exitCode; // 1
this.proofResultListener = prListener;
try {
if (args == null) {
uninitializedLog().error("jml.main.null.args","org.jmlspecs.openjml.Main.main");
errorcode = com.sun.tools.javac.main.Main.Result.CMDERR.exitCode; // 2
} else {
// We create an instance of main through which to call the
// actual compile method. Note though that the compile method
// does its own initialization (in the super class). Thus the
// context and any option processing in the constructor call
// are thrown away. That is also why we do the hack of saving
// the options to a private variable, just to be able to
// apply them in the compile() call below.
savedOptions = Options.instance(context());
initialize(writer, diagListener, options, emptyArgs);
// The following lines do an end-to-end compile, in a fresh context
errorcode = compile(args).exitCode; // context and new options are created in here
if (errorcode > Result.CMDERR.exitCode ||
Utils.instance(context()).jmlverbose > Utils.PROGRESS) {
writer.println("ENDING with exit code " + errorcode); // TODO - not sure we want this - but we'll need to change the tests
}
}
} catch (JmlCanceledException e) {
// Error message already issued
errorcode = EXIT_CANCELED; // Indicates being cancelled
} catch (Exception e) {
// Most exceptions are caught prior to this, so this will happen only for the
// most catastrophic kinds of failure such as failures to initialize
// properly. (You can test this by programmatically throwing an exception in the try
// block above.)
uninitializedLog().error("jml.toplevel.exception",e);
e.printStackTrace(System.err);
errorcode = com.sun.tools.javac.main.Main.Result.SYSERR.exitCode; // 3
}
return errorcode;
}
/** This is a convenience method to initialize just enough that we can log
* an error or warning message for issues that arise before the compiler
* is properly initialized.
* @return a Log instance to use
*/
static protected Log uninitializedLog() {
Context context = new Context();
// This is a temporary context just for logging error messages when
// overall initialization fails.
// It is not the one used for the options and compilation
JavacMessages.instance(context).add(Strings.messagesJML);
return Log.instance(context);
}
/** Just used to communicate a set of options from execute(...) to
* compile(...), since it cannot be passed through the super call.
*/
static private Options savedOptions = null;
/** This method is overwritten so that the JML compiler can register its
* own tools for the various phases. This fits in just the right place in
* the compiler initialization and invocation sequence, but I'm not sure how
* fragile it is with updates and reorganizations of the code.
*
* @param args the command-line arguments
* @param context the compilation context to use
* @return the value to use as the command-line exit code
*/
//@ requires args != null && \nonnullelements(args);
//@ requires context != null;
//@Override
//public int compile(String[] args, Context context) {
@Override
public Main.Result compile(String[] args,
Context context,
List<JavaFileObject> fileObjects,
Iterable<? extends Processor> processors) {
this.context = context;
register(context);
context.put(IAPI.IProofResultListener.class, proofResultListener);
initializeOptions(savedOptions);
if (args.length == 0
&& (processors == null || !processors.iterator().hasNext())
&& fileObjects.isEmpty()) {
help();
return Result.CMDERR;
}
// Note that the Java option processing happens in compile method call below.
// Those options are not read at the time of the register call,
// but the register call has to happen before compile is called.
canceled = false;
Main.Result exit = super.compile(args,context,fileObjects,processors);
// if (Options.instance(context).get(helpOption) != null) {
// helpJML(out);
// }
if (canceled) exit = Result.CANCELLED;
return exit;
}
@Override
protected void help() {
Option.HELP.process(optionHelper,"-help");
PrintWriter p = optionHelper.getLog().getWriter(WriterKind.NOTICE);
helpJML(p);
JmlOptions.instance(context).put(Option.HELP,"");
}
/** This is a utility method to print out all of the JML help information */
protected void helpJML(PrintWriter w) {
w.print(JmlOption.helpInfo());
w.flush();
}
/** This method scans the input sequence of command-line arguments,
* processing any that are recognized as JML options. Those options
* are registered in the Options map. The String[] returned contains
* all of the command-line arguments in the input, omitting those recognized
* as JML options.
* @param args the input command-line arguments
* @param options the Options map in which recognized options are recorded
* @return all arguments not recognized as JML
*/
//@ requires \nonnullelements(args);
//@ ensures \result != null && \nonnullelements(\result);
String[] processJmlArgs(@NonNull String [] args, @NonNull Options options, ListBuffer<File> jmlfiles) {
java.util.List<String> newargs = new ArrayList<String>();
java.util.List<String> files = new ArrayList<String>();
int i = 0;
while (i<args.length) {
i = processJmlArg(args,i,options,newargs,files);
}
//newargs.addAll(computeDependencyClosure(files));
newargs.addAll(files);
File f;
Iterator<String> iter = newargs.iterator();
while (iter.hasNext()) {
String s = iter.next();
if (s.endsWith(Strings.specsSuffix) && (f=new File(s)).exists()) {
jmlfiles.add(f);
iter.remove();
}
}
return newargs.toArray(new String[newargs.size()]);
}
// TODO _ document
public java.util.List<String> computeDependencyClosure(java.util.List<String> files) {
// fill out dependencies
java.util.List<String> newargs = new ArrayList<String>();
// Dependencies d = Dependencies.instance(context);
Set<String> done = new HashSet<String>();
java.util.List<String> todo = new LinkedList<String>();
//JavacFileManager jfm = (JavacFileManager)context.get(JavaFileManager.class);; // FIXME - this causes Lint to be initialized before the Java options are registered
for (String s: files) {
// FIXME - don't hardcode this list of suffixes here
if (Utils.instance(context).hasValidSuffix(s)) {
todo.add(s);
}
}
while (!todo.isEmpty()) {
String o = todo.remove(0);
if (done.contains(o)) continue;
done.add(o);
// if (o.getName().endsWith(".java")) newargs.add("C:" + o.toUri().getPath()); // FIXME - a hack to get the full path
if (o.endsWith(".java")) newargs.add(o); // FIXME FOR REAL - this needs to be made sane
// Set<JavaFileObject> affected = d.getAffected(o);
// if (affected != null) {
// todo.addAll(affected);
// //Log.instance(context).noticeWriter.println("Adding " + affected.size() + " dependencies for " + o);
// }
}
return newargs;
}
/** Processes a single JML command-line option and any arguments.
*
* @param args the full array of command-line arguments
* @param i the index of the argument to be processed
* @param options the options object to be adjusted as JML options are found
* @param remainingArgs any arguments that are not JML options
* @return the index of the next argument to be processed
*/
//@ requires \nonnullelements(args);
//@ requires (* elements of remainingArgs are non-null *);
//@ requires 0<= i && i< args.length;
//@ ensures \result > i;
int processJmlArg(@NonNull String[] args, int i, @NonNull Options options, @NonNull java.util.List<String> remainingArgs, @NonNull java.util.List<String> files ) {
String res = "";
String s = args[i++];
if (s == null || s.isEmpty()) return i; // For convenience, allow but ignore null or empty arguments
if (s.length() > 1 && s.charAt(0) == '"' && s.charAt(s.length()-1) == '"') {
s = s.substring(1,s.length()-1);
}
boolean negate = false;
if (s.startsWith("-no-")) {
negate = true;
s = s.substring("-no".length());
}
IOption o = JmlOption.find(s);
while (o!=null && o.synonym()!=null) {
s = o.synonym();
if (s.startsWith("-no-")) {
negate = !negate;
s = s.substring("-no".length());
}
o = JmlOption.find(s);
}
if (o == null) {
int k = s.indexOf('=');
if (k != -1) {
res = s.substring(k+1,s.length());
s = s.substring(0,k);
o = JmlOption.find(s);
if (o != null) {
if (o.hasArg()) {}
else if ("false".equals(res)) negate = true;
else if ("true".equals(res)) res = "";
else {
res = "";
Log.instance(context).warning("jml.ignoring.parameter",s);
}
} else if (s.isEmpty()) {
res = o.defaultValue().toString();
}
}
} else if (!negate && o.hasArg()) {
if (i < args.length) {
res = args[i++];
if (res != null && res.length() > 1 && res.charAt(0) == '"' && s.charAt(res.length()-1) == '"') {
res = res.substring(1,res.length()-1);
}
} else {
res = "";
Log.instance(context).warning("jml.expected.parameter",s);
}
}
if (o == null) {
if (s.equals("-help")) {
help();
} else {
remainingArgs.add(s);
}
} else if (JmlOption.DIR.optionName().equals(s) || JmlOption.DIRS.optionName().equals(s)) {
java.util.List<File> todo = new LinkedList<File>();
todo.add(new File(res));
if (JmlOption.DIRS.optionName().equals(s)) {
while (i<args.length && (res=args[i]).length() > 0 && res.charAt(0) != '-') {
todo.add(new File(res));
i++;
}
}
Utils utils = Utils.instance(context);
while (!todo.isEmpty()) {
File file = todo.remove(0);
if (file.isDirectory()) {
File[] fileArray = file.listFiles();
// Comparator is intentionally reversed, so we push items on the front of the queue in reverse order
Arrays.sort(fileArray, new java.util.Comparator<File>(){ public int compare(File f, File ff) { return -f.getPath().compareTo(ff.getPath()); }});
for (File ff: fileArray) {
todo.add(0,ff);
}
} else if (file.isFile()) {
String ss = file.toString();
if (utils.hasJavaSuffix(ss)) files.add(ss); // FIXME - if we allow .jml files on the command line, we have to guard against parsing them twice
}
}
} else {
if (negate) {
if (o.defaultValue() instanceof Boolean) {
JmlOption.setOption(context, o, false);
} else if (o.defaultValue() == null) {
options.put(s,null);
} else {
options.put(s,o.defaultValue().toString());
}
} else {
if (o.defaultValue() instanceof Boolean) {
JmlOption.setOption(context, o, true);
} else {
options.put(s,res);
}
}
}
if(o != null && o.equals(JmlOption.PROPERTIES)){
Properties properties = System.getProperties();
String file = JmlOption.value(context,JmlOption.PROPERTIES);
try {
if(file != null){
Utils.readProps(properties,file);
}
} catch (java.io.IOException e) {
Log.instance(context).getWriter(WriterKind.NOTICE).println("Failed to read property file " + file); // FIXME - review
}
setPropertiesFileOptions(options, properties);
}
return i;
}
/** This method is called after options are read, but before tools are
* registered and before compilation actually begins;
* here any additional option checking or
* processing can be performed.
*/
// This should be able to be called without difficulty whenever any option
// is changed
public boolean setupOptions() {
// CAUTION: If tools cache values of options and have their singleton
// instance created before the options are completely processed, the
// tool will grab some default version of the option.
// Crucially, Log does this.
Options options = Options.instance(context);
Utils utils = Utils.instance(context);
// if (options.get(helpOption) != null) {
// return false;
// }
String t = options.get(JmlOption.JMLTESTING.optionName());
Utils.testingMode = ( t != null && !t.equals("false"));
if (Utils.testingMode) {
if (options.get(JmlOption.BENCHMARKS.optionName()) == null) {
options.put(JmlOption.BENCHMARKS.optionName(),"benchmarks");
}
}
String benchmarkDir = options.get(JmlOption.BENCHMARKS.optionName());
if (benchmarkDir != null) {
new File(benchmarkDir).mkdir();
}
utils.jmlverbose = Utils.NORMAL;
String n = JmlOption.VERBOSENESS.optionName().trim();
String levelstring = options.get(n);
if (levelstring != null) {
levelstring = levelstring.trim();
if (!levelstring.isEmpty()) try {
utils.jmlverbose = Integer.parseInt(levelstring);
} catch (NumberFormatException e) {
Log.instance(context).warning("jml.message","The value of the " + n + " option or the " + Strings.optionPropertyPrefix + n.substring(1) + " property should be the string representation of an integer: \"" + levelstring + "\"");
}
}
if (utils.jmlverbose >= Utils.PROGRESS) {
// We check for an existing delegate, because if someone is calling
// OpenJML programmatically, they may have set one up already.
// Note, though that this won't udo the setting, if verbosity is
// turned off.
if (!progressDelegate.hasDelegate()) progressDelegate.setDelegate(new PrintProgressReporter(context,out));
} else {
progressDelegate.setDelegate(null);
}
boolean b = JmlOption.isOption(context,JmlOption.USEJAVACOMPILER);
if (b) {
Log.instance(context).getWriter(WriterKind.NOTICE).println("The -java option is ignored unless it is the first command-line argument"); // FIXME - change to a warning
}
Cmd cmd = Cmd.CHECK; // default
String val = options.get(JmlOption.COMMAND.optionName());
try {
if (val != null) cmd = Cmd.valueOf(val.toUpperCase());
} catch (IllegalArgumentException e) {
Log.instance(context).error("jml.bad.command",val);
return false;
}
utils.rac = cmd == Cmd.RAC;
utils.esc = cmd == Cmd.ESC;
utils.check = cmd == Cmd.CHECK;
utils.compile = cmd == Cmd.COMPILE;
boolean picked = utils.rac||utils.esc||utils.check||utils.compile;
if (!picked && cmd != null) {
Log.instance(context).error("jml.unimplemented.command",cmd);
return false;
}
if (!picked) utils.check = true;
String keysString = options.get(JmlOption.KEYS.optionName());
utils.commentKeys = new HashSet<String>();
if (keysString != null && !keysString.isEmpty()) {
String[] keys = keysString.split(",");
for (String k: keys) utils.commentKeys.add(k);
}
if (utils.esc) utils.commentKeys.add("ESC");
if (utils.rac) utils.commentKeys.add("RAC");
utils.commentKeys.add("OPENJML");
JmlSpecs.instance(context).initializeSpecsPath();
if (JmlOption.isOption(context,JmlOption.INTERNALRUNTIME)) appendRuntime(context);
String limit = JmlOption.value(context,JmlOption.ESC_MAX_WARNINGS);
if (limit == null || limit.equals("all")) {
utils.maxWarnings = Integer.MAX_VALUE; // no limit is the default
} else {
try {
int k = Integer.parseInt(limit);
utils.maxWarnings = k <= 0 ? Integer.MAX_VALUE : k;
} catch (NumberFormatException e) {
// FIXME _ change to an error
Log.instance(context).getWriter(WriterKind.NOTICE).println("Expected a number or 'all' as argument for -escMaxWarnings: " + limit);
utils.maxWarnings = Integer.MAX_VALUE;
}
}
String check = JmlOption.value(context,JmlOption.FEASIBILITY);
if (check == null || check.equals(Strings.feas_default)) {
options.put(JmlOption.FEASIBILITY.optionName(),check=Strings.feas_defaults);
} else if (check.equals(Strings.feas_all)) {
options.put(JmlOption.FEASIBILITY.optionName(),check=Strings.feas_alls);
}
String badString = Strings.isOK(check);
if (badString != null) {
Log.instance(context).getWriter(WriterKind.NOTICE).println("Unexpected value as argument for -checkFeasibility: " + badString);
}
Extensions.register(context);
return true;
}
/** We override this method in order to process the JML command-line
* arguments and to do any tool-specific initialization
* after the command-line arguments are processed.
*/
// @edu.umd.cs.findbugs.annotations.SuppressWarnings("ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD")
@Override
public Collection<File> processArgs(String[] oargs, String[] classNames) {
Options options = Options.instance(this.context);
ListBuffer<File> jmlfiles = new ListBuffer<File>();
String[] args = processJmlArgs(oargs,options,jmlfiles);
if (filenames == null) filenames = new TreeSet<File>(); // needed when called from the API
Collection<File> files = super.processArgs(args,classNames);
if (files != null) files.addAll(jmlfiles);
//args.addAll(computeDependencyClosure(files));
if (!setupOptions()) return null;
String showOptions = JmlOption.value(context,JmlOption.SHOW_OPTIONS);
if (!showOptions.equals("none")) {
JmlOption.listOptions(context, showOptions.equals("all"));
}
return files;
}
/** This registers the JML versions of many of the tools (e.g. scanner, parser,
* specifications database,...) used by the compiler. They must be registered
* before the Java versions are invoked, since in most cases singleton
* values are lazily created. Note also that since tools can only be registered
* into the context once, circular dependencies can arise from pairs of tools
* that use each other, if you are not careful.
* @param context the compilation context into which to register the tools
*/
public void register(/*@ non_null @*/ Context context) {
this.context = context;// A hack so that context is available in processArgs()
if (progressDelegate != null) progressDelegate.setContext(context);
context.put(IProgressListener.class,progressDelegate);
context.put(key, this);
registerTools(context,out,diagListener);
}
public static Context.Key<Main> key = new Context.Key<Main>();
public static Main instance(Context context) {
return context.get(key);
}
/** Called to register the JML internal tools that replace the tools used
* in the Java compiler.
* @param context the compiler context in which the tools are to be used
* @param out the PrintWriter used for error and informational messages
* @param diagListener if not null, a listener that will receive reports
* of warnings and errors
*/
public static <S> void registerTools(@NonNull Context context,
@NonNull PrintWriter out,
@Nullable DiagnosticListener<S> diagListener) {
// We register the output writer for the Log first so that it is
// available if tool registration (or argument processing) needs the
// Log. However, note that if the Log itself is actually instantiated before
// Java arguments are read, then it is not set consistently with those
// options.
context.put(Log.outKey,out);
if (diagListener != null) context.put(DiagnosticListener.class, diagListener);
// These have to be first in case there are error messages during
// tool registration.
// registering an additional source of JML-specific error messages
JavacMessages.instance(context).add(Strings.messagesJML);
// These register JML versions of the various tools. These essentially
// register factories: no actual instances are created until
// instance(context) is called on the particular tool. Creating instances
// may trigger a cascade of tool instance generation, which can create
// tools (such as the Log) before the Options are processed and can
// trigger some circular dependencies in the constructors of the various
// tools.
// Any initialization of these tools that needs to be done based on
// options should be performed in setupOptions() in this class.
JmlOptions.preRegister(context);
JmlSpecs.preRegister(context); // registering the specifications repository
JmlFactory.preRegister(context); // registering a Jml-specific factory from which to generate JmlParsers
JmlScanner.JmlFactory.preRegister(context); // registering a Jml-specific factory from which to generate JmlScanners
JmlTree.Maker.preRegister(context); // registering a JML-aware factory for generating JmlTree nodes
JmlCompiler.preRegister(context);
JmlEnter.preRegister(context);
JmlResolve.preRegister(context);
JmlFlow.preRegister(context);
JmlMemberEnter.preRegister(context);
JmlTypes.preRegister(context);
JmlAttr.preRegister(context); // registering a JML-aware type checker
JmlCheck.preRegister(context);
JmlPretty.preRegister(context);
JmlDeferredAttr.preRegister(context); // registers when created
// Extensions are registered after options are processed
}
/** This is overridden so that serious internal bugs are reported as OpenJML
* rather than Javac bugs.
*/
@Override
protected void bugMessage(Throwable ex) {
out.println("Internal JML bug - please report. Build" + JavaCompiler.version());
ex.printStackTrace(out);
}
// EXTERNAL API FOR PROGRAMATIC ACCESS TO JML COMPILER INTERNALS
/** This method initializes the Options.instance(context) instance of
* Options class. If the options argument is not null, its content is used
* to initialize Options.instance(context); if options is null, then
* Options.instance(context) is initialized by reading
* the options specified in the environment (System properties +
* openjml properties files). Then the specified args are processed to make any
* further adjustments to the options. Any errors are reported through the
* log mechanism. Any non-options in the args list (e.g. files) are
* warned about and ignored.
*/
public void initializeOptions(@Nullable Options options, @NonNull String... args) {
Options opts = Options.instance(context);
setOptions(opts);
if (options == null) {
filenames = new TreeSet<File>();
classnames = new ListBuffer<String>();
coreDefaultOptions(opts);
setInitialOptions(opts, args);
Properties properties = Utils.findProperties(context);
setPropertiesFileOptions(opts, properties);
} else {
opts.putAll(options);
}
if (args != null && args.length > 0) try {
Collection<File> files = processArgs(CommandLine.parse(args));
if (files != null && !files.isEmpty()) {
Log.instance(context).warning("jml.ignore.extra.material",files.iterator().next().getName());
}
} catch (java.io.IOException e) {
Log.instance(context).error("jml.process.args.exception", e.toString());
}
}
protected void setPropertiesFileOptions(Options opts, Properties properties){
for (Map.Entry<Object,Object> p : properties.entrySet()) {
Object o = p.getKey();
if (!(o instanceof String)) {
Log.instance(context).warning("jml.ignoring.non.string.key", o.getClass());
continue;
}
String key = (String)o;
Object value = p.getValue();
if (!(value instanceof String)) {
Log.instance(context).warning("jml.ignoring.non.string.value", o.getClass(),key);
continue;
}
String v = (String)value;
if (key.startsWith(Strings.optionPropertyPrefix)) {
String rest = key.substring(Strings.optionPropertyPrefix.length());
if (v.equals("true")) value = "";
else if (v.equals("false")) value = null;
rest = "-" + rest;
opts.put(rest, v);
} else if (key.startsWith("openjml")) {
opts.put(key,v);
} else if (key.startsWith("org.openjml")) {
opts.put(key,v);
} else {
opts.put(key,v);
}
}
}
/** Sets default initial options to the Options instance that is the
* argument (does not change the compilation context Options directly);
* edit this method to set any defaults that would not be set by other
* means.
*/
protected void coreDefaultOptions(Options opts) {
opts.put(JmlOption.LOGIC.optionName(), "AUFLIA");
opts.put(JmlOption.PURITYCHECK.optionName(), null);
}
protected void setInitialOptions(Options opts, String ... args) {
for (int i=0; i<args.length; i++){
String s = args[i];
if(JmlOption.PROPERTIES_DEFAULT.optionName().equals(s)){
if(i+1 < args.length){
opts.put(JmlOption.PROPERTIES_DEFAULT.optionName(), args[i+1]);
}else{
Log.instance(context).warning("jml.expected.parameter",s);
}
}
}
}
/** Adds additional options to those already present (which may change
* previous settings). */
public void addOptions(String... args) {
processArgs(args);
}
/** Adds a custom option (not checked as a legitimate command-line option);
* may have an argument after a = symbol */
public void addUncheckedOption(String arg) {
int k = arg.indexOf('=');
if (k == -1) {
Options.instance(context).put(arg,"");
} else {
String value = arg.substring(k+1);
if (value.equals("false")) value = null;
Options.instance(context).put(arg.substring(0,k),value);
}
}
protected Name optionName;
public void pushOptions(JCModifiers mods) {
if (optionName == null) optionName = Names.instance(context).fromString("org.jmlspecs.annotation.Options");
((JmlOptions)JmlOptions.instance(context)).pushOptions();
JCAnnotation addedOptionsAnnotation = Utils.instance(context).findMod(mods, optionName);
if (addedOptionsAnnotation != null) {
List<JCExpression> exprs = addedOptionsAnnotation.getArguments();
JCExpression rhs = ((JCAssign)exprs.head).rhs;
String[] opts = rhs instanceof JCNewArray ? ((JCNewArray)rhs).elems.toString().split(",")
: rhs instanceof JCLiteral ? new String[]{ rhs.toString() }
: null;
// System.out.println(opts);
addOptions(opts);
setupOptions();
}
int v = Utils.instance(context).jmlverbose;
}
public void popOptions() {
((JmlOptions)JmlOptions.instance(context)).popOptions();
setupOptions();
int v = Utils.instance(context).jmlverbose;
}
static protected void fixClasspath(Context context) {
String cp = Options.instance(context).get("-classpath");
if (cp != null) return;
}
/** Appends the internal runtime directory to the -classpath option.
*/
static protected void appendRuntime(Context context) {
String jmlruntimePath = null;
/** This property is just used in testing. */ // TODO - check this
String sy = Options.instance(context).get(Strings.defaultRuntimeClassPath);
if (sy != null) {
jmlruntimePath = sy;
}
// Look for jmlruntime.jar in the classpath itself
if (jmlruntimePath == null) {
URL url = ClassLoader.getSystemResource(Strings.runtimeJarName);
if (url != null) {
jmlruntimePath = url.getFile();
if (jmlruntimePath.startsWith("file:/")) {
jmlruntimePath = jmlruntimePath.substring("file:/".length());
}
}
}
// Otherwise look for something in the same directory as something on the classpath
String classpath = System.getProperty("java.class.path");
if (jmlruntimePath == null) {
String[] ss = classpath.split(java.io.File.pathSeparator);
for (String s: ss) {
if (s.endsWith(".jar")) {
try {
File f = new File(s).getCanonicalFile().getParentFile();
if (f != null) {
f = new File(f,Strings.runtimeJarName);
if (f.isFile()) {
jmlruntimePath = f.getPath();
break;
}
}
} catch (IOException e) {
// Just skip
}
} else {
File f = new File(new File(s),Strings.runtimeJarName);
if (f.isFile()) {
jmlruntimePath = f.getPath();
break;
}
}
}
}
// The above all presume some variation on the conventional installation
// of the command-line tool. In the development environment, those
// presumptions do not hold. So in that case we use the appropriate
// bin directories directly. We make sure that we get both of them.
// This also takes care of the case in which the openjml.jar file is in
// the class path under a different name.
if (jmlruntimePath == null) {
URL url = ClassLoader.getSystemResource(Strings.jmlAnnotationPackage.replace('.','/'));
if (url != null) {
try {
String s = url.getPath();
if (s.startsWith("file:")) s = s.substring("file:".length());
int b = (s.length()> 2 && s.charAt(0) == '/' && s.charAt(2) == ':') ? 1 : 0;
int k = s.indexOf("!");
if (k >= 0) {
s = s.substring(b,k);
} else {
s = s.substring(b);
s = new File(s).getParentFile().getParentFile().getParent();
}
if (new File(s).exists()) jmlruntimePath = s;
url = ClassLoader.getSystemResource(Strings.jmlSpecsPackage.replace('.','/'));
if (url != null) {
s = url.getPath();
if (s.startsWith("file:")) s = s.substring("file:".length());
b = (s.length()> 2 && s.charAt(0) == '/' && s.charAt(2) == ':') ? 1 : 0;
k = s.indexOf("!");
if (k >= 0) {
s = s.substring(b,k);
} else {
s = s.substring(b);
s = new File(s).getParentFile().getParentFile().getParent();
}
if (new File(s).exists() && !s.equals(jmlruntimePath)) jmlruntimePath = jmlruntimePath + java.io.File.pathSeparator + s;
}
} catch (Exception e) {
// Just skip
}
}
}
if (jmlruntimePath != null) {
if (Utils.instance(context).jmlverbose >= Utils.JMLVERBOSE)
Log.instance(context).getWriter(WriterKind.NOTICE).println("Using internal runtime " + jmlruntimePath);
String cp = Options.instance(context).get("-classpath");
if (cp == null) cp = System.getProperty("java.class.path");
cp = cp==null ? jmlruntimePath : (cp + java.io.File.pathSeparator + jmlruntimePath);
Options.instance(context).put("-classpath",cp);
if (Utils.instance(context).jmlverbose >= Utils.JMLVERBOSE)
Log.instance(context).getWriter(WriterKind.NOTICE).println("Classpath: " + Options.instance(context).get("-classpath"));
} else {
Log.instance(context).warning("jml.no.internal.runtime");
}
}
/** An empty array used simply to avoid repeatedly creating one. */
private static final @NonNull String[] emptyArgs = new String[]{};
/** Returns a reference to the API's compilation context. */
public @Nullable Context context() {
return context;
}
/** An Enum type that gives a choice of various tools to be executed. */
public static enum Cmd { CHECK("check"), ESC("esc"), RAC("rac"), JMLDOC("doc"), COMPILE("compile");
String name;
public String toString() { return name; }
private Cmd(String name) { this.name = name; }
};
}