|  | // 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; | 
|  |  | 
|  | import 'package:js_ast/js_ast.dart' as js; | 
|  |  | 
|  | typedef PositionPredicate = bool Function(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 provided 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 nonNullValue = -1; // Unknown but not null. | 
|  | static const int unknownValue = -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 = PlaceholderSafetyAnalysis._( | 
|  | isNullableInput, | 
|  | ); | 
|  | analysis.visit(node); | 
|  | return analysis.maxSafePosition + 1; | 
|  | } | 
|  |  | 
|  | bool canBeNull(int value) { | 
|  | if (value == nonNullValue) return false; | 
|  | if (value == unknownValue) return true; | 
|  | return isNullableInput(value); | 
|  | } | 
|  |  | 
|  | int unsafe(int value) { | 
|  | safe = false; | 
|  | return value; | 
|  | } | 
|  |  | 
|  | int visit(js.Node node) { | 
|  | return node.accept(this); | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitNode(js.Node node) { | 
|  | safe = false; | 
|  | node.visitChildren(this); | 
|  | return unknownValue; | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitComment(js.Comment node) { | 
|  | return unknownValue; | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitLiteralNull(js.LiteralNull node) { | 
|  | return unknownValue; | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitLiteral(js.Literal node) { | 
|  | return nonNullValue; | 
|  | } | 
|  |  | 
|  | int handleInterpolatedNode(js.InterpolatedNode node) { | 
|  | assert(node.isPositional); | 
|  | int position = nextPosition++; | 
|  | if (safe) maxSafePosition = position; | 
|  | return position; | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitInterpolatedExpression(js.InterpolatedExpression node) { | 
|  | return handleInterpolatedNode(node); | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitInterpolatedLiteral(js.InterpolatedLiteral node) { | 
|  | return handleInterpolatedNode(node); | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitInterpolatedSelector(js.InterpolatedSelector node) { | 
|  | return handleInterpolatedNode(node); | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitInterpolatedStatement(js.InterpolatedStatement node) { | 
|  | return handleInterpolatedNode(node); | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitInterpolatedDeclaration(js.InterpolatedDeclaration node) { | 
|  | return handleInterpolatedNode(node); | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitObjectInitializer(js.ObjectInitializer node) { | 
|  | for (js.Property property in node.properties) { | 
|  | visit(property); | 
|  | } | 
|  | return nonNullValue; | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitProperty(js.Property node) { | 
|  | visit(node.name); | 
|  | visit(node.value); | 
|  | return unknownValue; | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitArrayInitializer(js.ArrayInitializer node) { | 
|  | node.elements.forEach(visit); | 
|  | return nonNullValue; | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitArrayHole(js.ArrayHole node) { | 
|  | return unknownValue; | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitAccess(js.PropertyAccess node) { | 
|  | int first = visit(node.receiver); | 
|  | visit(node.selector); | 
|  | // TODO(sra): If the JS is annotated as never throwing, we can avoid this. | 
|  | if (canBeNull(first)) safe = false; | 
|  | return unknownValue; | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitAssignment(js.Assignment node) { | 
|  | js.Expression left = node.leftHandSide; | 
|  | js.Expression right = node.value; | 
|  |  | 
|  | int leftToRight() { | 
|  | visit(left); | 
|  | visit(right); | 
|  | return unknownValue; | 
|  | } | 
|  |  | 
|  | 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); | 
|  | visit(left.selector); | 
|  | int value = visit(right); | 
|  | if (canBeNull(receiver)) safe = false; | 
|  | return value; | 
|  | } | 
|  | // Be conservative with unrecognized LHS expressions. | 
|  | safe = false; | 
|  | return leftToRight(); | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitVariableInitialization(js.VariableInitialization node) { | 
|  | js.Expression left = node.declaration; | 
|  | js.Expression? right = node.value; | 
|  |  | 
|  | visit(left); | 
|  | if (left is js.InterpolatedNode) { | 
|  | // A bare interpolated expression should not be the LHS of an initialized | 
|  | // variable declaration. | 
|  | safe = false; | 
|  | } | 
|  | if (right != null) { | 
|  | return visit(right); | 
|  | } | 
|  | return unknownValue; | 
|  | } | 
|  |  | 
|  | @override | 
|  | 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(unknownValue); | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitNew(js.New node) { | 
|  | visit(node.target); | 
|  | node.arguments.forEach(visit); | 
|  | return unsafe(nonNullValue); | 
|  | } | 
|  |  | 
|  | @override | 
|  | 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 "|": | 
|  | visit(node.left); | 
|  | visit(node.right); | 
|  | return nonNullValue; // Number, String, Boolean. | 
|  |  | 
|  | case ',': | 
|  | visit(node.left); | 
|  | int right = visit(node.right); | 
|  | return right; | 
|  |  | 
|  | case "&&": | 
|  | case "||": | 
|  | visit(node.left); | 
|  | // TODO(sra): Might be safe, e.g.  "x || 0". | 
|  | safe = false; | 
|  | visit(node.right); | 
|  | return unknownValue; | 
|  |  | 
|  | case "instanceof": | 
|  | case "in": | 
|  | visit(node.left); | 
|  | visit(node.right); | 
|  | return unknownValue; | 
|  |  | 
|  | default: | 
|  | return unsafe(unknownValue); | 
|  | } | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitConditional(js.Conditional node) { | 
|  | visit(node.condition); | 
|  | // TODO(sra): Might be safe, e.g.  "# ? 1 : 2". | 
|  | safe = false; | 
|  | visit(node.then); | 
|  | visit(node.otherwise); | 
|  | return unknownValue; | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitThrow(js.Throw node) { | 
|  | visit(node.expression); | 
|  | return unsafe(unknownValue); | 
|  | } | 
|  |  | 
|  | @override | 
|  | 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 nonNullValue; // 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 nonNullValue; | 
|  |  | 
|  | case 'typeof': | 
|  | return nonNullValue; // Always a string. | 
|  |  | 
|  | case 'void': | 
|  | return unknownValue; | 
|  |  | 
|  | case '--': | 
|  | case '++': | 
|  | return nonNullValue; // Always a number. | 
|  |  | 
|  | default: | 
|  | safe = false; | 
|  | return unknownValue; | 
|  | } | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitPostfix(js.Postfix node) { | 
|  | assert(node.op == '--' || node.op == '++'); | 
|  | visit(node.argument); | 
|  | return nonNullValue; // Always a number, even for "(a=null, a++)". | 
|  | } | 
|  |  | 
|  | @override | 
|  | 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 'Math': | 
|  | case 'Number': | 
|  | case 'Object': | 
|  | case 'RegExp': | 
|  | case 'String': | 
|  | case 'self': | 
|  | case 'window': | 
|  | return nonNullValue; | 
|  | default: | 
|  | return unsafe(unknownValue); | 
|  | } | 
|  | } | 
|  |  | 
|  | @override | 
|  | int visitFunctionExpression(js.FunctionExpression 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 nonNullValue; | 
|  | } | 
|  | } |