Revision: 3825
Author: [email protected]
Date: Thu Oct 22 14:07:39 2009
Log: Some extensions to Expression
http://codereview.appspot.com/135053

Added code to conservatively figure out what a typeof <the expression>
would yield, and conservative folding.  These help with static
analysis and code optimization.

And added a static utility method
  Operation.is(ParseTreeNode, Operator)
to replace a frequently appearing but hard to read idiom.

[email protected]

http://code.google.com/p/google-caja/source/detail?r=3825

Modified:
 /trunk/src/com/google/caja/parser/js/AbstractExpression.java
 /trunk/src/com/google/caja/parser/js/ArrayConstructor.java
 /trunk/src/com/google/caja/parser/js/BooleanLiteral.java
 /trunk/src/com/google/caja/parser/js/CajoledModuleExpression.java
 /trunk/src/com/google/caja/parser/js/Declaration.java
 /trunk/src/com/google/caja/parser/js/Expression.java
 /trunk/src/com/google/caja/parser/js/FunctionConstructor.java
 /trunk/src/com/google/caja/parser/js/NoChildren.java
 /trunk/src/com/google/caja/parser/js/NullLiteral.java
 /trunk/src/com/google/caja/parser/js/NumberLiteral.java
 /trunk/src/com/google/caja/parser/js/ObjectConstructor.java
 /trunk/src/com/google/caja/parser/js/Operation.java
 /trunk/src/com/google/caja/parser/js/Parser.java
 /trunk/src/com/google/caja/parser/js/QuotedExpression.java
 /trunk/src/com/google/caja/parser/js/RealLiteral.java
 /trunk/src/com/google/caja/parser/js/Reference.java
 /trunk/src/com/google/caja/parser/js/RegexpLiteral.java
 /trunk/src/com/google/caja/parser/js/StringLiteral.java
 /trunk/src/com/google/caja/parser/quasiliteral/AlphaRenaming.java
 /trunk/tests/com/google/caja/parser/js/ExpressionTest.java
 /trunk/tests/com/google/caja/plugin/templates/JsConcatenatorTest.java

=======================================
--- /trunk/src/com/google/caja/parser/js/AbstractExpression.java Wed Aug 26 17:12:16 2009 +++ /trunk/src/com/google/caja/parser/js/AbstractExpression.java Thu Oct 22 14:07:39 2009
@@ -46,4 +46,6 @@
   public Expression simplifyForSideEffect() { return this; }

   public Boolean conditionResult() { return null; }
-}
+
+  public Expression fold() { return this; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/ArrayConstructor.java Wed Aug 26 17:12:16 2009 +++ /trunk/src/com/google/caja/parser/js/ArrayConstructor.java Thu Oct 22 14:07:39 2009
@@ -61,8 +61,7 @@
         out.consume(",");
       }
       last = e;
-      if (!(e instanceof Operation
-            && Operator.COMMA == ((Operation) e).getOperator())) {
+      if (!Operation.is(e, Operator.COMMA)) {
         e.render(rc);
       } else {
         out.consume("(");
@@ -73,4 +72,6 @@
     out.mark(FilePosition.endOfOrNull(pos));
     out.consume("]");
   }
-}
+
+  public String typeOf() { return "object"; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/BooleanLiteral.java Wed Jul 15 11:24:34 2009 +++ /trunk/src/com/google/caja/parser/js/BooleanLiteral.java Thu Oct 22 14:07:39 2009
@@ -28,7 +28,7 @@
   public final boolean value;

   /** @param children unused.  This ctor is provided for reflection. */
-  @ReflectiveCtor
+  @ReflectiveCtor
   public BooleanLiteral(
FilePosition pos, Boolean value, List<? extends ParseTreeNode> children) {
     this(pos, value);
@@ -48,4 +48,6 @@
   public boolean getValueInBooleanContext() {
     return value;
   }
-}
+
+  public String typeOf() { return "boolean"; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/CajoledModuleExpression.java Wed Jul 22 17:07:41 2009 +++ /trunk/src/com/google/caja/parser/js/CajoledModuleExpression.java Thu Oct 22 14:07:39 2009
@@ -58,10 +58,12 @@

   public void render(RenderContext r) {
     ObjectConstructor oc = getCajoledModule().getModuleBody();
-
+
     Expression e = (Expression) QuasiBuilder.substV(
-        "___.prepareModule(@module);",
+        "___.prepareModule(@module)",
         "module", oc);
     e.render(r);
   }
-}
+
+  public String typeOf() { return null; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/Declaration.java Wed Jul 15 11:24:34 2009 +++ /trunk/src/com/google/caja/parser/js/Declaration.java Thu Oct 22 14:07:39 2009
@@ -96,11 +96,10 @@
     identifier.render(rc);
     if (null != initializer) {
       out.consume("=");
-      boolean isComma = initializer instanceof Operation
-          && Operator.COMMA == ((Operation) initializer).getOperator();
-      if (isComma) out.consume("(");
+      boolean isComma = Operation.is(initializer, Operator.COMMA);
+      if (isComma) { out.consume("("); }
       initializer.render(rc);
-      if (isComma) out.consume(")");
+      if (isComma) { out.consume(")"); }
     }
   }
 }
