Chromium Code Reviews| Index: dart/sdk/lib/_internal/lib/js_helper.dart |
| diff --git a/dart/sdk/lib/_internal/lib/js_helper.dart b/dart/sdk/lib/_internal/lib/js_helper.dart |
| index 32835bfeb9e33aff146b1bd5c487b55c5ebb4481..773eb6f4fa3b27f2c626619fa8bd2d1f75a46f91 100644 |
| --- a/dart/sdk/lib/_internal/lib/js_helper.dart |
| +++ b/dart/sdk/lib/_internal/lib/js_helper.dart |
| @@ -794,6 +794,368 @@ throwAbstractClassInstantiationError(className) { |
| throw new AbstractClassInstantiationError(className); |
| } |
| + |
| +/** |
| + * Helper class for building patterns recognizing native type errors. |
| + */ |
| +class TypeErrorDecoder { |
| + // Field names are private to help tree-shaking. |
| + |
| + /// A regular expression which matches is matched against an error message. |
| + final String _pattern; |
| + |
| + /// The group index of "arguments" in [_pattern], or -1 if _pattern has no |
| + /// match for "arguments". |
| + final int _arguments; |
| + |
| + /// The group index of "argumentsExpr" in [_pattern], or -1 if _pattern has |
| + /// no match for "argumentsExpr". |
| + final int _argumentsExpr; |
| + |
| + /// The group index of "expr" in [_pattern], or -1 if _pattern has no match |
| + /// for "expr". |
| + final int _expr; |
| + |
| + /// The group index of "method" in [_pattern], or -1 if _pattern has no match |
| + /// for "method". |
| + final int _method; |
| + |
| + /// The group index of "receiver" in [_pattern], or -1 if _pattern has no |
| + /// match for "receiver". |
| + final int _receiver; |
| + |
| + /// Pattern used to recognize a NoSuchMethodError error (and |
| + /// possibly extract the method name). |
| + static final TypeErrorDecoder noSuchMethodPattern = |
| + extractPattern(provokeCallErrorOn(buildJavaScriptObject())); |
| + |
| + /// Pattern used to recognize an "object not a closure" error (and |
| + /// possibly extract the method name). |
| + static final TypeErrorDecoder notClosurePattern = |
| + extractPattern(provokeCallErrorOn(buildJavaScriptObjectWithNonClosure())); |
| + |
| + /// Pattern used to recognize a NoSuchMethodError on JavaScript null |
| + /// call. |
| + static final TypeErrorDecoder nullCallPattern = |
| + extractPattern(provokeCallErrorOn(JS('', 'null'))); |
| + |
| + /// Pattern used to recognize a NoSuchMethodError on JavaScript literal null |
| + /// call. |
| + static final TypeErrorDecoder nullLiteralCallPattern = |
| + extractPattern(provokeCallErrorOnNull()); |
| + |
| + /// Pattern used to recognize a NoSuchMethodError on JavaScript |
| + /// undefined call. |
| + static final TypeErrorDecoder undefinedCallPattern = |
| + extractPattern(provokeCallErrorOn(JS('', 'void 0'))); |
| + |
| + /// Pattern used to recognize a NoSuchMethodError on JavaScript literal |
| + /// undefined call. |
| + static final TypeErrorDecoder undefinedLiteralCallPattern = |
| + extractPattern(provokeCallErrorOnUndefined()); |
| + |
| + /// Pattern used to recognize a NoSuchMethodError on JavaScript null |
| + /// property access. |
| + static final TypeErrorDecoder nullPropertyPattern = |
| + extractPattern(provokePropertyErrorOn(JS('', 'null'))); |
| + |
| + /// Pattern used to recognize a NoSuchMethodError on JavaScript literal null |
| + /// property access. |
| + static final TypeErrorDecoder nullLiteralPropertyPattern = |
| + extractPattern(provokePropertyErrorOnNull()); |
| + |
| + /// Pattern used to recognize a NoSuchMethodError on JavaScript |
| + /// undefined property access. |
| + static final TypeErrorDecoder undefinedPropertyPattern = |
| + extractPattern(provokePropertyErrorOn(JS('', 'void 0'))); |
| + |
| + /// Pattern used to recognize a NoSuchMethodError on JavaScript literal |
| + /// undefined property access. |
| + static final TypeErrorDecoder undefinedLiteralPropertyPattern = |
| + extractPattern(provokePropertyErrorOnUndefined()); |
| + |
| + TypeErrorDecoder(this._arguments, |
| + this._argumentsExpr, |
| + this._expr, |
| + this._method, |
| + this._receiver, |
| + this._pattern); |
| + |
| + /// Returns a JavaScript object literal (map) with at most the |
| + /// following keys: |
| + /// |
| + /// * arguments: The arguments as formatted by the JavaScript |
| + /// engine. No browsers are known to provide this information. |
| + /// |
| + /// * argumentsExpr: The syntax of the arguments (JavaScript source |
| + /// code). No browsers are known to provide this information. |
| + /// |
| + /// * expr: The syntax of the receiver expression (JavaScript source |
| + /// code). Firefox provides this information, for example: "$expr$.$method$ |
| + /// is not a function". |
| + /// |
| + /// * method: The name of the called method (mangled name). At least Firefox |
| + /// and Chrome/V8 provides this information, for example, "Object [object |
| + /// Object] has no method '$method$'". |
| + /// |
| + /// * receiver: The string representation of the receiver. Chrome/V8 |
| + /// used to provide this information (by calling user-defined |
| + /// JavaScript toString on receiver), but it has degenerated into |
| + /// "[object Object]" in recent versions. |
| + matchTypeError(message) { |
| + var match = JS('List|Null', 'new RegExp(#).exec(#)', _pattern, message); |
| + if (match == null) return null; |
| + var result = JS('', '{}'); |
| + if (_arguments != -1) { |
| + JS('', '#.arguments = #[# + 1]', result, match, _arguments); |
| + } |
| + if (_argumentsExpr != -1) { |
| + JS('', '#.argumentsExpr = #[# + 1]', result, match, _argumentsExpr); |
| + } |
| + if (_expr != -1) { |
| + JS('', '#.expr = #[# + 1]', result, match, _expr); |
| + } |
| + if (_method != -1) { |
| + JS('', '#.method = #[# + 1]', result, match, _method); |
| + } |
| + if (_receiver != -1) { |
| + JS('', '#.receiver = #[# + 1]', result, match, _receiver); |
| + } |
| + |
| + return result; |
| + } |
| + |
| + /// Builds a JavaScript Object with a toString method saying |
| + /// r"$receiver$". |
| + static buildJavaScriptObject() { |
| + return JS('', r'{ toString: function() { return "$receiver$"; } }'); |
| + } |
| + |
| + /// Builds a JavaScript Object with a toString method saying |
| + /// r"$receiver$". The property "$method" is defined, but is not a function. |
| + static buildJavaScriptObjectWithNonClosure() { |
| + return JS('', r'{ $method$: null, ' |
| + r'toString: function() { return "$receiver$"; } }'); |
| + } |
| + |
| + /// Extract a pattern from a JavaScript TypeError message. |
| + /// |
| + /// The patterns are extracted by forcing TypeErrors on known |
| + /// objects thus forcing known strings into the error message. The |
| + /// known strings are then replaced with wildcards which in theory |
| + /// makes it possible to recognize the desired information even if |
| + /// the error messages are reworded or translated. |
| + static extractPattern(String message) { |
| + // Some JavaScript implementations (V8 at least) include a |
| + // representation of the receiver in the error message, however, |
| + // this representation is not always [: receiver.toString() :], |
| + // sometimes it is [: Object.prototype.toString(receiver) :], and |
| + // sometimes it is an implementation specific method (but that |
| + // doesn't seem to happen for object literals). So sometimes we |
| + // get the text "[object Object]". The shortest way to get that |
| + // string is using "String({})". |
| + // See: http://code.google.com/p/v8/issues/detail?id=2519. |
| + message = JS('String', r"#.replace(String({}), '$receiver$')", message); |
| + |
| + // Since we want to create a new regular expression from an unknown string, |
| + // we must escape all regular expression syntax. |
| + message = JS('String', r"#.replace(new RegExp(#, 'g'), '\\$&')", |
| + message, ESCAPE_REGEXP); |
| + |
| + // Look for the special pattern \$camelCase\$ (all the $ symbols |
| + // have been escaped already), as we will soon be inserting |
| + // regular expression syntax that we want interpreted by RegExp. |
| + List<String> match = |
| + JS('List|Null', r"#.match(/\\\$[a-zA-Z]+\\\$/g)", message); |
| + if (match == null) match = []; |
| + |
| + // Find the positions within the substring matches of the error message |
| + // components. This will help us extract information later, such as the |
| + // method name. |
| + int arguments = JS('int', '#.indexOf(#)', match, r'\$arguments\$'); |
| + int argumentsExpr = JS('int', '#.indexOf(#)', match, r'\$argumentsExpr\$'); |
| + int expr = JS('int', '#.indexOf(#)', match, r'\$expr\$'); |
| + int method = JS('int', '#.indexOf(#)', match, r'\$method\$'); |
| + int receiver = JS('int', '#.indexOf(#)', match, r'\$receiver\$'); |
| + |
| + // Replace the patterns with a regular expression wildcard. |
| + // Note: in a perfect world, one would use "(.*)", but not in |
| + // JavaScript, "." does not match newlines. |
| + String pattern = JS('String', |
| + r"#.replace('\\$arguments\\$', '((?:x|[^x])*)')" |
| + r".replace('\\$argumentsExpr\\$', '((?:x|[^x])*)')" |
| + r".replace('\\$expr\\$', '((?:x|[^x])*)')" |
| + r".replace('\\$method\\$', '((?:x|[^x])*)')" |
| + r".replace('\\$receiver\\$', '((?:x|[^x])*)')", |
| + message); |
| + |
| + return new TypeErrorDecoder(arguments, |
| + argumentsExpr, |
| + expr, |
| + method, |
| + receiver, |
| + pattern); |
| + } |
| + |
| + /// Provokes a TypeError and returns its message. |
| + /// |
| + /// The error is provoked so all known variable content can be recognized and |
| + /// a pattern can be inferred. |
| + static String provokeCallErrorOn(expression) { |
| + // This function is carefully created to maximize the possibility |
|
ngeoffray
2013/07/22 09:08:56
The following comment applies to all provokeCallEr
ahe
2013/07/22 10:45:27
Done.
|
| + // of decoding the TypeError message and turning it into a general |
| + // pattern. |
| + // |
| + // The idea is to inject something known into something unknown. The |
| + // unknown entity is the error message that the browser provides with a |
| + // TypeError. It is a human readable message, possibly localized in a |
| + // language no dart2js engineer understand. We assume that $name$ would |
| + // never naturally occur in a human readable error message, yet it is easy |
| + // to decode. |
| + // |
| + // For example, evaluate this in V8 version 3.13.7.6: |
| + // |
| + // var $expr$ = null; $expr$.$method$() |
| + // |
| + // The VM throws an instance of TypeError whose message property contains |
| + // "Cannot call method '$method$' of null". We can then reasonably assume |
| + // that if the string contains $method$, that's where the method name will |
| + // be in general. Call this automatically reverse engineering the error |
| + // format string in V8. |
| + // |
| + // So the error message from V8 is turned into this regular expression: |
| + // |
| + // "Cannot call method '(.*)' of null" |
| + // |
| + // Similarly, if we evaluate: |
| + // |
| + // var $expr$ = {toString: function() { return '$receiver$'; }}; |
| + // $expr$.$method$() |
| + // |
| + // We get this message: "Object $receiver$ has no method '$method$'" |
| + // |
| + // Which is turned into this regular expression: |
| + // |
| + // "Object (.*) has no method '(.*)'" |
| + // |
| + // Firefox/jsshell is slightly different, it tries to include the source |
| + // code that caused the exception, so we get this message: "$expr$.$method$ |
| + // is not a function" which is turned into this regular expression: |
| + // |
| + // "(.*)\\.(.*) is not a function" |
| + |
| + var function = JS('', r"""function($expr$) { |
| + var $argumentsExpr$ = '$arguments$' |
| + try { |
| + $expr$.$method$($argumentsExpr$); |
| + } catch (e) { |
| + return e.message; |
| + } |
| +}"""); |
| + return JS('String', '(#)(#)', function, expression); |
| + } |
| + |
| + static String provokeCallErrorOnNull() { |
| + var function = JS('', r"""function() { |
| + var $argumentsExpr$ = '$arguments$' |
| + try { |
| + null.$method$($argumentsExpr$); |
| + } catch (e) { |
| + return e.message; |
| + } |
| +}"""); |
| + return JS('String', '(#)()', function); |
| + } |
| + |
| + static String provokeCallErrorOnUndefined() { |
| + var function = JS('', r"""function() { |
| + var $argumentsExpr$ = '$arguments$' |
| + try { |
| + (void 0).$method$($argumentsExpr$); |
| + } catch (e) { |
| + return e.message; |
| + } |
| +}"""); |
| + return JS('String', '(#)()', function); |
| + } |
| + |
| + /// Similar to [provokeCallErrorOn], but provokes a property access |
| + /// error. |
| + static String provokePropertyErrorOn(expression) { |
| + var function = JS('', r"""function($expr$) { |
| + try { |
| + $expr$.$method$; |
| + } catch (e) { |
| + return e.message; |
| + } |
| +}"""); |
| + return JS('String', '(#)(#)', function, expression); |
| + } |
| + |
| + static String provokePropertyErrorOnNull() { |
| + var function = JS('', r"""function() { |
| + try { |
| + null.$method$; |
| + } catch (e) { |
| + return e.message; |
| + } |
| +}"""); |
| + return JS('String', '(#)()', function); |
| + } |
| + |
| + static String provokePropertyErrorOnUndefined() { |
| + var function = JS('', r"""function() { |
| + try { |
| + (void 0).$method$; |
| + } catch (e) { |
| + return e.message; |
| + } |
| +}"""); |
| + return JS('String', '(#)()', function); |
| + } |
| +} |
| + |
| +class NullError implements NoSuchMethodError { |
| + final String _message; |
| + final String _method; |
| + |
| + NullError(this._message, match) |
| + : _method = match == null ? null : JS('', '#.method', match); |
| + |
| + String toString() { |
| + if (_method == null) return 'NullError: $_message'; |
| + return 'NullError: Cannot call "$_method" on null'; |
| + } |
| +} |
| + |
| +class JsNoSuchMethodError implements NoSuchMethodError { |
| + final String _message; |
| + final String _method; |
| + final String _receiver; |
| + |
| + JsNoSuchMethodError(this._message, match) |
| + : _method = match == null ? null : JS('String|Null', '#.method', match), |
| + _receiver = |
| + match == null ? null : JS('String|Null', '#.receiver', match); |
| + |
| + String toString() { |
| + if (_method == null) return 'NoSuchMethodError: $_message'; |
| + if (_receiver == null) { |
| + return 'NoSuchMethodError: Cannot call "$_method" ($_message)'; |
| + } |
| + return 'NoSuchMethodError: Cannot call "$_method" on "$_receiver" ' |
| + '($_message)'; |
| + } |
| +} |
| + |
| +class UnknownJsTypeError implements Error { |
| + final String _message; |
| + |
| + UnknownJsTypeError(this._message); |
| + |
| + String toString() => _message.isEmpty ? 'Error' : 'Error: $_message'; |
| +} |
| + |
| /** |
| * Called from catch blocks in generated code to extract the Dart |
| * exception from the thrown value. The thrown value may have been |
| @@ -818,54 +1180,81 @@ unwrapException(ex) { |
| // all supported browsers. |
| var message = JS('var', r'#.message', ex); |
| - if (JS('bool', r'# instanceof TypeError', ex)) { |
| - // The type and arguments fields are Chrome specific but they |
| - // allow us to get very detailed information about what kind of |
| - // exception occurred. |
| - var type = JS('var', r'#.type', ex); |
| - var name = JS('var', r'#.arguments ? #.arguments[0] : ""', ex, ex); |
| - if (contains(message, 'JSNull') || |
| - type == 'property_not_function' || |
| - type == 'called_non_callable' || |
| - type == 'non_object_property_call' || |
| - type == 'non_object_property_load') { |
| - return new NoSuchMethodError(null, name, [], {}); |
| - } else if (type == 'undefined_method') { |
| - return new NoSuchMethodError('', name, [], {}); |
| + // Internet Explorer has an error number. This is the most reliable way to |
| + // detect specific errors, so check for this first. |
| + if (JS('bool', '"number" in #', ex) |
| + && JS('bool', 'typeof #.number == "number"', ex)) { |
| + int number = JS('int', '#.number', ex); |
| + |
| + // From http://msdn.microsoft.com/en-us/library/ie/hc53e755(v=vs.94).aspx |
| + // "number" is a 32-bit word. The error code is the low 16 bits, and the |
| + // facility code is the upper 16 bits. |
| + var ieErrorCode = number & 0xffff; |
| + var ieFacilityNumber = (number >> 16) & 0x1fff; |
| + |
| + // http://msdn.microsoft.com/en-us/library/aa264975(v=vs.60).aspx |
| + // http://msdn.microsoft.com/en-us/library/ie/1dk3k160(v=vs.94).aspx |
| + if (ieFacilityNumber == 10) { |
| + switch (ieErrorCode) { |
| + case 438: |
| + return new JsNoSuchMethodError('$message (Error $ieErrorCode)', null); |
| + case 445: |
| + case 5007: |
| + return new NullError('$message (Error $ieErrorCode)', null); |
| + } |
| } |
| + } |
| - var ieErrorCode = JS('int', '#.number & 0xffff', ex); |
| - var ieFacilityNumber = JS('int', '#.number>>16 & 0x1FFF', ex); |
| - // If we cannot use [type] to determine what kind of exception |
| - // we're dealing with we fall back on looking at the exception |
| - // message if it is available and a string. |
| - if (message is String) { |
| - if (message == 'null has no properties' || |
| - message == "'null' is not an object" || |
| - message == "'undefined' is not an object" || |
| - message.endsWith('is null') || |
| - message.endsWith('is undefined') || |
| - message.endsWith('is null or undefined') || |
| - message.endsWith('of undefined') || |
| - message.endsWith('of null')) { |
| - return new NoSuchMethodError(null, message, [], {}); |
| - } else if (contains(message, ' has no method ') || |
| - contains(message, ' is not a function') || |
| - (ieErrorCode == 438 && ieFacilityNumber == 10)) { |
| - // Examples: |
| - // x.foo is not a function |
| - // 'undefined' is not a function (evaluating 'x.foo(1,2,3)') |
| - // Object doesn't support property or method 'foo' which sets the error |
| - // code 438 in IE. |
| - // TODO(kasperl): Compute the right name if possible. |
| - return new NoSuchMethodError('', message, [], {}); |
| - } |
| + if (JS('bool', r'# instanceof TypeError', ex)) { |
| + var match; |
| + // Using JS to give type hints to the compiler to help tree-shaking. |
|
ngeoffray
2013/07/22 09:08:56
Type inference should now be able to know.
ahe
2013/07/22 10:45:27
I'll try that in another CL.
|
| + var nsme = |
| + JS('TypeErrorDecoder', '#', TypeErrorDecoder.noSuchMethodPattern); |
| + var notClosure = |
| + JS('TypeErrorDecoder', '#', TypeErrorDecoder.notClosurePattern); |
| + var nullCall = |
| + JS('TypeErrorDecoder', '#', TypeErrorDecoder.nullCallPattern); |
| + var nullLiteralCall = |
| + JS('TypeErrorDecoder', '#', TypeErrorDecoder.nullLiteralCallPattern); |
| + var undefCall = |
| + JS('TypeErrorDecoder', '#', TypeErrorDecoder.undefinedCallPattern); |
| + var undefLiteralCall = |
| + JS('TypeErrorDecoder', '#', |
| + TypeErrorDecoder.undefinedLiteralCallPattern); |
| + var nullProperty = |
| + JS('TypeErrorDecoder', '#', TypeErrorDecoder.nullPropertyPattern); |
| + var nullLiteralProperty = |
| + JS('TypeErrorDecoder', '#', |
| + TypeErrorDecoder.nullLiteralPropertyPattern); |
| + var undefProperty = |
| + JS('TypeErrorDecoder', '#', TypeErrorDecoder.undefinedPropertyPattern); |
| + var undefLiteralProperty = |
| + JS('TypeErrorDecoder', '#', |
| + TypeErrorDecoder.undefinedLiteralPropertyPattern); |
| + if ((match = nsme.matchTypeError(message)) != null) { |
| + return new JsNoSuchMethodError(message, match); |
| + } else if ((match = notClosure.matchTypeError(message)) != null) { |
| + // notClosure may match "({c:null}).c()" or "({c:1}).c()", so we |
| + // cannot tell if this an attempt to invoke call on null or a |
| + // non-function object. |
| + // But we do know the method name is "call". |
| + JS('', '#.method = "call"', match); |
| + return new JsNoSuchMethodError(message, match); |
| + } else if ((match = nullCall.matchTypeError(message)) != null || |
| + (match = nullLiteralCall.matchTypeError(message)) != null || |
| + (match = undefCall.matchTypeError(message)) != null || |
| + (match = undefLiteralCall.matchTypeError(message)) != null || |
| + (match = nullProperty.matchTypeError(message)) != null || |
| + (match = nullLiteralCall.matchTypeError(message)) != null || |
| + (match = undefProperty.matchTypeError(message)) != null || |
| + (match = undefLiteralProperty.matchTypeError(message)) != null) { |
| + return new NullError(message, match); |
| } |
| // If we cannot determine what kind of error this is, we fall back |
| - // to reporting this as a generic exception. It's probably better |
| - // than nothing. |
| - return new Exception(message is String ? message : ''); |
| + // to reporting this as a generic error. It's probably better than |
| + // nothing. |
| + return new UnknownJsTypeError(message is String ? message : ''); |
| } |
| if (JS('bool', r'# instanceof RangeError', ex)) { |