/* * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.dev.js; import com.google.gwt.dev.cfg.ConfigurationProperties; import com.google.gwt.dev.cfg.PermutationProperties; import com.google.gwt.dev.jjs.HasSourceInfo; import com.google.gwt.dev.jjs.SourceInfo; import com.google.gwt.dev.jjs.ast.JDeclaredType; import com.google.gwt.dev.jjs.ast.JMethod; import com.google.gwt.dev.jjs.ast.JProgram; import com.google.gwt.dev.jjs.ast.RuntimeConstants; import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap; import com.google.gwt.dev.js.ast.HasArguments; import com.google.gwt.dev.js.ast.HasName; import com.google.gwt.dev.js.ast.JsArrayAccess; import com.google.gwt.dev.js.ast.JsArrayLiteral; import com.google.gwt.dev.js.ast.JsBinaryOperation; import com.google.gwt.dev.js.ast.JsBinaryOperator; import com.google.gwt.dev.js.ast.JsBlock; import com.google.gwt.dev.js.ast.JsBooleanLiteral; import com.google.gwt.dev.js.ast.JsCatch; import com.google.gwt.dev.js.ast.JsContext; import com.google.gwt.dev.js.ast.JsExprStmt; import com.google.gwt.dev.js.ast.JsExpression; import com.google.gwt.dev.js.ast.JsFor; import com.google.gwt.dev.js.ast.JsFunction; import com.google.gwt.dev.js.ast.JsInvocation; import com.google.gwt.dev.js.ast.JsModVisitor; import com.google.gwt.dev.js.ast.JsName; import com.google.gwt.dev.js.ast.JsNameRef; import com.google.gwt.dev.js.ast.JsNew; import com.google.gwt.dev.js.ast.JsNode; import com.google.gwt.dev.js.ast.JsNullLiteral; import com.google.gwt.dev.js.ast.JsNumberLiteral; import com.google.gwt.dev.js.ast.JsPostfixOperation; import com.google.gwt.dev.js.ast.JsPrefixOperation; import com.google.gwt.dev.js.ast.JsProgram; import com.google.gwt.dev.js.ast.JsPropertyInitializer; import com.google.gwt.dev.js.ast.JsReturn; import com.google.gwt.dev.js.ast.JsRootScope; import com.google.gwt.dev.js.ast.JsStatement; import com.google.gwt.dev.js.ast.JsStringLiteral; import com.google.gwt.dev.js.ast.JsThrow; import com.google.gwt.dev.js.ast.JsTry; import com.google.gwt.dev.js.ast.JsUnaryOperation; import com.google.gwt.dev.js.ast.JsUnaryOperator; import com.google.gwt.dev.js.ast.JsVars; import com.google.gwt.dev.js.ast.JsVars.JsVar; import com.google.gwt.dev.js.ast.JsVisitor; import com.google.gwt.dev.js.ast.JsWhile; import com.google.gwt.dev.util.collect.Lists; import com.google.gwt.dev.util.collect.Maps; import java.io.File; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; /** * Emulates the JS stack in order to provide useful stack traces on browsers that * do not provide useful stack information. * * @see com.google.gwt.core.client.impl.StackTraceCreator */ public class JsStackEmulator { /** * Resets the global stack depth to the local stack index and top stack frame * after calls to Exceptions.toJava. This is created by * {@link EntryExitVisitor#visit(JsCatch, JsContext)}. */ private class CatchStackReset extends JsModVisitor { /** * The local stackIndex variable in the function. */ private final EntryExitVisitor eeVisitor; public CatchStackReset(EntryExitVisitor eeVisitor) { this.eeVisitor = eeVisitor; } @Override public void endVisit(JsExprStmt x, JsContext ctx) { if (!isExceptionWrappingCode(x)) { return; } // $stackDepth = stackIndex SourceInfo info = x.getSourceInfo(); JsBinaryOperation reset = new JsBinaryOperation(info, JsBinaryOperator.ASG, stackDepth.makeRef(info), eeVisitor.stackIndexRef(info)); ctx.insertAfter(reset.makeStmt()); } } private boolean isExceptionWrappingCode(JsExprStmt x) { // Looking for e = Exceptions.toJava(e); JsExpression expr = x.getExpression(); if (!(expr instanceof JsBinaryOperation)) { return false; } JsBinaryOperation op = (JsBinaryOperation) expr; if (!(op.getArg2() instanceof JsInvocation)) { return false; } JsInvocation i = (JsInvocation) op.getArg2(); JsExpression q = i.getQualifier(); if (!(q instanceof JsNameRef)) { return false; } JsName name = ((JsNameRef) q).getName(); if (name == null) { return false; } // caughtFunction is the JsFunction translated from Exceptions.toJava if (name != wrapFunctionName) { return false; } return true; } /** * The EntryExitVisitor handles pushing and popping frames onto the emulated * stack. It will operate on exactly one JsFunction. The basic transformation * is to add a push operation at every function entry, and then a pop * operation for every statement that might be the final statement executed by * the function. * <p> * General stack depth entry/exit code: * * <pre> * function foo() { * var stackIndex; * $stack[stackIndex = ++$stackDepth] = foo; * * ... do stuff .. * * $stackDepth = stackIndex - 1; * } * </pre> * <p> * For more complicated control flows involving return statements in try * blocks with as associated finally block, it is necessary to introduce a * local variable to indicate if control flow is expected to terminate * normally at the end of the finally block: * * <pre> * var exitingEarly; * try { * if (...) { * return (exitingEarly = true, new Foo()); * } * ... * } finally { * ... existing finally code .. * exitingEarly && $stackDepth = stackIndex - 1; * } * </pre> * A separate local variable is used for each try/finally nested within a * finally block. * <p> * Try statements without a catch block will have a catch block added to them * so that catch blocks are the only places where flow-control may jump to. * All catch blocks are altered so that the global $stackDepth variable is * reset to the local stack index value. This allows browser-native exceptions * to be created with the correct stack trace before the finally code is * executed with a correct stack depth. * * <pre> * try { * foo(); * } finally { * bar(); * } * </pre> * * becomes * * <pre> * try { * foo(); * } catch (e) { * e = Exceptions.toJava(e); * $stackDepth = stackIndex; * throw e; * } finally { * bar(); * } * <p> * Note that there is no specific handling for explicit throw statements, as * the stack instrumentation must also handle browser-generated exceptions * (e.g. <code>null.a()</code>). */ private class EntryExitVisitor extends JsModVisitor { /** * The name of a function-local variable to hold the invocation's slot in * the stack. */ protected JsName stackIndex; private final JsFunction currentFunction; /** * Maps finally blocks to the local variable name which is used to indicate * if that finally block will exit the function early. This is a map and not * a single value because a finally block might be nested in another exit * block. */ private Map<JsBlock, JsName> finallyBlocksToExitVariables = Maps.create(); /** * This variable will indicate the finally block that contains the last * statement that will be executed if an unconditional flow control change * were to occur within the associated try block. */ private JsBlock outerFinallyBlock; /** * Used if a return statement's expression could potentially trigger an * exception. */ private JsName returnTemp; /** * Final cleanup for any new local variables that need to be created. */ private List<JsVar> varsToAdd = Lists.create(); public EntryExitVisitor(JsFunction currentFunction) { this.currentFunction = currentFunction; } /** * If the visitor is exiting the current function's block, add additional * local variables and the final stack-pop instructions. */ @Override public void endVisit(JsBlock x, JsContext ctx) { if (x == currentFunction.getBody()) { // Add the entry code List<JsStatement> statements = x.getStatements(); int idx = statements.isEmpty() || !(statements.get(0) instanceof JsVars) ? 0 : 1; // Add push and pop statements statements.add(idx, push(currentFunction)); addPopAtEndOfBlock(x, false); // Add any needed variables JsVars vars; if (statements.get(0) instanceof JsVars) { vars = (JsVars) statements.get(0); } else { vars = new JsVars(currentFunction.getSourceInfo()); statements.add(0, vars); } for (JsVar var : varsToAdd) { vars.add(var); } } } @Override public void endVisit(JsReturn x, JsContext ctx) { if (outerFinallyBlock != null) { // There is a finally block, so we need to set the early-exit flag JsBinaryOperation asg = new JsBinaryOperation(x.getSourceInfo(), JsBinaryOperator.ASG, earlyExitRef(outerFinallyBlock), JsBooleanLiteral.get(true)); if (x.getExpr() == null) { if (ctx.canInsert()) { // exitingEarly = true; return; ctx.insertBefore(asg.makeStmt()); } else { // {exitingEarly = true; return;} JsBlock block = new JsBlock(x.getSourceInfo()); block.getStatements().add(asg.makeStmt()); block.getStatements().add(x); ctx.replaceMe(block); } } else { // return (exitingEarly = true, expr); JsBinaryOperation op = new JsBinaryOperation(x.getSourceInfo(), JsBinaryOperator.COMMA, asg, x.getExpr()); x.setExpr(op); } } else { if (x.getExpr() != null && x.getExpr().hasSideEffects()) { // temp = expr; pop(); return temp; SourceInfo info = x.getSourceInfo(); JsBinaryOperation asg = new JsBinaryOperation(info, JsBinaryOperator.ASG, returnTempRef(info), x.getExpr()); x.setExpr(returnTempRef(info)); pop(x, asg, ctx); } else { // Otherwise, pop the stack frame pop(x, null, ctx); } } } /** * We want to look at unaltered versions of the catch block, so this is a * <code>visit<code> and not a <code>endVisit</code>. */ @Override public boolean visit(JsCatch x, JsContext ctx) { // Reset the stack depth to the local index new CatchStackReset(this).accept(x); return true; } @Override public boolean visit(JsFunction x, JsContext ctx) { // Will be taken care of by the Bootstrap visitor return false; } @Override public boolean visit(JsTry x, JsContext ctx) { /* * Only the outermost finally block needs special treatment; try/finally * block within try blocks do not receive special treatment. */ JsBlock finallyBlock = x.getFinallyBlock(); if (finallyBlock != null && outerFinallyBlock == null) { outerFinallyBlock = finallyBlock; // Manual traversal accept(x.getTryBlock()); if (x.getCatches().isEmpty()) { JsCatch c = makeSyntheticCatchBlock(x); x.getCatches().add(c); } assert x.getCatches().size() >= 1; acceptList(x.getCatches()); // Exceptions in the finally block just exit the function assert outerFinallyBlock == finallyBlock; outerFinallyBlock = null; accept(finallyBlock); // Stack-pop instruction addPopAtEndOfBlock(finallyBlock, true); // Clean up entry after adding pop instruction finallyBlocksToExitVariables = Maps.remove( finallyBlocksToExitVariables, finallyBlock); return false; } // Normal visit return true; } /** * Create a reference to the function-local stack index variable, possibly * allocating it. */ protected JsNameRef stackIndexRef(SourceInfo info) { if (stackIndex == null) { stackIndex = currentFunction.getScope().declareName( "JsStackEmulator_stackIndex", "stackIndex"); JsVar var = new JsVar(info, stackIndex); varsToAdd = Lists.add(varsToAdd, var); } return stackIndex.makeRef(info); } /** * Code-gen function for generating the stack-pop statement at the end of a * block. A no-op if the last statement is a <code>throw</code> or * <code>return</code> statement, since it will have already caused a pop * statement to have been added. * * @param checkEarlyExit if <code>true</code>, generates * <code>earlyExit && pop()</code> */ private void addPopAtEndOfBlock(JsBlock x, boolean checkEarlyExit) { JsStatement last = x.getStatements().isEmpty() ? null : x.getStatements().get(x.getStatements().size() - 1); if (last instanceof JsReturn || last instanceof JsThrow) { /* * Don't need a pop after a throw or break statement. This is an * optimization for the common case of returning a value as the last * statement, but doesn't cover all flow-control cases. */ return; } else if (checkEarlyExit && !finallyBlocksToExitVariables.containsKey(x)) { /* * No early-exit variable was ever allocated for this block. This means * that the variable can never be true, and thus the stack-popping * expression will never be executed. */ return; } // pop() SourceInfo info = x.getSourceInfo(); JsExpression op = pop(info); if (checkEarlyExit) { // earlyExit && pop() op = new JsBinaryOperation(info, JsBinaryOperator.AND, earlyExitRef(x), op); } x.getStatements().add(op.makeStmt()); } /** * Generate a name reference to the early-exit variable for a given block, * possibly allocating a new variable. */ private JsNameRef earlyExitRef(JsBlock x) { JsName earlyExitName = finallyBlocksToExitVariables.get(x); if (earlyExitName == null) { earlyExitName = currentFunction.getScope().declareName( "JsStackEmulator_exitingEarly" + finallyBlocksToExitVariables.size(), "exitingEarly"); finallyBlocksToExitVariables = Maps.put(finallyBlocksToExitVariables, x, earlyExitName); JsVar var = new JsVar(x.getSourceInfo(), earlyExitName); varsToAdd = Lists.add(varsToAdd, var); } return earlyExitName.makeRef(x.getSourceInfo()); } private JsCatch makeSyntheticCatchBlock(JsTry x) { /* * catch (e) { e = Exceptions.toJava(e); throw Exceptions.toJs(e); } */ SourceInfo info = x.getSourceInfo(); JsCatch c = new JsCatch(info, currentFunction.getScope(), "e"); JsName paramName = c.getParameter().getName(); // Exceptiobs.toJava(e) JsInvocation wrapCall = new JsInvocation(info, wrapFunctionName.makeRef(info), paramName.makeRef(info)); // e = Exceptions.toJava(e) JsBinaryOperation asg = new JsBinaryOperation(info, JsBinaryOperator.ASG, paramName.makeRef(info), wrapCall); // Exceptions.toJs(e) JsInvocation unwrapCall = new JsInvocation(info, unwrapFunctionName.makeRef(info), paramName.makeRef(info)); // throw Exceptions.toJs(e) JsThrow throwStatement = new JsThrow(info, unwrapCall); JsBlock body = new JsBlock(info); body.getStatements().add(asg.makeStmt()); body.getStatements().add(throwStatement); c.setBody(body); return c; } /** * Pops the stack frame. */ private void pop(JsStatement x, JsExpression expr, JsContext ctx) { // $stackDepth = stackIndex - 1 SourceInfo info = x.getSourceInfo(); JsExpression op = pop(info); if (ctx.canInsert()) { if (expr != null) { ctx.insertBefore(expr.makeStmt()); } ctx.insertBefore(op.makeStmt()); } else { JsBlock block = new JsBlock(info); if (expr != null) { block.getStatements().add(expr.makeStmt()); } block.getStatements().add(op.makeStmt()); block.getStatements().add(x); ctx.replaceMe(block); } } /** * Decrement the $stackDepth variable. */ private JsExpression pop(SourceInfo info) { JsBinaryOperation sub = new JsBinaryOperation(info, JsBinaryOperator.SUB, stackIndexRef(info), new JsNumberLiteral(info, 1)); JsBinaryOperation op = new JsBinaryOperation(info, JsBinaryOperator.ASG, stackDepth.makeRef(info), sub); return op; } /** * Create the function-entry code. */ private JsStatement push(HasSourceInfo x) { SourceInfo info = x.getSourceInfo(); JsNameRef stackRef = stack.makeRef(info); JsNameRef stackDepthRef = stackDepth.makeRef(info); JsExpression currentFunctionRef; if (currentFunction.getName() == null) { // Anonymous currentFunctionRef = JsNullLiteral.INSTANCE; } else { currentFunctionRef = currentFunction.getName().makeRef(info); } // ++stackDepth JsUnaryOperation inc = new JsPrefixOperation(info, JsUnaryOperator.INC, stackDepthRef); // stackIndex = ++stackDepth JsBinaryOperation stackIndexOp = new JsBinaryOperation(info, JsBinaryOperator.ASG, stackIndexRef(info), inc); // stack[stackIndex = ++stackDepth] JsArrayAccess access = new JsArrayAccess(info, stackRef, stackIndexOp); // stack[stackIndex = ++stackDepth] = currentFunction JsBinaryOperation op = new JsBinaryOperation(info, JsBinaryOperator.ASG, access, currentFunctionRef); return op.makeStmt(); } private JsNameRef returnTempRef(SourceInfo info) { if (returnTemp == null) { returnTemp = currentFunction.getScope().declareName( "JsStackEmulator_returnTemp", "returnTemp"); JsVar var = new JsVar(info, returnTemp); varsToAdd = Lists.add(varsToAdd, var); } return returnTemp.makeRef(info); } } /** * Creates a visitor to instrument each JsFunction in the jsProgram. */ private class InstrumentAllFunctions extends JsVisitor { @Override public void endVisit(JsFunction x, JsContext ctx) { if (x.getBody().getStatements().isEmpty() || !shouldInstrumentFunction(x)) { return; } if (recordLineNumbers) { (new LocationVisitor(x)).accept(x.getBody()); } else { (new EntryExitVisitor(x)).accept(x.getBody()); } } } /** * Extends EntryExit visitor to record location information in the AST. This * visitor will modify every JsExpression that can potentially result in a * change of flow control with file and line number data. * <p> * This simply generates code to set entries in the <code>$location</code> * stack, parallel to <code>$stack</code>: * * <pre> * ($location[stackIndex] = 'Foo.java:' + 42, expr); * </pre> * * Inclusion of file names is dependent on the value of the * {@link JsStackEmulator#recordFileNames} field. */ private class LocationVisitor extends EntryExitVisitor { private String lastFile; private int lastLine; /** * Nodes in this set are used in a context that expects a reference, not * just an arbitrary expression. For example, <code>delete</code> takes a * reference. These are tracked because it wouldn't be safe to rewrite * <code>delete foo.bar</code> to <code>delete (line='123',foo).bar</code>. */ private final Set<JsNode> nodesInRefContext = new HashSet<JsNode>(); public LocationVisitor(JsFunction function) { super(function); clearLocation(); } @Override public void endVisit(JsArrayAccess x, JsContext ctx) { record(x, ctx); } @Override public void endVisit(JsBinaryOperation x, JsContext ctx) { if (x.getOperator().isAssignment()) { record(x, ctx); } } @Override public void endVisit(JsInvocation x, JsContext ctx) { nodesInRefContext.remove(x.getQualifier()); // Record the location as close as possible to calling the function. List<JsExpression> args = x.getArguments(); if (!args.isEmpty()) { recordAfterLastArg(x); return; } JsNameRef qualifier = getPossibleMethod(x); if (qualifier == null) { record(x, ctx); return; } // This is a call using a qualified name like foo.bar() // Record the location after evaluating foo. // (Doing it after evaluating .bar causes lots of tests to fail.) SourceInfo locationToRecord = x.getSourceInfo(); if (sameAsLastLocation(locationToRecord)) { return; } qualifier.setQualifier(recordAfter(qualifier.getQualifier(), locationToRecord)); setLastLocation(locationToRecord); didChange = true; } @Override public void endVisit(JsNameRef x, JsContext ctx) { record(x, ctx); } @Override public void endVisit(JsNew x, JsContext ctx) { nodesInRefContext.remove(x.getConstructorExpression()); // Record the location as close as possible to calling the constructor. if (!x.getArguments().isEmpty()) { recordAfterLastArg(x); } else { record(x, ctx); } } @Override public void endVisit(JsPostfixOperation x, JsContext ctx) { record(x, ctx); } @Override public void endVisit(JsPrefixOperation x, JsContext ctx) { record(x, ctx); nodesInRefContext.remove(x.getArg()); } @Override public boolean visit(JsExprStmt x, JsContext ctx) { if (isExceptionWrappingCode(x)) { // Don't instrument exception wrapping code. return false; } return true; } /** * This is essentially a hacked-up version of JsFor.traverse to account for * flow control differing from visitation order. It resets lastFile and * lastLine before the condition and increment expressions in the for loop * so that location data will be recorded correctly. */ @Override public boolean visit(JsFor x, JsContext ctx) { if (x.getInitExpr() != null) { x.setInitExpr(accept(x.getInitExpr())); } else if (x.getInitVars() != null) { x.setInitVars(accept(x.getInitVars())); } if (x.getCondition() != null) { clearLocation(); x.setCondition(accept(x.getCondition())); } if (x.getIncrExpr() != null) { clearLocation(); x.setIncrExpr(accept(x.getIncrExpr())); } accept(x.getBody()); return false; } @Override public boolean visit(JsInvocation x, JsContext ctx) { nodesInRefContext.add(x.getQualifier()); return true; } @Override public boolean visit(JsNew x, JsContext ctx) { nodesInRefContext.add(x.getConstructorExpression()); return true; } @Override public boolean visit(JsPrefixOperation x, JsContext ctx) { if (x.getOperator() == JsUnaryOperator.DELETE || x.getOperator() == JsUnaryOperator.TYPEOF) { nodesInRefContext.add(x.getArg()); } return true; } @Override public boolean visit(JsPropertyInitializer x, JsContext ctx) { // do not instrument left hand side of initializer. x.setValueExpr(accept(x.getValueExpr())); return false; } /** * Similar to JsFor, this resets the current location information before * evaluating the condition. */ @Override public boolean visit(JsWhile x, JsContext ctx) { clearLocation(); x.setCondition(accept(x.getCondition())); accept(x.getBody()); return false; } /** * If the invocation might be a method call, return its NameRef. * Otherwise, return null. */ private JsNameRef getPossibleMethod(JsInvocation x) { if (!(x.getQualifier() instanceof JsNameRef)) { return null; } JsNameRef ref = (JsNameRef) x.getQualifier(); if (ref.getQualifier() == null) { return null; } return ref; } /** * Strips off the final name segment. */ private String baseName(String fileName) { // Try the system path separator int lastIndex = fileName.lastIndexOf(File.separator); if (lastIndex == -1) { // Otherwise, try URL path separator lastIndex = fileName.lastIndexOf('/'); } if (lastIndex != -1) { return fileName.substring(lastIndex + 1); } else { return fileName; } } /** * Given an expression and its context, record the location before * evaluating the expression, under the following conditions: * * - We are in a context where this is allowed. * - we have not previously called record() with the same location. * * Note that record() must be called in the same order that the expressions * will be evaluated at runtime. When this isn't true, {@link #clearLocation} * must be called first. * * Side-effect: updates lastLine and possibly lastFile. */ private void record(JsExpression x, JsContext ctx) { if (ctx.isLvalue()) { // Assignments to comma expressions aren't legal return; } else if (nodesInRefContext.contains(x)) { // Don't modify references into non-references return; } SourceInfo locationToRecord = x.getSourceInfo(); if (sameAsLastLocation(locationToRecord)) { return; // no change } JsBinaryOperation comma = new JsBinaryOperation(locationToRecord, JsBinaryOperator.COMMA, assignLocation(locationToRecord), x); ctx.replaceMe(comma); setLastLocation(locationToRecord); } /** * Records the position after evaluating the last argument. * This must be called after visiting the arguments. * * Side-effect: updates lastLine and possibly lastFile. */ private <T extends JsExpression & HasArguments> void recordAfterLastArg(T x) { SourceInfo locationToRecord = x.getSourceInfo(); if (sameAsLastLocation(locationToRecord)) { return; // no change } List<JsExpression> args = x.getArguments(); JsExpression last = args.get(args.size() - 1); args.set(args.size() - 1, recordAfter(last, locationToRecord)); setLastLocation(locationToRecord); didChange = true; } /** * Sets the last location recorded. (Used to avoid repeating the same location * in the next call to {@link #record}.) */ private void setLastLocation(SourceInfo recordedLocation) { lastLine = recordedLocation.getStartLine(); if (recordFileNames) { lastFile = recordedLocation.getFileName(); } } /** * Ensures that the next call to record() will record the location. */ private void clearLocation() { lastFile = ""; lastLine = -1; } private boolean sameAsLastLocation(SourceInfo info) { return info.getStartLine() == lastLine && (!recordFileNames || info.getFileName().equals(lastFile)); } /** * Wrap an expression so that we record a location after evaluating it. * (Requires a temporary variable.) */ private JsExpression recordAfter(JsExpression x, SourceInfo locationToRecord) { // ($tmp = x, $locations[stackIndex] = "{fileName}:" + "{lineNumber}", $tmp) SourceInfo info = x.getSourceInfo(); JsExpression setTmp = new JsBinaryOperation(info, JsBinaryOperator.ASG, tmp.makeRef(info), x); return new JsBinaryOperation(info, JsBinaryOperator.COMMA, new JsBinaryOperation(info, JsBinaryOperator.COMMA, setTmp, assignLocation(locationToRecord)), tmp.makeRef(info)); } /** * Returns an expression that assigns the location. */ private JsExpression assignLocation(SourceInfo info) { // If filenames are on: // $locations[stackIndex] = "{fileName}:" + "{lineNumber}"; // Otherwise: // $locations[stackIndex] = "{lineNumber}"; JsExpression location = new JsStringLiteral(info, String.valueOf(info.getStartLine())); if (recordFileNames) { // 'fileName:' + lineNumber JsStringLiteral stringLit = new JsStringLiteral(info, baseName(info.getFileName()) + ":"); location = new JsBinaryOperation(info, JsBinaryOperator.ADD, stringLit, location); } JsArrayAccess access = new JsArrayAccess(info, lineNumbers.makeRef(info), stackIndexRef(info)); return new JsBinaryOperation(info, JsBinaryOperator.ASG, access, location); } } /** * The StackTraceCreator code refers to identifiers defined in JsRootScope, * which are unobfuscatable. This visitor replaces references to those symbols * with references to our locally-defined, obfuscatable names. */ private class ReplaceUnobfuscatableNames extends JsModVisitor { // See JsRootScope for the definition of these names private final JsName rootLineNumbers = JsRootScope.INSTANCE.findExistingUnobfuscatableName("$location"); private final JsName rootStack = JsRootScope.INSTANCE.findExistingUnobfuscatableName("$stack"); private final JsName rootStackDepth = JsRootScope.INSTANCE.findExistingUnobfuscatableName("$stackDepth"); @Override public void endVisit(JsNameRef x, JsContext ctx) { JsName name = x.getName(); JsNameRef newRef = null; if (name == rootStack) { newRef = stack.makeRef(x.getSourceInfo()); } else if (name == rootStackDepth) { newRef = stackDepth.makeRef(x.getSourceInfo()); } else if (name == rootLineNumbers) { newRef = lineNumbers.makeRef(x.getSourceInfo()); } if (newRef == null) { return; } assert x.getQualifier() == null; ctx.replaceMe(newRef); } } /** * Corresponds to property compiler.stackMode in EmulateJsStack.gwt.xml * module. */ public enum StackMode { STRIP, NATIVE, EMULATED } public static void exec(JProgram jprogram, JsProgram jsProgram, PermutationProperties properties, JavaToJavaScriptMap jjsmap) { if (getStackMode(properties) == StackMode.EMULATED) { new JsStackEmulator(jprogram, jsProgram, jjsmap, properties.getConfigurationProperties()) .execImpl(); } } public static StackMode getStackMode(PermutationProperties properties) { String value = properties.mustGetString("compiler.stackMode"); return StackMode.valueOf(value.toUpperCase(Locale.ROOT)); } private JsName wrapFunctionName; private JsName unwrapFunctionName; private JsName lineNumbers; private JProgram jprogram; private final JsProgram jsProgram; private JavaToJavaScriptMap jjsmap; private final boolean recordFileNames; private final boolean recordLineNumbers; private JsName stack; private JsName stackDepth; private JsName tmp; private JDeclaredType exceptionsClass; private JsStackEmulator(JProgram jprogram, JsProgram jsProgram, JavaToJavaScriptMap jjsmap, ConfigurationProperties config) { this.jprogram = jprogram; this.jsProgram = jsProgram; this.jjsmap = jjsmap; this.exceptionsClass = jprogram.getFromTypeMap("com.google.gwt.lang.Exceptions"); recordFileNames = config.getBoolean("compiler.emulatedStack.recordFileNames", false); recordLineNumbers = recordFileNames || config.getBoolean("compiler.emulatedStack.recordLineNumbers", false); } private boolean shouldInstrumentFunction(JsExpression functionExpression) { if (!(functionExpression instanceof HasName)) { return true; } /** * Do not instrument function in the Exceptions class (those are in involved in the * exception handling machinery) nor immortal codegen types as their code is executed * for setup and the stack emulation variables may have not been defined yet. */ JMethod method = jjsmap.nameToMethod(((HasName) functionExpression).getName()); return method == null || method.getEnclosingType() != exceptionsClass || jprogram.immortalCodeGenTypes.contains(method.getEnclosingType()); } private void execImpl() { wrapFunctionName = JsUtils.getJsNameForMethod(jjsmap, jprogram, RuntimeConstants.EXCEPTIONS_TO_JAVA); unwrapFunctionName = JsUtils.getJsNameForMethod(jjsmap, jprogram, RuntimeConstants.EXCEPTIONS_TO_JS); if (wrapFunctionName == null) { // No exceptions caught? Weird, but possible. return; } assert unwrapFunctionName != null; initNames(); makeVars(); (new ReplaceUnobfuscatableNames()).accept(jsProgram); (new InstrumentAllFunctions()).accept(jsProgram); } private void initNames() { stack = jsProgram.getScope().declareName("$JsStackEmulator_stack", "$stack"); stackDepth = jsProgram.getScope().declareName("$JsStackEmulator_stackDepth", "$stackDepth"); lineNumbers = jsProgram.getScope().declareName("$JsStackEmulator_location", "$location"); tmp = jsProgram.getScope().declareName("$JsStackEmulator_tmp", "$tmp"); } private void makeVars() { SourceInfo info = jsProgram.createSourceInfoSynthetic(getClass()); JsVar stackVar = new JsVar(info, stack); stackVar.setInitExpr(new JsArrayLiteral(info)); JsVar stackDepthVar = new JsVar(info, stackDepth); stackDepthVar.setInitExpr(new JsNumberLiteral(info, (-1))); JsVar lineNumbersVar = new JsVar(info, lineNumbers); lineNumbersVar.setInitExpr(new JsArrayLiteral(info)); JsVar tmpVar = new JsVar(info,tmp); JsVars vars; JsStatement first = jsProgram.getGlobalBlock().getStatements().get(0); if (first instanceof JsVars) { vars = (JsVars) first; } else { vars = new JsVars(info); jsProgram.getGlobalBlock().getStatements().add(0, vars); } vars.add(stackVar); vars.add(stackDepthVar); vars.add(lineNumbersVar); vars.add(tmpVar); } }