OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 |
| 6 library js.safety; |
| 7 |
| 8 import "js.dart" as js; |
| 9 |
| 10 typedef bool PositionPredicate(int position); |
| 11 |
| 12 /// PlaceholderSafetyAnalysis determines which placeholders in a JavaScript |
| 13 /// template may be replaced with an arbitrary expression. Placeholders may be |
| 14 /// replaced with an arbitrary expression providied the template ensures the |
| 15 /// placeholders are evaluated in the same left-to-right order with no |
| 16 /// additional effects interleaved. |
| 17 /// |
| 18 /// The result is semi-conservative, giving reasonable results for many simple |
| 19 /// JS fragments. The non-conservative part is the assumption that arithmetic |
| 20 /// operators are used on 'good' operands that do not force arbitrary code to be |
| 21 /// executed via conversions (valueOf() and toString() methods). |
| 22 class PlaceholderSafetyAnalysis extends js.BaseVisitor<int> { |
| 23 final PositionPredicate isNullableInput; |
| 24 int nextPosition = 0; |
| 25 int maxSafePosition = -1; |
| 26 bool safe = true; |
| 27 |
| 28 // We do a crude abstract interpretation to find operations that might throw |
| 29 // exceptions. The possible values of expressions are represented by |
| 30 // integers. Small non-negative integers 0, 1, 2, ... represent the values of |
| 31 // the placeholders. Other values are: |
| 32 static const int NONNULL_VALUE = -1; // Unknown but not null. |
| 33 static const int UNKNOWN_VALUE = -2; // Unknown and might be null. |
| 34 |
| 35 PlaceholderSafetyAnalysis._(this.isNullableInput); |
| 36 |
| 37 /// Returns the number of placeholders that can be substituted into the |
| 38 /// template AST [node] without changing the order of observable effects. |
| 39 /// [isNullableInput] is a function that takes the 0-based index of a |
| 40 /// placeholder and returns `true` if expression at run time may be null, and |
| 41 /// `false` if the value is never null. |
| 42 static int analyze(js.Node node, PositionPredicate isNullableInput) { |
| 43 PlaceholderSafetyAnalysis analysis = |
| 44 new PlaceholderSafetyAnalysis._(isNullableInput); |
| 45 analysis.visit(node); |
| 46 return analysis.maxSafePosition + 1; |
| 47 } |
| 48 |
| 49 bool canBeNull(int value) { |
| 50 if (value == NONNULL_VALUE) return false; |
| 51 if (value == UNKNOWN_VALUE) return true; |
| 52 return isNullableInput(value); |
| 53 } |
| 54 |
| 55 int unsafe(int value) { |
| 56 safe = false; |
| 57 return value; |
| 58 } |
| 59 |
| 60 int visit(js.Node node) { |
| 61 return node.accept(this); |
| 62 } |
| 63 |
| 64 int visitNode(js.Node node) { |
| 65 safe = false; |
| 66 super.visitNode(node); |
| 67 return UNKNOWN_VALUE; |
| 68 } |
| 69 |
| 70 int visitLiteralNull(js.LiteralNull node) { |
| 71 return UNKNOWN_VALUE; |
| 72 } |
| 73 |
| 74 int visitLiteral(js.Literal node) { |
| 75 return NONNULL_VALUE; |
| 76 } |
| 77 |
| 78 int handleInterpolatedNode(js.InterpolatedNode node) { |
| 79 assert(node.isPositional); |
| 80 int position = nextPosition++; |
| 81 if (safe) maxSafePosition = position; |
| 82 return position; |
| 83 } |
| 84 |
| 85 int visitInterpolatedExpression(js.InterpolatedExpression node) { |
| 86 return handleInterpolatedNode(node); |
| 87 } |
| 88 |
| 89 int visitInterpolatedLiteral(js.InterpolatedLiteral node) { |
| 90 return handleInterpolatedNode(node); |
| 91 } |
| 92 |
| 93 int visitInterpolatedSelector(js.InterpolatedSelector node) { |
| 94 return handleInterpolatedNode(node); |
| 95 } |
| 96 |
| 97 int visitInterpolatedStatement(js.InterpolatedStatement node) { |
| 98 return handleInterpolatedNode(node); |
| 99 } |
| 100 |
| 101 int visitInterpolatedDeclaration(js.InterpolatedDeclaration node) { |
| 102 return handleInterpolatedNode(node); |
| 103 } |
| 104 |
| 105 int visitObjectInitializer(js.ObjectInitializer node) { |
| 106 for (js.Property property in node.properties) { |
| 107 visit(property); |
| 108 } |
| 109 return NONNULL_VALUE; |
| 110 } |
| 111 |
| 112 int visitProperty(js.Property node) { |
| 113 visit(node.name); |
| 114 visit(node.value); |
| 115 return UNKNOWN_VALUE; |
| 116 } |
| 117 |
| 118 int visitArrayInitializer(js.ArrayInitializer node) { |
| 119 node.elements.forEach(visit); |
| 120 return NONNULL_VALUE; |
| 121 } |
| 122 |
| 123 int visitArrayHole(js.ArrayHole node) { |
| 124 return UNKNOWN_VALUE; |
| 125 } |
| 126 |
| 127 int visitAccess(js.PropertyAccess node) { |
| 128 int first = visit(node.receiver); |
| 129 int second = visit(node.selector); |
| 130 // TODO(sra): If the JS is annotated as never throwing, we can avoid this. |
| 131 if (canBeNull(first)) safe = false; |
| 132 return UNKNOWN_VALUE; |
| 133 } |
| 134 |
| 135 int visitAssignment(js.Assignment node) { |
| 136 js.Expression left = node.leftHandSide; |
| 137 js.Expression right = node.value; |
| 138 |
| 139 int leftToRight() { |
| 140 visit(left); |
| 141 visit(right); |
| 142 return UNKNOWN_VALUE; |
| 143 } |
| 144 |
| 145 if (left is js.InterpolatedNode) { |
| 146 // A bare interpolated expression should not be the LHS of an assignment. |
| 147 safe = false; |
| 148 return leftToRight(); |
| 149 } |
| 150 |
| 151 // Assignment operators dereference the LHS before evaluating the RHS. |
| 152 if (node.op != null) return leftToRight(); |
| 153 |
| 154 // Assignment (1) evaluates the LHS as a Reference `lval`, (2) evaluates the |
| 155 // RHS as a value, (3) dereferences the `lval` in PutValue. |
| 156 if (left is js.VariableReference) { |
| 157 int value = visit(right); |
| 158 // Assignment could change an observed global or cause a ReferenceError. |
| 159 safe = false; |
| 160 return value; |
| 161 } |
| 162 if (left is js.PropertyAccess) { |
| 163 // "a.b.x = c.y" gives a TypeError for null values in this order: `a`, |
| 164 // `c`, `a.b`. |
| 165 int receiver = visit(left.receiver); |
| 166 int selector = visit(left.selector); |
| 167 int value = visit(right); |
| 168 if (canBeNull(receiver)) safe = false; |
| 169 return value; |
| 170 } |
| 171 // Be conserative with unrecognized LHS expressions. |
| 172 safe = false; |
| 173 return leftToRight(); |
| 174 } |
| 175 |
| 176 int visitCall(js.Call node) { |
| 177 // TODO(sra): Recognize JavaScript built-ins like |
| 178 // 'Object.prototype.hasOwnProperty.call'. |
| 179 visit(node.target); |
| 180 node.arguments.forEach(visit); |
| 181 return unsafe(UNKNOWN_VALUE); |
| 182 } |
| 183 |
| 184 int visitNew(js.New node) { |
| 185 visit(node.target); |
| 186 node.arguments.forEach(visit); |
| 187 return unsafe(NONNULL_VALUE); |
| 188 } |
| 189 |
| 190 int visitBinary(js.Binary node) { |
| 191 switch (node.op) { |
| 192 // We make the non-conservative assumption that these operations are not |
| 193 // used in ways that force calling arbitrary code via valueOf() or |
| 194 // toString(). |
| 195 case "*": |
| 196 case "/": |
| 197 case "%": |
| 198 case "+": |
| 199 case "-": |
| 200 case "<<": |
| 201 case ">>": |
| 202 case ">>>": |
| 203 case "<": |
| 204 case ">": |
| 205 case "<=": |
| 206 case ">=": |
| 207 case "==": |
| 208 case "===": |
| 209 case "!=": |
| 210 case "!==": |
| 211 case "&": |
| 212 case "^": |
| 213 case "|": |
| 214 int left = visit(node.left); |
| 215 int right = visit(node.right); |
| 216 return NONNULL_VALUE; // Number, String, Boolean. |
| 217 |
| 218 case ',': |
| 219 int left = visit(node.left); |
| 220 int right = visit(node.right); |
| 221 return right; |
| 222 |
| 223 case "&&": |
| 224 case "||": |
| 225 int left = visit(node.left); |
| 226 // TODO(sra): Might be safe, e.g. "x || 0". |
| 227 safe = false; |
| 228 int right = visit(node.right); |
| 229 return UNKNOWN_VALUE; |
| 230 |
| 231 case "instanceof": |
| 232 case "in": |
| 233 int left = visit(node.left); |
| 234 int right = visit(node.right); |
| 235 return UNKNOWN_VALUE; |
| 236 |
| 237 default: |
| 238 return unsafe(UNKNOWN_VALUE); |
| 239 } |
| 240 } |
| 241 |
| 242 int visitConditional(js.Conditional node) { |
| 243 int cond = visit(node.condition); |
| 244 // TODO(sra): Might be safe, e.g. "# ? 1 : 2". |
| 245 safe = false; |
| 246 int thenValue = visit(node.then); |
| 247 int elseValue = visit(node.otherwise); |
| 248 return UNKNOWN_VALUE; |
| 249 } |
| 250 |
| 251 int visitThrow(js.Throw node) { |
| 252 visit(node.expression); |
| 253 return unsafe(UNKNOWN_VALUE); |
| 254 } |
| 255 |
| 256 int visitPrefix(js.Prefix node) { |
| 257 if (node.op == 'typeof') { |
| 258 // "typeof a" first evaluates to a Reference. If the Reference is to a |
| 259 // variable that is not present, "undefined" is returned without |
| 260 // dereferencing. |
| 261 if (node.argument is js.VariableUse) return NONNULL_VALUE; // A string. |
| 262 } |
| 263 |
| 264 visit(node.argument); |
| 265 |
| 266 switch (node.op) { |
| 267 case '+': |
| 268 case '-': |
| 269 case '!': |
| 270 case '~': |
| 271 // Non-conservative assumption that these operators are used on values |
| 272 // that do not call arbitrary code via valueOf() or toString(). |
| 273 return NONNULL_VALUE; |
| 274 |
| 275 case 'typeof': |
| 276 return NONNULL_VALUE; // Always a string. |
| 277 |
| 278 case 'void': |
| 279 return UNKNOWN_VALUE; |
| 280 |
| 281 case '--': |
| 282 case '++': |
| 283 return NONNULL_VALUE; // Always a number. |
| 284 |
| 285 default: |
| 286 safe = false; |
| 287 return UNKNOWN_VALUE; |
| 288 } |
| 289 } |
| 290 |
| 291 int visitPostfix(js.Postfix node) { |
| 292 assert(node.op == '--' || node.op == '++'); |
| 293 visit(node.argument); |
| 294 return NONNULL_VALUE; // Always a number, even for "(a=null, a++)". |
| 295 } |
| 296 |
| 297 int visitVariableUse(js.VariableUse node) { |
| 298 // We could get a ReferenceError unless the variable is in scope. For JS |
| 299 // fragments, the only use of VariableUse outside a `function(){...}` should |
| 300 // be for global references. Certain global names are almost certainly not |
| 301 // reference errors, e.g 'Array'. |
| 302 switch (node.name) { |
| 303 case 'Array': |
| 304 case 'Date': |
| 305 case 'Function': |
| 306 case 'Number': |
| 307 case 'Object': |
| 308 case 'RegExp': |
| 309 case 'String': |
| 310 case 'self': |
| 311 case 'window': |
| 312 return NONNULL_VALUE; |
| 313 default: |
| 314 return unsafe(UNKNOWN_VALUE); |
| 315 } |
| 316 } |
| 317 |
| 318 int visitFun(js.Fun node) { |
| 319 bool oldSafe = safe; |
| 320 int oldNextPosition = nextPosition; |
| 321 visit(node.body); |
| 322 // Creating a function has no effect on order unless there are embedded |
| 323 // placeholders. |
| 324 safe = (nextPosition == oldNextPosition) && oldSafe; |
| 325 return NONNULL_VALUE; |
| 326 } |
| 327 } |
OLD | NEW |