=======================================
--- /trunk/src/com/google/caja/parser/js/Expression.java Wed Aug 26 17:12:16 2009 +++ /trunk/src/com/google/caja/parser/js/Expression.java Thu Oct 22 14:07:39 2009
@@ -37,4 +37,23 @@
    * an exception, then it may return any result.
    */
   Boolean conditionResult();
-}
+
+  /**
+   * {...@code null} or the result of applying the {...@code typeof} operator 
to
+   * the result of this expression.
+   *
+   * @return if the expression yields a result with the same {...@code typeof}
+   *     in all environments in which it returns normally, then returns the
+   *     result of applying the {...@code typeof} operator to the result.
+   *     {...@code null} if the type cannot be determined.
+ * This method is conservative, so it may return null where it is possible
+   *     to prove a bound.
+   */
+  String typeOf();
+
+  /**
+   * This expression or a semantically equivalent simpler expression.
+   * This method does not recurse.
+   */
+  Expression fold();
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/FunctionConstructor.java Wed Aug 26 17:12:16 2009 +++ /trunk/src/com/google/caja/parser/js/FunctionConstructor.java Thu Oct 22 14:07:39 2009
@@ -108,4 +108,6 @@
     out.consume(")");
     body.renderBlock(rc, false);
   }
-}
+
+  public String typeOf() { return "function"; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/NoChildren.java Fri Apr 17 11:17:36 2009 +++ /trunk/src/com/google/caja/parser/js/NoChildren.java Thu Oct 22 14:07:39 2009
@@ -28,4 +28,5 @@
   @Override
   public Object getValue() { throw new AssertionError(); }
   public void render(RenderContext rc) { throw new AssertionError(); }
-}
+  public String typeOf() { throw new AssertionError(); }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/NullLiteral.java Wed Aug 26 17:12:16 2009 +++ /trunk/src/com/google/caja/parser/js/NullLiteral.java Thu Oct 22 14:07:39 2009
@@ -60,4 +60,6 @@
   public boolean getValueInBooleanContext() {
     return false;
   }
-}
+
+  public String typeOf() { return "object"; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/NumberLiteral.java Wed Feb 11 20:17:12 2009 +++ /trunk/src/com/google/caja/parser/js/NumberLiteral.java Thu Oct 22 14:07:39 2009
@@ -179,4 +179,6 @@
           + ((n - 1) < 0 ? "e-" : "e+") + Math.abs(n - 1);
     }
   }
-}
+
+  public String typeOf() { return "number"; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/ObjectConstructor.java Wed Aug 26 17:12:16 2009 +++ /trunk/src/com/google/caja/parser/js/ObjectConstructor.java Thu Oct 22 14:07:39 2009
@@ -122,8 +122,7 @@
       }
       key.render(rc);
       out.consume(":");
-      if (!(value instanceof Operation
-            && Operator.COMMA == ((Operation) value).getOperator())) {
+      if (!Operation.is(value, Operator.COMMA)) {
         value.render(rc);
       } else {
         out.mark(value.getFilePosition());
@@ -135,4 +134,6 @@
     out.mark(FilePosition.endOfOrNull(getFilePosition()));
     out.consume("}");
   }
-}
+
+  public String typeOf() { return "object"; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/Operation.java Wed Aug 26 17:12:16 2009 +++ /trunk/src/com/google/caja/parser/js/Operation.java Thu Oct 22 14:07:39 2009
@@ -38,6 +38,10 @@
     if (null == op) { throw new NullPointerException(); }
     createMutation().appendChildren(params).execute();
   }
