Fixes issue #1458; corrects some cases of bad JS output from JS code generation.  Also optimizes parenthesis output where it's not really needed.

Patch by: sgross
Review by: me

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@1511 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/js/JsConstructExpressionVisitor.java b/dev/core/src/com/google/gwt/dev/js/JsConstructExpressionVisitor.java
new file mode 100644
index 0000000..a244456
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/js/JsConstructExpressionVisitor.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2007 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.js.ast.JsArrayAccess;
+import com.google.gwt.dev.js.ast.JsArrayLiteral;
+import com.google.gwt.dev.js.ast.JsContext;
+import com.google.gwt.dev.js.ast.JsExpression;
+import com.google.gwt.dev.js.ast.JsFunction;
+import com.google.gwt.dev.js.ast.JsInvocation;
+import com.google.gwt.dev.js.ast.JsNameRef;
+import com.google.gwt.dev.js.ast.JsNew;
+import com.google.gwt.dev.js.ast.JsObjectLiteral;
+import com.google.gwt.dev.js.ast.JsVisitable;
+import com.google.gwt.dev.js.ast.JsVisitor;
+
+/**
+ * Searches for method invocations in constructor expressions that would not
+ * normally be surrounded by parentheses.
+ */
+public class JsConstructExpressionVisitor extends JsVisitor {
+
+  private static final int PRECEDENCE_NEW = JsPrecedenceVisitor.exec(new JsNew());
+
+  public static boolean exec(JsExpression expression) {
+    if (JsPrecedenceVisitor.exec(expression) < PRECEDENCE_NEW) {
+      return true;
+    }
+    JsConstructExpressionVisitor visitor = new JsConstructExpressionVisitor();
+    visitor.accept(expression);
+    return visitor.containsInvocation;
+  }
+
+  private boolean containsInvocation = false;
+
+  private JsConstructExpressionVisitor() {
+  }
+
+  /**
+   * We only look at the array expression since the index has its own scope.
+   */
+  @Override
+  public boolean visit(JsArrayAccess x, JsContext<JsExpression> ctx) {
+    accept(x.getArrayExpr());
+    return false;
+  }
+
+  /**
+   * Array literals have their own scoping.
+   */
+  @Override
+  public boolean visit(JsArrayLiteral x, JsContext<JsExpression> ctx) {
+    return false;
+  }
+
+  /**
+   * Functions have their own scoping.
+   */
+  @Override
+  public boolean visit(JsFunction x, JsContext<JsExpression> ctx) {
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsInvocation x, JsContext<JsExpression> ctx) {
+    containsInvocation = true;
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsNameRef x, JsContext<JsExpression> ctx) {
+    if (!x.isLeaf()) {
+      accept(x.getQualifier());
+    }
+    return false;
+  }
+
+  /**
+   * New constructs bind to the nearest set of parentheses.
+   */
+  @Override
+  public boolean visit(JsNew x, JsContext<JsExpression> ctx) {
+    return false;
+  }
+
+  /**
+   * Object literals have their own scope.
+   */
+  @Override
+  public boolean visit(JsObjectLiteral x, JsContext<JsExpression> ctx) {
+    return false;
+  }
+
+  /**
+   * We only look at nodes that would not normally be surrounded by parentheses.
+   */
+  protected <T extends JsVisitable<T>> T doAccept(T node) {
+    if (node instanceof JsExpression) {
+      JsExpression expression = (JsExpression) node;
+      int precedence = JsPrecedenceVisitor.exec(expression);
+      // Only visit expressions that won't automatically be surrounded by
+      // parentheses
+      if (precedence < PRECEDENCE_NEW) {
+        return node;
+      }
+    }
+    return super.doAccept(node);
+  }
+
+}
diff --git a/dev/core/src/com/google/gwt/dev/js/JsFirstExpressionVisitor.java b/dev/core/src/com/google/gwt/dev/js/JsFirstExpressionVisitor.java
new file mode 100644
index 0000000..683fcc6
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/js/JsFirstExpressionVisitor.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2007 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.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.JsConditional;
+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.JsFunction;
+import com.google.gwt.dev.js.ast.JsInvocation;
+import com.google.gwt.dev.js.ast.JsNameRef;
+import com.google.gwt.dev.js.ast.JsNew;
+import com.google.gwt.dev.js.ast.JsObjectLiteral;
+import com.google.gwt.dev.js.ast.JsPostfixOperation;
+import com.google.gwt.dev.js.ast.JsPrefixOperation;
+import com.google.gwt.dev.js.ast.JsRegExp;
+import com.google.gwt.dev.js.ast.JsVisitor;
+
+/**
+ * Determines if an expression statement needs to be surrounded by parentheses.
+ * 
+ * The statement or the left-most expression needs to be surrounded by
+ * parentheses if the left-most expression is an object literal or a function
+ * object. Function declarations do not need parentheses.
+ * 
+ * For example the following require parentheses:<br>
+ * <ul>
+ * <li>{ key : 'value'}</li>
+ * <li>{ key : 'value'}.key</li>
+ * <li>function () {return 1;}()</li>
+ * <li>function () {return 1;}.prototype</li>
+ * </ul>
+ * 
+ * The following do not require parentheses:<br>
+ * <ul>
+ * <li>var x = { key : 'value'}</li>
+ * <li>"string" + { key : 'value'}.key</li>
+ * <li>function func() {}</li>
+ * <li>function() {}</li>
+ * </ul>
+ */
+public class JsFirstExpressionVisitor extends JsVisitor {
+
+  public static boolean exec(JsExprStmt statement) {
+    JsFirstExpressionVisitor visitor = new JsFirstExpressionVisitor();
+    JsExpression expression = statement.getExpression();
+    // Pure function declarations do not need parentheses
+    if (expression instanceof JsFunction) {
+      return false;
+    }
+    visitor.accept(statement.getExpression());
+    return visitor.needsParentheses;
+  }
+
+  private boolean needsParentheses = false;
+
+  private JsFirstExpressionVisitor() {
+  }
+
+  public boolean visit(JsArrayAccess x, JsContext<JsExpression> ctx) {
+    accept(x.getArrayExpr());
+    return false;
+  }
+
+  public boolean visit(JsArrayLiteral x, JsContext<JsExpression> ctx) {
+    return false;
+  }
+
+  public boolean visit(JsBinaryOperation x, JsContext<JsExpression> ctx) {
+    accept(x.getArg1());
+    return false;
+  }
+
+  public boolean visit(JsConditional x, JsContext<JsExpression> ctx) {
+    accept(x.getTestExpression());
+    return false;
+  }
+
+  public boolean visit(JsFunction x, JsContext<JsExpression> ctx) {
+    needsParentheses = true;
+    return false;
+  }
+
+  public boolean visit(JsInvocation x, JsContext<JsExpression> ctx) {
+    accept(x.getQualifier());
+    return false;
+  }
+
+  public boolean visit(JsNameRef x, JsContext<JsExpression> ctx) {
+    if (!x.isLeaf()) {
+      accept(x.getQualifier());
+    }
+    return false;
+  }
+
+  public boolean visit(JsNew x, JsContext<JsExpression> ctx) {
+    return false;
+  }
+
+  public boolean visit(JsObjectLiteral x, JsContext<JsExpression> ctx) {
+    needsParentheses = true;
+    return false;
+  }
+
+  public boolean visit(JsPostfixOperation x, JsContext<JsExpression> ctx) {
+    accept(x.getArg());
+    return false;
+  }
+
+  public boolean visit(JsPrefixOperation x, JsContext<JsExpression> ctx) {
+    return false;
+  }
+
+  public boolean visit(JsRegExp x, JsContext<JsExpression> ctx) {
+    return false;
+  }
+
+}
diff --git a/dev/core/src/com/google/gwt/dev/js/JsPrecedenceVisitor.java b/dev/core/src/com/google/gwt/dev/js/JsPrecedenceVisitor.java
index 719bcc3..09770df 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsPrecedenceVisitor.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsPrecedenceVisitor.java
@@ -67,10 +67,12 @@
  * Precedence indices from "JavaScript - The Definitive Guide" 4th Edition (page
  * 57)
  * 
