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/pkgs/string_scanner/lib/string_scanner.dart b/pkgs/string_scanner/lib/string_scanner.dart index 624c090..31ccd62 100644 --- a/pkgs/string_scanner/lib/string_scanner.dart +++ b/pkgs/string_scanner/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/pkgs/string_scanner/test/error_test.dart b/pkgs/string_scanner/test/error_test.dart new file mode 100644 index 0000000..6432197 --- /dev/null +++ b/pkgs/string_scanner/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/pkgs/string_scanner/test/error_format_test.dart b/pkgs/string_scanner/test/expect_error_test.dart similarity index 78% rename from pkgs/string_scanner/test/error_format_test.dart rename to pkgs/string_scanner/test/expect_error_test.dart index 1187344..3596e15 100644 --- a/pkgs/string_scanner/test/error_format_test.dart +++ b/pkgs/string_scanner/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/pkgs/string_scanner/test/utils.dart b/pkgs/string_scanner/test/utils.dart new file mode 100644 index 0000000..eee93d8 --- /dev/null +++ b/pkgs/string_scanner/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; + })); +}