+
+  public static boolean is(ParseTreeNode n, Operator op) {
+    return n instanceof Operation && op == ((Operation) n).getOperator();
+  }

   @Override
   public List<? extends Expression> children() {
@@ -100,9 +104,15 @@
         Expression sb = operands.get(1).simplifyForSideEffect();
         return sb == null ? sa : sa == null ? sb : this;
       }
-      case VOID:
+      case TERNARY:
         operands = children();
-        return operands.get(0).simplifyForSideEffect();
+        if (operands.get(1).simplifyForSideEffect() == null
+            && operands.get(2).simplifyForSideEffect() == null) {
+          return operands.get(0).simplifyForSideEffect();
+        }
+        return this;
+ case NOT: case TYPEOF: return children().get(0).simplifyForSideEffect();
+      case VOID: return children().get(0).simplifyForSideEffect();
       default: break;
     }
     return this;
@@ -118,9 +128,7 @@
       case CONSTRUCTOR:
         return true;
       case FUNCTION_CALL:
-        return (operands.get(0) instanceof Operation
-                && Operator.CONSTRUCTOR == operands.get(0).getValue())
-            ? true : null;
+        return is(operands.get(0), Operator.CONSTRUCTOR) ? true : null;
       case LOGICAL_AND:
         opResult = operands.get(0).conditionResult();
         if (opResult != null) {
@@ -336,7 +344,7 @@
         // By inspection of the grammar, a slash after a function call
         // or a member access is a division op, so no chance of
         // lexical ambiguity here.  These are also common enough that
-        // unecessarily parenthesizing them things less readable.
+        // unnecessarily parenthesizing them things less readable.
         if (childOp != Operator.FUNCTION_CALL
             && childOp != Operator.MEMBER_ACCESS) {
           return true;
@@ -390,4 +398,233 @@
     if (op == Operator.FUNCTION_CALL) { return Integer.MAX_VALUE; }
     return op.getType().getArity();
   }
-}
+
+  public String typeOf() {
+    List<? extends Expression> operands = children();
+    switch (getOperator()) {
+ case PRE_INCREMENT: case PRE_DECREMENT: case TO_NUMBER: case NEGATION:
+      case INVERSE: case MULTIPLICATION: case DIVISION: case MODULUS:
+      case SUBTRACTION: case LSHIFT: case RSHIFT: case RUSHIFT:
+      case BITWISE_AND: case BITWISE_OR: case BITWISE_XOR:
+      case ASSIGN_MUL: case ASSIGN_DIV: case ASSIGN_MOD: case ASSIGN_SUB:
+      case ASSIGN_LSH: case ASSIGN_RSH: case ASSIGN_USH: case ASSIGN_AND:
+      case ASSIGN_XOR: case ASSIGN_OR:
+        return "number";
+      case DELETE: case IN: case NOT: case LESS_THAN: case GREATER_THAN:
+      case LESS_EQUALS: case GREATER_EQUALS: case INSTANCE_OF: case EQUAL:
+      case NOT_EQUAL: case STRICTLY_EQUAL: case STRICTLY_NOT_EQUAL:
+        return "boolean";
+      case VOID: return "undefined";
+      case LOGICAL_OR: case LOGICAL_AND: case TERNARY: {
+        String t1 = operands.get(operands.size() - 2).typeOf();
+        String t2 = operands.get(operands.size() - 1).typeOf();
+        return (t1 != null && t1.equals(t2)) ? t1 : null;
+      }
+      case ADDITION: {
+        String t1 = operands.get(operands.size() - 2).typeOf();
+        String t2 = operands.get(operands.size() - 1).typeOf();
+ if ("string".equals(t1) || "string".equals(t2)) { return "string"; } + if ("number".equals(t1) && "number".equals(t2)) { return "number"; }
+        return null;
+      }
+      case TYPEOF: return "string";  // Trippy
+      case COMMA:
+        return operands.get(1).typeOf();
+      default: return null;
+    }
+  }
+
+  @Override
+  public Expression fold() {
+    if (getOperator() == Operator.FUNCTION_CALL) { return foldCall(); }
+    switch (children().size()) {
+      case 1: return foldUnaryOp();
+      case 2: return foldBinaryOp();
+      case 3: return foldTernaryOp();
+      default: return this;
+    }
+  }
+
+  private Expression foldUnaryOp() {
+    Expression operand = children().get(0);
+    switch (op) {
+      case NOT:
+        Boolean b = operand.conditionResult();
+        if (b != null && operand.simplifyForSideEffect() == null) {
+          return new BooleanLiteral(getFilePosition(), !b);
+        }
+        return this;
+      case TYPEOF:
+        String type = operand.typeOf();
+        if (type != null && operand.simplifyForSideEffect() == null) {
+          return StringLiteral.valueOf(getFilePosition(), type);
+        }
+        return this;
+      default:
+        break;
+    }
+    Object v = toLiteralValue(operand);
+    if (v != null) {
+      FilePosition pos = getFilePosition();
+      switch (getOperator()) {
+        case NEGATION:
+          if (v instanceof Number) {
+            if (operand instanceof IntegerLiteral) {
+              long n = ((IntegerLiteral) operand).getValue().longValue();
+              if (n != Long.MIN_VALUE) {
+                return new IntegerLiteral(pos, -n);
+              }
+            }
+            return new RealLiteral(pos, -((Number) v).doubleValue());
+          }
+          break;
+        case INVERSE:
+          if (v instanceof Number) {
+            return new IntegerLiteral(pos, ~((Number) v).longValue());
+          }
+          break;
+        case TO_NUMBER:
+          if (v instanceof Number) { return children().get(0); }
+          break;
+        default: break;
+      }
+    }
+    return this;
+  }
+
+  private Expression foldTernaryOp() {
+    if (getOperator() == Operator.TERNARY) {
+      Expression cond = children().get(0);
+      Boolean condResult = cond.conditionResult();
+      if (condResult != null && cond.simplifyForSideEffect() == null) {
+        return children().get(condResult ? 1 : 2);
+      }
+    }
+    return this;
+  }
+
+  private Expression foldBinaryOp() {
+    List<? extends Expression> operands = children();
+    Expression left = operands.get(0), right = operands.get(1);
+    Operator op = getOperator();
+    if (op == Operator.LOGICAL_AND || op == Operator.LOGICAL_OR) {
+      Boolean bv = left.conditionResult();
+      if (bv != null) {
+        Expression sideEffect = left.simplifyForSideEffect();
+        if (bv == (op == Operator.LOGICAL_AND)) {
+          return sideEffect == null
+              ? right
+              : Operation.createInfix(Operator.COMMA, sideEffect, right);
+        } else {
+          return left;
+        }
+      }
+    } else if (op == Operator.MEMBER_ACCESS) {
+      if (left instanceof StringLiteral) {
+        Reference r = (Reference) right;
+        if ("length".equals(r.getIdentifierName())) {
+          return new IntegerLiteral(
+              getFilePosition(),
+              ((StringLiteral) left).getUnquotedValue().length());
+        }
+      }
+    }
+    Object lhs = toLiteralValue(left);
+    Object rhs = toLiteralValue(right);
+    if (lhs != null && rhs != null) {
+      FilePosition pos = getFilePosition();
+      switch (op) {
+        case EQUAL: case STRICTLY_EQUAL:
+        case NOT_EQUAL: case STRICTLY_NOT_EQUAL:
+          boolean isStrict =  op == Operator.STRICTLY_EQUAL
+              || op == Operator.STRICTLY_NOT_EQUAL;
+          boolean isEqual = op == Operator.EQUAL
+              || op == Operator.STRICTLY_EQUAL;
+          boolean areEqual = lhs.equals(rhs);
+          if (isStrict || areEqual
+              || lhs.getClass().equals(rhs.getClass())) {
+            return new BooleanLiteral(pos, areEqual == isEqual);
+          }
+          break;
+        case ADDITION:
+          if ((lhs instanceof String) || (rhs instanceof String)) {
+            if (lhs instanceof Number) {
+              lhs = NumberLiteral.numberToString(
+                  ((Number) lhs).doubleValue());
+            } else if (rhs instanceof Number) {
+              rhs = NumberLiteral.numberToString(
+                  ((Number) rhs).doubleValue());
+            }
+            return StringLiteral.valueOf(pos, "" + lhs + rhs);
+          }
+          break;
+        default: break;
+      }
+      if (lhs instanceof Number && rhs instanceof Number) {
+        double a = ((Number) lhs).doubleValue();
+        double b = ((Number) rhs).doubleValue();
+        double result = Double.NaN;
+        switch (op) {
+          case ADDITION: result = a + b; break;
+          case SUBTRACTION: result = a - b; break;
+          case MULTIPLICATION: result = a * b; break;
+          case DIVISION: result = a / b; break;
+          case MODULUS: result = Math.IEEEremainder(a, b); break;
+          default: break;
+        }
+        if (!Double.isNaN(result)) {
+          return new RealLiteral(pos, result);
+        }
+      }
+    }
+    return this;
+  }
+
+  /**
+ * Fold some common string operations like indexOf which show up frequently
+   * in userAgent testing code.  This helps when we've inlined the value of
+   * navigator.userAgent.
+   */
+  private Expression foldCall() {
+    List<? extends Expression> operands = children();
+    Expression fn = operands.get(0);
+    if (is(fn, Operator.MEMBER_ACCESS)
+        && fn.children().get(0) instanceof StringLiteral) {
+      StringLiteral sl = (StringLiteral) fn.children().get(0);
+      String methodName = ((Reference) fn.children().get(1))
+          .getIdentifierName();
+      if ("indexOf".equals(methodName)) {
+        if (operands.size() == 2) {
+          Expression target = operands.get(1);
+          if (target instanceof StringLiteral) {
+            int index = sl.getUnquotedValue().indexOf(
+                ((StringLiteral) target).getUnquotedValue());
+            return new IntegerLiteral(getFilePosition(), index);
+          }
+        }
+      }
+    }
+    return this;
+  }
+
+  private static final Object UNDEFINED = new Object() {
+    @Override public String toString() { return "undefined"; }
+  };
+  private static Object toLiteralValue(Expression e) {
+    if (is(e, Operator.VOID) && e.simplifyForSideEffect() == null) {
+      return UNDEFINED;
+    }
+    if (e instanceof Literal) {
+      if (e instanceof StringLiteral) {
+        return ((StringLiteral) e).getUnquotedValue();
+      }
+      if (e instanceof NumberLiteral) {
+        return ((NumberLiteral) e).doubleValue();
+      }
+      if (e instanceof BooleanLiteral || e instanceof NullLiteral) {
+        return e.getValue();
+      }
+    }
+    return null;
+  }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/Parser.java Thu Oct 15 15:42:13 2009 +++ /trunk/src/com/google/caja/parser/js/Parser.java Thu Oct 22 14:07:39 2009
@@ -46,7 +46,7 @@
  *
  * <p>
  * The grammar below is a context-free representation of the grammar this
