Fixes issue #978.
- Removes extra whitespace and parentheses around negative numbers
- Removes unnecessary semicolons after the last statement of a block

Suggested by: fredsa
Patch by: sgross
Review by: me


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@1805 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/js/JsRequiresSemiVisitor.java b/dev/core/src/com/google/gwt/dev/js/JsRequiresSemiVisitor.java
new file mode 100644
index 0000000..87f46ff
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/js/JsRequiresSemiVisitor.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2008 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.JsBlock;
+import com.google.gwt.dev.js.ast.JsBreak;
+import com.google.gwt.dev.js.ast.JsContext;
+import com.google.gwt.dev.js.ast.JsDebugger;
+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.JsFor;
+import com.google.gwt.dev.js.ast.JsForIn;
+import com.google.gwt.dev.js.ast.JsIf;
+import com.google.gwt.dev.js.ast.JsLabel;
+import com.google.gwt.dev.js.ast.JsReturn;
+import com.google.gwt.dev.js.ast.JsStatement;
+import com.google.gwt.dev.js.ast.JsSwitch;
+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.JsVisitor;
+import com.google.gwt.dev.js.ast.JsWhile;
+
+/**
+ * Determines if a statement at the end of a block requires a semicolon.
+ * 
+ * For example, the following statements require semicolons:<br>
+ * <ul>
+ * <li>if (cond);</li>
+ * <li>while (cond);</li>
+ * </ul>
+ * 
+ * The following do not require semicolons:<br>
+ * <ul>
+ * <li>return 1</li>
+ * <li>do {} while(true)</li>
+ * </ul>
+ */
+public class JsRequiresSemiVisitor extends JsVisitor {
+
+  public static boolean exec(JsStatement lastStatement) {
+    JsRequiresSemiVisitor visitor = new JsRequiresSemiVisitor();
+    visitor.accept(lastStatement);
+    return visitor.needsSemicolon;
+  }
+
+  private boolean needsSemicolon = false;
+
+  private JsRequiresSemiVisitor() {
+  }
+
+  public boolean visit(JsBlock x, JsContext<JsStatement> ctx) {
+    return false;
+  }
+
+  public boolean visit(JsBreak x, JsContext<JsStatement> ctx) {
+    return false;
+  }
+
+  public boolean visit(JsDebugger x, JsContext<JsStatement> ctx) {
+    return false;
+  }
+
+  public boolean visit(JsDoWhile x, JsContext<JsStatement> ctx) {
+    return false;
+  }
+
+  public boolean visit(JsEmpty x, JsContext<JsStatement> ctx) {
+    return false;
+  }
+
+  public boolean visit(JsExprStmt x, JsContext<JsStatement> ctx) {
+    return false;
+  }
+
+  public boolean visit(JsFor x, JsContext<JsStatement> ctx) {
+    if (x.getBody() instanceof JsEmpty) {
+      needsSemicolon = true;
+    }
+    return false;
+  }
+
+  public boolean visit(JsForIn x, JsContext<JsStatement> ctx) {
+    if (x.getBody() instanceof JsEmpty) {
+      needsSemicolon = true;
+    }
+    return false;
+  }
+
+  public boolean visit(JsIf x, JsContext<JsStatement> ctx) {
+    JsStatement thenStmt = x.getThenStmt();
+    JsStatement elseStmt = x.getElseStmt();
+    if (elseStmt instanceof JsEmpty
+        || (elseStmt == null && thenStmt instanceof JsEmpty)) {
+      needsSemicolon = true;
+    }
+    return false;
+  }
+
+  public boolean visit(JsLabel x, JsContext<JsStatement> ctx) {
+    if (x.getStmt() instanceof JsEmpty) {
+      needsSemicolon = true;
+    }
+    return false;
+  }
+
+  public boolean visit(JsReturn x, JsContext<JsStatement> ctx) {
+    return false;
+  }
+
+  public boolean visit(JsSwitch x, JsContext<JsStatement> ctx) {
+    return false;
+  }
+
+  public boolean visit(JsThrow x, JsContext<JsStatement> ctx) {
+    return false;
+  }
+
+  public boolean visit(JsTry x, JsContext<JsStatement> ctx) {
+    return false;
+  }
+
+  public boolean visit(JsVars x, JsContext<JsStatement> ctx) {
+    return false;
+  }
+
+  public boolean visit(JsWhile x, JsContext<JsStatement> ctx) {
+    if (x.getBody() instanceof JsEmpty) {
+      needsSemicolon = true;
+    }
+    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 bc035ff..23ba302 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 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
@@ -301,12 +301,7 @@
 
   @Override
   public boolean visit(JsDecimalLiteral x, JsContext<JsExpression> ctx) {
-    String s = x.getValue();
-    // TODO: optimize this to only the cases that need it
-    if (s.startsWith("-")) {
-      _space();
-    }
-    p.print(s);
+    p.print(x.getValue());
     return false;
   }
 
@@ -510,15 +505,7 @@
 
   @Override
   public boolean visit(JsIntegralLiteral x, JsContext<JsExpression> ctx) {
-    String s = x.getValue().toString();
-    boolean needParens = s.startsWith("-");
-    if (needParens) {
-      _lparen();
-    }
-    p.print(s);
-    if (needParens) {
-      _rparen();
-    }
+    p.print(x.getValue().toString());
     return false;
   }
 
@@ -823,17 +810,30 @@
       accept(stmt);
       if (needSemi) {
         /*
-         * Special treatment of function decls: function decls always set
-         * needSemi back to true. But if they are the only item in a statement
-         * (i.e. not part of an assignment operation), just give them a newline
-         * instead of a semi since it makes obfuscated code so much "nicer"
-         * (sic).
+         * Special treatment of function decls: If they are the only item in a
+         * statement (i.e. not part of an assignment operation), just give them
+         * a newline instead of a semi.
          */
-        if (stmt instanceof JsExprStmt
-            && ((JsExprStmt) stmt).getExpression() instanceof JsFunction) {
-          _newline();
+        boolean functionStmt = stmt instanceof JsExprStmt
+            && ((JsExprStmt) stmt).getExpression() instanceof JsFunction;
+        /*
+         * Special treatment of the last statement in a block: only a few
+         * statements at the end of a block require semicolons.
+         */
+        boolean lastStatement = !iter.hasNext() && needBraces
+            && !JsRequiresSemiVisitor.exec(stmt);
+        if (functionStmt) {
+          if (lastStatement) {
+            _newlineOpt();
+          } else {
+            _newline();
+          }
         } else {
-          _semi();
+          if (lastStatement) {
+            _semiOpt();
+          } else {
+            _semi();
+          }
           _newlineOpt();
         }
       }
@@ -1070,6 +1070,10 @@
     p.print(';');
   }
 
+  private void _semiOpt() {
+    p.printOpt(';');
+  }
+
   private boolean _sepCommaOptSpace(boolean sep) {
     if (sep) {
       p.print(',');
@@ -1096,6 +1100,16 @@
           && (op2 == JsUnaryOperator.DEC || op2 == JsUnaryOperator.NEG)
           || (op == JsBinaryOperator.ADD && op2 == JsUnaryOperator.INC);
     }
+    if (arg instanceof JsIntegralLiteral) {
+      JsIntegralLiteral literal = (JsIntegralLiteral) arg;
+      return (op == JsBinaryOperator.SUB || op == JsUnaryOperator.NEG)
+          && (literal.getValue().signum() == -1);
+    }
+    if (arg instanceof JsDecimalLiteral) {
+      JsDecimalLiteral literal = (JsDecimalLiteral) arg;
+      return (op == JsBinaryOperator.SUB || op == JsUnaryOperator.NEG)
+          && (literal.getValue().startsWith("-"));
+    }
     return false;
   }
 
diff --git a/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorAccuracyTest.java b/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorAccuracyTest.java
index fe4041a..82c62e2 100644
--- a/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorAccuracyTest.java
+++ b/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorAccuracyTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 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
@@ -108,6 +108,14 @@
     doTest("var x = -(-(--y))");
   }
 
+  public void testEmptyStatements() throws Exception {
+    doTest("function f() {if (x);}");
+    doTest("function f() {while (x);}");
+    doTest("function f() {label:;}");
+    doTest("function f() {for (i=0;i<n;i++);}");
+    doTest("function f() {for (var x in s);}");
+  }
+
   private void doTest(String js) throws Exception {
     List<JsStatement> expected = parser.parse(new JsProgram().getScope(),
         new StringReader(js), 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
index a0943a7..edcf1c8 100644
--- a/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorConcisenessTest.java
+++ b/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorConcisenessTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 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
@@ -68,6 +68,13 @@
     assertEquals("var x={1.2:'b'}", parse("var x = {1.2 : 'b'}"));
   }
 
+  public void testLastStatement() throws Exception {
+    assertEquals("function(){x++;i++}", parse("function() {x++;i++;}"));
+    assertEquals("function(){do{}while(x)}",
+        parse("function() {do{} while(x);}"));
+    assertEquals("function(){if(b){}}", parse("function() {if(b){}}"));
+  }
+
   private String parse(String js) throws Exception {
     List<JsStatement> statements = parser.parse(new JsProgram().getScope(),
         new StringReader(js), 0);