/*
* This file is part of the OpenJML project.
* Author: David R. Cok
*/
// FIXME - do a review
package org.jmlspecs.openjml;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import javax.tools.JavaFileObject;
import org.jmlspecs.openjml.JmlTree.JmlClassDecl;
import org.jmlspecs.openjml.JmlTree.JmlCompilationUnit;
import org.jmlspecs.openjml.esc.JmlAssertionAdder;
import org.jmlspecs.openjml.esc.JmlEsc;
import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.comp.AttrContext;
import com.sun.tools.javac.comp.Env;
import com.sun.tools.javac.comp.JmlAttr;
import com.sun.tools.javac.comp.JmlEnter;
import com.sun.tools.javac.comp.JmlMemberEnter;
import com.sun.tools.javac.comp.JmlResolve;
import com.sun.tools.javac.comp.Resolve;
import com.sun.tools.javac.jvm.ClassReader;
import com.sun.tools.javac.main.JavaCompiler;
import com.sun.tools.javac.comp.CompileStates.CompileState;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCImport;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Log.WriterKind;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Pair;
import com.sun.tools.javac.util.PropagatedException;
/**
* This class extends the JavaCompiler class in order to find and parse
* specification files when a Java source file is parsed.
*
* @author David Cok
*/
public class JmlCompiler extends JavaCompiler {
/** Registers a factory for producing JmlCompiler tools.
* There is one instance for each instance of context.
* @param context the compilation context used for tools
*/
public static void preRegister(final Context context) {
context.put(compilerKey, new Context.Factory<JavaCompiler>() {
public JmlCompiler make(Context context) {
return new JmlCompiler(context); // registers itself
}
});
}
/** Cached value of the class loader */
protected JmlResolve resolver;
/** Cached value of the utilities object */
protected Utils utils;
/** A constructor for this tool, but do not use it directly - use instance()
* instead to get a unique instance of this class for the context.
* @param context the compilation context for which this instance is being created
*/
protected JmlCompiler(Context context) {
super(context);
//shouldStopPolicy = CompileState.GENERATE;
this.context = context;
this.utils = Utils.instance(context);
this.verbose = utils.jmlverbose >= Utils.JMLVERBOSE;
this.resolver = JmlResolve.instance(context);
}
/** A flag that controls whether to get specs during a parse or not (if false
* then do, if true then do not). This should be left in a false state
* after being used to preclude parsing specs.
*/
public boolean inSequence = false;
/** This method is overridden in order to parse specification files along
* with parsing a Java file. Note that it is called directly from
* JavaCompiler.complete and JavaCompiler.parse to do the actual parsing.
* Thus when parsing an individual file (such as a spec file) it is also
* called (through parse). Consequently we have to do this little trick
* with the "inSequence" field to avoid trying to parse the specifications
* of specification files.
* <P>
* <UL>
* <LI>If inSequence is false, then this method parses the given content and associated specs.
* The JmlCompilationUnit for the specs is assigned to the specsCompilationUnit field of the
* JmlCompilationUnit for the .java file.
* <LI>If inSequence is true, then this method parses just the given content.
* <LI>In either case a JmlCompilationUnit is returned.
* However, see the FIXME below regarding adding the .java file into an empty specs list.
* </UL>
* <p>
* THis method is eventually called for (1) source files specified on the command-line,
* through Enter.main and (2) classes referenced in other files that need to be compiled
*/
// TODO - when called from JavaCompiler.complete it seems that the end position information is not recorded
// in the way that happens when called from JavaCompiler.parse. Is this a problem in the Javac compiler?
@Override
public JCCompilationUnit parse(JavaFileObject fileobject, CharSequence content) {
// TODO: Use a TaskEvent and a TaskListener here?
if (utils.jmlverbose >= Utils.JMLVERBOSE) context.get(Main.IProgressListener.class).report(0,2,"parsing " + fileobject.toUri() );
JCCompilationUnit cu = super.parse(fileobject,content);
if (inSequence) {
return cu;
}
if (cu instanceof JmlCompilationUnit) {
JmlCompilationUnit jmlcu = (JmlCompilationUnit)cu;
if (fileobject.getKind() == JavaFileObject.Kind.SOURCE) { // A .java file
jmlcu.mode = JmlCompilationUnit.JAVA_SOURCE_PARTIAL;
JavaFileObject specsFile = JmlSpecs.instance(context).findSpecs(jmlcu,true);
if (specsFile != null && Utils.ifSourcesEqual(specsFile, jmlcu.getSourceFile())) {
if (utils.jmlverbose >= Utils.JMLDEBUG) log.getWriter(WriterKind.NOTICE).println("The java file is its own specs for " + specsFile);
jmlcu.specsCompilationUnit = jmlcu;
} else {
jmlcu.specsCompilationUnit = parseSingleFile(specsFile);
}
if (jmlcu.specsCompilationUnit == null) {
// If there are no specs, that means that not even the .java file is
// on the specification path. That may well be something to warn
// about. For now (and for the sake of the tests), we will be
// helpful and add the .java file to the specs sequence despite it
// not being on the specification path.
// TODO log.warning("jml.no.specs",filename.getName());
jmlcu.specsCompilationUnit = jmlcu;
} else {
JmlCompilationUnit jcu = jmlcu.specsCompilationUnit;
if (jcu != cu) {
jcu.mode = JmlCompilationUnit.SPEC_FOR_SOURCE;
// Insert import statements
// FIXME - This is not the best solution - since it adds imports to the Java compilation unit, which may make it invalid
Map<String,JCImport> map = new HashMap<String,JCImport>();
ListBuffer<JCTree> extras = new ListBuffer<JCTree>();
for (JCImport imp: jmlcu.getImports()) map.put(imp.qualid.toString(),imp);
for (JCImport def: jcu.getImports()) {
JCImport imp = map.get(def.qualid.toString());
if (imp == null) extras.add(def);
}
cu.defs = cu.defs.appendList(extras);
}
}
// FIXME - record dependencies
} else {
// Parsing a specification file
jmlcu.mode = JmlCompilationUnit.SPEC_FOR_SOURCE;
JavaFileObject javaFile = JmlSpecs.instance(context).findSpecs(jmlcu,false); // look for corresponding java file
JmlCompilationUnit javacu = parseSingleFile(javaFile);
if (javacu != null) {
javacu.specsCompilationUnit = jmlcu;
javacu.mode = JmlCompilationUnit.JAVA_SOURCE_PARTIAL;
cu = javacu;
} else {
log.warning("jml.no.java.file",jmlcu.sourcefile);
}
}
} else {
log.error("jml.internal",
"JmlCompiler.parse expects to receive objects of type JmlCompilationUnit, but it found a "
+ cu.getClass() + " instead, for source " + cu.getSourceFile().toUri().getPath());
}
try {
if (cu.endPositions != null) { // FIXME - is this ever non-null? and why only of we are in the mode of parsing multiple files
JavaFileObject prev = log.useSource(fileobject);
log.setEndPosTable(fileobject,cu.endPositions);
log.useSource(prev);
}
} catch (Exception e) {
// End-position table set twice - so far just encountered this when a class name is used but is not defined in the file by that name
log.error("jml.file.class.mismatch",fileobject.getName());
}
return cu;
}
/** Parses the specs for a class - used when we need the specs corresponding to a binary file;
* this may only be called for public top-level classes (the specs for non-public or
* nested classes are part of the same file with the corresponding public class).
* Returns null if no specifications file is found.
* @param typeSymbol the symbol of the type whose specs are sought
* @return the possibly null parsed compilation unit, as an AST
*/
/*@Nullable*/
public JmlCompilationUnit parseSpecs(Symbol.TypeSymbol typeSymbol) {
String typeName = typeSymbol.flatName().toString();
JavaFileObject f = JmlSpecs.instance(context).findAnySpecFile(typeName);
/*@Nullable*/ JmlCompilationUnit speccu = parseSingleFile(f);
if (speccu != null) speccu.packge = (Symbol.PackageSymbol)typeSymbol.outermostClass().getEnclosingElement();
return speccu;
}
/** Parses the given file as a JmlCompilationUnit (either Java source or JML specifications);
* does not seek any specification file. Retruns a best guess compilation unit if there are parse errors.
* @param f the file object to parse, if any
* @param javaCU the compilation unit that provoked this parsing, if any
* @return the possibly empty list of parsed compilation units, as ASTs; possibly returns null
*/
//@ nullable
public JmlCompilationUnit parseSingleFile(/*@ nullable*/JavaFileObject f) {
inSequence = true;
try {
if (f != null) {
JCCompilationUnit result = parse(f);
if (result instanceof JmlCompilationUnit) {
return (JmlCompilationUnit)result;
} else {
log.error("jml.internal","The result of a parse is a JCCompilationUnit instead of a JmlCompilationUnit");
return null;
}
} else {
return null;
}
} finally {
inSequence = false;
}
}
/** Parses the list of file objects (using parse(fileobject)), returning a list of JmlCompilationUnits;
* parsing a source file will cause a search for and parsing of the specification file */
@Override
public List<JCCompilationUnit> parseFiles(Iterable<JavaFileObject> fileObjects) {
List<JCCompilationUnit> list = super.parseFiles(fileObjects);
for (JCCompilationUnit cu: list) {
((JmlCompilationUnit)cu).mode = JmlCompilationUnit.JAVA_SOURCE_FULL; // FIXME - does this matter? is it right? there could be jml files on the command line
}
return list;
}
private int nestingLevel = 0;
/** Parses and enters specs for binary classes, given a ClassSymbol. This is
* called when a name is resolved to a binary type; the Java type itself is
* loaded (and symbols entered) by the conventional Java means. Here we need
* to add to that by parsing the specs and entering any new declarations
* into the scope tables (via JmlEnter and JmlMemberEnter). This method is
* also called when during type attribution a new unattributed type is found
* that does not have any specs associated with it. We call this to get the
* specs. If ever a Java file is loaded by conventional means and gets its
* source file through parsing, the specs will be obtained using parse()
* above.
*
* @param env the environment representing the source for the given class;
* may be null for a PUBLIC top-level class
* @param csymbol the class whose specs are wanted
*/ // FIXME - what should we use for env for non-public binary classes
// FIXME - move this to JmlResolve
public void loadSpecsForBinary(Env<AttrContext> env, ClassSymbol csymbol) {
// The binary Java class itself is already loaded - it is needed to produce the classSymbol itself
// Don't load specs over again
if (JmlSpecs.instance(context).get(csymbol) != null) return;
// if (csymbol.toString().equals("java.lang.Object")) Utils.stop();
// FIXME - need to figure out what the environment should be
if (!binaryEnterTodo.contains(csymbol)) {
nestingLevel++;
try {
if (csymbol.getSuperclass() != Type.noType) loadSpecsForBinary(env, (ClassSymbol)csymbol.getSuperclass().tsym);
for (Type t: csymbol.getInterfaces()) {
loadSpecsForBinary(env, (ClassSymbol)t.tsym); // FIXME - env is not necessarily the tree for the classSymbol
}
// It can happen that the specs are loaded during the loading of the super class
// since complete() may be called on the class in order to fetch its superclass
JmlSpecs.TypeSpecs tspecs = JmlSpecs.instance(context).get(csymbol);
if (tspecs == null) {
// Note: classes and interfaces may be entered in this queue multiple times. The check for specs at the beginning of this method
// does not prevent unloaded classes from begin added to the queue more than once, because the specs are not loaded until completeBinaryEnterTodo
if (utils.jmlverbose >= Utils.JMLDEBUG) log.getWriter(WriterKind.NOTICE).println("QUEUING BINARY ENTER " + csymbol);
binaryEnterTodo.prepend(csymbol);
}
} finally {
nestingLevel --;
}
}
// This nesting level is used to be sure we queue up a whole set of
// classes, do their 'enter' processing to record any types before we
// do their member processing to record all their members. We need the
// types recorded so that we can look up types for the members (e.g. do
// method resolution). This is the same two-phase processing as the
// Java handling uses, we just don't use the same todo list.
if (nestingLevel==0) completeBinaryEnterTodo();
}
ListBuffer<ClassSymbol> binaryEnterTodo = new ListBuffer<ClassSymbol>();
// FIXME - do we really need this deferred processing?
public void completeBinaryEnterTodo() {
while (!binaryEnterTodo.isEmpty()) {
ClassSymbol csymbol = binaryEnterTodo.remove();
if (JmlSpecs.instance(context).get(csymbol) != null) continue;
//if (csymbol.toString().contains("AbstractStringBuilder")) Utils.stop();
// Record default specs just to show they are in process
// If there are actual specs, they will be recorded later
// We do this, in combination with the check above, to avoid recursive loops
((JmlEnter)enter).recordEmptySpecs(csymbol);
JmlCompilationUnit speccu = parseSpecs(csymbol);
if (speccu != null) {
// csymbol.flags_field |= Flags.UNATTRIBUTED;
if (speccu.sourcefile.getKind() == JavaFileObject.Kind.SOURCE) speccu.mode = JmlCompilationUnit.JAVA_AS_SPEC_FOR_BINARY;
else speccu.mode = JmlCompilationUnit.SPEC_FOR_BINARY;
//if (speccu.sourcefile.toString().contains("File")) Utils.stop();
nestingLevel++;
try {
boolean ok = ((JmlEnter)enter).binaryEnter(speccu);
// ((JmlEnter)enter).binaryEnvs.add(speccu);
// specscu.defs is empty if nothing was declared or if all class declarations were removed because of errors
if (ok) {
for (JCTree d: speccu.defs) {
if (d instanceof JmlClassDecl) todo.append(((JmlClassDecl)d).env);
}
}
// memberEnter.enterSpecsForBinaryClasses(csymbol,List.<JCTree>of(speccu));
} finally {
nestingLevel--;
}
}
}
}
/** Overridden in order to put out some information about stopping */
@Override
public <T> List<T> stopIfError(CompileState cs, List<T> list) {
if (shouldStop(cs)) {
if (JmlOption.isOption(context,JmlOption.STOPIFERRORS)) {
if (utils.jmlverbose >= Utils.PROGRESS) context.get(Main.IProgressListener.class).report(0,1,"Stopping because of parsing errors");
return List.<T>nil();
} else {
if (utils.jmlverbose >= Utils.PROGRESS) context.get(Main.IProgressListener.class).report(0,1,"Continuing bravely despite parsing errors");
}
}
return list;
}
/** We override this method instead of the desugar method that does one
* env because we have to do all the rac before any of the desugaring
*/
@Override
public Queue<Pair<Env<AttrContext>, JCClassDecl>> desugar(Queue<Env<AttrContext>> envs) {
ListBuffer<Pair<Env<AttrContext>, JCClassDecl>> results = new ListBuffer<>();
if (envs.isEmpty()) {
if (utils.esc) context.get(Main.IProgressListener.class).report(0,1,"Operation not performed because of parse or type errors");
// try {
// Thread.sleep(10000);
// } catch (InterruptedException e) {
// }
return results;
}
if (utils.check || utils.doc) {
// Stop here
return results; // Empty list - do nothing more
} else if (utils.esc) {
try {
for (Env<AttrContext> env: envs)
esc(env);
} catch (PropagatedException e) {
// cancelation
}
return results; // Empty list - Do nothing more
} else if (utils.rac) {
for (Env<AttrContext> env: envs) {
JCTree t = env.tree;
env = rac(env);
if (env == null) continue; // FIXME - error? just keep oroginal env?
if (utils.jmlverbose >= Utils.JMLVERBOSE)
context.get(Main.IProgressListener.class).report(0,2,"desugar " + todo.size() + " " +
(t instanceof JCTree.JCCompilationUnit ? ((JCTree.JCCompilationUnit)t).sourcefile:
t instanceof JCTree.JCClassDecl ? ((JCTree.JCClassDecl)t).name : t.getClass()));
}
// Continue with the usual compilation phases
for (Env<AttrContext> env: envs)
desugar(env, results);
} else {
for (Env<AttrContext> env: envs)
desugar(env, results);
}
return stopIfError(CompileState.FLOW, results);
}
/** This is overridden so that if attribute() returns null, processing continues (instead of crashing). */
// FIXME - why might it return null, and should we stop if it does?
@Override
public Queue<Env<AttrContext>> attribute(Queue<Env<AttrContext>> envs) {
ListBuffer<Env<AttrContext>> results = new ListBuffer<>();
while (!envs.isEmpty()) {
Env<AttrContext> env = attribute(envs.remove());
if (env != null) results.append(env);
}
((JmlAttr)attr).completeTodo();
return stopIfError(CompileState.ATTR, results);
}
// FIXME - the following control flow is a bit convoluted and needsd to be explained and cleaned up
/** Overridden to remove binary/spec entries from the list of Envs after processing */
@Override
protected void flow(Env<AttrContext> env, Queue<Env<AttrContext>> results) {
if (env.toplevel.sourcefile.getKind() != JavaFileObject.Kind.SOURCE) unconditionallyStop = true;
super.flow(env,results);
}
// FIXME - this design prevents flow from running on spec files - we want actually to stop after the spec files are processed
@Override
protected boolean shouldStop(CompileState cs) {
if (unconditionallyStop) { unconditionallyStop = false; return true; }
return super.shouldStop(cs);
}
protected boolean unconditionallyStop = false;
/** Overridden simply to do a sanity check that annotation processing does not produce a JavaCompiler instead of a JmlCompiler. */
@Override
public JavaCompiler processAnnotations(List<JCCompilationUnit> roots, List<String> classnames) {
JavaCompiler result = super.processAnnotations(roots,classnames);
if (!(result instanceof JmlCompiler)) {
log.error("jml.internal","annotation processing produced a new instance of JavaCompiler, disabling further JML processing");
}
return result;
}
// FIXME _ review
/** Does the RAC processing on the argument. */
protected Env<AttrContext> rac(Env<AttrContext> env) {
JCTree tree = env.tree;
PrintWriter noticeWriter = log.getWriter(WriterKind.NOTICE);
// TODO - will sourcefile always exist? -- JLS
String currentFile = env.toplevel.sourcefile.getName();
if (tree instanceof JCClassDecl) {
JmlTree.Maker M = JmlTree.Maker.instance(context);
JCClassDecl that = (JCClassDecl)tree;
if (((JmlAttr)attr).hasAnnotation(that.sym,JmlTokenKind.SKIP_RAC)) {
utils.progress(1,1,"Skipping RAC of " + that.name.toString() + " (SkipRac annotation)");
return env;
}
// The class named here must match that in org.jmlspecs.utils.Utils.isRACCompiled
Name n = names.fromString("org.jmlspecs.annotation.RACCompiled");
ClassSymbol sym = ClassReader.instance(context).enterClass(n);
Attribute.Compound ac = new Attribute.Compound(sym.type, List.<Pair<Symbol.MethodSymbol,Attribute>>nil());
that.sym.appendAttributes(List.<Attribute.Compound>of(ac));
}
// if (!JmlCompilationUnit.isJava(((JmlCompilationUnit)env.toplevel).mode)) {
// // TODO - explain why we remove these from the symbol tables
// if (env.tree instanceof JCClassDecl) {
// Symbol c = ((JCClassDecl)env.tree).sym;
// //((JmlEnter)enter).remove(c);
// } else if (env.toplevel instanceof JCCompilationUnit) {
// for (JCTree t : ((JCCompilationUnit)env.toplevel).defs) {
// if (t instanceof JCClassDecl) ((JmlEnter)enter).remove(((JCClassDecl)t).sym);
// }
// } else {
// // This is a bug, but we can probably get by with just not instrumenting
// // whatever this is.
// log.warning("jml.internal.notsobad","Did not expect to encounter this option in JmlCompiler.rac: " + env.tree.getClass());
// }
// return null;
// }
// Note that if env.tree is a class, we translate just that class.
// We have to adjust the toplevel tree accordingly. Presumably other
// class declarations in the compilation unit will be translated on
// other calls.
utils.progress(0,1,"RAC-Compiling " + utils.envString(env));
if (utils.jmlverbose >= Utils.JMLDEBUG) noticeWriter.println("rac " + utils.envString(env));
if (env.tree instanceof JCClassDecl) {
JCTree newtree;
if (JmlOption.isOption(context,JmlOption.SHOW)) {
// FIXME - these are not writing out during rac, at least in debug in development, to the console
noticeWriter.println(String.format("[jmlrac] Translating: %s", currentFile));
noticeWriter.println(
JmlPretty.toFancyLineFormat(
currentFile,
JmlPretty.racFormatter, // the formatter
JmlPretty.write(env.toplevel,true) // the source to format
));
noticeWriter.println("");
}
newtree = new JmlAssertionAdder(context,false,true).convert(env.tree);
// When we do the RAC translation, we create a new instance
// of the JCClassDecl for the class. So we have to find where
// it is kept in the JCCompilationUnit and replace it there.
// If there is more than one class in the compilation unit, we are
// presuming that each one that is to be translated will be
// separately called - so we just translate each one when it comes.
for (List<JCTree> l = env.toplevel.defs; l.nonEmpty(); l = l.tail) {
if(l.head == env.tree){
env.tree = newtree;
l.head = newtree;
break;
}
}
// it's not enough to update the toplevels. If you have nested classes, you must
// update the type envs, otherwise the wrong typeenv gets selected during the desugaring phase
if(newtree instanceof JmlClassDecl){
updateTypeEnvs((JmlClassDecl)newtree);
}
// After adding the assertions, we will need to add the OpenJML libraries
// to the import directives.
// Add the Import: import org.jmlspecs.utils.*;
if (JmlOption.isOption(context,JmlOption.SHOW)) {
noticeWriter.println(String.format("[jmlrac] RAC Transformed: %s", currentFile));
// this could probably be better - is it OK to modify the AST beforehand? JLS
noticeWriter.println(
JmlPretty.toFancyLineFormat(
currentFile,
JmlPretty.racFormatter, // the formatter
"import org.jmlspecs.utils.*;", // a header prefix to print
JmlPretty.write(env.toplevel,true) // the source to format
));
}
} else {
// FIXME - does this happen?
JCCompilationUnit newtree = new JmlAssertionAdder(context,false,true).convert(env.toplevel);
env.toplevel = newtree;
}
// flow(env); // FIXME - give a better explanation if this produces errors.
// IF it does, it is because we have done the RAC translation wrong.
return env;
}
// FIXME - review
/** Recursively updates nested class declarations */
protected void updateTypeEnvs(JmlClassDecl tree){
enter.getEnv(tree.sym).tree = tree;
for(List<JCTree> l = tree.defs; l.nonEmpty(); l=l.tail){
if(l.head instanceof JmlClassDecl){
updateTypeEnvs((JmlClassDecl)l.head);
}
}
}
/** Does the ESC processing for the given class
*
* @param env the env for a class
*/ // FIXME - check that we always get classes, not CUs and adjust the logic accordingly
protected void esc(Env<AttrContext> env) {
// Only run ESC on source files
if (((JmlCompilationUnit)env.toplevel).mode != JmlCompilationUnit.JAVA_SOURCE_FULL) return;
JmlEsc esc = JmlEsc.instance(context); // FIXME - get this once at initialization?
esc.check(env.tree);
return;
}
// FIXME - we are overriding to only allow SIMPLE compile policy
protected void compile2(CompilePolicy compPolicy) {
//super.compile2(CompilePolicy.BY_TODO);
super.compile2(CompilePolicy.SIMPLE);
}
}