- * parser parses.  It disagrees with EcmaScript 262 Edition 3 (ES3) where
+ * parser parses.  It disagrees with EcmaScript 262 Edition 3 (ES3) where
* implementations disagree with ES3. The rules for semicolon insertion and
  * the possible backtracking in expressions needed to properly handle
  * backtracking are commented thoroughly in code, since semicolon insertion
@@ -1418,6 +1418,8 @@
     public void render(RenderContext rc) {
       throw new UnsupportedOperationException();
     }
+
+    public String typeOf() { return null; }
   }

   private static void issueLintWarningsForProblematicEscapes(
=======================================
--- /trunk/src/com/google/caja/parser/js/QuotedExpression.java Wed Jul 15 11:24:34 2009 +++ /trunk/src/com/google/caja/parser/js/QuotedExpression.java Thu Oct 22 14:07:39 2009
@@ -55,4 +55,6 @@
   }

   public Expression unquote() { return (Expression) children().get(0); }
-}
+
+  public String typeOf() { return null; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/RealLiteral.java Wed Jul 15 11:24:34 2009 +++ /trunk/src/com/google/caja/parser/js/RealLiteral.java Thu Oct 22 14:07:39 2009
@@ -60,7 +60,7 @@
       out.consume(")");
     } else if (Double.isInfinite(value)) {
       out.consume("(");
-      if (value >= 0) { out.consume("-"); }
+      if (value < 0) { out.consume("-"); }
       out.consume("1");
       out.consume("/");
       out.consume("0");
=======================================
--- /trunk/src/com/google/caja/parser/js/Reference.java Wed Jul 15 11:24:34 2009 +++ /trunk/src/com/google/caja/parser/js/Reference.java Thu Oct 22 14:07:39 2009
@@ -78,4 +78,6 @@
     }
     ident.render(rc);
   }
