Download
Getting Started
Members
Projects
Community
Marketplace
Events
Planet Eclipse
Newsletter
Videos
Participate
Report a Bug
Forums
Mailing Lists
Wiki
IRC
How to Contribute
Working Groups
Automotive
Internet of Things
LocationTech
Long-Term Support
PolarSys
Science
OpenMDM
More
Community
Marketplace
Events
Planet Eclipse
Newsletter
Videos
Participate
Report a Bug
Forums
Mailing Lists
Wiki
IRC
How to Contribute
Working Groups
Automotive
Internet of Things
LocationTech
Long-Term Support
PolarSys
Science
OpenMDM
Toggle navigation
Bugzilla – Attachment 112477 Details for
Bug 247207
runtime (non-debug) string evaluation
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Requests
|
Help
|
Log In
[x]
|
Terms of Use
|
Copyright Agent
[patch]
java source file implementing the feature
EvaluationEngine.java (text/plain), 19.55 KB, created by
Florin Mateoc
on 2008-09-12 21:51:56 EDT
(
hide
)
Description:
java source file implementing the feature
Filename:
MIME Type:
Creator:
Florin Mateoc
Created:
2008-09-12 21:51:56 EDT
Size:
19.55 KB
patch
obsolete
>import java.io.OutputStream; >import java.io.PrintWriter; >import java.util.ArrayList; >import java.util.List; > >import org.eclipse.jdt.core.compiler.CategorizedProblem; >import org.eclipse.jdt.core.compiler.InvalidInputException; >import org.eclipse.jdt.internal.compiler.ASTVisitor; >import org.eclipse.jdt.internal.compiler.ClassFile; >import org.eclipse.jdt.internal.compiler.CompilationResult; >import org.eclipse.jdt.internal.compiler.ast.Expression; >import org.eclipse.jdt.internal.compiler.ast.MessageSend; >import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration; >import org.eclipse.jdt.internal.compiler.ast.ReturnStatement; >import org.eclipse.jdt.internal.compiler.ast.Statement; >import org.eclipse.jdt.internal.compiler.ast.ThisReference; >import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration; >import org.eclipse.jdt.internal.compiler.batch.CompilationUnit; >import org.eclipse.jdt.internal.compiler.batch.Main; >import org.eclipse.jdt.internal.compiler.lookup.BlockScope; >import org.eclipse.jdt.internal.compiler.lookup.ClassScope; > >/** > * The class EvaluationEngine is just a holder for the static method evaluate(Object,String,boolean), > * which does all the work. It also contains a helper method for error reporting > * > * @author Florin Mateoc > * @version 1.3 > * > */ > >public class EvaluationEngine { > > public static StringBuffer sourceLinesWithErrorReportingEmbedded; > > /** > * This method takes a snippet of code and optionally an object to be used as the "this" within the snippet, > * it creates a class on the fly to hold the snippet as a method, it creates a discardable class loader > * to temporarily load the class, and then it invokes the snippet as method. > * > * @param thisObject The object to be used as the "this" in the code snippet (can be null) > * > * @param input The code snippet as a string (one or more statements). > * The last statement may be just an expression, in which case its value, > * if there are no empty return statements, will be returned as the result. > * If there are any non-empty return statements and the last statement is not > * an expression, a "return null" will be added as the last statement > * > * @return The result of the evaluation. Null if compilation fails > * In case of failure, the static variable sourceLinesWithErrorReportingEmbedded > * is also filled so that the caller can return it as feedback to the user > */ > public static Object evaluate(Object thisObject, final String input) { > System.out.println("Evaluation started"); > sourceLinesWithErrorReportingEmbedded = null; > > //We start by assuming that the method is void > final StringBuffer s0 = new StringBuffer("public class RuntimeEvaluator { public static void value(final Object thisObject) {\n"); > int chunkStart = s0.length(); > s0.append(input); > //Just in case the user has not finished with a semicolon > String trimmed = input.trim(); > char lastChar = trimmed.charAt(trimmed.length() - 1); > if(lastChar != ';' && lastChar != '}') > s0.append(";"); > int chunkEnd = s0.length(); > s0.append("}}"); > > //We don't care about any compilation logs, > //this first compilation round is just for parsing (getting the AST) > OutputStream sink = new OutputStream() { > public void write(byte[] b, int off, int len) {} > public void write(int b) {} > }; > Main compilationMain = new Main(new PrintWriter(sink), new PrintWriter(sink), false /* systemExit */, null /* options */, null /* progress */) > { > public CompilationUnit[] getCompilationUnits() throws InvalidInputException { > //Main sets these next options to false, but our source is initially most likely incorrect, > //usually it is missing the return keyword, and it may be using "this" in a static method. > //As a result of these errors, no statements would be generated in the AST for our method, but > //we still need to generate the statements in order to visit them and get > //the information needed to patch them. > //This is a hack, relying on the fact that the actual compilation is performed > //after calling this method, and also after the options were already set to false > this.compilerOptions.performStatementsRecovery = true; > this.compilerOptions.performMethodsFullRecovery = true; > return new CompilationUnit[] {new CompilationUnit(s0.toString().toCharArray(), filenames[0], null)}; > } > }; > compilationMain.compile(new String[]{"-5" /* for boxing/unboxing */, "-nowarn", "-proc:only" /* do not generate classfiles */, "-proceedOnError", "RuntimeEvaluator.java"}); > final ArrayList thisReferences = new ArrayList(); > final int[] missingReturn = new int[1]; > final boolean[] shouldBeVoid = new boolean[1]; > ASTVisitor v = new ASTVisitor() { > public void endVisit(ThisReference thisReference, BlockScope scope) { > //These are the vanilla references to "this" which we want to transform to references to > //the method argument "thisObject". > //We cannot refactor super sends, and we don't bother with inner classes. > //Also, because for now we only support implicit "this" references within message sends > //(and not within field references), and we don't have a parent pointer in the ASTNode, > //so we cannot distinguish between the two kinds of implicits in this visitor method, > //we skip the implicit "this" references here and will instead add them when visiting messages > if(thisReference.getClass() == ThisReference.class && !thisReference.isImplicitThis()) > thisReferences.add(thisReference); > } > public void endVisit(MessageSend messageSend, BlockScope scope) { > if(messageSend.receiver.isImplicitThis()) > //We do want to allow for implicit this in message sends. > //An instance of ThisReference already exists here as the receiver, > //but we cannot use it since it has no sourceStart and sourceEnd set, so we have to > //create a new one here to record the position. > //It will be later recognizable (although it would respond false to "isImplicitThis") > //by the fact that it has its sourceStart equal to its sourceEnd > thisReferences.add(new ThisReference(messageSend.sourceStart, messageSend.sourceStart)); > } > public void endVisit(ReturnStatement returnStatement, BlockScope scope) { > if(returnStatement.expression == null) > shouldBeVoid[0] = true; > } > public void endVisit(MethodDeclaration methodDeclaration, ClassScope scope) { > if(shouldBeVoid[0]) > return; > //We know that the user has not used any explicit empty returns (which would have forced us > //to declare the method void) > //At the same time, if the user has ended the snippet with (or perhaps the whole snippet is just) > //an expression, they are probably interested in its value, so we have to insert a return > //in front of it > Statement last = methodDeclaration.statements[methodDeclaration.statements.length - 1]; > if(last instanceof Expression ) > missingReturn[0] = last.sourceStart; > } > }; > TypeDeclaration t = compilationMain.batchCompiler.parser.compilationUnit.declarationOfType(new char[][]{"RuntimeEvaluator".toCharArray()}); > //although we only have one method in our source class declaration, a default constructor is automatically added, > //so our method node is the second one in the methods array > t.methods[1].traverse(v, (ClassScope)null); > > final StringBuffer s1 = new StringBuffer(s0.capacity()); > s1.append("public class RuntimeEvaluator { public static "); > if(shouldBeVoid[0]) > s1.append("void"); > else > s1.append("Object"); > s1.append(" value(final Object thisObject) {\n"); > > String referenceWithCast = null; > //We replace references to 'this' in the user supplied code with references to our static method's argument 'thisObject' > //We also have to cast them to the runtime type of the object, since the users most likely know the runtime type when writing the code, > //(they may be looking at the object in an inspector), so they expect it to understand all its runtime type's methods. > //We may also need to interleave the this->thisObject replacements with insertions of 'return' keywords > if(thisObject != null) > referenceWithCast = "((" + thisObject.getClass().getName() + ") thisObject)"; > //We also need to keep track of source position modifications, so that we can translate back > //any error messages to the original source: > //Every time we have an insertion, we map the positions of all the newly inserted characters to the insertion point in the original > //When we have a replacement, we distribute the positions (see below) > ArrayList translatedPositions = new ArrayList(s0.length()); > for(int index = 0; index < thisReferences.size(); index++) { > int i = ((ThisReference)thisReferences.get(index)).sourceStart; > if(missingReturn[0] <= i && (index + 1 == thisReferences.size() || > missingReturn[0] > ((ThisReference)thisReferences.get(index + 1)).sourceStart)) { > s1.append(s0, chunkStart, missingReturn[0]); > for(int e=chunkStart; e < missingReturn[0]; e++) > translatedPositions.add(e); > s1.append("return "); > for(int e=0; e < "return ".length(); e++) > translatedPositions.add(missingReturn[0]); > chunkStart = missingReturn[0]; > } > s1.append(s0, chunkStart, i); > for(int e=chunkStart; e < i; e++) > translatedPositions.add(e); > s1.append(referenceWithCast); > if(((ThisReference)thisReferences.get(index)).sourceEnd == i) { > //we have our own kind of an implicit this reference here > s1.append("."); > for(int e=0; e < referenceWithCast.length() + 1; e++) > translatedPositions.add(i); > chunkStart = i; > } > else { > //"((SomeFullyQualifiedTypeCast)thisObject)" is longer than "this", so we distribute the positions: > //(everything up to and including the first 't' in "thisObject" corresponds to the 't' in "this"; > //the next 3 characters correspond to the 'h', the next 3 to the 'i', and the last 4 (3+1) to the 's' > for(int e=0; e < referenceWithCast.length(); e++) > translatedPositions.add(i); > for(int e=1; e<4; e++) { > translatedPositions.set(translatedPositions.size() + 3 * e - 13, i + e); > translatedPositions.set(translatedPositions.size() + 3 * e - 12, i + e); > translatedPositions.set(translatedPositions.size() + 3 * e - 11, i + e); > } > translatedPositions.set(translatedPositions.size() - 1, i + 3); > chunkStart = ((ThisReference)thisReferences.get(index)).sourceEnd + 1; > } > } > if(chunkStart <= missingReturn[0]) { > s1.append(s0, chunkStart, missingReturn[0]); > for(int e=chunkStart; e < missingReturn[0]; e++) > translatedPositions.add(e); > s1.append("return "); > for(int e=0; e < "return ".length(); e++) > translatedPositions.add(missingReturn[0]); > s1.append(s0, missingReturn[0], chunkEnd); > for(int e=missingReturn[0]; e < chunkEnd; e++) > translatedPositions.add(e); > } > else { > s1.append(s0, chunkStart, chunkEnd); > for(int e=chunkStart; e < chunkEnd; e++) > translatedPositions.add(e); > } > if(!shouldBeVoid[0] && missingReturn[0] == 0) { > //We did not insert a return to transform the ending expression into a statement, yet the method is declared as non-void > //We insert "return null" as the last statement. This could be unreachable code, but instead of trying to figure it out, > //it is cheaper to let the compiler find that out for us and report it as a problem > s1.append("\n return null;}}"); > } > else > s1.append("\n}}"); > for(int i=translatedPositions.size(); i<s1.length(); i++) > translatedPositions.add(chunkEnd); > > //This is the actual compilation of the fixed-up user code > final ClassFile[] clazzes = new ClassFile[1]; > compilationMain = new Main(new PrintWriter(System.out), new PrintWriter(System.err), false /* systemExit */, null /* options */, null /* progress */) > { > public CompilationUnit[] getCompilationUnits() throws InvalidInputException { > return new CompilationUnit[] {new CompilationUnit(s1.toString().toCharArray(), filenames[0], null)}; > } > public void outputClassFiles(CompilationResult unitResult) { > clazzes[0] = unitResult.getClassFiles()[0]; > } > }; > if(!compilationMain.compile(new String[]{"-5", "-nowarn", "RuntimeEvaluator.java"})) { > CompilationResult r = compilationMain.batchCompiler.parser.compilationUnit.compilationResult; > String[] originalLines = input.split("\n"); > > //Compilation failed, we check to see if it was caused by our inserted final "return null" statement > //This can happen if the previous statement (the last statement in the user-provided snippet) > //is either directly a return or a throw, or e.g. an if-else with both branches ending in returns or throws > boolean succeeded = false; > if(!shouldBeVoid[0] && missingReturn[0] == 0) { > //We did append a return > for(CategorizedProblem p : r.problems) { > if(p.getSourceLineNumber() - 2 == originalLines.length) { > //This error does point to the additional return that we inserted > //We'll try again without it > s1.replace(s1.lastIndexOf("\n return null;}}"), s1.length(), "\n}}"); > compilationMain = new Main(new PrintWriter(System.out), new PrintWriter(System.err), false /* systemExit */, null /* options */, null /* progress */) > { > public CompilationUnit[] getCompilationUnits() throws InvalidInputException { > return new CompilationUnit[] {new CompilationUnit(s1.toString().toCharArray(), filenames[0], null)}; > } > public void outputClassFiles(CompilationResult unitResult) { > clazzes[0] = unitResult.getClassFiles()[0]; > } > }; > if(!compilationMain.compile(new String[]{"-5", "-nowarn", "RuntimeEvaluator.java"})) { > if(compilationMain.batchCompiler.parser.compilationUnit.compilationResult.problemCount <= r.problemCount) > //Even though the compilation did not succeed this second round either, > //by removing the return that we had appended to the user snippet, the problemCount has not increased, > //so we keep these second round of results. > //No point in confusing the users with errors caused by code they did not write > r = compilationMain.batchCompiler.parser.compilationUnit.compilationResult; > } > else > succeeded = true; > break; > } > } > } > if(!succeeded) { > //Compilation failed, we interleave error reporting (caret diagnostics) with the original source, > //so that the caller can use this to report back some useful feedback > > int i = 0; > sourceLinesWithErrorReportingEmbedded = new StringBuffer(); > for(CategorizedProblem p : r.problems) { > for(; i < p.getSourceLineNumber() - 1 /* the sourceLineNumber starts counting from 1 */; i++) { > if(i < originalLines.length) > sourceLinesWithErrorReportingEmbedded.append(originalLines[i]); > sourceLinesWithErrorReportingEmbedded.append("\n"); > } > sourceLinesWithErrorReportingEmbedded.append(errorReportSource(p, r.compilationUnit.getContents(), translatedPositions)); > sourceLinesWithErrorReportingEmbedded.append("\n"); > sourceLinesWithErrorReportingEmbedded.append(p.getMessage()); > sourceLinesWithErrorReportingEmbedded.append("\n----------\n"); > } > for(; i < originalLines.length; i++) { > sourceLinesWithErrorReportingEmbedded.append(originalLines[i]); > sourceLinesWithErrorReportingEmbedded.append("\n"); > } > > return null; > } > } > > //The compilation succeeded, we now have to load the newly created class. > //Since we only do an in-memory operation (we do not write any files (neither .java nor .class)), > //we need a custom class loader. > //Also, we want to discard the class after using it (so that we can reuse its name/skeleton for other evaluations) > //For that we use a discardable class loader (that should be garbage collected together with our > //lightweight class), that is restricted to only load our class (if it needs anything else, > //it delegates to its parent class loader, which is the loader of the EvaluationEngine class itself) > Class evaluatorClass = null; > try { > evaluatorClass = (new ClassLoader(EvaluationEngine.class.getClassLoader()) { > public Class loadClass(String class_name, boolean resolve) throws ClassNotFoundException { > Class cl = null; > if(class_name.equals("RuntimeEvaluator")) { > byte[] bytes = clazzes[0].getBytes(); > cl = defineClass(class_name, bytes, 0, bytes.length); > } > else > cl = getParent().loadClass(class_name); > if(resolve) > resolveClass(cl); > return cl; > } > }).loadClass("RuntimeEvaluator", true); > } catch (ClassNotFoundException e) { > e.printStackTrace(); > } > Object value = null; > try { > value = evaluatorClass.getMethod("value", new Class[] {Object.class}).invoke(null, new Object[] {thisObject}); > System.out.print("Evaluation succeeded: "); > System.out.println(value); > } catch (Exception e) { > e.printStackTrace(); > } > return value; > } > > /** > * This method is copied almost identically from Main$Logger > * We need a copy here because on one hand the original is private, on the other hand we have to add our own processing - > * the caret positions in the error message are for the source as submitted for compilation, which was modified from > * what the user provided. We therefore have to map the caret positions back to the original source. > * To make our lives easier, the user snippet portion of the unitSource always follows a newline (and only one), > * so the translation does not care about the offset caused by the fixed skeleton (class and method declaration) > * that precede it (we will just have to remember that we are one source line off). > * Also, our transformations do not add or remove lines, nor do they move code from a line to another > */ > private static String errorReportSource(CategorizedProblem problem, char[] unitSource, List translatedPositions) { > //extra from the source the innacurate token > //and "highlight" it using some underneath ^^^^^ > //put some context around too. > > //this code assumes that the font used in the console is fixed size > > int startPosition = problem.getSourceStart(); > int endPosition = problem.getSourceEnd(); > int length = unitSource.length; > > StringBuffer errorBuffer = new StringBuffer(); > > char c; > final char SPACE = '\u0020'; > final char MARK = '^'; > final char TAB = '\t'; > //the next code tries to underline the token..... > //it assumes (for a good display) that token source does not > //contain any \r \n. This is false on statements ! > //(the code still works but the display is not optimal !) > > // expand to line limits > int begin; > int end; > for (begin = startPosition >= length ? length - 1 : startPosition; begin > 0; begin--) { > if ((c = unitSource[begin - 1]) == '\n' || c == '\r') break; > } > for (end = endPosition >= length ? length - 1 : endPosition ; end+1 < length; end++) { > if ((c = unitSource[end + 1]) == '\r' || c == '\n') break; > } > > // trim left and right spaces/tabs > while ((c = unitSource[begin]) == ' ' || c == '\t') begin++; > while ((c = unitSource[end]) == ' ' || c == '\t') end--; > > // compute underline > int actualStart = (Integer) translatedPositions.get(startPosition); > for (int i = begin; i < actualStart; i++) { > errorBuffer.append((unitSource[i] == TAB) ? TAB : SPACE); > } > int actualEnd = (Integer) translatedPositions.get(endPosition); > for (int i = actualStart; i <= (actualEnd >= length ? length - 1 : actualEnd); i++) { > errorBuffer.append(MARK); > } > return errorBuffer.toString(); > } > >}
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
Attachments on
bug 247207
: 112477