- * Precedence 16 is for indivisible primaries that either don't have children,
+ * Precedence 17 is for indivisible primaries that either don't have children,
  * or provide their own delimiters.
  * 
- * Precedence 15 is for really important things that have their own AST classes.
+ * Precedence 16 is for really important things that have their own AST classes.
+ * 
+ * Precedence 15 is for the new construct.
  * 
  * Precedence 14 is for unary operators.
  * 
@@ -100,13 +102,13 @@
 
   @Override
   public boolean visit(JsArrayAccess x, JsContext<JsExpression> ctx) {
-    answer = 15;
+    answer = 16;
     return false;
   }
 
   @Override
   public boolean visit(JsArrayLiteral x, JsContext<JsExpression> ctx) {
-    answer = 16; // primary
+    answer = 17; // primary
     return false;
   }
 
@@ -123,7 +125,7 @@
 
   @Override
   public boolean visit(JsBooleanLiteral x, JsContext<JsExpression> ctx) {
-    answer = 16; // primary
+    answer = 17; // primary
     return false;
   }
 
@@ -160,7 +162,7 @@
 
   @Override
   public boolean visit(JsDecimalLiteral x, JsContext<JsExpression> ctx) {
-    answer = 16; // primary
+    answer = 17; // primary
     return false;
   }
 