-}
+
+  public String typeOf() { return null; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/RegexpLiteral.java Mon Aug 31 13:09:32 2009 +++ /trunk/src/com/google/caja/parser/js/RegexpLiteral.java Thu Oct 22 14:07:39 2009
@@ -163,4 +163,7 @@
     }
     return true;
   }
-}
+
+  // "function" on some interpreters, "object" on others.
+  public String typeOf() { return null; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/StringLiteral.java Wed Jul 15 11:24:34 2009 +++ /trunk/src/com/google/caja/parser/js/StringLiteral.java Thu Oct 22 14:07:39 2009
@@ -164,4 +164,6 @@
     m.appendTail(sb);
     return sb.toString();
   }
-}
+
+  public String typeOf() { return "string"; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/quasiliteral/AlphaRenaming.java Thu Sep 24 10:51:59 2009 +++ /trunk/src/com/google/caja/parser/quasiliteral/AlphaRenaming.java Thu Oct 22 14:07:39 2009
@@ -197,12 +197,10 @@
     } else if (n instanceof Reference) {
       String name = ((Reference) n).getIdentifierName();
       if (isOuter(name, s)) { outers.add(name); }
-    } else if (n instanceof Operation) {
+    } else if (Operation.is(n, Operator.MEMBER_ACCESS)) {
       Operation op = (Operation) n;
-      if (op.getOperator() == Operator.MEMBER_ACCESS) {
-        checkScope(op.children().get(0), s, outers);
-        return;
-      }
+      checkScope(op.children().get(0), s, outers);
+      return;
     }
     for (ParseTreeNode child : n.children()) {
       checkScope(child, childScope, outers);
=======================================
--- /trunk/tests/com/google/caja/parser/js/ExpressionTest.java Wed Aug 26 17:12:16 2009 +++ /trunk/tests/com/google/caja/parser/js/ExpressionTest.java Thu Oct 22 14:07:39 2009
@@ -105,6 +105,93 @@
     assertSimplified(null, "1 && 2");
assertSimplified("x + y", "x + y"); // coercion might be side-effecting
   }
