Revision: 3842
Author: [email protected]
Date: Thu Nov 12 13:05:03 2009
Log: Fix rendering of Conditionals
http://codereview.appspot.com/154078
It is possible to programatically construct a parse tree containing
conditionals where the rendered form attaches an else to the wrong
conditional.
For e.g., it is possible to programatically move
if (foo) bar();
into the body of
if (baz) @body else boo();
which should result in the structure
if (baz) { if (foo) bar(); } else boo();
but which naively renders to
if (baz) if (foo) bar(); else boo();
which is equivalent to
if (baz) { if (foo) bar(); else boo(); }
Our code does not do this right now -- we are liberal with blocks --
so this does not constitute an attack vector.
This adds a method to statement, hasHangingConditional, and changes
the Conditional.render to add curly braces when the structure would
require them to resolve ambiguity.
Please look especially at the definition and use of
hasHangingConditional in Conditional.java and the tests in ParserTest.java
[email protected]
http://code.google.com/p/google-caja/source/detail?r=3842
Modified:
/trunk/src/com/google/caja/parser/js/Block.java
/trunk/src/com/google/caja/parser/js/BreakStmt.java
/trunk/src/com/google/caja/parser/js/CatchStmt.java
/trunk/src/com/google/caja/parser/js/Conditional.java
/trunk/src/com/google/caja/parser/js/ContinueStmt.java
/trunk/src/com/google/caja/parser/js/DebuggerStmt.java
/trunk/src/com/google/caja/parser/js/Declaration.java
/trunk/src/com/google/caja/parser/js/DoWhileLoop.java
/trunk/src/com/google/caja/parser/js/ExpressionStmt.java
/trunk/src/com/google/caja/parser/js/FinallyStmt.java
/trunk/src/com/google/caja/parser/js/ForEachLoop.java
/trunk/src/com/google/caja/parser/js/ForLoop.java
/trunk/src/com/google/caja/parser/js/LabeledStmtWrapper.java
/trunk/src/com/google/caja/parser/js/MultiDeclaration.java
/trunk/src/com/google/caja/parser/js/Noop.java
/trunk/src/com/google/caja/parser/js/ReturnStmt.java
/trunk/src/com/google/caja/parser/js/Statement.java
/trunk/src/com/google/caja/parser/js/SwitchCase.java
/trunk/src/com/google/caja/parser/js/SwitchStmt.java
/trunk/src/com/google/caja/parser/js/ThrowStmt.java
/trunk/src/com/google/caja/parser/js/TranslatedCode.java
/trunk/src/com/google/caja/parser/js/TryStmt.java
/trunk/src/com/google/caja/parser/js/UseSubsetDirective.java
/trunk/src/com/google/caja/parser/js/WhileLoop.java
/trunk/src/com/google/caja/parser/js/WithStmt.java
/trunk/tests/com/google/caja/parser/js/ParserTest.java
=======================================
--- /trunk/src/com/google/caja/parser/js/Block.java Mon Nov 2 13:56:19 2009
+++ /trunk/src/com/google/caja/parser/js/Block.java Thu Nov 12 13:05:03 2009
@@ -95,4 +95,6 @@
public boolean isTerminal() {
return true;
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/BreakStmt.java Wed Jul 15 11:24:34
2009
+++ /trunk/src/com/google/caja/parser/js/BreakStmt.java Thu Nov 12 13:05:03
2009
@@ -64,4 +64,6 @@
out.consume(label);
}
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/CatchStmt.java Mon Nov 2 13:56:19
2009
+++ /trunk/src/com/google/caja/parser/js/CatchStmt.java Thu Nov 12 13:05:03
2009
@@ -69,4 +69,6 @@
out.consume(")");
body.renderBlock(rc, false);
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/Conditional.java Wed Jul 15
11:24:34 2009
+++ /trunk/src/com/google/caja/parser/js/Conditional.java Thu Nov 12
13:05:03 2009
@@ -77,7 +77,10 @@
out.consume("(");
condition.render(rc);
out.consume(")");
+ boolean hanging = body.hasHangingConditional();
+ if (hanging) { out.consume("{"); }
body.renderBlock(rc, i + 2 < n);
+ if (hanging) { out.consume("}"); }
}
if (i < n) {
Statement body = (Statement) children.get(i);
@@ -85,4 +88,15 @@
body.renderBlock(rc, false);
}
}
-}
+
+ public boolean hasHangingConditional() {
+ List<? extends ParseTreeNode> children = children();
+ int n = children.size();
+ // If there is no else clause, then an else following would change the
+ // meaning.
+ if ((n & 1) == 0) { return true; }
+ // Otherwise if the else clause has a hanging conditional, then an else
+ // would change the meaning.
+ return ((Statement) children.get(n - 1)).hasHangingConditional();
+ }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/ContinueStmt.java Wed Jul 15
11:24:34 2009
+++ /trunk/src/com/google/caja/parser/js/ContinueStmt.java Thu Nov 12
13:05:03 2009
@@ -63,4 +63,6 @@
out.consume(label);
}
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/DebuggerStmt.java Wed Jul 15
11:24:34 2009
+++ /trunk/src/com/google/caja/parser/js/DebuggerStmt.java Thu Nov 12
13:05:03 2009
@@ -49,4 +49,6 @@
out.mark(getFilePosition());
out.consume("debugger");
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/Declaration.java Thu Oct 22
14:07:39 2009
+++ /trunk/src/com/google/caja/parser/js/Declaration.java Thu Nov 12
13:05:03 2009
@@ -102,4 +102,6 @@
if (isComma) { out.consume(")"); }
}
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/DoWhileLoop.java Wed Jul 15
11:24:34 2009
+++ /trunk/src/com/google/caja/parser/js/DoWhileLoop.java Thu Nov 12
13:05:03 2009
@@ -68,4 +68,6 @@
condition.render(rc);
out.consume(")");
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/ExpressionStmt.java Wed Jul 15
11:24:34 2009
+++ /trunk/src/com/google/caja/parser/js/ExpressionStmt.java Thu Nov 12
13:05:03 2009
@@ -84,4 +84,6 @@
expr.render(rc);
}
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/FinallyStmt.java Mon Nov 2
13:56:19 2009
+++ /trunk/src/com/google/caja/parser/js/FinallyStmt.java Thu Nov 12
13:05:03 2009
@@ -55,4 +55,6 @@
rc.getOut().consume("finally");
body.renderBlock(rc, false);
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/ForEachLoop.java Thu Oct 15
15:42:13 2009
+++ /trunk/src/com/google/caja/parser/js/ForEachLoop.java Thu Nov 12
13:05:03 2009
@@ -102,4 +102,8 @@
out.consume(")");
body.renderBlock(rc, false);
}
-}
+
+ public boolean hasHangingConditional() {
+ return body.hasHangingConditional();
+ }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/ForLoop.java Wed Jul 15 11:24:34
2009
+++ /trunk/src/com/google/caja/parser/js/ForLoop.java Thu Nov 12 13:05:03
2009
@@ -94,4 +94,8 @@
out.consume(")");
getBody().renderBlock(rc, false);
}
-}
+
+ public boolean hasHangingConditional() {
+ return body.hasHangingConditional();
+ }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/LabeledStmtWrapper.java Thu Oct 15
15:42:13 2009
+++ /trunk/src/com/google/caja/parser/js/LabeledStmtWrapper.java Thu Nov 12
13:05:03 2009
@@ -68,4 +68,8 @@
}
body.render(rc);
}
-}
+
+ public boolean hasHangingConditional() {
+ return body.hasHangingConditional();
+ }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/MultiDeclaration.java Wed Jul 15
11:24:34 2009
+++ /trunk/src/com/google/caja/parser/js/MultiDeclaration.java Thu Nov 12
13:05:03 2009
@@ -78,4 +78,6 @@
decl.renderShort(rc);
}
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/Noop.java Wed Jul 15 11:24:34 2009
+++ /trunk/src/com/google/caja/parser/js/Noop.java Thu Nov 12 13:05:03 2009
@@ -39,4 +39,6 @@
public void render(RenderContext rc) {
// render for Noop is a noop as renderBlock will take care of the
semicolon
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/ReturnStmt.java Wed Jul 15
11:24:34 2009
+++ /trunk/src/com/google/caja/parser/js/ReturnStmt.java Thu Nov 12
13:05:03 2009
@@ -68,4 +68,6 @@
returnValue.render(rc);
}
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/Statement.java Mon Mar 31 10:43:02
2008
+++ /trunk/src/com/google/caja/parser/js/Statement.java Thu Nov 12 13:05:03
2009
@@ -60,4 +60,10 @@
* semicolon if not a block.
*/
void renderBlock(RenderContext rc, boolean terminate);
-}
+
+ /**
+ * True if the rendered form of the statement would consume more tokens
if
+ * parsed followed by the tokens "else" and ";".
+ */
+ boolean hasHangingConditional();
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/SwitchCase.java Wed Feb 11
20:17:12 2009
+++ /trunk/src/com/google/caja/parser/js/SwitchCase.java Thu Nov 12
13:05:03 2009
@@ -17,8 +17,12 @@
import com.google.caja.lexer.FilePosition;
import com.google.caja.parser.ParseTreeNode;
-/** @see SwitchStmt */
+/**
+ * Base class for case and default blocks.
+ * @see SwitchStmt
+ */
public abstract class SwitchCase extends AbstractStatement {
protected SwitchCase(FilePosition pos) { super(pos,
ParseTreeNode.class); }
- // base class for case and default blocks
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/SwitchStmt.java Thu Oct 15
15:42:13 2009
+++ /trunk/src/com/google/caja/parser/js/SwitchStmt.java Thu Nov 12
13:05:03 2009
@@ -104,4 +104,6 @@
public boolean isTerminal() {
return true;
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/ThrowStmt.java Thu Oct 15 15:42:13
2009
+++ /trunk/src/com/google/caja/parser/js/ThrowStmt.java Thu Nov 12 13:05:03
2009
@@ -59,4 +59,6 @@
rc.getOut().consume("throw");
exception.render(rc);
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/TranslatedCode.java Fri Jul 24
17:10:36 2009
+++ /trunk/src/com/google/caja/parser/js/TranslatedCode.java Thu Nov 12
13:05:03 2009
@@ -74,6 +74,8 @@
children().get(0).render(rc);
out.consume("/* End translated code */");
}
+
+ public boolean hasHangingConditional() { return false; }
@Override
public boolean isTerminal() {
=======================================
--- /trunk/src/com/google/caja/parser/js/TryStmt.java Mon Nov 2 13:56:19
2009
+++ /trunk/src/com/google/caja/parser/js/TryStmt.java Thu Nov 12 13:05:03
2009
@@ -92,4 +92,6 @@
fin.renderBlock(rc, false);
}
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/UseSubsetDirective.java Fri Aug 28
19:16:06 2009
+++ /trunk/src/com/google/caja/parser/js/UseSubsetDirective.java Thu Nov 12
13:05:03 2009
@@ -31,12 +31,12 @@
* <p>
* "Use subsets" are they were defined in ES5.
* Since then the grammar was changed to use a separate string literal per
- * use-directive. The use-directives do not necessarily define subsets. But
- * they typically should define at least almost subsets in practice so
- * the code within their scope will still work on browsers that do not
- * recognize the "subset" to be used. For example, ES5/strict is not a
subset
+ * use-directive. The use-directives do not necessarily define subsets. But
+ * they typically should define at least almost subsets in practice so
+ * the code within their scope will still work on browsers that do not
+ * recognize the "subset" to be used. For example, ES5/strict is not a
subset
* of ES5/non-strict. See issue 1111.
- *
+ *
* <p>An <a
href="http://wiki.ecmascript.org/lib/exe/fetch.php?id=es3.1%3Aes3.1_proposal_working_draft&cache=cache&media=es3.1:tc39-es31-draft27oct08.pdf"
* >ES5 draft</a> defined the use grammar as:<blockquote><pre>
* UseSubsetDirective opt :
@@ -54,7 +54,7 @@
* <li>Evaluate SubsetList
* <li>Return Result(1)
* </ol>
- *
+ *
* <p>
* The production SubsetList : Subset is evaluated as follows:<ol>
* <li>If Subset is not the name of a usage subset that is supported by
this
@@ -142,4 +142,6 @@
public boolean isTerminal() {
return true;
}
-}
+
+ public boolean hasHangingConditional() { return false; }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/WhileLoop.java Wed Jul 15 11:24:34
2009
+++ /trunk/src/com/google/caja/parser/js/WhileLoop.java Thu Nov 12 13:05:03
2009
@@ -67,4 +67,8 @@
out.consume(")");
body.renderBlock(rc, false);
}
-}
+
+ public boolean hasHangingConditional() {
+ return body.hasHangingConditional();
+ }
+}
=======================================
--- /trunk/src/com/google/caja/parser/js/WithStmt.java Fri Aug 28 19:16:06
2009
+++ /trunk/src/com/google/caja/parser/js/WithStmt.java Thu Nov 12 13:05:03
2009
@@ -25,7 +25,7 @@
* ES3-12.10: The with statement adds a computed object to the front
* of the scope chain of the current execution context, then executes a
* statement with this augmented scope chain, then restores the scope
- * chain. (ES5 expresses this in terms of environment records but with
+ * chain. (ES5 expresses this in terms of environment records but with
* similar effect.)
*
* @author [email protected]
@@ -71,4 +71,8 @@
out.consume(")");
getBody().renderBlock(rc, false);
}
-}
+
+ public boolean hasHangingConditional() {
+ return getBody().hasHangingConditional();
+ }
+}
=======================================
--- /trunk/tests/com/google/caja/parser/js/ParserTest.java Fri Aug 28
19:16:06 2009
+++ /trunk/tests/com/google/caja/parser/js/ParserTest.java Thu Nov 12
13:05:03 2009
@@ -19,7 +19,9 @@
import com.google.caja.lexer.TokenConsumer;
import com.google.caja.lexer.ParseException;
import com.google.caja.parser.AncestorChain;
+import com.google.caja.parser.MutableParseTreeNode;
import com.google.caja.parser.ParseTreeNode;
+import com.google.caja.parser.Visitor;
import com.google.caja.render.Concatenator;
import com.google.caja.render.JsPrettyPrinter;
import com.google.caja.reporting.Message;
@@ -29,7 +31,9 @@
import com.google.caja.reporting.MessageType;
import com.google.caja.reporting.RenderContext;
import com.google.caja.util.CajaTestCase;
+import com.google.caja.util.Lists;
import com.google.caja.util.MoreAsserts;
+import com.google.caja.util.Pair;
import com.google.caja.util.Strings;
import com.google.caja.util.TestUtil;
@@ -345,6 +349,23 @@
MessageLevel.ERROR,
MessagePart.Factory.valueOf("C\0327"));
}
+
+ /**
+ * If conditionals are moved around, they can get in a configuration that
+ * would be impossible to parse.
+ */
+ public final void testRenderingOfRebuiltConditionals() throws
ParseException {
+ assertEquals(
+ render(js(fromString("if (foo) { if (bar);} else baz"))),
+ render(stripBlocks(js(fromString("if (foo) { if (bar) {} } else
baz"))))
+ );
+ assertEquals(
+ render(js(fromString(
+ "if (foo) { while (foo) if (bar) break; } else baz"))),
+ render(stripBlocks(js(fromString(
+ "if (foo) { while (foo) if (bar) break; } else baz"))))
+ );
+ }
public void assertExpectedSemi() {
assertParseFails("foo(function () {return;");
@@ -603,4 +624,27 @@
throw new RuntimeException(msg, ex);
}
}
-}
+
+ private static Statement stripBlocks(Block b) {
+ b.acceptPostOrder(new Visitor() {
+ public boolean visit(AncestorChain<?> chain) {
+ if (chain.node instanceof Block && chain.parent != null) {
+ Block b = chain.cast(Block.class).node;
+ List<? extends Statement> children = b.children();
+ switch (children.size()) {
+ case 0:
+
chain.parent.cast(MutableParseTreeNode.class).node.replaceChild(
+ new Noop(b.getFilePosition()), b);
+ break;
+ case 1:
+
chain.parent.cast(MutableParseTreeNode.class).node.replaceChild(
+ children.get(0), b);
+ break;
+ }
+ }
+ return true;
+ }
+ }, null);
+ return b;
+ }
+}