Index: pkg/compiler/lib/src/js/placeholder_safety.dart |
diff --git a/pkg/compiler/lib/src/js/placeholder_safety.dart b/pkg/compiler/lib/src/js/placeholder_safety.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..67ff5631a58d195d50550993a5647abbc77a2b78 |
--- /dev/null |
+++ b/pkg/compiler/lib/src/js/placeholder_safety.dart |
@@ -0,0 +1,327 @@ |
+// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+ |
+library js.safety; |
+ |
+import "js.dart" as js; |
+ |
+typedef bool PositionPredicate(int position); |
+ |
+/// PlaceholderSafetyAnalysis determines which placeholders in a JavaScript |
+/// template may be replaced with an arbitrary expression. Placeholders may be |
+/// replaced with an arbitrary expression providied the template ensures the |
+/// placeholders are evaluated in the same left-to-right order with no |
+/// additional effects interleaved. |
+/// |
+/// The result is semi-conservative, giving reasonable results for many simple |
+/// JS fragments. The non-conservative part is the assumption that arithmetic |
+/// operators are used on 'good' operands that do not force arbitrary code to be |
+/// executed via conversions (valueOf() and toString() methods). |
+class PlaceholderSafetyAnalysis extends js.BaseVisitor<int> { |
+ final PositionPredicate isNullableInput; |
+ int nextPosition = 0; |
+ int maxSafePosition = -1; |
+ bool safe = true; |
+ |
+ // We do a crude abstract interpretation to find operations that might throw |
+ // exceptions. The possible values of expressions are represented by |
+ // integers. Small non-negative integers 0, 1, 2, ... represent the values of |
+ // the placeholders. Other values are: |
+ static const int NONNULL_VALUE = -1; // Unknown but not null. |
+ static const int UNKNOWN_VALUE = -2; // Unknown and might be null. |
+ |
+ PlaceholderSafetyAnalysis._(this.isNullableInput); |
+ |
+ /// Returns the number of placeholders that can be substituted into the |
+ /// template AST [node] without changing the order of observable effects. |
+ /// [isNullableInput] is a function that takes the 0-based index of a |
+ /// placeholder and returns `true` if expression at run time may be null, and |
+ /// `false` if the value is never null. |
+ static int analyze(js.Node node, PositionPredicate isNullableInput) { |
+ PlaceholderSafetyAnalysis analysis = |
+ new PlaceholderSafetyAnalysis._(isNullableInput); |
+ analysis.visit(node); |
+ return analysis.maxSafePosition + 1; |
+ } |
+ |
+ bool canBeNull(int value) { |
+ if (value == NONNULL_VALUE) return false; |
+ if (value == UNKNOWN_VALUE) return true; |
+ return isNullableInput(value); |
+ } |
+ |
+ int unsafe(int value) { |
+ safe = false; |
+ return value; |
+ } |
+ |
+ int visit(js.Node node) { |
+ return node.accept(this); |
+ } |
+ |
+ int visitNode(js.Node node) { |
+ safe = false; |
+ super.visitNode(node); |
+ return UNKNOWN_VALUE; |
+ } |
+ |
+ int visitLiteralNull(js.LiteralNull node) { |
+ return UNKNOWN_VALUE; |
+ } |
+ |
+ int visitLiteral(js.Literal node) { |
+ return NONNULL_VALUE; |
+ } |
+ |
+ int handleInterpolatedNode(js.InterpolatedNode node) { |
+ assert(node.isPositional); |
+ int position = nextPosition++; |
+ if (safe) maxSafePosition = position; |
+ return position; |
+ } |
+ |
+ int visitInterpolatedExpression(js.InterpolatedExpression node) { |
+ return handleInterpolatedNode(node); |
+ } |
+ |
+ int visitInterpolatedLiteral(js.InterpolatedLiteral node) { |
+ return handleInterpolatedNode(node); |
+ } |
+ |
+ int visitInterpolatedSelector(js.InterpolatedSelector node) { |
+ return handleInterpolatedNode(node); |
+ } |
+ |
+ int visitInterpolatedStatement(js.InterpolatedStatement node) { |
+ return handleInterpolatedNode(node); |
+ } |
+ |
+ int visitInterpolatedDeclaration(js.InterpolatedDeclaration node) { |
+ return handleInterpolatedNode(node); |
+ } |
+ |
+ int visitObjectInitializer(js.ObjectInitializer node) { |
+ for (js.Property property in node.properties) { |
+ visit(property); |
+ } |
+ return NONNULL_VALUE; |
+ } |
+ |
+ int visitProperty(js.Property node) { |
+ visit(node.name); |
+ visit(node.value); |
+ return UNKNOWN_VALUE; |
+ } |
+ |
+ int visitArrayInitializer(js.ArrayInitializer node) { |
+ node.elements.forEach(visit); |
+ return NONNULL_VALUE; |
+ } |
+ |
+ int visitArrayHole(js.ArrayHole node) { |
+ return UNKNOWN_VALUE; |
+ } |
+ |
+ int visitAccess(js.PropertyAccess node) { |
+ int first = visit(node.receiver); |
+ int second = visit(node.selector); |
+ // TODO(sra): If the JS is annotated as never throwing, we can avoid this. |
+ if (canBeNull(first)) safe = false; |
+ return UNKNOWN_VALUE; |
+ } |
+ |
+ int visitAssignment(js.Assignment node) { |
+ js.Expression left = node.leftHandSide; |
+ js.Expression right = node.value; |
+ |
+ int leftToRight() { |
+ visit(left); |
+ visit(right); |
+ return UNKNOWN_VALUE; |
+ } |
+ |
+ if (left is js.InterpolatedNode) { |
+ // A bare interpolated expression should not be the LHS of an assignment. |
+ safe = false; |
+ return leftToRight(); |
+ } |
+ |
+ // Assignment operators dereference the LHS before evaluating the RHS. |
+ if (node.op != null) return leftToRight(); |
+ |
+ // Assignment (1) evaluates the LHS as a Reference `lval`, (2) evaluates the |
+ // RHS as a value, (3) dereferences the `lval` in PutValue. |
+ if (left is js.VariableReference) { |
+ int value = visit(right); |
+ // Assignment could change an observed global or cause a ReferenceError. |
+ safe = false; |
+ return value; |
+ } |
+ if (left is js.PropertyAccess) { |
+ // "a.b.x = c.y" gives a TypeError for null values in this order: `a`, |
+ // `c`, `a.b`. |
+ int receiver = visit(left.receiver); |
+ int selector = visit(left.selector); |
+ int value = visit(right); |
+ if (canBeNull(receiver)) safe = false; |
+ return value; |
+ } |
+ // Be conserative with unrecognized LHS expressions. |
+ safe = false; |
+ return leftToRight(); |
+ } |
+ |
+ int visitCall(js.Call node) { |
+ // TODO(sra): Recognize JavaScript built-ins like |
+ // 'Object.prototype.hasOwnProperty.call'. |
+ visit(node.target); |
+ node.arguments.forEach(visit); |
+ return unsafe(UNKNOWN_VALUE); |
+ } |
+ |
+ int visitNew(js.New node) { |
+ visit(node.target); |
+ node.arguments.forEach(visit); |
+ return unsafe(NONNULL_VALUE); |
+ } |
+ |
+ int visitBinary(js.Binary node) { |
+ switch (node.op) { |
+ // We make the non-conservative assumption that these operations are not |
+ // used in ways that force calling arbitrary code via valueOf() or |
+ // toString(). |
+ case "*": |
+ case "/": |
+ case "%": |
+ case "+": |
+ case "-": |
+ case "<<": |
+ case ">>": |
+ case ">>>": |
+ case "<": |
+ case ">": |
+ case "<=": |
+ case ">=": |
+ case "==": |
+ case "===": |
+ case "!=": |
+ case "!==": |
+ case "&": |
+ case "^": |
+ case "|": |
+ int left = visit(node.left); |
+ int right = visit(node.right); |
+ return NONNULL_VALUE; // Number, String, Boolean. |
+ |
+ case ',': |
+ int left = visit(node.left); |
+ int right = visit(node.right); |
+ return right; |
+ |
+ case "&&": |
+ case "||": |
+ int left = visit(node.left); |
+ // TODO(sra): Might be safe, e.g. "x || 0". |
+ safe = false; |
+ int right = visit(node.right); |
+ return UNKNOWN_VALUE; |
+ |
+ case "instanceof": |
+ case "in": |
+ int left = visit(node.left); |
+ int right = visit(node.right); |
+ return UNKNOWN_VALUE; |
+ |
+ default: |
+ return unsafe(UNKNOWN_VALUE); |
+ } |
+ } |
+ |
+ int visitConditional(js.Conditional node) { |
+ int cond = visit(node.condition); |
+ // TODO(sra): Might be safe, e.g. "# ? 1 : 2". |
+ safe = false; |
+ int thenValue = visit(node.then); |
+ int elseValue = visit(node.otherwise); |
+ return UNKNOWN_VALUE; |
+ } |
+ |
+ int visitThrow(js.Throw node) { |
+ visit(node.expression); |
+ return unsafe(UNKNOWN_VALUE); |
+ } |
+ |
+ int visitPrefix(js.Prefix node) { |
+ if (node.op == 'typeof') { |
+ // "typeof a" first evaluates to a Reference. If the Reference is to a |
+ // variable that is not present, "undefined" is returned without |
+ // dereferencing. |
+ if (node.argument is js.VariableUse) return NONNULL_VALUE; // A string. |
+ } |
+ |
+ visit(node.argument); |
+ |
+ switch (node.op) { |
+ case '+': |
+ case '-': |
+ case '!': |
+ case '~': |
+ // Non-conservative assumption that these operators are used on values |
+ // that do not call arbitrary code via valueOf() or toString(). |
+ return NONNULL_VALUE; |
+ |
+ case 'typeof': |
+ return NONNULL_VALUE; // Always a string. |
+ |
+ case 'void': |
+ return UNKNOWN_VALUE; |
+ |
+ case '--': |
+ case '++': |
+ return NONNULL_VALUE; // Always a number. |
+ |
+ default: |
+ safe = false; |
+ return UNKNOWN_VALUE; |
+ } |
+ } |
+ |
+ int visitPostfix(js.Postfix node) { |
+ assert(node.op == '--' || node.op == '++'); |
+ visit(node.argument); |
+ return NONNULL_VALUE; // Always a number, even for "(a=null, a++)". |
+ } |
+ |
+ int visitVariableUse(js.VariableUse node) { |
+ // We could get a ReferenceError unless the variable is in scope. For JS |
+ // fragments, the only use of VariableUse outside a `function(){...}` should |
+ // be for global references. Certain global names are almost certainly not |
+ // reference errors, e.g 'Array'. |
+ switch (node.name) { |
+ case 'Array': |
+ case 'Date': |
+ case 'Function': |
+ case 'Number': |
+ case 'Object': |
+ case 'RegExp': |
+ case 'String': |
+ case 'self': |
+ case 'window': |
+ return NONNULL_VALUE; |
+ default: |
+ return unsafe(UNKNOWN_VALUE); |
+ } |
+ } |
+ |
+ int visitFun(js.Fun node) { |
+ bool oldSafe = safe; |
+ int oldNextPosition = nextPosition; |
+ visit(node.body); |
+ // Creating a function has no effect on order unless there are embedded |
+ // placeholders. |
+ safe = (nextPosition == oldNextPosition) && oldSafe; |
+ return NONNULL_VALUE; |
+ } |
+} |