Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(7)

Unified Diff: dart/sdk/lib/_internal/lib/js_helper.dart

Issue 11973018: Improve decoding of JS TypeError. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge
Patch Set: Update status files Created 7 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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)) {

Powered by Google App Engine
This is Rietveld 408576698