+
+  public final void testTypeOf() throws Exception {
+    assertTypeOf("boolean", "true");
+    assertTypeOf("boolean", "false");
+    assertTypeOf("number", "0");
+    assertTypeOf("number", "1");
+    assertTypeOf("number", "-1.5");
+    assertTypeOf("number", "6.02e+23");
+    assertTypeOf("string", "''");
+    assertTypeOf("string", "'foo'");
+    assertTypeOf("object", "null");
+    assertTypeOf("object", "{}");
+    assertTypeOf("function", "function () {}");
+    assertTypeOf("boolean", "!x");
+    assertTypeOf("boolean", "!x || !y");
+    assertTypeOf(null, "!x || y");
+    assertTypeOf(null, "x || !y");
+    assertTypeOf("number", "+x");
+    assertTypeOf("number", "-x");
+    assertTypeOf("number", "x - y");
+    assertTypeOf(null, "y++");  // See browser-expectations.html
+    assertTypeOf(null, "z--");
+    assertTypeOf("number", "++y");
+    assertTypeOf("number", "--z");
+    assertTypeOf(null, "a + b");
+    assertTypeOf("number", "+a + 1");
+    assertTypeOf("string", "'' + b");
+    assertTypeOf("number", "'4' - 1");
+    assertTypeOf("string", "foo() ? 'bar' : 'baz'");
+    assertTypeOf(null, "foo() ? 'bar' : null");
+    assertTypeOf(null, "foo() ? bar : 'baz'");
+    assertTypeOf("string", "'bar' && ('' + baz)");
+    assertTypeOf(null, "'bar' && null");
+    assertTypeOf(null, "bar && 'baz'");
+    assertTypeOf("boolean", "foo(), !x");
+    assertTypeOf(null, "'' + foo(), x");
+    assertTypeOf(null, "/./");
+  }
+
+  public final void testFold() throws Exception {
+    assertFolded("4.0", "1 + 3");
+    assertFolded("'13'", "1 + '3'");
+    assertFolded("'13'", "'1' + 3");
+    assertFolded("'13'", "'1' + '3'");
+    assertFolded("'-1.5'", "'' + -1.5");
+    assertFolded("'undefined'", "'' + void 0");
+    assertFolded("'null'", "null + ''");
+    assertFolded("null + 0", "null + 0");  // 0 in JS
+    assertFolded("-2.0", "1 - 3");
+    assertFolded("4.0", "2 * 2");
+    assertFolded("true", "!''");
+    assertFolded("true", "!0");
+    assertFolded("false", "!'0'");
+    assertFolded("true", "!null");
+    assertFolded("true", "!(void 0)");
+    assertFolded("! (void foo())", "!(void foo())");
+    assertFolded("false", "!(4,true)");
+    assertFolded("! (foo() || true)", "!(foo()||true)");
+    assertFolded("true", "'foo' == 'foo'");
+    assertFolded("true", "'foo' === 'foo'");
+    assertFolded("false", "'foo' == 'bar'");
+    assertFolded("false", "'foo' === 'bar'");
+    assertFolded("false", "4 === '4'");
+    assertFolded("4 == '4'", "4 == '4'");
+    assertFolded("false", "'foo' != 'foo'");
+    assertFolded("false", "'foo' !== 'foo'");
+    assertFolded("true", "'foo' != 'bar'");
+    assertFolded("true", "'foo' !== 'bar'");
+    assertFolded("true", "4 !== '4'");
+    assertFolded("4 != '4'", "4 != '4'");
+    assertFolded("0.5", "1 / 2");
+    assertFolded("1 / '2'", "1 / '2'");
+    assertFolded("(1/0)", "1 / 0");
+    assertFolded("(-1/0)", "-1 / 0");
+    assertFolded("0 / 0", "0 / 0");
+    assertFolded("1.0", "1 % 3");
+    assertFolded("1.0", "1 % -3");
+    assertFolded("-1.0", "-1 % 3");
+    assertFolded("-1.0", "-1 % -3");
+    assertFolded("4.0", "4.0");
+    assertFolded("4.0", "+4.0");
+    assertFolded("-1", "~0");
+    assertFolded("-1", "-1");
+    assertFolded("3", "'foo'.length");
+    assertFolded("1", "'foo'.indexOf('o')");
+    assertFolded("-1", "'foo'.indexOf('bar')");
+  }

   private void assertSimplified(String golden, String input)
       throws ParseException {
@@ -115,4 +202,24 @@
     }
