/* * This file is part of the OpenJML plugin project. * Copyright (c) 2006-2013 David R. Cok */ package org.jmlspecs.openjml.eclipse; import java.io.File; import java.io.PrintWriter; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.tools.Diagnostic; import javax.tools.DiagnosticListener; import javax.tools.JavaFileObject; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; 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.core.runtime.CoreException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.core.*; import org.eclipse.jdt.internal.compiler.problem.ProblemSeverities; import org.eclipse.jdt.internal.core.JavaProject; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.jmlspecs.annotation.NonNull; import org.jmlspecs.annotation.Nullable; import org.jmlspecs.annotation.SpecPublic; import org.jmlspecs.openjml.*; import org.jmlspecs.openjml.JmlSpecs.FieldSpecs; import org.jmlspecs.openjml.JmlSpecs.TypeSpecs; import org.jmlspecs.openjml.JmlTree.JmlCompilationUnit; import org.jmlspecs.openjml.JmlTree.JmlMethodDecl; import org.jmlspecs.openjml.JmlTree.JmlVariableDecl; import org.jmlspecs.openjml.Main; import org.jmlspecs.openjml.Main.JmlCanceledException; import org.jmlspecs.openjml.Strings; import org.jmlspecs.openjml.eclipse.Utils.OpenJMLException; import org.jmlspecs.openjml.proverinterface.IProverResult; import org.jmlspecs.openjml.proverinterface.IProverResult.ICounterexample; import org.jmlspecs.openjml.proverinterface.ProverResult; import com.sun.tools.javac.code.Scope; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.code.Symbol.VarSymbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.TypeTag; import com.sun.tools.javac.comp.AttrContext; import com.sun.tools.javac.comp.Enter; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.PropagatedException; // FIXME - needs review - OpenJMLInterface.java /** * This class is the interface between the Eclipse UI that serves as view and * controller and the openjml packages that execute operations on JML annotations. * Typically there is just one fairly persistent instance of this class for * each Java project. Note that we only allow .java files under the control of * one Java project to be compiled together - this is because Eclipse manages * options on a per project basis. Technically if all the relevant projects had * the same set of options, they could be combined, but the implementation * currently does not do that. */ public class OpenJMLInterface implements IAPI.IProofResultListener { /** The API object corresponding to this Interface class. */ @NonNull protected IAPI api; /** The common instance of a UI Utils object that provides various * utility methods. We initialize this when an OpenJMLInterface * object is created rather than making it static because the * singleton object is not created until the plugin is actually * started. */ @NonNull final protected Utils utils = Activator.utils(); /** Retrieves the descriptive version string from OpenJML * @return returns the descriptive version string from OpenJML */ @NonNull public String version() { return api.version(); } /** The Java project for this object. */ @NonNull final protected IJavaProject jproject; /** The problem requestor that reports problems it is told about to the user * in some fashion (here via Eclipse problem markers). */ @NonNull protected JmlProblemRequestor preq; /** The constructor, which initializes all of the fields of the object. * * @param jproject The java project associated with this instance of OpenJMLInterface */ public OpenJMLInterface(@NonNull IJavaProject jproject) { this.jproject = jproject; initialize(null); } /** Initializes new OpenJMLInterface objects */ protected void initialize(/*@nullable*/ Main.Cmd cmd) { preq = new JmlProblemRequestor(jproject); PrintWriter w = Log.log.listener() != null ? new PrintWriter(Log.log.listener().getStream(),true) : null; List<String> opts = getOptions(jproject,cmd); try { api = Factory.makeAPI(w,new EclipseDiagnosticListener(preq), null, opts.toArray(new String[0])); api.setProofResultListener(this); api.main().proofResultListener = this; } catch (Exception e) { Log.errorlog("Failed to create an interface to OpenJML",e); //$NON-NLS-1$ } } /** * A private helper method that checks to see whether a given IFolder contains * at least one source file (that is, a file ending in .jml or .java, constants * declared in OpenJML's Strings class). * * @param folder The folder to check. * @return true if the folder contains at least one source file, false otherwise. */ private boolean hasAtLeastOneSourceFile(final IFolder folder) { boolean result = false; try { IResource[] members = folder.members(); for (int i = 0; !result && i < members.length; i++) { if (members[i] instanceof IFolder) { result |= hasAtLeastOneSourceFile((IFolder) members[i]); } else if (members[i] instanceof IFile) { final IFile file = (IFile) members[i]; result |= file.getName().endsWith(Strings.javaSuffix); result |= file.getName().endsWith(Strings.specsSuffix); } } } catch (final CoreException e) { // if we couldn't even check for the folder's members, obviously // something's wrong - we'll return false } return result; } /** * Adds appropriate arguments to a list of OpenJML command line arguments * for all the non-empty source folders in the specified IProject. * * @param project The project. * @param args The argument list to add to; this is potentially changed by * calling this method. * @return the number of path and file arguments added to the argument list; this * does _not_ include OpenJML "-dir" arguments, which are also added as necessary. * If result == 0, the arguments were unchanged. */ private int addSourceFoldersToCommandLine(final IProject project, final List<String> args) { int result = 0; if (JavaProject.hasJavaNature(project)) { // should always be true, we should always be in a Java project // so let's get all the source folders final IJavaProject javaProject = JavaCore.create(project); try { for (IClasspathEntry entry : javaProject .getResolvedClasspath(true)) { // get the classpath for the project if (entry.getContentKind() == IPackageFragmentRoot.K_SOURCE) { // it's a source folder, let's see if it's empty final IFolder folder = ResourcesPlugin.getWorkspace().getRoot().getFolder(entry.getPath()); if (hasAtLeastOneSourceFile(folder)) { // there are files in the source folder args.add(JmlOption.DIR.optionName()); args.add(folder.getLocation().toOSString()); result = result + 1; } } } } catch (final JavaModelException e) { // ignore this exception for now } } return result; } /** Executes the JML Check (syntax and typechecking) or the RAC compiler * operations on the given set of resources, creating a new compilation context. * Must be called in a computational thread. * @param command either CHECK or RAC * @param files the set of files (or containers) to check * @param monitor the progress monitor the UI is using */ public void executeExternalCommand(Main.Cmd command, Collection<IResource> files, @Nullable IProgressMonitor monitor, boolean auto) { boolean verboseProgress = utils.openjmlVerbose() >= Utils.NORMAL; try { if (files.isEmpty()) { if (!auto) { if (verboseProgress) Log.log("Nothing applicable to process"); //$NON-NLS-1$ Activator.utils().showMessageInUI(null,"JML","Nothing applicable to process"); //$NON-NLS-1$ //$NON-NLS-2$ } return; } IJavaProject jp = JavaCore.create(files.iterator().next().getProject()); List<String> args; if (command == Main.Cmd.CHECK) { api.close(); initialize(command); args = new ArrayList<String>(); args = getOptions(jp,command); // FIXME - somehow the options are not propagating through } else { args = getOptions(jp,command); String racdir = utils.getRacDir(); if (jp.getProject().findMember(racdir) == null) { try { // FIXME - is it a problem that this is done in the UI thread; is local=true correct? jp.getProject().getFolder(racdir).create(IResource.FORCE,true,null); } catch (CoreException e) { if (verboseProgress) Log.errorlog("Failed to create the RAC output folder",e); //$NON-NLS-1$ Activator.utils().showExceptionInUI(null,"Failed to create the RAC output folder",e); //$NON-NLS-1$ return; } } args.add(Strings.outputOptionName); args.add(jp.getProject().getLocation().append(racdir).toString()); } boolean addedSomething = false; for (IResource r : files) { if (r instanceof IProject) { addedSomething |= addSourceFoldersToCommandLine((IProject) r, args) > 0; } else if (r instanceof IFolder) { // the user explicitly selected a folder, and we don't allow empty folders if (hasAtLeastOneSourceFile((IFolder) r)) { args.add(JmlOption.DIR.optionName()); args.add(r.getLocation().toString()); addedSomething = true; } } else { // the user explicitly selected a file, so presumably they know what they're doing args.add(r.getLocation().toString()); addedSomething = true; } } if (!addedSomething) { if (monitor != null) monitor.subTask("OpenJML: No files to check"); //$NON-NLS-1$ if (verboseProgress) Log.log(Timer.timer.getTimeString() + " No files to check"); //$NON-NLS-1$ return; // we don't call OpenJML with no files } Timer.timer.markTime(); if (verboseProgress) { String s = files.size() == 1 ? files.iterator().next().getName() : (files.size() + " items"); //$NON-NLS-1$ Log.log(Timer.timer.getTimeString() + " Executing openjml on " + s); //$NON-NLS-1$ } if (monitor != null) { monitor.setTaskName(command == Main.Cmd.RAC ? "JML RAC" : "JML Checking"); //$NON-NLS-1$ //$NON-NLS-2$ monitor.subTask("Executing openjml"); //$NON-NLS-1$ } try { setMonitor(monitor); int ret = api.execute(null,args.toArray(new String[args.size()])); if (ret == Main.Result.OK.exitCode) { if (verboseProgress) Log.log(Timer.timer.getTimeString() + " Completed"); //$NON-NLS-1$ } else if (ret == Main.EXIT_CANCELED) { throw new Main.JmlCanceledException(Utils.emptyString); } else if (ret == Main.Result.ERROR.exitCode) { if (verboseProgress) Log.log(Timer.timer.getTimeString() + " Completed with errors"); //$NON-NLS-1$ } else if (ret == Main.Result.CMDERR.exitCode) { StringBuilder ss = new StringBuilder(); for (String r: args) { ss.append(r); ss.append(Utils.space); } Log.errorlog("INVALID COMMAND LINE: return code = " + ret + " Command: " + ss,null); // FIXME - the reason for the bad command line is lost (it would be an internal error) //$NON-NLS-1$//$NON-NLS-2$ Activator.utils().showMessageInUI(null,"Execution Failure","Invalid commandline - return code is " + ret + Strings.eol + ss); //$NON-NLS-1$//$NON-NLS-2$ } else if (ret >= Main.Result.SYSERR.exitCode) { StringBuilder ss = new StringBuilder(); for (String r: args) { ss.append(r); ss.append(Utils.space); } Log.errorlog("INTERNAL ERROR: return code = " + ret + " Command: " + ss,null); // FIXME - when the error is the result of an exception, we don't see the result //$NON-NLS-1$ //$NON-NLS-2$ Activator.utils().showMessageInUI(null,"OpenJML Execution Failure","Internal failure in openjml - return code is " + ret + " " + ss); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ } } catch (JmlCanceledException e) { throw e; } catch (PropagatedException e) { throw e.getCause(); } catch (Throwable e) { StringBuilder ss = new StringBuilder(); for (String c: args) { ss.append(c); ss.append(Utils.space); } Log.errorlog("Failure to execute openjml: "+ss,e); //$NON-NLS-1$ Activator.utils().showExceptionInUI(null,"Failure to execute openjml: " + ss,e); //$NON-NLS-1$ } if (monitor != null) monitor.subTask("Completed openjml"); //$NON-NLS-1$ } catch (JmlCanceledException e) { if (monitor != null) monitor.subTask("OpenJML Canceled: " + e.getMessage()); //$NON-NLS-1$ if (verboseProgress) Log.log(Timer.timer.getTimeString() + " Operation canceled"); //$NON-NLS-1$ } if (command == Main.Cmd.ESC) utils.refreshView(); } /** Executes the jmldoc tool on the given project, producing output according * to the current set of options. * @param p the project whose jmldocs are to be produced * @return 0 for success, 1 for command-line errors, 2 for system errors, * 3 for internal or otherwise catastrophic errors */ public int generateJmldoc(IJavaProject p) { List<String> args = getOptions(p,Main.Cmd.JMLDOC); args.add(Strings.outputOptionName); args.add(p.getProject().getLocation().toString() + File.separator + "docx"); //$NON-NLS-1$ //args.add(JmlOption.DIRS.optionName()); try { for (IPackageFragmentRoot pfr : p.getPackageFragmentRoots()) { // PackageFragmentRoots can be source folders or jar files // We want just the source folders IResource f = (IResource)pfr.getAdapter(IResource.class); if (!(f instanceof IFolder)) continue; IJavaElement[] packages = pfr.getChildren(); for (IJavaElement je : packages) { IPackageFragment pf = (IPackageFragment)je; // FIXME - will the following include folders of .jml files? what is the guard trying to exclude? if (pf.containsJavaResources()) args.add(pf.getElementName()); // getElementName gives the fully qualified package name } } // FIXME - we need to do the following in the computational thread int ret = Factory.makeAPI().jmldoc(args.toArray(new String[args.size()])); return ret; } catch (JavaModelException e) { Log.errorlog("INTERNAL EXCEPTION while generating jmldoc",e); //$NON-NLS-1$ } catch (Exception e) { Log.errorlog("INTERNAL EXCEPTION while generating jmldoc",e); //$NON-NLS-1$ } return 3; } /** Executes the JML ESC (static checking) operation * on the given set of resources. Must be called in a computational thread. * @param command should be ESC * @param things the set of files (or containers) or Java elements to check * @param monitor the progress monitor the UI is using */ // TODO - Review this public void executeESCCommand(Main.Cmd command, List<?> things, IProgressMonitor monitor) { try { if (things.isEmpty()) { Log.log("Nothing applicable to process"); Activator.utils().showMessageInUI(null,"JML","Nothing applicable to process"); return; } // if (api == null) { // // FIXME - presumes the listener is a ConsoleLogger // PrintWriter w = new PrintWriter(((ConsoleLogger)Log.log.listener()).getConsoleStream()); // api = new API(w,new EclipseDiagnosticListener(preq)); // api.setProgressReporter(new UIProgressReporter(api.context(),monitor,null)); // } setMonitor(monitor); List<String> args = getOptions(jproject,Main.Cmd.ESC); api.initOptions(null, args.toArray(new String[args.size()])); // args.clear(); List<IJavaElement> elements = new LinkedList<IJavaElement>(); IResource rr; int count = 0; utils.refreshView(); // Sets up the view - then each result is added incrementally for (Object r: things) { try { if (r instanceof IPackageFragment) { count += utils.countMethods((IPackageFragment)r); } else if (r instanceof IJavaProject) { count += utils.countMethods((IJavaProject)r); } else if (r instanceof IProject) { count += utils.countMethods((IProject)r); } else if (r instanceof IPackageFragmentRoot) { count += utils.countMethods((IPackageFragmentRoot)r); } else if (r instanceof ICompilationUnit) { count += utils.countMethods((ICompilationUnit)r); } else if (r instanceof IType) { count += utils.countMethods((IType)r); } else if (r instanceof IMethod) { count += 1; } else if (r instanceof IFile || r instanceof IFolder) { // If a file is not part of a source folder, then we // don't have Java elements and it is not a ICompilationUnit // So we can't really count the methods in it. // The number used here is arbitrary, and will result in // a bad estimate of the work to be done. // TODO - count the methods using the OpenJML AST. count += 2; } else { Log.log("Can't count methods in a " + r.getClass()); } } catch (Exception e) { // FIXME - record exception } } final int oldArgsSize = args.size(); for (Object r: things) { // an IType is adaptable to an IResource (the containing file), but we want it left as an IType if (!(r instanceof IType) && r instanceof IAdaptable && (rr=(IResource)((IAdaptable)r).getAdapter(IResource.class)) != null) { r = rr; } if (r instanceof IFolder) { if (hasAtLeastOneSourceFile((IFolder) r)) { // we don't process empty folders args.add(JmlOption.DIR.optionName()); args.add(((IResource)r).getLocation().toString()); } } else if (r instanceof IProject) { // we only want to add source folders, and only if they actually have source in them addSourceFoldersToCommandLine((IProject) r, args); } else if (r instanceof IResource) { args.add(((IResource)r).getLocation().toString()); } else if (r instanceof IType) { // Here we want types and methods, but a JavaProject is also an IJavaElement elements.add((IJavaElement)r); } else if (r instanceof IMethod) { // Here we want types and methods, but a JavaProject is also an IJavaElement elements.add((IJavaElement)r); } else if (r instanceof IJavaElement) { // Here we want types and methods, but a JavaProject is also an IJavaElement elements.add((IJavaElement)r); } else Log.log("Ignoring " + r.getClass() + Strings.space + r.toString()); //$NON-NLS-1$ } if (args.size() == oldArgsSize && elements.isEmpty()) { Log.log("No files or elements to process"); Activator.utils().showMessageInUI(null,"JML","No files or elements to process"); return; } if (monitor != null) { monitor.beginTask("Static checks in project " + jproject.getElementName(), 2*count); // ticks at begin and end of check monitor.subTask("Starting ESC on " + jproject.getElementName()); } Timer.timer.markTime(); if (args.size() != oldArgsSize) { if (Options.uiverboseness) { Log.log(Timer.timer.getTimeString() + " Executing static checks"); } try { int ret = api.execute(null,args.toArray(new String[args.size()])); if (ret == Main.Result.OK.exitCode) Log.log(Timer.timer.getTimeString() + " Completed"); if (ret == Main.Result.CANCELLED.exitCode) Log.log(Timer.timer.getTimeString() + " Cancelled"); else if (ret == Main.Result.ERROR.exitCode) Log.log(Timer.timer.getTimeString() + " Completed with errors"); else if (ret >= Main.Result.CMDERR.exitCode) { StringBuilder ss = new StringBuilder(); for (String c: args) { ss.append(c); ss.append(" "); } Log.errorlog("INTERNAL ERROR: return code = " + ret + " Command: " + ss,null); // FIXME _ dialogs are not working Activator.utils().showMessageInUI(null,"Execution Failure","Internal failure in openjml - return code is " + ret + " " + ss); // FIXME - fix line ending } else if (ret == Main.EXIT_CANCELED) { // Cancelled throw new Main.JmlCanceledException("ESC Canceled"); } } catch (Main.JmlCanceledException e) { throw e; } catch (Throwable e) { StringBuilder ss = new StringBuilder(); for (String c: args) { ss.append(c); ss.append(" "); } Log.errorlog("Failure to execute openjml: "+ss,e); Activator.utils().showMessageInUI(null,"JML Execution Failure","Failure to execute openjml: " + e + " " + ss); } } // Now do individual methods // Delete all markers first, so that subsequent proofs do not delete // the markers from earlier proofs if (!elements.isEmpty()) { for (IJavaElement je: elements) { utils.deleteMarkers(je.getResource(),null); // FIXME - would prefer to delete markers and highlighting on just the method. } for (IJavaElement je: elements) { if (je instanceof IMethod) { MethodSymbol msym = convertMethod((IMethod)je); if (msym == null) { IResource r = je.getResource(); String filename = null; try { filename = r.getLocation().toString(); int errors = api.typecheck(api.parseFiles(filename)); if (errors == 0) { msym = convertMethod((IMethod)je); } else { utils.showMessageInUI(null,"JML Error","ESC could not be performed because of JML typechecking errors"); msym = null; } } catch (java.io.IOException e) { // FIXME - record or show exception? utils.showMessageInUI(null,"JML Error","ESC could not be performed because of JML typechecking errors"); msym = null; } } if (msym != null) { Timer t = new Timer(); if (Options.uiverboseness) Log.log("Beginning ESC on " + msym); if (monitor != null) monitor.subTask("ESChecking " + msym); IProverResult res = api.doESC(msym); highlightCounterexamplePath((IMethod)je,res,null); } else {} // ERROR - FIXME } else if (je instanceof IType) { ClassSymbol csym = convertType((IType)je); if (csym != null) api.doESC(csym); else {} // ERROR - FIXME } } if (Options.uiverboseness) Log.log(Timer.timer.getTimeString() + " Completed ESC operation on individual methods"); } if (monitor != null) { monitor.subTask("Completed ESC operation"); } } catch (Main.JmlCanceledException e) { if (monitor != null) { monitor.subTask("Canceled ESC operation"); } if (Options.uiverboseness) Log.log(Timer.timer.getTimeString() + " Canceled ESC operation"); } } // public void highlightCounterexamplePath(IMethod je) { // MethodSymbol msym = convertMethod(je); // if (msym != null) highlightCounterexamplePath(je, msym, null); // } public void highlightCounterexamplePath(IMethod je, IProverResult res, ICounterexample cce) { if (res == null) { utils.deleteHighlights(je.getResource(), null); return; } // Log.log("TIMES " + je.getResource().getLocalTimeStamp() + " " + res.timestamp().getTime() + " " + (je.getResource().getLocalTimeStamp() > res.timestamp().getTime())); if (je.getResource().getLocalTimeStamp() > res.timestamp().getTime()) { utils.showMessageInUI(null, "OpenJML", "The file is newer than the counterexample information - the highlighting may be offset."); } utils.deleteHighlights(je.getResource(), null); if (res != null) { IProverResult.ICounterexample ce = cce != null ? cce : res.counterexample(); if (ce != null && ce.getPath() != null) { for (IProverResult.Span span: ce.getPath()) { if (span.end > span.start) { // The test is just defensive utils.highlight(je.getResource(), span.start, span.end, span.type); } else { Log.log("BAD HIGHLIGHT RANGE " + span.start + " " + span.end + " " + span.type); } } } } } /** Converts Eclipse's representation of a type to OpenJML's, that is * from an IType to a ClassSymbol. * @param type the Eclipse representation of a type * @return the corresponding OpenJML ClassSymbol * @throws OpenJMLException if the corresponding class symbol does not exist in OpenJML * (e.g. the files have not been entered) */ public @NonNull ClassSymbol convertType(IType type) throws Utils.OpenJMLException { String className = type.getFullyQualifiedName(); // FIXME - for files that are not actually Java files, the fully qualified name does not include the package ??? if (className.indexOf('.') == -1) { // No dot in the name. The class might be in the default package, // but also if the IType came from a file that is not in a source // folder, then the fullyQualifiedName does not include the package // I don't know whether this a bug or whether it is the intended // behavior for a non-real Java file such as a specification file. // In any case, we try to make amends. try { //System.out.println("NAME OF " + type.getElementName() + " " + className); ICompilationUnit cu = type.getCompilationUnit(); //String s = type.getTypeQualifiedName(); IPackageDeclaration[] pks = cu.getPackageDeclarations(); if (pks.length > 0) { // Documentation says not to expect more than one String pname = pks[0].getElementName(); className = pname + Strings.dot + className; } } catch (Exception e) { throw new Utils.OpenJMLException("Could not find a class symbol for " + className,e); //$NON-NLS-1$ } } ClassSymbol csym = api.getClassSymbol(className); return csym; } public IResource getResourceFor(Symbol sym) { if (sym instanceof MethodSymbol) sym = sym.owner; if (sym instanceof ClassSymbol) { IType t = convertType((ClassSymbol)sym); return t.getResource(); } return null; } /** Converts Eclipse's representation of a method (an IMethod) to OpenJML's * (a MethodSymbol) * @param method the Eclipse IMethod for the method * @return OpenJML's MethodSymbol for the same method * @throws Utils.OpenJMLException if there is no match */ public @Nullable MethodSymbol convertMethod(IMethod method) { ClassSymbol csym = convertType(method.getDeclaringType()); if (csym == null) return null; try { com.sun.tools.javac.util.Name name = com.sun.tools.javac.util.Names.instance(api.context()).fromString( method.isConstructor() ? "<init>" : method.getElementName()); //$NON-NLS-1$ Scope.Entry e = csym.members().lookup(name); // FIXME - Need to match types & number MethodSymbol firstsym = null; outer: while (e != null && e.sym != null) { Symbol sym = e.sym; e = e.next(); if (!(sym instanceof MethodSymbol)) continue; MethodSymbol msym = (MethodSymbol)sym; List<VarSymbol> params = msym.getParameters(); String[] paramtypes = method.getParameterTypes(); if (params.size() != paramtypes.length) continue; firstsym = msym; int i = 0; for (VarSymbol v: params) { // Does v.type match paramtypes[i] if (!typeMatches(v.type,paramtypes[i++])) continue outer; } return msym; } if (firstsym != null) return firstsym; // FIXME: hack because of poor type matching String error = "No matching method in OpenJML: " + method.getDeclaringType().getFullyQualifiedName() + Strings.dot + method; //$NON-NLS-1$ Log.errorlog(error,null); throw new Utils.OpenJMLException(error); } catch (JavaModelException e) { throw new Utils.OpenJMLException("Unable to convert method " + method.getElementName() + " of " + method.getDeclaringType().getFullyQualifiedName(),e); } } public IMethod convertMethod(MethodSymbol msym) { try { String className = msym.owner.getQualifiedName().toString(); IType eClass = jproject.findType(className); // FIXME - OK for nested classes? String mname = msym.name.toString(); int nargs = msym.params.size(); // FIXME - finds first match on name, not on tyep signature if (eClass == null) return null; // FIXME - this can happen if the class is not in a source folder for (IMethod m: eClass.getMethods()) { if (nargs == m.getNumberOfParameters() && mname.equals(m.getElementName())) return m; } return null; //eClass.getMethod(msym.name,toString(),) } catch (Exception e) { return null; } } public IType convertType(ClassSymbol sym) { try { String className = sym.getQualifiedName().toString(); IType eClass = jproject.findType(className); // FIXME - OK for nested classes? return eClass; } catch (Exception e) { return null; } } // FIXME - review and document public boolean typeMatches(Type t, String tstring) { String vt = t.toString(); if (tstring.charAt(0) == '[') { int k = 1; while (tstring.charAt(k) == '[') k++; int n = vt.length()-2*k; if (n < 0 || vt.charAt(n) != '[') return false; vt = vt.substring(0,vt.length()-2*k); tstring = tstring.substring(k); } char c = tstring.charAt(0); if (tstring.equals("I")) tstring = "int"; else if (tstring.equals("F")) tstring = "float"; else if (tstring.equals("D")) tstring = "double"; else if (tstring.equals("C")) tstring = "char"; else if (tstring.equals("J")) tstring = "long"; else if (tstring.equals("S")) tstring = "short"; else if (tstring.equals("B")) tstring = "byte"; else if (tstring.equals("Z")) tstring = "boolean"; else if (c == 'Q') tstring = tstring.substring(1,tstring.length()-1); else return false; if (c == 'Q') { int p = tstring.indexOf('<'); if (p >= 0) { String prefix = tstring.substring(0,p); //String rest = tstring.substring(p+1); if (!((ClassSymbol)t.tsym).getQualifiedName().toString().endsWith(prefix)) return false; return true; // FIXME // List<Type> targs = t.getTypeArguments(); // for (Type ta : targs) { // p = rest.index // } } } if (vt.equals(tstring)) { // Log.log(tstring + " matches " + vt); } else if (vt.endsWith(tstring)) { // FIXME - trouble with matching type parameters // Log.log(tstring + " ends " + vt); } else { // Log.log(tstring + " does not match " + vt); return false; } return true; } /** Return the specs of the given type, including all inherited specs, * pretty-printed as a String. This * may be an empty String if there are no specifications. * @param type the Eclipse IType for the class whose specs are wanted * @return the corresponding specs, as a String */ public @Nullable String getAllSpecs(@NonNull IType type) { ClassSymbol csym = convertType(type); if (csym == null) { return null; } List<TypeSpecs> typeSpecs = api.getAllSpecs(csym); try { StringBuilder sb = new StringBuilder(); for (TypeSpecs ts: typeSpecs) { sb.append("From " + ts.file.getName() + Strings.eol); //$NON-NLS-1$ sb.append(api.prettyPrintJML(ts.modifiers)); sb.append(Strings.eol); sb.append(ts.toString()); sb.append(Strings.eol); } return sb.toString(); } catch (Exception e) { return "<Exception>: " + e; //$NON-NLS-1$ } } /** Return the specs of the given method, including all inherited specs, * pretty-printed as a String. This * may be an empty String if there are no specifications. * @param method the Eclipse IMethod for the method whose specs are wanted * @return the corresponding specs, as a String */ public @Nullable String getAllSpecs(@NonNull IMethod method) { MethodSymbol msym = convertMethod(method); if (msym == null) return null; List<JmlSpecs.MethodSpecs> methodSpecs = api.getAllSpecs(msym); try { StringBuilder sb = new StringBuilder(); for (JmlSpecs.MethodSpecs ts: methodSpecs) { sb.append("From " + ts.cases.decl.sourcefile.getName()); //$NON-NLS-1$ sb.append(Strings.eol); sb.append(api.prettyPrintJML(ts.mods)); // FIXME - want the collected mods in the JmlMethodSpecs sb.append(Strings.eol); sb.append(api.prettyPrintJML(ts.cases)); sb.append(Strings.eol); } return sb.toString(); } catch (Exception e) { return "<Exception>: " + e; //$NON-NLS-1$ } } /** Returns the specs of the given field as a String * @param field the Eclipse IField value denoting the field whose specs are wanted * @return the specs of the field, as a String; or null, if specs not available */ public @NonNull String getSpecs(@NonNull IField field) { String s = field.getDeclaringType().getFullyQualifiedName(); ClassSymbol csym = api.getClassSymbol(s); if (csym == null) return null; VarSymbol fsym = api.getVarSymbol(csym,field.getElementName()); FieldSpecs fspecs = api.getSpecs(fsym); try { return fspecs.toString(); } catch (Exception e) { return "<Exception>: " + e; //$NON-NLS-1$ } } // FIXME - revise this /** Show information about the most recent proof of the given method in a * dialog box for the given Shell. This information states whether the * proof was performed, whether it was successful, and launches editor * windows containing the proof's counterexample context, trace, and * basic block program. * @param method the Eclipse IMethod for the method whose proof is wanted * @param shell the shell to be used to parent any dialog windows */ public void showProofInfo(@NonNull IMethod method, @NonNull Shell shell, boolean showDetails) { IProverResult r = getProofResult(method); MethodSymbol msym = r.methodSymbol(); utils.setTraceView(keyForSym(msym), method.getJavaProject()); if (!showDetails) return; utils.showMessage(shell, "OpenJML", "Detailed proof information is not yet implemented"); // DETAILS // FIXME - show altered program, basic block program, SMT program // String s = ((API)api).getBasicBlockProgram(msym); // utils.launchEditor(s,msym.owner.name + Strings.dot + msym.name); return; } // FIXME - documentation public String getCEValue(int pos, int end, String text, IResource r) { IJavaElement je = (IJavaElement)r.getAdapter(IJavaElement.class); ClassSymbol csym = convertType(((ICompilationUnit)je).findPrimaryType()); if (api == null) return "No proof information available"; com.sun.tools.javac.comp.Env<AttrContext> env = Enter.instance(api.context()).getEnv(csym); if (env == null) return "No proof information available"; JmlCompilationUnit tree = (JmlCompilationUnit)env.toplevel; if (tree == null) return "No proof information available"; API.Finder finder = api.findMethod(tree,pos,end,text,r.getLocation().toString()); JmlMethodDecl parentMethod = finder.parentMethod; JCTree node = finder.found; if (parentMethod == null) return "No containing method found"; // if (parentMethod.sym != mostRecentProofMethod) { // return "Selected text is not within the method of the most recent proof (which is " + mostRecentProofMethod + ")"; // } String out; if (node instanceof JmlVariableDecl) { // This happens when we have selected a method parameter or the variable within a declaration // continue String name = ((JmlVariableDecl)node).name.toString(); out = text == null ? null : ("Found declaration: " + name + "\n"); //node = ((JmlVariableDecl)node).ident; if (text == null) out = "Declaration " + name + " <B>is</B> "; } else if (!(node instanceof JCTree.JCExpression)) { return text == null ? null : ("Selected text is not an expression (" + node.getClass() + "): " + text); } else { if (text == null) out = node.toString().replace("<", "<") + " <B>is</B> "; else out = "Found expression node: " + node.toString() + "\n"; } IProverResult res = getProofResult(parentMethod.sym); if (res != null) { ICounterexample ce = res.counterexample(); if (ce == null) return null; String value = ce.get(node); if (value != null) { if (text == null) out = out + value; else out = out + "Value " + node.type + " : " + value; if (node.type.getTag() == TypeTag.CHAR) { try { out = out + " ('" + (char)Integer.parseInt(value) + "')"; } catch (NumberFormatException e) { // ignore } } } else out = text == null ? null : (out + "Value is unknown (type " + node.type + ")"); return out; } return null; //return "No counterexample information available"; } /** Instances of this class can be registered as OpenJML DiagnosticListeners (that is, * listeners for errors from tools in the javac framework); they then convert * any diagnostic into an Eclipse problem (a JmlEclipseProblem) of the * appropriate severity, which is * then fed to the given IProblemRequestor. * * @author David Cok * */ static public class EclipseDiagnosticListener implements DiagnosticListener<JavaFileObject> { /** When this listener hears a problem from OpenJML, * it needs to report the problem to Eclipse, by calling report * on this IProblemRequestor */ @NonNull @SpecPublic protected IProblemRequestor preq; /** Creates a listener which will translate any diagnostics it hears and * send them off to the given IProblemRequestor. * @param p the problem requestor that wants to receive any diagnostics */ //@ assignable this.*; //@ ensures preq == p; public EclipseDiagnosticListener(@NonNull IProblemRequestor p) { preq = p; } /** This is the method that is called whenever a diagnostic is issued; * it will convert and pass it on to the problem requestor. If the * diagnostic cannot be converted to an Eclipse IProblem, then it is * printed to Log.log or Log.errorlog. * * @param diagnostic the diagnostic to be translated and forwarded */ @Override @SuppressWarnings("restriction") public void report(@NonNull Diagnostic<? extends JavaFileObject> diagnostic) { JavaFileObject javaFileObject = diagnostic.getSource(); String message = diagnostic.getMessage(null); // uses default locale int id = 0; // TODO - we are not providing numerical ids for problems Diagnostic.Kind kind = diagnostic.getKind(); // Here the Diagnostic.Kind is the categorization of errors from OpenJDK // We use: ERROR - compilation and JML typechecking errors // WARNING - less serious JML typechecking errors (e.g. deprecated syntax) // NOTE - informational - e.g. features not implemented and hence ignored // OTHER - used for static checking warnings // The ProblemSeverities are Eclipse's categorization of errors // There are various kinds of errors, of which we only use Error (e.g. also Fatal, Abort, ...) // As you see we map ERROR -> Error, WARNING -> Warning, // NOTE -> Ignore, OTHER (static check warnings) -> SecondaryError // Static check warnings could be mapped to Warning, but we want to // distinguish them from Errors and Warnings. This is a little problem // prone. Also, I think that some compiler options might turn off // the NOTE/Ignore markers - check this - TODO int severity = kind == Diagnostic.Kind.ERROR ? ProblemSeverities.Error : kind == Diagnostic.Kind.WARNING ? ProblemSeverities.Warning : kind == Diagnostic.Kind.MANDATORY_WARNING ? ProblemSeverities.Error : kind == Diagnostic.Kind.NOTE ? ProblemSeverities.Ignore : kind == Diagnostic.Kind.OTHER ? ProblemSeverities.SecondaryError : ProblemSeverities.Error; // There should not be anything else, but we'll call it an error just in case. long pos = diagnostic.getPosition(); long col = diagnostic.getColumnNumber();// 1-based, from beginning of line long line = diagnostic.getLineNumber(); // 1-based long startPos = diagnostic.getStartPosition(); //0-based, from beginning of file long endPos = diagnostic.getEndPosition(); if (endPos == -1) endPos = startPos; // This is defensive - sometimes there is no end position set int ENOPOS = -1; // Eclipse value for not having position information long startPosition = pos == Diagnostic.NOPOS ? ENOPOS : (startPos-pos) + col; // FIXME - a hack - what if crosses lines long endPosition = pos == Diagnostic.NOPOS ? ENOPOS : (endPos-pos) + col; // FIXME - a hack - what if crosses lines long lineStart = pos == Diagnostic.NOPOS ? ENOPOS : (pos - col); // FIXME - a hack long lineEnd = pos == Diagnostic.NOPOS ? ENOPOS : lineStart + 100; // FIXME - a real hack IWorkspace workspace = ResourcesPlugin.getWorkspace(); IWorkspaceRoot root = workspace.getRoot(); IFile resource = null; if (javaFileObject != null) { // if null, there is no source file URI filename = javaFileObject.toUri(); // This should be the full, absolute path IPath path = new Path(filename.getPath()); resource = root.getFileForLocation(path); // For this to be non-null, // the file must be in the workspace. This means that all // source AND SPECS must be in open projects in the workspace. //FIXME - linked resources will not work here I think } if (resource == null) { // This can happen when the file to which the diagnostic points // is not an IResource. For example, the Associated Declaration // to an error may be in a system specifications file. // FIXME - we should load a read-only text file, with the markers // for this case. if (!diagnostic.toString().contains("Associated declaration")) { if (severity == ProblemSeverities.Error) Log.errorlog(diagnostic.toString(),null); else Log.log(diagnostic.toString()); } } else { String text = null; // FIXME - the following does not work // try { // text = javaFileObject == null ? null : javaFileObject.getCharContent(true).toString(); // } catch (java.io.IOException e) { // // ignore - just leave text as null; // } JmlEclipseProblem problem = new JmlEclipseProblem(resource, message, id, severity, (int)startPosition, (int)endPosition, (int)line, text, (int)lineStart, (int)lineEnd); problem.sourceSymbol = currentMethod; preq.acceptProblem(problem); // Log it as well if (Options.uiverboseness) { if (severity == ProblemSeverities.Error) Log.errorlog(diagnostic.toString(),null); else Log.log(diagnostic.toString()); } } } } /** This class is a specific instantiation of the IProgressReporter interface; * it receives progress report calls and both logs them and displays them * in the progress monitor supplied by the Eclipse UI. * @author David R. Cok */ public static class UIProgressListener implements org.jmlspecs.openjml.Main.IProgressListener { /** The associated Eclipse progress monitor */ protected @Nullable IProgressMonitor monitor; /** The associated OpenJML compilation context */ protected @NonNull Context context; /** The UI shell in which to display dialogs */ protected @Nullable Shell shell = null; /** Instantiates a listener for progress reports that displays them in * the Eclipse UI. * @param context the OpenJML compilation context * @param monitor the Eclipse progress monitor * @param shell the Eclipse UI shell */ //@ assignable this.*; public UIProgressListener(@NonNull Context context, @Nullable IProgressMonitor monitor, @Nullable Shell shell) { this.monitor = monitor; this.context = context; this.shell = shell; } /** This is the method called by OpenJML when a progress message is sent. * The implementation here logs the message if the level is <=1 and * displays it in the progress monitor if one was supplied in the * constructor. The UI work is done in a Runnable spawned from * Display.asyncExec . * @return true if the progress reporter was canceled */ //@ ensures not_assigned(this.*); @Override public boolean report(final int ticks, final int level, final String message) { Display d = shell == null ? Display.getDefault() : shell.getDisplay(); // Singleton reported that with asynExec, this would hang d.syncExec(new Runnable() { public void run() { if (monitor != null) { if (level <= 1) monitor.subTask(message); monitor.worked(ticks); } Log.log(message); } }); boolean cancel = monitor != null && monitor.isCanceled(); // if (cancel) { // cancel = true; // //throw new Main.JmlCanceledException("Operation cancelled"); //$NON-NLS-1$ // //throw new PropagatedException(new Main.JmlCanceledException("Operation cancelled")); //$NON-NLS-1$ // } return cancel; } /** Sets the OpenJML compilation context associated with this listener. */ @Override //@ assignable this.context; //@ ensures this.context == context; public void setContext(@NonNull Context context) { this.context = context; } } Map<String,IProverResult> proofResults = new HashMap<String,IProverResult>(); Map<IMethod,String> methodResults = new HashMap<IMethod,String>(); protected String keyForSym(Symbol sym) { if (sym instanceof MethodSymbol) { return sym.owner.getQualifiedName().toString() + Strings.dot + sym.toString(); } else if (sym.name.isEmpty()) { return ((ClassSymbol)sym).flatname.toString(); // for anonymous types } else { return sym.getQualifiedName().toString(); } } @Override public void reportProofResult(MethodSymbol msym, IProverResult result) { if (result.result() == IProverResult.RUNNING) { currentMethod = msym; } if (result.result() == IProverResult.COMPLETED) { currentMethod = null; return; } String key = keyForSym(msym); proofResults.put(key,result); IMethod m = convertMethod(msym); methodResults.put(m, key); utils.refreshView(key); } static MethodSymbol currentMethod; public @Nullable IProverResult getProofResult(MethodSymbol msym) { return proofResults.get(keyForSym(msym)); } public @Nullable IProverResult getProofResult(IMethod m) { String key = methodResults.get(m); if (key == null) return null; return proofResults.get(key); } public @Nullable Map<String,IProverResult> getProofResults() { return proofResults; } public void clearProofResults(IJavaProject currentProject) { proofResults.clear(); methodResults.clear(); } /** Sets the monitor to be used to show progress * @param monitor the monitor to be used, if any */ public void setMonitor(@Nullable IProgressMonitor monitor) { if (monitor != null) { api.setProgressListener(new UIProgressListener(api.context(),monitor,null)); } else { api.setProgressListener(null); } } /** Retrieves the options from the preference page, determines the * corresponding options for OpenJML, tailored to the given Cmd. * If cmd is null, all options are retrieved. * @param jproject * @param cmd The command to be executed, or null to get all options * @return the list of options and arguments */ public @NonNull List<String> getOptions(IJavaProject jproject, /*@nullable*/Main.Cmd cmd) { //com.sun.tools.javac.util.Options openjmlOptions = com.sun.tools.javac.util.Options.instance(api.context()); String eq = "="; //$NON-NLS-1$ //Options opt = Activator.options; List<String> opts = new LinkedList<String>(); if (cmd != null) { opts.add(JmlOption.COMMAND.optionName() +eq+ cmd); } { opts.add(JmlOption.STRICT.optionName() +eq+ Options.isOption(Options.strictKey)); opts.add(JmlOption.PURITYCHECK.optionName() +eq+ !Options.isOption(Options.noCheckPurityKey)); opts.add(JmlOption.SHOW.optionName() +eq+ Options.isOption(Options.showKey)); } if (cmd == Main.Cmd.ESC || cmd == null) { String prover = Options.value(Options.defaultProverKey); opts.add(JmlOption.PROVER.optionName() +eq+ prover); opts.add(JmlOption.PROVEREXEC.optionName() +eq+ Options.value(Options.proverPrefix + prover)); opts.add(JmlOption.ESC_MAX_WARNINGS.optionName() +eq+ Options.value(Options.escMaxWarningsKey)); opts.add(JmlOption.TRACE.optionName() +eq+ "true"); opts.add(JmlOption.SUBEXPRESSIONS.optionName() +eq+ "true"); opts.add(JmlOption.FEASIBILITY.optionName() +eq+ Options.value(Options.feasibilityKey)); String v = Options.value(Options.timeoutKey); if (v != null && !v.isEmpty()) opts.add(JmlOption.TIMEOUT.optionName() +eq+ v); // FIXME - add an actual option opts.add("-code-math=java"); } if (cmd == Main.Cmd.RAC || cmd == null) { opts.add(JmlOption.RAC_SHOW_SOURCE.optionName() +eq+ !Options.isOption(Options.racNoShowSource)); opts.add(JmlOption.RAC_CHECK_ASSUMPTIONS.optionName() +eq+ Options.isOption(Options.racCheckAssumptions)); opts.add(JmlOption.RAC_PRECONDITION_ENTRY.optionName() +eq+ Options.isOption(Options.racPreconditionEntry)); opts.add(JmlOption.RAC_JAVA_CHECKS.optionName() +eq+ Options.isOption(Options.racCheckJavaFeatures)); opts.add(JmlOption.RAC_COMPILE_TO_JAVA_ASSERT.optionName() +eq+ Options.isOption(Options.compileToJavaAssert)); } opts.add(JmlOption.VERBOSENESS.optionName()+eq+Options.value(Options.verbosityKey)); if (Options.isOption(Options.javaverboseKey)) { opts.add(com.sun.tools.javac.main.Option.VERBOSE.getText()); } if (Options.isOption(Options.showNotImplementedKey)) opts.add(JmlOption.SHOW_NOT_IMPLEMENTED.optionName()); if (Options.isOption(Options.showNotExecutableKey)) opts.add(JmlOption.SHOW_NOT_EXECUTABLE.optionName()); if (Options.isOption(Options.checkSpecsPathKey)) opts.add(JmlOption.CHECKSPECSPATH.optionName()); opts.add(JmlOption.NULLABLEBYDEFAULT.optionName()+eq+Options.isOption(Options.nullableByDefaultKey)); String other = Options.value(Options.otherOptionsKey); if (other != null) { other = other.trim(); // Split by whitespace (won't handle quoted strings with whitespace) if (!other.isEmpty()) for (String o : other.split("\\s")) { //$NON-NLS-1$ opts.add(o); } } if (cmd == Main.Cmd.JMLDOC) { // jmldoc specific options opts.add("-private"); //$NON-NLS-1$ } // The plug-in adds the internal specs (or asks the user for external specs) // So, openjml itself never needs to opts.add("-no"+JmlOption.INTERNALSPECS.getText()); opts.add(JmlOption.SPECS.optionName()); opts.add(PathItem.getAbsolutePath(jproject,Env.specsKey)); opts.add(com.sun.tools.javac.main.Option.SOURCEPATH.getText()); opts.add(PathItem.getAbsolutePath(jproject,Env.sourceKey)); // Handle the classpath and internal runtime library if needed List<String> cpes = utils.getClasspath(jproject); boolean first = true; StringBuilder ss = new StringBuilder(); for (String s: cpes) { if (first) first = false; else ss.append(File.pathSeparator); ss.append(s); } // The runtime library is always either in the classpath or added // here by the plugin, so openjml itself never adds it opts.add("-no"+JmlOption.INTERNALRUNTIME.optionName()); if (!Options.isOption(Options.noInternalRuntimeKey)) { String runtime = utils.fetchRuntimeLibEntry(); if (runtime != null) { ss.append(File.pathSeparator); ss.append(runtime); } } opts.add(Strings.classpathOptionName); opts.add(ss.toString()); // FIXME - what about these options // roots, nojml, nojavacompiler // trace subexpressions counterexample // specs, stopiferrors // Java options, Jmldoc options if (Options.uiverboseness) { StringBuilder s = new StringBuilder(); s.append("Options collected by UI to send to OpenJML: "); //$NON-NLS-1$ for (String opt: opts) { s.append(Strings.space); s.append(opt); } Log.log(s.toString()); } return opts; } /** Adds additional command-line options in the OpenJml object */ public void addOptions(String... args) { api.addOptions(args); } /** Gets a String representation of the BasicBlock encoding of the method * body for the given method. * @param msym the method whose body is to be returned * @return a String representation of the Basic Bloxk program for the method body */ public @NonNull String getBasicBlockProgram(@NonNull MethodSymbol msym) { return ""; // FIXME: ((API)api).getBasicBlockProgram(msym); } // /** Converts a name into a flatname with dots between the components; this // * is particularly suited to converting a tree-form of a package name into // * dot-separated name (as a String) // * @param n the input Name // * @return the output dot-separated name as a String // */ // static public @NonNull String getPackageDotName(@NonNull Name n) { // if (n instanceof SimpleName) return ((SimpleName)n).getIdentifier(); // QualifiedName qn = (QualifiedName)n; // return getPackageDotName(qn.getQualifier()) + "." + qn.getName().getIdentifier(); // } }