Convert shelf to use the string_scanner package.
This also adds support for [StringScanner.error], which produces a
nicely-formatted scanning error.
R=kevmoo@google.com
Review URL: https://codereview.chromium.org//222843003
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/string_scanner@34669 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/lib/string_scanner.dart b/lib/string_scanner.dart
index 624c090..31ccd62 100644
--- a/lib/string_scanner.dart
+++ b/lib/string_scanner.dart
@@ -5,6 +5,8 @@
/// A library for parsing strings using a sequence of patterns.
library string_scanner;
+import 'dart:math' as math;
+
// TODO(nweiz): Add some integration between this and source maps.
/// A class that scans through a string using [Pattern]s.
class StringScanner {
@@ -89,10 +91,38 @@
return _lastMatch != null;
}
- // TODO(nweiz): Make this handle long lines more gracefully.
- /// Throws a [FormatException] describing that [name] is expected at the
- /// current position in the string.
- void _fail(String name) {
+ /// Throws a [FormatException] with [message] as well as a detailed
+ /// description of the location of the error in the string.
+ ///
+ /// [match] is the match information for the span of the string with which the
+ /// error is associated. This should be a match returned by this scanner's
+ /// [lastMatch] property. By default, the error is associated with the last
+ /// match.
+ ///
+ /// If [position] and/or [length] are passed, they are used as the error span
+ /// instead. If only [length] is passed, [position] defaults to the current
+ /// position; if only [position] is passed, [length] defaults to 1.
+ ///
+ /// It's an error to pass [match] at the same time as [position] or [length].
+ void error(String message, {Match match, int position, int length}) {
+ if (match != null && (position != null || length != null)) {
+ throw new ArgumentError("Can't pass both match and position/length.");
+ }
+
+ if (position != null && position < 0) {
+ throw new RangeError("position must be greater than or equal to 0.");
+ }
+
+ if (length != null && length < 1) {
+ throw new RangeError("length must be greater than or equal to 0.");
+ }
+
+ if (match == null && position == null && length == null) match = lastMatch;
+ if (position == null) {
+ position = match == null ? this.position : match.start;
+ }
+ if (length == null) length = match == null ? 1 : match.end - match.start;
+
var newlines = "\n".allMatches(string.substring(0, position)).toList();
var line = newlines.length + 1;
var column;
@@ -104,10 +134,29 @@
column = position - newlines.last.end + 1;
lastLine = string.substring(newlines.last.end, position);
}
- lastLine += rest.replaceFirst(new RegExp(r"\n.*"), '');
+
+ var remaining = string.substring(position);
+ var nextNewline = remaining.indexOf("\n");
+ if (nextNewline == -1) {
+ lastLine += remaining;
+ } else {
+ length = math.min(length, nextNewline);
+ lastLine += remaining.substring(0, nextNewline);
+ }
+
+ var spaces = new List.filled(column - 1, ' ').join();
+ var underline = new List.filled(length, '^').join();
+
throw new FormatException(
- "Expected $name on line $line, column $column.\n"
+ "Error on line $line, column $column: $message\n"
"$lastLine\n"
- "${new List.filled(column - 1, ' ').join()}^");
+ "$spaces$underline");
+ }
+
+ // TODO(nweiz): Make this handle long lines more gracefully.
+ /// Throws a [FormatException] describing that [name] is expected at the
+ /// current position in the string.
+ void _fail(String name) {
+ error("expected $name.", position: this.position, length: 1);
}
}
diff --git a/test/error_test.dart b/test/error_test.dart
new file mode 100644
index 0000000..6432197
--- /dev/null
+++ b/test/error_test.dart
@@ -0,0 +1,168 @@
+// Copyright (c) 2014, 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 string_scanner.error_test;
+
+import 'package:string_scanner/string_scanner.dart';
+import 'package:unittest/unittest.dart';
+
+import 'utils.dart';
+
+void main() {
+ test('defaults to the last match', () {
+ var scanner = new StringScanner('foo bar baz');
+ scanner.expect('foo ');
+ scanner.expect('bar');
+ expect(() => scanner.error('oh no!'), throwsFormattedError('''
+Error on line 1, column 5: oh no!
+foo bar baz
+ ^^^'''));
+ });
+
+ group("with match", () {
+ test('supports an earlier match', () {
+ var scanner = new StringScanner('foo bar baz');
+ scanner.expect('foo ');
+ var match = scanner.lastMatch;
+ scanner.expect('bar');
+ expect(() => scanner.error('oh no!', match: match),
+ throwsFormattedError('''
+Error on line 1, column 1: oh no!
+foo bar baz
+^^^^'''));
+ });
+
+ test('supports a match on a previous line', () {
+ var scanner = new StringScanner('foo bar baz\ndo re mi\nearth fire water');
+ scanner.expect('foo bar baz\ndo ');
+ scanner.expect('re');
+ var match = scanner.lastMatch;
+ scanner.expect(' mi\nearth ');
+ expect(() => scanner.error('oh no!', match: match),
+ throwsFormattedError('''
+Error on line 2, column 4: oh no!
+do re mi
+ ^^'''));
+ });
+
+ test('supports a multiline match', () {
+ var scanner = new StringScanner('foo bar baz\ndo re mi\nearth fire water');
+ scanner.expect('foo bar ');
+ scanner.expect('baz\ndo');
+ var match = scanner.lastMatch;
+ scanner.expect(' re mi');
+ expect(() => scanner.error('oh no!', match: match),
+ throwsFormattedError('''
+Error on line 1, column 9: oh no!
+foo bar baz
+ ^^^'''));
+ });
+
+ test('supports a match after position', () {
+ var scanner = new StringScanner('foo bar baz');
+ scanner.expect('foo ');
+ scanner.expect('bar');
+ var match = scanner.lastMatch;
+ scanner.position = 0;
+ expect(() => scanner.error('oh no!', match: match),
+ throwsFormattedError('''
+Error on line 1, column 5: oh no!
+foo bar baz
+ ^^^'''));
+ });
+ });
+
+ group("with position and/or length", () {
+ test('defaults to length 1', () {
+ var scanner = new StringScanner('foo bar baz');
+ scanner.expect('foo ');
+ expect(() => scanner.error('oh no!', position: 1),
+ throwsFormattedError('''
+Error on line 1, column 2: oh no!
+foo bar baz
+ ^'''));
+ });
+
+ test('defaults to the current position', () {
+ var scanner = new StringScanner('foo bar baz');
+ scanner.expect('foo ');
+ expect(() => scanner.error('oh no!', length: 3),
+ throwsFormattedError('''
+Error on line 1, column 5: oh no!
+foo bar baz
+ ^^^'''));
+ });
+
+ test('supports an earlier position', () {
+ var scanner = new StringScanner('foo bar baz');
+ scanner.expect('foo ');
+ expect(() => scanner.error('oh no!', position: 1, length: 2),
+ throwsFormattedError('''
+Error on line 1, column 2: oh no!
+foo bar baz
+ ^^'''));
+ });
+
+ test('supports a position on a previous line', () {
+ var scanner = new StringScanner('foo bar baz\ndo re mi\nearth fire water');
+ scanner.expect('foo bar baz\ndo re mi\nearth');
+ expect(() => scanner.error('oh no!', position: 15, length: 2),
+ throwsFormattedError('''
+Error on line 2, column 4: oh no!
+do re mi
+ ^^'''));
+ });
+
+ test('supports a multiline length', () {
+ var scanner = new StringScanner('foo bar baz\ndo re mi\nearth fire water');
+ scanner.expect('foo bar baz\ndo re mi\nearth');
+ expect(() => scanner.error('oh no!', position: 8, length: 8),
+ throwsFormattedError('''
+Error on line 1, column 9: oh no!
+foo bar baz
+ ^^^'''));
+ });
+
+ test('supports a position after the current one', () {
+ var scanner = new StringScanner('foo bar baz');
+ expect(() => scanner.error('oh no!', position: 4, length: 3),
+ throwsFormattedError('''
+Error on line 1, column 5: oh no!
+foo bar baz
+ ^^^'''));
+ });
+ });
+
+ group("argument errors", () {
+ var scanner;
+ setUp(() {
+ scanner = new StringScanner('foo bar baz');
+ scanner.scan('foo');
+ });
+
+ test("if match is passed with position", () {
+ expect(
+ () => scanner.error("oh no!", match: scanner.lastMatch, position: 1),
+ throwsArgumentError);
+ });
+
+ test("if match is passed with length", () {
+ expect(
+ () => scanner.error("oh no!", match: scanner.lastMatch, length: 1),
+ throwsArgumentError);
+ });
+
+ test("if position is negative", () {
+ expect(() => scanner.error("oh no!", position: -1), throwsArgumentError);
+ });
+
+ test("if position is outside the string", () {
+ expect(() => scanner.error("oh no!", position: 100), throwsArgumentError);
+ });
+
+ test("if length is zero", () {
+ expect(() => scanner.error("oh no!", length: 0), throwsArgumentError);
+ });
+ });
+}
diff --git a/test/error_format_test.dart b/test/expect_error_test.dart
similarity index 78%
rename from test/error_format_test.dart
rename to test/expect_error_test.dart
index 1187344..3596e15 100644
--- a/test/error_format_test.dart
+++ b/test/expect_error_test.dart
@@ -2,17 +2,19 @@
// 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 string_scanner.error_format_test;
+library string_scanner.expect_error_test;
import 'package:string_scanner/string_scanner.dart';
import 'package:unittest/unittest.dart';
+import 'utils.dart';
+
void main() {
test('points to the first unconsumed character', () {
var scanner = new StringScanner('foo bar baz');
scanner.expect('foo ');
expect(() => scanner.expect('foo'), throwsFormattedError('''
-Expected "foo" on line 1, column 5.
+Error on line 1, column 5: expected "foo".
foo bar baz
^'''));
});
@@ -21,7 +23,7 @@
var scanner = new StringScanner('foo bar baz\ndo re mi\nearth fire water');
scanner.expect('foo bar baz\ndo ');
expect(() => scanner.expect('foo'), throwsFormattedError('''
-Expected "foo" on line 2, column 4.
+Error on line 2, column 4: expected "foo".
do re mi
^'''));
});
@@ -29,7 +31,7 @@
test('handles the beginning of the string correctly', () {
var scanner = new StringScanner('foo bar baz');
expect(() => scanner.expect('zap'), throwsFormattedError('''
-Expected "zap" on line 1, column 1.
+Error on line 1, column 1: expected "zap".
foo bar baz
^'''));
});
@@ -38,14 +40,14 @@
var scanner = new StringScanner('foo bar baz');
scanner.expect('foo bar baz');
expect(() => scanner.expect('bang'), throwsFormattedError('''
-Expected "bang" on line 1, column 12.
+Error on line 1, column 12: expected "bang".
foo bar baz
^'''));
});
test('handles an empty string correctly', () {
expect(() => new StringScanner('').expect('foo'), throwsFormattedError('''
-Expected "foo" on line 1, column 1.
+Error on line 1, column 1: expected "foo".
^'''));
});
@@ -54,7 +56,7 @@
test("uses the provided name", () {
expect(() => new StringScanner('').expect('foo bar', name: 'zap'),
throwsFormattedError('''
-Expected zap on line 1, column 1.
+Error on line 1, column 1: expected zap.
^'''));
});
@@ -62,7 +64,7 @@
test("escapes string quotes", () {
expect(() => new StringScanner('').expect('foo"bar'),
throwsFormattedError('''
-Expected "foo\\"bar" on line 1, column 1.
+Error on line 1, column 1: expected "foo\\"bar".
^'''));
});
@@ -70,7 +72,7 @@
test("escapes string backslashes", () {
expect(() => new StringScanner('').expect('foo\\bar'),
throwsFormattedError('''
-Expected "foo\\\\bar" on line 1, column 1.
+Error on line 1, column 1: expected "foo\\\\bar".
^'''));
});
@@ -78,7 +80,7 @@
test("prints PERL-style regexps", () {
expect(() => new StringScanner('').expect(new RegExp(r'foo')),
throwsFormattedError('''
-Expected /foo/ on line 1, column 1.
+Error on line 1, column 1: expected /foo/.
^'''));
});
@@ -86,7 +88,7 @@
test("escape regexp forward slashes", () {
expect(() => new StringScanner('').expect(new RegExp(r'foo/bar')),
throwsFormattedError('''
-Expected /foo\\/bar/ on line 1, column 1.
+Error on line 1, column 1: expected /foo\\/bar/.
^'''));
});
@@ -94,17 +96,9 @@
test("does not escape regexp backslashes", () {
expect(() => new StringScanner('').expect(new RegExp(r'foo\bar')),
throwsFormattedError('''
-Expected /foo\\bar/ on line 1, column 1.
+Error on line 1, column 1: expected /foo\\bar/.
^'''));
});
});
}
-
-Matcher throwsFormattedError(String format) {
- return throwsA(predicate((error) {
- expect(error, isFormatException);
- expect(error.message, equals(format));
- return true;
- }));
-}
diff --git a/test/utils.dart b/test/utils.dart
new file mode 100644
index 0000000..eee93d8
--- /dev/null
+++ b/test/utils.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2014, 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 string_scanner.test.utils;
+
+import 'package:unittest/unittest.dart';
+
+/// Returns a matcher that asserts that a closure throws a [FormatException]
+/// with the given [message].
+Matcher throwsFormattedError(String message) {
+ return throwsA(predicate((error) {
+ expect(error, isFormatException);
+ expect(error.message, equals(message));
+ return true;
+ }));
+}