@@ -196,7 +198,7 @@
 
   @Override
   public boolean visit(JsFunction x, JsContext<JsExpression> ctx) {
-    answer = 16; // primary
+    answer = 17; // primary
     return false;
   }
 
@@ -207,13 +209,13 @@
 
   @Override
   public boolean visit(JsIntegralLiteral x, JsContext<JsExpression> ctx) {
-    answer = 16; // primary
+    answer = 17; // primary
     return false;
   }
 
   @Override
   public boolean visit(JsInvocation x, JsContext<JsExpression> ctx) {
-    answer = 15;
+    answer = 16;
     return false;
   }
 
@@ -225,9 +227,9 @@
   @Override
   public boolean visit(JsNameRef x, JsContext<JsExpression> ctx) {
     if (x.isLeaf()) {
-      answer = 16; // primary
+      answer = 17; // primary
     } else {
-      answer = 15; // property access
+      answer = 16; // property access
     }
     return false;
   }
@@ -240,13 +242,13 @@
 
   @Override
   public boolean visit(JsNullLiteral x, JsContext<JsExpression> ctx) {
-    answer = 16; // primary
+    answer = 17; // primary
     return false;
   }
 
   @Override
   public boolean visit(JsObjectLiteral x, JsContext<JsExpression> ctx) {
-    answer = 16; // primary
+    answer = 17; // primary
     return false;
   }
 
@@ -275,13 +277,13 @@
   @Override
   public boolean visit(JsPropertyInitializer x,
       JsContext<JsPropertyInitializer> ctx) {
-    answer = 16; // primary
+    answer = 17; // primary
     return false;
   }
 
   @Override
   public boolean visit(JsRegExp x, JsContext<JsExpression> ctx) {
-    answer = 16; // primary
+    answer = 17; // primary
     return false;
   }
 
@@ -292,7 +294,7 @@
 
   @Override
   public boolean visit(JsStringLiteral x, JsContext<JsExpression> ctx) {
-    answer = 16; // primary
+    answer = 17; // primary
     return false;
   }
 
@@ -303,7 +305,7 @@
 
   @Override
   public boolean visit(JsThisRef x, JsContext<JsExpression> ctx) {
-    answer = 16; // primary
+    answer = 17; // primary
     return false;
   }
 
diff --git a/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java b/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
index c884737..88c9686 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
@@ -47,6 +47,7 @@
 import com.google.gwt.dev.js.ast.JsNew;
 import com.google.gwt.dev.js.ast.JsNullLiteral;
 import com.google.gwt.dev.js.ast.JsObjectLiteral;
+import com.google.gwt.dev.js.ast.JsOperator;
 import com.google.gwt.dev.js.ast.JsParameter;
 import com.google.gwt.dev.js.ast.JsPostfixOperation;
 import com.google.gwt.dev.js.ast.JsPrefixOperation;
@@ -69,6 +70,7 @@
 import com.google.gwt.dev.util.TextOutput;
 
 import java.util.Iterator;
+import java.util.regex.Pattern;
 
 /**
  * Produces text output from a JavaScript AST.
@@ -108,6 +110,14 @@
    */
   private static final int JSBLOCK_LINES_TO_PRINT = 3;
 
+  /**
+   * A variable name is valid if it contains only letters, numbers, _, $ and
+   * does not begin with a number. There are actually other valid variable
+   * names, such as ones that contain escaped Unicode characters, but we
+   * surround those names with quotes in property initializers to be safe.
+   */
+  private static final Pattern VALID_NAME_PATTERN = Pattern.compile("[a-zA-Z_$][\\w$]*");
+
   protected boolean needSemi = true;
   private final TextOutput p;
 