assertEquals(input, render(jsExpr(fromString(golden))), render(simple));
   }
-}
+
+ private void assertTypeOf(String type, String expr) throws ParseException {
+    assertEquals(type, jsExpr(fromString(expr)).typeOf());
+  }
+
+ private void assertFolded(String result, String expr) throws ParseException {
+    Expression input = jsExpr(fromString(expr));
+      // Fold operands so we can test negative numbers.
+      if (input instanceof Operation) {
+        Operation op = (Operation) input;
+        for (Expression operand : op.children()) {
+          if (Operation.is(operand, Operator.NEGATION)
+              && operand.children().get(0) instanceof NumberLiteral) {
+            op.replaceChild(operand.fold(), operand);
+          }
+        }
+      }
+    Expression actual = input.fold();
+    assertEquals(result, actual != null ? render(actual) : null);
+  }
+}
=======================================
--- /trunk/tests/com/google/caja/plugin/templates/JsConcatenatorTest.java Thu Aug 27 13:59:50 2009 +++ /trunk/tests/com/google/caja/plugin/templates/JsConcatenatorTest.java Thu Oct 22 14:07:39 2009
@@ -106,7 +106,7 @@
     JsConcatenator concat = new JsConcatenator();
     for (String part : parts) {
       Expression e = jsExpr(fromString(part));
-      if (e instanceof Operation && Operator.VOID == e.getValue()) {
+      if (Operation.is(e, Operator.VOID)) {
         concat.forSideEffect(((Operation) e).children().get(0));
       } else {
         concat.append(e);

Reply via email to