From d983493c1a01074a33a055654c1a506e289fbb57 Mon Sep 17 00:00:00 2001 From: "lrn@google.com" Date: Mon, 14 Jul 2014 07:47:45 +0000 Subject: [PATCH] Add extra information to FormatException. R=floitsch@google.com Review URL: https://codereview.chromium.org//389603002 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@38181 260f80e4-7a28-3924-810f-c04153c831b5 --- .../test/server/parameters_test.dart | 7 +- runtime/lib/convert_patch.dart | 13 +- runtime/lib/double_patch.dart | 2 +- runtime/lib/integers_patch.dart | 25 ++-- sdk/lib/core/date_time.dart | 4 +- sdk/lib/core/exceptions.dart | 113 +++++++++++++++++- sdk/lib/core/uri.dart | 51 ++------ tests/corelib/format_exception_test.dart | 73 +++++++++++ 8 files changed, 217 insertions(+), 71 deletions(-) create mode 100644 tests/corelib/format_exception_test.dart diff --git a/pkg/json_rpc_2/test/server/parameters_test.dart b/pkg/json_rpc_2/test/server/parameters_test.dart index 64fe23260e7..3de03598c93 100644 --- a/pkg/json_rpc_2/test/server/parameters_test.dart +++ b/pkg/json_rpc_2/test/server/parameters_test.dart @@ -229,7 +229,8 @@ void main() { test("[].asDateTime fails for invalid date/times", () { expect(() => parameters['string'].asDateTime, throwsInvalidParams('Parameter "string" for method "foo" must be a ' - 'valid date/time, but was "zap".')); + 'valid date/time, but was "zap".\n' + 'Invalid date format')); }); test("[].asUri returns URI parameters", () { @@ -262,9 +263,7 @@ void main() { expect(() => parameters['invalid-uri'].asUri, throwsInvalidParams('Parameter "invalid-uri" for method "foo" must ' 'be a valid URI, but was "http://[::1".\n' - 'Missing end `]` to match `[` in host at position 7.\n' - 'http://[::1\n' - ' ^')); + 'Missing end `]` to match `[` in host')); }); group("with a nested parameter map", () { diff --git a/runtime/lib/convert_patch.dart b/runtime/lib/convert_patch.dart index 25346bd1ef8..fa684a4dd2c 100644 --- a/runtime/lib/convert_patch.dart +++ b/runtime/lib/convert_patch.dart @@ -33,8 +33,6 @@ abstract class _JsonListener { void beginArray() {} void arrayElement() {} void endArray() {} - /** Called on failure to parse [source]. */ - void fail(String source, int position, String message) {} } /** @@ -568,16 +566,7 @@ class _JsonParser { void fail(int position, [String message]) { if (message == null) message = "Unexpected character"; - listener.fail(source, position, message); - // If the listener didn't throw, do it here. - String slice; - int sliceEnd = position + 20; - if (sliceEnd > source.length) { - slice = "'${source.substring(position)}'"; - } else { - slice = "'${source.substring(position, sliceEnd)}...'"; - } - throw new FormatException("Unexpected character at $position: $slice"); + throw new FormatException(message, source, position); } } diff --git a/runtime/lib/double_patch.dart b/runtime/lib/double_patch.dart index a239a7523ce..d8e0cbd0c84 100644 --- a/runtime/lib/double_patch.dart +++ b/runtime/lib/double_patch.dart @@ -24,7 +24,7 @@ patch class double { [double onError(String str)]) { var result = _parse(str); if (result == null) { - if (onError == null) throw new FormatException(str); + if (onError == null) throw new FormatException("Invalid double", str); return onError(str); } return result; diff --git a/runtime/lib/integers_patch.dart b/runtime/lib/integers_patch.dart index 36c4d8fc812..7ce5ef5dab4 100644 --- a/runtime/lib/integers_patch.dart +++ b/runtime/lib/integers_patch.dart @@ -67,8 +67,8 @@ patch class int { static int _native_parse(String str) native "Integer_parse"; - static int _throwFormatException(String source) { - throw new FormatException(source); + static int _throwFormatException(String source, int position) { + throw new FormatException("", source, position); } /* patch */ static int parse(String source, @@ -78,7 +78,7 @@ patch class int { int result = _parse(source); if (result == null) { if (onError == null) { - throw new FormatException(source); + throw new FormatException("", source); } return onError(source); } @@ -97,12 +97,14 @@ patch class int { if (radix < 2 || radix > 36) { throw new RangeError("Radix $radix not in range 2..36"); } - if (onError == null) { - onError = _throwFormatException; - } // Remove leading and trailing white space. - source = source.trim(); - if (source.isEmpty) return onError(source); + int start = source._firstNonWhitespace(); + int i = start; + if (onError == null) onError = (source) { + throw new FormatException("Invalid radix-$radix number", source, i); + }; + if (start == source.length) return onError(source); + int end = source._lastNonWhitespace() + 1; bool negative = false; int result = 0; @@ -118,12 +120,11 @@ patch class int { 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, NA, NA, NA, NA, NA, // 0x70 ]; - int i = 0; int code = source.codeUnitAt(i); if (code == 0x2d || code == 0x2b) { // Starts with a plus or minus-sign. negative = (code == 0x2d); - if (source.length == 1) return onError(source); - i = 1; + i++; + if (i == end) return onError(source); code = source.codeUnitAt(i); } do { @@ -132,7 +133,7 @@ patch class int { if (digit >= radix) return onError(source); result = result * radix + digit; i++; - if (i == source.length) break; + if (i == end) break; code = source.codeUnitAt(i); } while (true); return negative ? -result : result; diff --git a/sdk/lib/core/date_time.dart b/sdk/lib/core/date_time.dart index 2e93ad1206e..5cc9707a88e 100644 --- a/sdk/lib/core/date_time.dart +++ b/sdk/lib/core/date_time.dart @@ -282,13 +282,13 @@ class DateTime implements Comparable { int millisecondsSinceEpoch = _brokenDownDateToMillisecondsSinceEpoch( years, month, day, hour, minute, second, millisecond, isUtc); if (millisecondsSinceEpoch == null) { - throw new FormatException(formattedString); + throw new FormatException("Time out of range", formattedString); } if (addOneMillisecond) millisecondsSinceEpoch++; return new DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch, isUtc: isUtc); } else { - throw new FormatException(formattedString); + throw new FormatException("Invalid date format", formattedString); } } diff --git a/sdk/lib/core/exceptions.dart b/sdk/lib/core/exceptions.dart index 800a6529ab2..bccc8b2f0b8 100644 --- a/sdk/lib/core/exceptions.dart +++ b/sdk/lib/core/exceptions.dart @@ -44,13 +44,122 @@ class FormatException implements Exception { * A message describing the format error. */ final String message; + /** + * The source that caused the error. + * + * This is usually a [String], but can be other types too. If it is a string, + * parts of it may be included in the [toString] message. + * + * May also be `null` if omitted. + */ + final source; + /** + * The position in source where the error was detected. + * + * May be omitted. If present, [source] should also be present. + */ + final int position; /** * Creates a new FormatException with an optional error [message]. + * + * Optionally also supply the [source] that had the incorrect format, and + * even the [position] in the format where this was detected. */ - const FormatException([this.message = ""]); + const FormatException([this.message = "", this.source, this.position]); - String toString() => "FormatException: $message"; + /** + * Returns a description of the format exception. + * + * The description always contains the [message]. + * If [source] was provided, the description will contain (at least a part of) + * the source. + * If [position] is also provided, the part of the source included will + * contain that position, and the position will be marked. + * + * If the source contains a line break before position, only the line + * containing position will be included, and its line number will also be + * part of the description. Line and character offsets are 1-based. + */ + String toString() { + String report = "FormatException"; + if (message != null && message.isNotEmpty) { + report = "$report: $message"; + } + int position = this.position; + if (source is! String) { + if (position != null) { + report += " (at position $position)"; + } + return report; + } + if (position != null && (position < 0 || position > source.length)) { + position = null; + } + // Source is string and position is null or valid. + if (position == null) { + String source = this.source; + if (source.length > 78) { + source = source.substring(0, 75) + "..."; + } + return "$report\n$source"; + } + int lineNum = 1; + int lineStart = 0; + bool lastWasCR; + for (int i = 0; i < position; i++) { + int char = source.codeUnitAt(i); + if (char == 0x0a) { + if (lineStart != i || !lastWasCR) { + lineNum++; + } + lineStart = i + 1; + lastWasCR = false; + } else if (char == 0x0d) { + lineNum++; + lineStart = i + 1; + lastWasCR = true; + } + } + if (lineNum > 1) { + report += " (at line $lineNum, character ${position - lineStart + 1})\n"; + } else { + report += " (at character ${position + 1})\n"; + } + int lineEnd = source.length; + for (int i = position; i < source.length; i++) { + int char = source.codeUnitAt(i); + if (char == 0x0a || char == 0x0d) { + lineEnd = i; + break; + } + } + int length = lineEnd - lineStart; + int start = lineStart; + int end = lineEnd; + String prefix = ""; + String postfix = ""; + if (length > 78) { + // Can't show entire line. Try to anchor at the nearest end, if + // one is within reach. + int index = position - lineStart; + if (index < 75) { + end = start + 75; + postfix = "..."; + } else if (end - position < 75) { + start = end - 75; + prefix = "..."; + } else { + // Neither end is near, just pick an area around the position. + start = position - 36; + end = position + 36; + prefix = postfix = "..."; + } + } + String slice = source.substring(start, end); + int markOffset = position - start + prefix.length; + return "$report$prefix$slice$postfix\n${" " * markOffset}^\n"; + } } class IntegerDivisionByZeroException implements Exception { diff --git a/sdk/lib/core/uri.dart b/sdk/lib/core/uri.dart index 8529de9347e..1e26d01f5a2 100644 --- a/sdk/lib/core/uri.dart +++ b/sdk/lib/core/uri.dart @@ -396,34 +396,7 @@ class Uri { // Report a parse failure. static void _fail(String uri, int index, String message) { - // TODO(lrn): Consider adding this to FormatException. - if (index == uri.length) { - message += " at end of input."; - } else { - message += " at position $index.\n"; - // Pick a slice of uri containing index and, if - // necessary, truncate the ends to ensure the entire - // slice fits on one line. - int min = 0; - int max = uri.length; - String pre = ""; - String post = ""; - if (uri.length > 78) { - min = index - 10; - if (min < 0) min = 0; - int max = min + 72; - if (max > uri.length) { - max = uri.length; - min = max - 72; - } - if (min != 0) pre = "..."; - if (max != uri.length) post = "..."; - } - // Combine message, slice and a caret pointing to the error index. - message = "$message$pre${uri.substring(min, max)}$post\n" - "${' ' * (pre.length + index - min)}^"; - } - throw new FormatException(message); + throw new FormatException(message, uri, index); } /// Internal non-verifying constructor. Only call with validated arguments. @@ -608,13 +581,15 @@ class Uri { if (authority.codeUnitAt(hostEnd) == _RIGHT_BRACKET) break; } if (hostEnd == authority.length) { - throw new FormatException("Invalid IPv6 host entry."); + throw new FormatException("Invalid IPv6 host entry.", + authority, hostStart); } parseIPv6Address(authority, hostStart + 1, hostEnd); hostEnd++; // Skip the closing bracket. if (hostEnd != authority.length && authority.codeUnitAt(hostEnd) != _COLON) { - throw new FormatException("Invalid end of authority"); + throw new FormatException("Invalid end of authority", + authority, hostEnd); } } // Split host and port. @@ -1811,16 +1786,16 @@ class Uri { // - One (and only one) wildcard (`::`) may be present, representing a fill // of 0's. The IPv6 `::` is thus 16 bytes of `0`. // - The last two parts may be replaced by an IPv4 address. - void error(String msg) { - throw new FormatException('Illegal IPv6 address, $msg'); + void error(String msg, [position]) { + throw new FormatException('Illegal IPv6 address, $msg', host, position); } int parseHex(int start, int end) { if (end - start > 4) { - error('an IPv6 part can only contain a maximum of 4 hex digits'); + error('an IPv6 part can only contain a maximum of 4 hex digits', start); } int value = int.parse(host.substring(start, end), radix: 16); if (value < 0 || value > (1 << 16) - 1) { - error('each part must be in the range of `0x0..0xFFFF`'); + error('each part must be in the range of `0x0..0xFFFF`', start); } return value; } @@ -1835,14 +1810,14 @@ class Uri { // If we see a `:` in the beginning, expect wildcard. i++; if (host.codeUnitAt(i) != _COLON) { - error('invalid start colon.'); + error('invalid start colon.', i); } partStart = i; } if (i == partStart) { // Wildcard. We only allow one. if (wildcardSeen) { - error('only one wildcard `::` is allowed'); + error('only one wildcard `::` is allowed', i); } wildcardSeen = true; parts.add(-1); @@ -1857,7 +1832,7 @@ class Uri { bool atEnd = (partStart == end); bool isLastWildcard = (parts.last == -1); if (atEnd && !isLastWildcard) { - error('expected a part after last `:`'); + error('expected a part after last `:`', end); } if (!atEnd) { try { @@ -1869,7 +1844,7 @@ class Uri { parts.add(last[0] << 8 | last[1]); parts.add(last[2] << 8 | last[3]); } catch (e) { - error('invalid end of IPv6 address.'); + error('invalid end of IPv6 address.', partStart); } } } diff --git a/tests/corelib/format_exception_test.dart b/tests/corelib/format_exception_test.dart new file mode 100644 index 00000000000..f17bcdc74ea --- /dev/null +++ b/tests/corelib/format_exception_test.dart @@ -0,0 +1,73 @@ +// Copyright (c) 2011, 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 format_exception_test; +import "package:expect/expect.dart"; + +test(exn, message, source, position, toString) { + Expect.equals(message, exn.message); + Expect.equals(source, exn.source); + Expect.equals(position, exn.position); + Expect.equals(toString, exn.toString()); +} + +main() { + var e; + e = new FormatException(); + test(e, "", null, null, "FormatException"); + e = new FormatException(""); + test(e, "", null, null, "FormatException"); + e = new FormatException(null); + test(e, null, null, null, "FormatException"); + + e = new FormatException("message"); + test(e, "message", null, null, "FormatException: message"); + + e = new FormatException("message", "source"); + test(e, "message", "source", null, "FormatException: message\nsource"); + + e = new FormatException("message", "source" * 25); + test(e, "message", "source" * 25, null, "FormatException: message\n" + + "source" * 12 + "sou..."); + e = new FormatException("message", "source" * 25); + test(e, "message", "source" * 25, null, "FormatException: message\n" + + "source" * 12 + "sou..."); + e = new FormatException("message", "s1\nsource\ns2"); + test(e, "message", "s1\nsource\ns2", null, "FormatException: message\n" + + "s1\nsource\ns2"); + + var o = new Object(); + e = new FormatException("message", o, 10); + test(e, "message", o, 10, "FormatException: message (at position 10)"); + + e = new FormatException("message", "source", 3); + test(e, "message", "source", 3, + "FormatException: message (at character 4)\nsource\n ^\n"); + + e = new FormatException("message", "s1\nsource\ns2", 6); + test(e, "message", "s1\nsource\ns2", 6, + "FormatException: message (at line 2, character 4)\nsource\n ^\n"); + + var longline = "watermelon cantaloupe " * 8 + "watermelon"; // Length > 160. + var longsource = (longline + "\n") * 25; + var line10 = (longline.length + 1) * 9; + e = new FormatException("message", longsource, line10); + test(e, "message", longsource, line10, + "FormatException: message (at line 10, character 1)\n" + "${longline.substring(0, 75)}...\n^\n"); + + e = new FormatException("message", longsource, line10 - 1); + test(e, "message", longsource, line10 - 1, + "FormatException: message (at line 9, " + "character ${longline.length + 1})\n" + "...${longline.substring(longline.length - 75)}\n" + "${' ' * 78}^\n"); + + var half = longline.length ~/ 2; + e = new FormatException("message", longsource, line10 + half); + test(e, "message", longsource, line10 + half, + "FormatException: message (at line 10, character ${half + 1})\n" + "...${longline.substring(half - 36, half + 36)}...\n" + "${' ' * 39}^\n"); +}