@@ -148,8 +158,7 @@
     JsExpression arg1 = x.getArg1();
     _parenPush(x, arg1, !op.isLeftAssociative());
     accept(arg1);
-    boolean needSpace = op.isKeyword() || arg1 instanceof JsPostfixOperation;
-    if (needSpace) {
+    if (op.isKeyword()) {
       _parenPopOrSpace(x, arg1, !op.isLeftAssociative());
     } else {
       _parenPop(x, arg1, !op.isLeftAssociative());
@@ -157,8 +166,7 @@
     }
     p.print(op.getSymbol());
     JsExpression arg2 = x.getArg2();
-    needSpace = op.isKeyword() || arg2 instanceof JsPrefixOperation;
-    if (needSpace) {
+    if (_spaceCalc(op, arg2)) {
       _parenPushOrSpace(x, arg2, op.isLeftAssociative());
     } else {
       _spaceOpt();
@@ -251,16 +259,16 @@
     // right associative
     {
       JsExpression testExpression = x.getTestExpression();
-      _parenPush(x, testExpression, true);
+      _parenPush(x, testExpression, false);
       accept(testExpression);
-      _parenPop(x, testExpression, true);
+      _parenPop(x, testExpression, false);
     }
     _questionMark();
     {
       JsExpression thenExpression = x.getThenExpression();
-      _parenPush(x, thenExpression, true);
+      _parenPush(x, thenExpression, false);
       accept(thenExpression);
-      _parenPop(x, thenExpression, true);
+      _parenPop(x, thenExpression, false);
     }
     _colon();
     {
@@ -350,8 +358,15 @@
 
   @Override
   public boolean visit(JsExprStmt x, JsContext<JsStatement> ctx) {
-    final JsExpression expr = x.getExpression();
+    boolean surroundWithParentheses = JsFirstExpressionVisitor.exec(x);
+    if (surroundWithParentheses) {
+      _lparen();
+    }
+    JsExpression expr = x.getExpression();
     accept(expr);
+    if (surroundWithParentheses) {
+      _rparen();
+    }
     return false;
   }
 
@@ -552,9 +567,14 @@
     _space();
 
     JsExpression ctorExpr = x.getConstructorExpression();
-    _parenPush(x, ctorExpr, true);
+    boolean needsParens = JsConstructExpressionVisitor.exec(ctorExpr);
+    if (needsParens) {
+      _lparen();
+    }
     accept(ctorExpr);
-    _parenPop(x, ctorExpr, true);
+    if (needsParens) {
+      _rparen();
+    }
 
     _lparen();
     boolean sep = false;
@@ -584,9 +604,14 @@
       sep = _sepCommaOptSpace(sep);
       JsPropertyInitializer propInit = (JsPropertyInitializer) element;
       JsExpression labelExpr = propInit.getLabelExpr();
-      _parenPushIfConditional(labelExpr);
-      accept(labelExpr);
-      _parenPopIfConditional(labelExpr);
+      // labels can be either string, integral, or decimal literals
+      if (labelExpr instanceof JsStringLiteral
+          && VALID_NAME_PATTERN.matcher(
+              ((JsStringLiteral) labelExpr).getValue()).matches()) {
+        p.print(((JsStringLiteral) labelExpr).getValue());
+      } else {
+        accept(labelExpr);
+      }
       _colon();
       JsExpression valueExpr = propInit.getValueExpr();
       _parenPushIfConditional(valueExpr);
@@ -608,9 +633,9 @@
     JsUnaryOperator op = x.getOperator();
     JsExpression arg = x.getArg();
     // unary operators always associate correctly (I think)
-    _parenPush(x, arg, true);
+    _parenPush(x, arg, false);
     accept(arg);
-    _parenPop(x, arg, true);
+    _parenPop(x, arg, false);
     p.print(op.getSymbol());
     return false;
   }
@@ -619,14 +644,14 @@
   public boolean visit(JsPrefixOperation x, JsContext<JsExpression> ctx) {
     JsUnaryOperator op = x.getOperator();
     p.print(op.getSymbol());
-    if (op.isKeyword()) {
+    JsExpression arg = x.getArg();
+    if (_spaceCalc(op, arg)) {
       _space();
     }
-    JsExpression arg = x.getArg();
     // unary operators always associate correctly (I think)
-    _parenPush(x, arg, true);
+    _parenPush(x, arg, false);
     accept(arg);
-    _parenPop(x, arg, true);
+    _parenPop(x, arg, false);
     return false;
   }
 
@@ -637,7 +662,8 @@
   }
 
   @Override
-  public boolean visit(JsPropertyInitializer x, JsContext<JsPropertyInitializer> ctx) {
+  public boolean visit(JsPropertyInitializer x,
+      JsContext<JsPropertyInitializer> ctx) {
     // Since there are separators, we actually print the property init
     // in visit(JsObjectLiteral).
     //
@@ -1070,6 +1096,19 @@
     p.print(' ');
   }
 
+  private boolean _spaceCalc(JsOperator op, JsExpression arg) {
+    if (op.isKeyword()) {
+      return true;
+    }
+    if (arg instanceof JsPrefixOperation) {
+      JsOperator op2 = ((JsPrefixOperation) arg).getOperator();
+      return (op == JsBinaryOperator.SUB || op == JsUnaryOperator.NEG)
+          && (op2 == JsUnaryOperator.DEC || op2 == JsUnaryOperator.NEG)
+          || (op == JsBinaryOperator.ADD && op2 == JsUnaryOperator.INC);
+    }
+    return false;
+  }
+
   private void _spaceOpt() {
     p.printOpt(' ');
   }
diff --git a/dev/core/test/com/google/gwt/dev/js/ComparingVisitor.java b/dev/core/test/com/google/gwt/dev/js/ComparingVisitor.java
new file mode 100644
index 0000000..573d436
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/js/ComparingVisitor.java
@@ -0,0 +1,390 @@
+/*
+ * Copyright 2007 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.js.FlatteningVisitor.TreeNode;
+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.JsBlock;
+import com.google.gwt.dev.js.ast.JsBooleanLiteral;
+import com.google.gwt.dev.js.ast.JsBreak;
+import com.google.gwt.dev.js.ast.JsCase;
+import com.google.gwt.dev.js.ast.JsCatch;
+import com.google.gwt.dev.js.ast.JsConditional;
+import com.google.gwt.dev.js.ast.JsContext;
+import com.google.gwt.dev.js.ast.JsContinue;
+import com.google.gwt.dev.js.ast.JsDebugger;
+import com.google.gwt.dev.js.ast.JsDecimalLiteral;
+import com.google.gwt.dev.js.ast.JsDefault;
+import com.google.gwt.dev.js.ast.JsDoWhile;
+import com.google.gwt.dev.js.ast.JsEmpty;
+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.JsForIn;
+import com.google.gwt.dev.js.ast.JsFunction;
+import com.google.gwt.dev.js.ast.JsIf;
+import com.google.gwt.dev.js.ast.JsIntegralLiteral;
+import com.google.gwt.dev.js.ast.JsInvocation;
+import com.google.gwt.dev.js.ast.JsLabel;
+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.JsNullLiteral;
+import com.google.gwt.dev.js.ast.JsObjectLiteral;
+import com.google.gwt.dev.js.ast.JsParameter;
+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.JsRegExp;
+import com.google.gwt.dev.js.ast.JsReturn;
+import com.google.gwt.dev.js.ast.JsStatement;
+import com.google.gwt.dev.js.ast.JsStringLiteral;
+import com.google.gwt.dev.js.ast.JsSwitch;
+import com.google.gwt.dev.js.ast.JsSwitchMember;
+import com.google.gwt.dev.js.ast.JsThisRef;
+import com.google.gwt.dev.js.ast.JsThrow;
+import com.google.gwt.dev.js.ast.JsTry;
+import com.google.gwt.dev.js.ast.JsVars;
+import com.google.gwt.dev.js.ast.JsVisitable;
+import com.google.gwt.dev.js.ast.JsVisitor;
+import com.google.gwt.dev.js.ast.JsWhile;
+import com.google.gwt.dev.js.ast.JsVars.JsVar;
+
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+import java.util.List;
+
+public class ComparingVisitor extends JsVisitor {
+
+  public static void exec(List<JsStatement> expected, List<JsStatement> actual) {
+    TreeNode expectedTree = FlatteningVisitor.exec(expected);
+    TreeNode actualTree = FlatteningVisitor.exec(actual);
+    compare(expectedTree, actualTree);
+  }
+
+  private static void compare(JsVisitable<?> expected, JsVisitable<?> actual) {
+    if (expected == actual) {
+      return;
+    }
+    Assert.assertNotNull(expected);
+    Assert.assertNotNull(actual);
+    ComparingVisitor visitor = new ComparingVisitor(expected);
+    visitor.accept(actual);
+  }
+
+  private static void compare(TreeNode expected, TreeNode actual) {
+    compare(expected.node, actual.node);
+    List<TreeNode> expectedChildren = expected.children;
+    List<TreeNode> actualChildren = actual.children;
+    Assert.assertEquals(expectedChildren.size(), actualChildren.size());
+    for (int i = 0; i < expectedChildren.size(); i++) {
+      compare(expectedChildren.get(i), actualChildren.get(i));
+    }
+  }
+
+  private final JsVisitable<?> other;
+
+  private ComparingVisitor(JsVisitable<?> other) {
+    this.other = other;
+  }
+
+  @Override
+  public boolean visit(JsArrayAccess x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsArrayAccess);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsArrayLiteral x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsArrayLiteral);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsBinaryOperation x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsBinaryOperation);
+    Assert.assertEquals(((JsBinaryOperation) other).getOperator().getSymbol(),
+        x.getOperator().getSymbol());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsBlock x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsBlock);
+    Assert.assertEquals(((JsBlock) other).isGlobalBlock(), x.isGlobalBlock());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsBooleanLiteral x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsBooleanLiteral);
+    Assert.assertEquals(((JsBooleanLiteral) other).getValue(), x.getValue());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsBreak x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsBreak);
+    Assert.assertEquals(((JsBreak) other).getLabel().getIdent(),
+        x.getLabel().getIdent());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsCase x, JsContext<JsSwitchMember> ctx) {
+    Assert.assertTrue(other instanceof JsCase);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsCatch x, JsContext<JsCatch> ctx) {
+    Assert.assertTrue(other instanceof JsCatch);
+    Assert.assertEquals(((JsCatch) other).getParameter().getName().getIdent(),
+        x.getParameter().getName().getIdent());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsConditional x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsConditional);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsContinue x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsContinue);
+    Assert.assertEquals(((JsContinue) other).getLabel().getIdent(),
+        x.getLabel().getIdent());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsDebugger x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsDebugger);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsDecimalLiteral x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsDecimalLiteral);
+    Assert.assertEquals(((JsDecimalLiteral) other).getValue(), x.getValue());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsDefault x, JsContext<JsSwitchMember> ctx) {
+    Assert.assertTrue(other instanceof JsDefault);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsDoWhile x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsDoWhile);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsEmpty x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsEmpty);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsExprStmt x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsExprStmt);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsFor x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsFor);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsForIn x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsForIn);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsFunction x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsFunction);
+    JsFunction otherFunc = (JsFunction) other;
+    JsName otherName = otherFunc.getName();
+    JsName name = x.getName();
+    if (name != otherName) {
+      Assert.assertEquals(otherName.getIdent(), name.getIdent());
+    }
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsIf x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsIf);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsIntegralLiteral x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsIntegralLiteral);
+    Assert.assertEquals(((JsIntegralLiteral) other).getValue(), x.getValue());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsInvocation x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsInvocation);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsLabel x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsLabel);
+    Assert.assertEquals(((JsLabel) other).getName().getIdent(),
+        x.getName().getIdent());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsNameRef x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsNameRef);
+    Assert.assertEquals(((JsNameRef) other).getIdent(), x.getIdent());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsNew x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsNew);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsNullLiteral x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsNullLiteral);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsObjectLiteral x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsObjectLiteral);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsParameter x, JsContext<JsParameter> ctx) {
+    Assert.assertTrue(other instanceof JsParameter);
+    Assert.assertEquals(((JsParameter) other).getName().getIdent(),
+        x.getName().getIdent());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsPostfixOperation x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsPostfixOperation);
+    Assert.assertEquals(((JsPostfixOperation) other).getOperator().getSymbol(),
+        x.getOperator().getSymbol());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsPrefixOperation x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsPrefixOperation);
+    Assert.assertEquals(((JsPrefixOperation) other).getOperator().getSymbol(),
+        x.getOperator().getSymbol());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsProgram x, JsContext<JsProgram> ctx) {
+    Assert.assertTrue(other instanceof JsProgram);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsPropertyInitializer x,
+      JsContext<JsPropertyInitializer> ctx) {
+    Assert.assertTrue(other instanceof JsPropertyInitializer);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsRegExp x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsRegExp);
+    Assert.assertEquals(((JsRegExp) other).getFlags(), x.getFlags());
+    Assert.assertEquals(((JsRegExp) other).getPattern(), x.getPattern());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsReturn x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsReturn);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsStringLiteral x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsStringLiteral);
+    Assert.assertEquals(((JsStringLiteral) other).getValue(), x.getValue());
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsSwitch x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsSwitch);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsThisRef x, JsContext<JsExpression> ctx) {
+    Assert.assertTrue(other instanceof JsThisRef);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsThrow x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsThrow);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsTry x, JsContext<JsStatement> ctx) {
+    Assert.assertTrue(other instanceof JsTry);
+    return false;
+  }
+
+  @Override
+  public boolean visit(JsVar x, JsContext<JsVar> ctx) {
+    TestCase.assertTrue(other instanceof JsVar);
+    TestCase.assertEquals(((JsVar) other).getName().getIdent(),
+        x.getName().getIdent());
+    return false;
+  }
+
+  public boolean visit(JsVars x, JsContext<JsStatement> ctx) {
+    TestCase.assertTrue(other instanceof JsVars);
+    return false;
+  }
+
+  public boolean visit(JsWhile x, JsContext<JsStatement> ctx) {
+    TestCase.assertTrue(other instanceof JsWhile);
+    return false;
+  }
+}
diff --git a/dev/core/test/com/google/gwt/dev/js/FlatteningVisitor.java b/dev/core/test/com/google/gwt/dev/js/FlatteningVisitor.java
new file mode 100644
index 0000000..1dc0dbf
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/js/FlatteningVisitor.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2007 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.js.ast.JsStatement;
+import com.google.gwt.dev.js.ast.JsVisitable;
+import com.google.gwt.dev.js.ast.JsVisitor;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+class FlatteningVisitor extends JsVisitor {
+
+  public static TreeNode exec(List<JsStatement> statements) {
+    FlatteningVisitor visitor = new FlatteningVisitor();
+    visitor.acceptList(statements);
+    return visitor.root;
+  }
+
+  public static class TreeNode {
+    public final JsVisitable<?> node;
+    public final List<TreeNode> children = new ArrayList<TreeNode>();
+
+    public TreeNode(JsVisitable<?> node) {
+      this.node = node;
+    }
+  }
+
+  private TreeNode root;
+
+  private FlatteningVisitor() {
+    root = new TreeNode(null);
+  }
+
+  protected <T extends JsVisitable<T>> T doAccept(T node) {
+    TreeNode oldRoot = root;
+    root = new TreeNode(node);
+    oldRoot.children.add(root);
+    super.doAccept(node);
+    root = oldRoot;
+    return node;
+  }
+
+  protected <T extends JsVisitable<T>> void doAcceptList(List<T> collection) {
+    for (Iterator<T> it = collection.iterator(); it.hasNext();) {
+      doAccept(it.next());
+    }
+  }
+}
diff --git a/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorAccuracyTest.java b/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorAccuracyTest.java
new file mode 100644
index 0000000..6c05c22
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorAccuracyTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2007 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.js.ast.JsProgram;
+import com.google.gwt.dev.js.ast.JsStatement;
+import com.google.gwt.dev.js.ast.JsVisitor;
+import com.google.gwt.dev.util.DefaultTextOutput;
+import com.google.gwt.dev.util.TextOutput;
+
+import junit.framework.TestCase;
+
+import java.io.StringReader;
+import java.util.List;
+
+public class JsToStringGenerationVisitorAccuracyTest extends TestCase {
+
+  private JsParser parser = new JsParser();
+
+  public void testAdditionPositive() throws Exception {
+    // x plus positive 3
+    doTest("x + +3");
+  }
+
+  public void testArithmetic() throws Exception {
+    doTest("a + (b * (c - d)) / (e / f) % x");
+  }
+
+  public void testArrayDeclarationArrayAccess() throws Exception {
+    doTest("[1,2,3,4][2]");
+  }
+
+  public void testComplexConstruction() throws Exception {
+    doTest("(new (new (a(({a : 'b', c : 'd'}),[1,2,3,x,y,z]))())())()");
+  }
+
+  public void testConstructionInvocation() throws Exception {
+    doTest("(new a())()");
+  }
+
+  public void testDecrement() throws Exception {
+    doTest("(x--)-(-(--y))");
+  }
+
+  public void testFunctionDeclarationInvocation() throws Exception {
+    doTest("(function () {})()");
+  }
+
+  public void testInvocationConstruction() throws Exception {
+    doTest("new ((a.b.c()).d.e)(1,2,3)");
+  }
+
+  public void testNestedConstruction() throws Exception {
+    doTest("new (new (new MyClass()))");
+  }
+
+  public void testObjectDeclarationArrayAccess() throws Exception {
+    doTest("({ a : 'b'})['a']");
+  }
+
+  public void testObjectDeclarationMemberAccess() throws Exception {
+    doTest("({ a : 'b'}).a");
+  }
+
+  public void testObjectLiteral() throws Exception {
+    // declaring an object requires parentheses
+    doTest("({ 'property' : 'value'})");
+  }
+
+  public void testObjectLiteralDeclaration() throws Exception {
+    // quotes are necessary around some property variables
+    doTest("var x = {'abc\\'' : 'value'}");
+    doTest("var x = {\"a.1\" : 'value'}");
+    doTest("var x = {\"\\'\\\"\" : 'value'}");
+  }
+
+  public void testUnaryOperations() throws Exception {
+    // spaces or parentheses are necessary to separate negation and decrement
+    doTest("var x = -(-(--y))");
+  }
+
+  private void doTest(String js) throws Exception {
+    List<JsStatement> expected = parser.parse(new JsProgram().getScope(),
+        new StringReader(js), 0);
+    List<JsStatement> actual = parse(expected, true);
+    ComparingVisitor.exec(expected, actual);
+
+    actual = parse(expected, false);
+    ComparingVisitor.exec(expected, actual);
+  }
+
+  private List<JsStatement> parse(List<JsStatement> expected, boolean compact)
+      throws Exception {
+    TextOutput text = new DefaultTextOutput(compact);
+    JsVisitor generator = new JsToStringGenerationVisitor(text);
+    generator.acceptList(expected);
+    return parser.parse(new JsProgram().getScope(), new StringReader(
+        text.toString()), 0);
+  }
+}
diff --git a/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorConcisenessTest.java b/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorConcisenessTest.java
new file mode 100644
index 0000000..e77f01b
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorConcisenessTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2007 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.js.ast.JsProgram;
+import com.google.gwt.dev.js.ast.JsStatement;
+import com.google.gwt.dev.js.ast.JsVisitor;
+import com.google.gwt.dev.util.DefaultTextOutput;
+import com.google.gwt.dev.util.TextOutput;
+
+import junit.framework.TestCase;
+
+import java.io.StringReader;
+import java.util.List;
+
+public class JsToStringGenerationVisitorConcisenessTest extends TestCase {
+
+  private JsParser parser = new JsParser();
+
+  public void testComplexDecrement() throws Exception {
+    String output = parse("var x = -(-(-(--y)))");
+    assertEquals("var x=- - - --y", output);
+  }
+
+  public void testComplexIncrement() throws Exception {
+    String output = parse("var x = (y++) + (-(--z))");
+    assertEquals("var x=y+++- --z", output);
+  }
+
+  public void testConstruction() throws Exception {
+    String output = parse("new a.b(c,d)");
+    assertEquals("new a.b(c,d)", output);
+  }
+
+  public void testIncrement() throws Exception {
+    // y++-x is valid
+    assertEquals("var x=y++-z", parse("var x = (y++) - z"));
+  }
+
+  public void testObjectLiteralDeclarationConcise() throws Exception {
+    // quotes are not necessary around many property variables in object
+    // literals
+    assertEquals("var x={1:'b'}", parse("var x = {1 : 'b'}"));
+    assertEquals("var x={$a_:'b'}", parse("var x = {'$a_' : 'b'}"));
+    assertEquals("var x={1.2:'b'}", parse("var x = {1.2 : 'b'}"));
+  }
+
+  private String parse(String js) throws Exception {
+    List<JsStatement> statements = parser.parse(new JsProgram().getScope(),
+        new StringReader(js), 0);
+    TextOutput text = new DefaultTextOutput(true);
+    JsVisitor generator = new JsToStringGenerationVisitor(text);
+    generator.acceptList(statements);
+    return text.toString();
+  }
+}