Add LineScanner and SpanScanner classes to string_scanner. R=rnystrom@google.com Review URL: https://codereview.chromium.org//299973002 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/string_scanner@36815 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/pkgs/string_scanner/CHANGELOG.md b/pkgs/string_scanner/CHANGELOG.md new file mode 100644 index 0000000..2c41c9d --- /dev/null +++ b/pkgs/string_scanner/CHANGELOG.md
@@ -0,0 +1,19 @@ +## 0.0.2 + +* `new StringScanner()` now takes an optional `sourceUrl` argument that provides + the URL of the source file. This is used for error reporting. + +* Add `StringScanner.readChar()` and `StringScanner.peekChar()` methods for + doing character-by-character scanning. + +* Scanners now throw `StringScannerException`s which provide more detailed + access to information about the errors that were thrown and can provide + terminal-colored messages. + +* Add a `LineScanner` subclass of `StringScanner` that automatically tracks line + and column information of the text being scanned. + +* Add a `SpanScanner` subclass of `LineScanner` that exposes matched ranges as + [source map][] `Span` objects. + +[source_map]: http://pub.dartlang.org/packages/source_maps
diff --git a/pkgs/string_scanner/lib/src/exception.dart b/pkgs/string_scanner/lib/src/exception.dart new file mode 100644 index 0000000..c398687 --- /dev/null +++ b/pkgs/string_scanner/lib/src/exception.dart
@@ -0,0 +1,37 @@ +// 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.exception; + +import 'package:source_maps/source_maps.dart'; + +/// An exception thrown by a [StringScanner] that failed to parse a string. +class StringScannerException implements FormatException { + /// The error message. + final String message; + + /// The source string being parsed. + final String string; + + /// The URL of the source file being parsed. + /// + /// This may be `null`, indicating that the source URL is unknown. + final Uri sourceUrl; + + /// The span within [string] that caused the exception. + final Span span; + + StringScannerException(this.message, this.string, this.sourceUrl, this.span); + + /// Returns a detailed description of this exception. + /// + /// If [useColors] is true, the section of the source that caused the + /// exception will be colored using ANSI color codes. By default, it's colored + /// red, but a different ANSI code may passed via [color]. + String toString({bool useColors: false, String color}) { + return "Error on " + span.getLocationMessage( + message, useColors: useColors, color: color); + } +} +
diff --git a/pkgs/string_scanner/lib/src/line_scanner.dart b/pkgs/string_scanner/lib/src/line_scanner.dart new file mode 100644 index 0000000..4e12173 --- /dev/null +++ b/pkgs/string_scanner/lib/src/line_scanner.dart
@@ -0,0 +1,108 @@ +// 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.line_scanner; + +import 'string_scanner.dart'; + +/// A subclass of [StringScanner] that tracks line and column information. +class LineScanner extends StringScanner { + /// The scanner's current (zero-based) line number. + int get line => _line; + int _line = 0; + + /// The scanner's current (zero-based) column number. + int get column => _column; + int _column = 0; + + /// The scanner's state, including line and column information. + /// + /// This can be used to efficiently save and restore the state of the scanner + /// when backtracking. A given [LineScannerState] is only valid for the + /// [LineScanner] that created it. + LineScannerState get state => + new LineScannerState._(this, position, line, column); + + set state(LineScannerState state) { + if (!identical(state._scanner, this)) { + throw new ArgumentError("The given LineScannerState was not returned by " + "this LineScanner."); + } + + super.position = state.position; + _line = state.line; + _column = state.column; + } + + set position(int newPosition) { + var oldPosition = position; + super.position = newPosition; + + if (newPosition > oldPosition) { + var newlines = "\n".allMatches(string.substring(oldPosition, newPosition)) + .toList(); + _line += newlines.length; + if (newlines.isEmpty) { + _column += newPosition - oldPosition; + } else { + _column = newPosition - newlines.last.end; + } + } else { + var newlines = "\n".allMatches(string.substring(newPosition, oldPosition)) + .toList(); + _line -= newlines.length; + if (newlines.isEmpty) { + _column -= oldPosition - newPosition; + } else { + _column = newPosition - string.lastIndexOf("\n", newPosition) - 1; + } + } + } + + LineScanner(String string, {sourceUrl, int position}) + : super(string, sourceUrl: sourceUrl, position: position); + + int readChar() { + var char = super.readChar(); + if (char == 0xA) { + _line += 1; + _column = 0; + } else { + _column += 1; + } + return char; + } + + bool scan(Pattern pattern) { + var oldPosition = position; + if (!super.scan(pattern)) return false; + + var newlines = "\n".allMatches(lastMatch[0]).toList(); + _line += newlines.length; + if (newlines.isEmpty) { + _column += lastMatch[0].length; + } else { + _column = lastMatch[0].length - newlines.last.end; + } + + return true; + } +} + +/// A class representing the state of a [LineScanner]. +class LineScannerState { + /// The [LineScanner] that created this. + final LineScanner _scanner; + + /// The position of the scanner in this state. + final int position; + + /// The zero-based line number of the scanner in this state. + final int line; + + /// The zero-based column number of the scanner in this state. + final int column; + + LineScannerState._(this._scanner, this.position, this.line, this.column); +}
diff --git a/pkgs/string_scanner/lib/src/span_scanner.dart b/pkgs/string_scanner/lib/src/span_scanner.dart new file mode 100644 index 0000000..9060e62 --- /dev/null +++ b/pkgs/string_scanner/lib/src/span_scanner.dart
@@ -0,0 +1,95 @@ +// 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.span_scanner; + +import 'package:source_maps/source_maps.dart'; + +import 'exception.dart'; +import 'line_scanner.dart'; +import 'string_scanner.dart'; +import 'utils.dart'; + +/// A subclass of [LineScanner] that exposes matched ranges as source map +/// [Span]s. +class SpanScanner extends StringScanner implements LineScanner { + /// The source of the scanner. + /// + /// This caches line break information and is used to generate [Span]s. + final SourceFile _sourceFile; + + int get line => _sourceFile.getLine(position); + int get column => _sourceFile.getColumn(line, position); + + LineScannerState get state => new _SpanScannerState(this, position); + + set state(LineScannerState state) { + if (state is! _SpanScannerState || + !identical((state as _SpanScannerState)._scanner, this)) { + throw new ArgumentError("The given LineScannerState was not returned by " + "this LineScanner."); + } + + this.position = state.position; + } + + /// The [Span] for [lastMatch]. + /// + /// This is the span for the entire match. There's no way to get spans for + /// subgroups since [Match] exposes no information about their positions. + Span get lastSpan => _lastSpan; + Span _lastSpan; + + /// Returns an empty span at the current location. + Span get emptySpan => _sourceFile.span(position); + + /// Creates a new [SpanScanner] that starts scanning from [position]. + /// + /// [sourceUrl] is used as [Location.sourceUrl] for the returned [Span]s as + /// well as for error reporting. + SpanScanner(String string, sourceUrl, {int position}) + : _sourceFile = new SourceFile.text( + sourceUrl is Uri ? sourceUrl.toString() : sourceUrl, string), + super(string, sourceUrl: sourceUrl, position: position); + + /// Creates a [Span] representing the source range between [startState] and + /// the current position. + Span spanFrom(LineScannerState startState) => + _sourceFile.span(startState.position, position); + + bool matches(Pattern pattern) { + if (!super.matches(pattern)) { + _lastSpan = null; + return false; + } + + _lastSpan = _sourceFile.span(position, lastMatch.end); + return true; + } + + void error(String message, {Match match, int position, int length}) { + validateErrorArgs(string, match, position, length); + + 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 span = _sourceFile.span(position, position + length); + throw new StringScannerException(message, string, sourceUrl, span); + } +} + +/// A class representing the state of a [SpanScanner]. +class _SpanScannerState implements LineScannerState { + /// The [SpanScanner] that created this. + final SpanScanner _scanner; + + final int position; + int get line => _scanner._sourceFile.getLine(position); + int get column => _scanner._sourceFile.getColumn(line, position); + + _SpanScannerState(this._scanner, this.position); +}
diff --git a/pkgs/string_scanner/lib/src/string_scanner.dart b/pkgs/string_scanner/lib/src/string_scanner.dart new file mode 100644 index 0000000..c9e7459 --- /dev/null +++ b/pkgs/string_scanner/lib/src/string_scanner.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.string_scanner; + +import 'package:source_maps/source_maps.dart'; + +import 'exception.dart'; +import 'utils.dart'; + +/// When compiled to JS, forward slashes are always escaped in [RegExp.pattern]. +/// +/// See issue 17998. +final _slashAutoEscape = new RegExp("/").pattern == "\\/"; + +/// A class that scans through a string using [Pattern]s. +class StringScanner { + /// The URL of the source of the string being scanned. + /// + /// This is used for error reporting. It may be `null`, indicating that the + /// source URL is unknown or unavailable. + final Uri sourceUrl; + + /// The string being scanned through. + final String string; + + /// The current position of the scanner in the string, in characters. + int get position => _position; + set position(int position) { + if (position < 0 || position > string.length) { + throw new ArgumentError("Invalid position $position"); + } + + _position = position; + } + int _position = 0; + + /// The data about the previous match made by the scanner. + /// + /// If the last match failed, this will be `null`. + Match get lastMatch => _lastMatch; + Match _lastMatch; + + /// The portion of the string that hasn't yet been scanned. + String get rest => string.substring(position); + + /// Whether the scanner has completely consumed [string]. + bool get isDone => position == string.length; + + /// Creates a new [StringScanner] that starts scanning from [position]. + /// + /// [position] defaults to 0, the beginning of the string. [sourceUrl] is the + /// URL of the source of the string being scanned, if available. It can be + /// either a [String] or a [Uri]. + StringScanner(this.string, {sourceUrl, int position}) + : sourceUrl = sourceUrl is String ? Uri.parse(sourceUrl) : sourceUrl { + if (position != null) this.position = position; + } + + /// Consumes a single character and returns its character code. + /// + /// This throws a [FormatException] if the string has been fully consumed. It + /// doesn't affect [lastMatch]. + int readChar() { + if (isDone) _fail("more input"); + return string.codeUnitAt(_position++); + } + + /// Returns the character code of the character [offset] away from [position]. + /// + /// [offset] defaults to zero, and may be negative to inspect already-consumed + /// characters. + /// + /// This returns `null` if [offset] points outside the string. It doesn't + /// affect [lastMatch]. + int peekChar([int offset]) { + if (offset == null) offset = 0; + var index = position + offset; + if (index < 0 || index >= string.length) return null; + return string.codeUnitAt(index); + } + + /// If [pattern] matches at the current position of the string, scans forward + /// until the end of the match. + /// + /// Returns whether or not [pattern] matched. + bool scan(Pattern pattern) { + var success = matches(pattern); + if (success) _position = _lastMatch.end; + return success; + } + + /// If [pattern] matches at the current position of the string, scans forward + /// until the end of the match. + /// + /// If [pattern] did not match, throws a [FormatException] describing the + /// position of the failure. [name] is used in this error as the expected name + /// of the pattern being matched; if it's `null`, the pattern itself is used + /// instead. + void expect(Pattern pattern, {String name}) { + if (scan(pattern)) return; + + if (name == null) { + if (pattern is RegExp) { + var source = pattern.pattern; + if (!_slashAutoEscape) source = source.replaceAll("/", "\\/"); + name = "/$source/"; + } else { + name = pattern.toString() + .replaceAll("\\", "\\\\").replaceAll('"', '\\"'); + name = '"$name"'; + } + } + _fail(name); + } + + /// If the string has not been fully consumed, this throws a + /// [FormatException]. + void expectDone() { + if (isDone) return; + _fail("no more input"); + } + + /// Returns whether or not [pattern] matches at the current position of the + /// string. + /// + /// This doesn't move the scan pointer forward. + bool matches(Pattern pattern) { + _lastMatch = pattern.matchAsPrefix(string, position); + return _lastMatch != null; + } + + /// 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}) { + validateErrorArgs(string, match, position, length); + + 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 url = sourceUrl == null ? null : sourceUrl.toString(); + var sourceFile = new SourceFile.text(url, string); + var span = sourceFile.span(position, position + length); + throw new StringScannerException(message, string, sourceUrl, span); + } + + // 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: 0); + } +}
diff --git a/pkgs/string_scanner/lib/src/utils.dart b/pkgs/string_scanner/lib/src/utils.dart new file mode 100644 index 0000000..e556236 --- /dev/null +++ b/pkgs/string_scanner/lib/src/utils.dart
@@ -0,0 +1,30 @@ +// 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.utils; + +/// Validates the arguments passed to [StringScanner.error]. +void validateErrorArgs(String string, 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) { + if (position < 0) { + throw new RangeError("position must be greater than or equal to 0."); + } else if (position > string.length) { + throw new RangeError("position must be less than or equal to the " + "string length."); + } + } + + if (length != null && length < 0) { + throw new RangeError("length must be greater than or equal to 0."); + } + + if (position != null && length != null && position + length > string.length) { + throw new RangeError("position plus length must not go beyond the end of " + "the string."); + } +} \ No newline at end of file
diff --git a/pkgs/string_scanner/lib/string_scanner.dart b/pkgs/string_scanner/lib/string_scanner.dart index 3e3913b..be294f4 100644 --- a/pkgs/string_scanner/lib/string_scanner.dart +++ b/pkgs/string_scanner/lib/string_scanner.dart
@@ -5,165 +5,7 @@ /// A library for parsing strings using a sequence of patterns. library string_scanner; -import 'dart:math' as math; - -/// When compiled to JS, forward slashes are always escaped in [RegExp.pattern]. -/// -/// See issue 17998. -final _slashAutoEscape = new RegExp("/").pattern == "\\/"; - -// TODO(nweiz): Add some integration between this and source maps. -/// A class that scans through a string using [Pattern]s. -class StringScanner { - /// The string being scanned through. - final String string; - - /// The current position of the scanner in the string, in characters. - int get position => _position; - set position(int position) { - if (position < 0 || position > string.length) { - throw new ArgumentError("Invalid position $position"); - } - - _position = position; - } - int _position = 0; - - /// The data about the previous match made by the scanner. - /// - /// If the last match failed, this will be `null`. - Match get lastMatch => _lastMatch; - Match _lastMatch; - - /// The portion of the string that hasn't yet been scanned. - String get rest => string.substring(position); - - /// Whether the scanner has completely consumed [string]. - bool get isDone => position == string.length; - - /// Creates a new [StringScanner] that starts scanning from [position]. - /// - /// [position] defaults to 0, the beginning of the string. - StringScanner(this.string, {int position}) { - if (position != null) this.position = position; - } - - /// If [pattern] matches at the current position of the string, scans forward - /// until the end of the match. - /// - /// Returns whether or not [pattern] matched. - bool scan(Pattern pattern) { - var success = matches(pattern); - if (success) _position = _lastMatch.end; - return success; - } - - /// If [pattern] matches at the current position of the string, scans forward - /// until the end of the match. - /// - /// If [pattern] did not match, throws a [FormatException] describing the - /// position of the failure. [name] is used in this error as the expected name - /// of the pattern being matched; if it's `null`, the pattern itself is used - /// instead. - void expect(Pattern pattern, {String name}) { - if (scan(pattern)) return; - - if (name == null) { - if (pattern is RegExp) { - var source = pattern.pattern; - if (!_slashAutoEscape) source = source.replaceAll("/", "\\/"); - name = "/$source/"; - } else { - name = pattern.toString() - .replaceAll("\\", "\\\\").replaceAll('"', '\\"'); - name = '"$name"'; - } - } - _fail(name); - } - - /// If the string has not been fully consumed, this throws a - /// [FormatException]. - void expectDone() { - if (isDone) return; - _fail("no more input"); - } - - /// Returns whether or not [pattern] matches at the current position of the - /// string. - /// - /// This doesn't move the scan pointer forward. - bool matches(Pattern pattern) { - _lastMatch = pattern.matchAsPrefix(string, position); - return _lastMatch != null; - } - - /// 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; - var lastLine; - if (newlines.isEmpty) { - column = position + 1; - lastLine = string.substring(0, position); - } else { - column = position - newlines.last.end + 1; - lastLine = string.substring(newlines.last.end, position); - } - - 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( - "Error on line $line, column $column: $message\n" - "$lastLine\n" - "$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); - } -} +export 'src/exception.dart'; +export 'src/line_scanner.dart'; +export 'src/span_scanner.dart'; +export 'src/string_scanner.dart';
diff --git a/pkgs/string_scanner/pubspec.yaml b/pkgs/string_scanner/pubspec.yaml index 3035901..86ae238 100644 --- a/pkgs/string_scanner/pubspec.yaml +++ b/pkgs/string_scanner/pubspec.yaml
@@ -1,9 +1,12 @@ name: string_scanner -version: 0.0.1 +version: 0.0.2 author: "Dart Team <misc@dartlang.org>" homepage: http://www.dartlang.org description: > A class for parsing strings using a sequence of patterns. +dependencies: + path: ">=1.2.0 <2.0.0" + source_maps: ">=0.9.0 <0.10.0" dev_dependencies: unittest: ">=0.10.0 <0.11.0" environment:
diff --git a/pkgs/string_scanner/test/error_test.dart b/pkgs/string_scanner/test/error_test.dart index 6432197..4fe9083 100644 --- a/pkgs/string_scanner/test/error_test.dart +++ b/pkgs/string_scanner/test/error_test.dart
@@ -14,10 +14,7 @@ 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 - ^^^''')); + expect(() => scanner.error('oh no!'), throwsStringScannerException('bar')); }); group("with match", () { @@ -27,10 +24,7 @@ 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 -^^^^''')); + throwsStringScannerException('foo ')); }); test('supports a match on a previous line', () { @@ -40,10 +34,7 @@ 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 - ^^''')); + throwsStringScannerException('re')); }); test('supports a multiline match', () { @@ -53,10 +44,7 @@ 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 - ^^^''')); + throwsStringScannerException('baz\ndo')); }); test('supports a match after position', () { @@ -66,10 +54,7 @@ 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 - ^^^''')); + throwsStringScannerException('bar')); }); }); @@ -78,59 +63,47 @@ 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 - ^''')); + throwsStringScannerException('o')); }); 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 - ^^^''')); + throwsStringScannerException('bar')); }); 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 - ^^''')); + throwsStringScannerException('oo')); }); 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 - ^^''')); + throwsStringScannerException('re')); }); 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 - ^^^''')); + throwsStringScannerException('baz\ndo r')); }); 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 - ^^^''')); + throwsStringScannerException('bar')); + }); + + test('supports a length of zero', () { + var scanner = new StringScanner('foo bar baz'); + expect(() => scanner.error('oh no!', position: 4, length: 0), + throwsStringScannerException('')); }); }); @@ -161,8 +134,13 @@ expect(() => scanner.error("oh no!", position: 100), throwsArgumentError); }); - test("if length is zero", () { - expect(() => scanner.error("oh no!", length: 0), throwsArgumentError); + test("if position + length is outside the string", () { + expect(() => scanner.error("oh no!", position: 7, length: 7), + throwsArgumentError); + }); + + test("if length is negative", () { + expect(() => scanner.error("oh no!", length: -1), throwsArgumentError); }); }); }
diff --git a/pkgs/string_scanner/test/expect_error_test.dart b/pkgs/string_scanner/test/expect_error_test.dart deleted file mode 100644 index 3596e15..0000000 --- a/pkgs/string_scanner/test/expect_error_test.dart +++ /dev/null
@@ -1,104 +0,0 @@ -// 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.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(''' -Error on line 1, column 5: expected "foo". -foo bar baz - ^''')); - }); - - test('prints the correct line', () { - var scanner = new StringScanner('foo bar baz\ndo re mi\nearth fire water'); - scanner.expect('foo bar baz\ndo '); - expect(() => scanner.expect('foo'), throwsFormattedError(''' -Error on line 2, column 4: expected "foo". -do re mi - ^''')); - }); - - test('handles the beginning of the string correctly', () { - var scanner = new StringScanner('foo bar baz'); - expect(() => scanner.expect('zap'), throwsFormattedError(''' -Error on line 1, column 1: expected "zap". -foo bar baz -^''')); - }); - - test('handles the end of the string correctly', () { - var scanner = new StringScanner('foo bar baz'); - scanner.expect('foo bar baz'); - expect(() => scanner.expect('bang'), throwsFormattedError(''' -Error on line 1, column 12: expected "bang". -foo bar baz - ^''')); - }); - - test('handles an empty string correctly', () { - expect(() => new StringScanner('').expect('foo'), throwsFormattedError(''' -Error on line 1, column 1: expected "foo". - -^''')); - }); - - group("expected name", () { - test("uses the provided name", () { - expect(() => new StringScanner('').expect('foo bar', name: 'zap'), - throwsFormattedError(''' -Error on line 1, column 1: expected zap. - -^''')); - }); - - test("escapes string quotes", () { - expect(() => new StringScanner('').expect('foo"bar'), - throwsFormattedError(''' -Error on line 1, column 1: expected "foo\\"bar". - -^''')); - }); - - test("escapes string backslashes", () { - expect(() => new StringScanner('').expect('foo\\bar'), - throwsFormattedError(''' -Error on line 1, column 1: expected "foo\\\\bar". - -^''')); - }); - - test("prints PERL-style regexps", () { - expect(() => new StringScanner('').expect(new RegExp(r'foo')), - throwsFormattedError(''' -Error on line 1, column 1: expected /foo/. - -^''')); - }); - - test("escape regexp forward slashes", () { - expect(() => new StringScanner('').expect(new RegExp(r'foo/bar')), - throwsFormattedError(''' -Error on line 1, column 1: expected /foo\\/bar/. - -^''')); - }); - - test("does not escape regexp backslashes", () { - expect(() => new StringScanner('').expect(new RegExp(r'foo\bar')), - throwsFormattedError(''' -Error on line 1, column 1: expected /foo\\bar/. - -^''')); - }); - }); -}
diff --git a/pkgs/string_scanner/test/line_scanner_test.dart b/pkgs/string_scanner/test/line_scanner_test.dart new file mode 100644 index 0000000..8cc79a6 --- /dev/null +++ b/pkgs/string_scanner/test/line_scanner_test.dart
@@ -0,0 +1,106 @@ +// 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.line_scanner_test; + +import 'package:string_scanner/string_scanner.dart'; +import 'package:unittest/unittest.dart'; + +void main() { + var scanner; + setUp(() { + scanner = new LineScanner('foo\nbar\nbaz'); + }); + + test('begins with line and column 0', () { + expect(scanner.line, equals(0)); + expect(scanner.column, equals(0)); + }); + + group("scan()", () { + test("consuming no newlines increases the column but not the line", () { + scanner.scan('foo'); + expect(scanner.line, equals(0)); + expect(scanner.column, equals(3)); + }); + + test("consuming a newline resets the column and increases the line", () { + scanner.expect('foo\nba'); + expect(scanner.line, equals(1)); + expect(scanner.column, equals(2)); + }); + + test("consuming multiple newlines resets the column and increases the line", + () { + scanner.expect('foo\nbar\nb'); + expect(scanner.line, equals(2)); + expect(scanner.column, equals(1)); + }); + }); + + group("readChar()", () { + test("on a non-newline character increases the column but not the line", + () { + scanner.readChar(); + expect(scanner.line, equals(0)); + expect(scanner.column, equals(1)); + }); + + test("consuming a newline resets the column and increases the line", () { + scanner.expect('foo'); + expect(scanner.line, equals(0)); + expect(scanner.column, equals(3)); + + scanner.readChar(); + expect(scanner.line, equals(1)); + expect(scanner.column, equals(0)); + }); + }); + + group("position=", () { + test("forward through newlines sets the line and column", () { + scanner.position = 9; // "foo\nbar\nb" + expect(scanner.line, equals(2)); + expect(scanner.column, equals(1)); + }); + + test("forward through no newlines sets the column", () { + scanner.position = 2; // "fo" + expect(scanner.line, equals(0)); + expect(scanner.column, equals(2)); + }); + + test("backward through newlines sets the line and column", () { + scanner.scan("foo\nbar\nbaz"); + scanner.position = 2; // "fo" + expect(scanner.line, equals(0)); + expect(scanner.column, equals(2)); + }); + + test("backward through no newlines sets the column", () { + scanner.scan("foo\nbar\nbaz"); + scanner.position = 9; // "foo\nbar\nb" + expect(scanner.line, equals(2)); + expect(scanner.column, equals(1)); + }); + }); + + test("state= restores the line, column, and position", () { + scanner.scan('foo\nb'); + var state = scanner.state; + + scanner.scan('ar\nba'); + scanner.state = state; + expect(scanner.rest, equals('ar\nbaz')); + expect(scanner.line, equals(1)); + expect(scanner.column, equals(1)); + }); + + test("state= rejects a foreign state", () { + scanner.scan('foo\nb'); + + expect(() => new LineScanner(scanner.string).state = scanner.state, + throwsArgumentError); + }); +}
diff --git a/pkgs/string_scanner/test/span_scanner_test.dart b/pkgs/string_scanner/test/span_scanner_test.dart new file mode 100644 index 0000000..93ba0b6 --- /dev/null +++ b/pkgs/string_scanner/test/span_scanner_test.dart
@@ -0,0 +1,60 @@ +// 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.span_scanner_test; + +import 'package:string_scanner/string_scanner.dart'; +import 'package:unittest/unittest.dart'; + +void main() { + var scanner; + setUp(() { + scanner = new SpanScanner('foo\nbar\nbaz', 'source'); + }); + + test("tracks the span for the last match", () { + scanner.scan('fo'); + scanner.scan('o\nba'); + + var span = scanner.lastSpan; + expect(span.start.offset, equals(2)); + expect(span.start.line, equals(0)); + expect(span.start.column, equals(2)); + expect(span.start.sourceUrl, equals('source')); + + expect(span.end.offset, equals(6)); + expect(span.end.line, equals(1)); + expect(span.end.column, equals(2)); + expect(span.start.sourceUrl, equals('source')); + + expect(span.text, equals('o\nba')); + }); + + test(".spanFrom() returns a span from a previous state", () { + scanner.scan('fo'); + var state = scanner.state; + scanner.scan('o\nba'); + scanner.scan('r\nba'); + + var span = scanner.spanFrom(state); + expect(span.text, equals('o\nbar\nba')); + }); + + test(".emptySpan returns an empty span at the current location", () { + scanner.scan('foo\nba'); + + var span = scanner.emptySpan; + expect(span.start.offset, equals(6)); + expect(span.start.line, equals(1)); + expect(span.start.column, equals(2)); + expect(span.start.sourceUrl, equals('source')); + + expect(span.end.offset, equals(6)); + expect(span.end.line, equals(1)); + expect(span.end.column, equals(2)); + expect(span.start.sourceUrl, equals('source')); + + expect(span.text, equals('')); + }); +}
diff --git a/pkgs/string_scanner/test/string_scanner_test.dart b/pkgs/string_scanner/test/string_scanner_test.dart index 0cab627..6144bf9 100644 --- a/pkgs/string_scanner/test/string_scanner_test.dart +++ b/pkgs/string_scanner/test/string_scanner_test.dart
@@ -31,6 +31,18 @@ expect(scanner.position, equals(0)); }); + test("readChar fails and doesn't change the state", () { + expect(scanner.readChar, throwsFormatException); + expect(scanner.lastMatch, isNull); + expect(scanner.position, equals(0)); + }); + + test("peekChar returns null and doesn't change the state", () { + expect(scanner.peekChar(), isNull); + expect(scanner.lastMatch, isNull); + expect(scanner.position, equals(0)); + }); + test("scan returns false and doesn't change the state", () { expect(scanner.scan(new RegExp('.')), isFalse); expect(scanner.lastMatch, isNull); @@ -85,6 +97,24 @@ expect(scanner.position, equals(0)); }); + test('readChar returns the first character and moves forward', () { + expect(scanner.readChar(), equals(0x66)); + expect(scanner.lastMatch, isNull); + expect(scanner.position, equals(1)); + }); + + test('peekChar returns the first character', () { + expect(scanner.peekChar(), equals(0x66)); + expect(scanner.lastMatch, isNull); + expect(scanner.position, equals(0)); + }); + + test('peekChar with an argument returns the nth character', () { + expect(scanner.peekChar(4), equals(0x62)); + expect(scanner.lastMatch, isNull); + expect(scanner.position, equals(0)); + }); + test("a matching scan returns true and changes the state", () { expect(scanner.scan(new RegExp('f(..)')), isTrue); expect(scanner.lastMatch[1], equals('oo')); @@ -200,6 +230,18 @@ expect(scanner.position, equals(7)); }); + test("readChar fails and doesn't change the state", () { + expect(scanner.readChar, throwsFormatException); + expect(scanner.lastMatch, isNotNull); + expect(scanner.position, equals(7)); + }); + + test("peekChar returns null and doesn't change the state", () { + expect(scanner.peekChar(), isNull); + expect(scanner.lastMatch, isNotNull); + expect(scanner.position, equals(7)); + }); + test("scan returns false and sets lastMatch to null", () { expect(scanner.scan(new RegExp('.')), isFalse); expect(scanner.lastMatch, isNull);
diff --git a/pkgs/string_scanner/test/utils.dart b/pkgs/string_scanner/test/utils.dart index eee93d8..3de601c 100644 --- a/pkgs/string_scanner/test/utils.dart +++ b/pkgs/string_scanner/test/utils.dart
@@ -4,14 +4,15 @@ library string_scanner.test.utils; +import 'package:string_scanner/string_scanner.dart'; import 'package:unittest/unittest.dart'; /// Returns a matcher that asserts that a closure throws a [FormatException] /// with the given [message]. -Matcher throwsFormattedError(String message) { +Matcher throwsStringScannerException(String text) { return throwsA(predicate((error) { - expect(error, isFormatException); - expect(error.message, equals(message)); + expect(error, new isInstanceOf<StringScannerException>()); + expect(error.span.text, equals(text)); return true; })); }