Add breaking changes and release 1.0.0.
This makes `.error()` default to empty spans rather than
single-character ones. This better represents the semantics of failing
at a particular position in the text.
It also makes `lastMatch` reset whenever the scanner's position changes.
This makes `.error()` behave more consistently when primarily doing
character-based scanning, since it won't unexpectedly emit an error for
stale match data.
R=jmesserly@google.com
Review URL: https://codereview.chromium.org//2056933002 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index db73686..910d30e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,16 @@
+## 1.0.0
+
+* **Breaking change**: `StringScanner.error()`'s `length` argument now defaults
+ to `0` rather than `1` when no match data is available.
+
+* **Breaking change**: `StringScanner.lastMatch` and related methods are now
+ reset when the scanner's position changes without producing a new match.
+
+**Note**: While the changes in `1.0.0` are user-visible, they're unlikely to
+actually break any code in practice. Unless you know that your package is
+incompatible with 0.1.x, consider using 0.1.5 as your lower bound rather
+than 1.0.0. For example, `string_scanner: ">=0.1.5 <2.0.0"`.
+
## 0.1.5
* Add `new SpanScanner.within()`, which scans within a existing `FileSpan`.
diff --git a/lib/src/line_scanner.dart b/lib/src/line_scanner.dart
index fe63592..06f1cbc 100644
--- a/lib/src/line_scanner.dart
+++ b/lib/src/line_scanner.dart
@@ -26,6 +26,8 @@
/// 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.
+ ///
+ /// This does not include the scanner's match information.
LineScannerState get state =>
new LineScannerState._(this, position, line, column);
diff --git a/lib/src/span_scanner.dart b/lib/src/span_scanner.dart
index dd7f0e4..043021b 100644
--- a/lib/src/span_scanner.dart
+++ b/lib/src/span_scanner.dart
@@ -38,7 +38,10 @@
///
/// 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.
- FileSpan get lastSpan => _lastSpan;
+ FileSpan get lastSpan {
+ if (lastMatch == null) _lastSpan = null;
+ return _lastSpan;
+ }
FileSpan _lastSpan;
/// The current location of the scanner.
@@ -102,7 +105,7 @@
if (position == null) {
position = match == null ? this.position : match.start;
}
- if (length == null) length = match == null ? 1 : match.end - match.start;
+ if (length == null) length = match == null ? 0 : match.end - match.start;
var span = _sourceFile.span(position, position + length);
throw new StringScannerException(message, span, string);
diff --git a/lib/src/string_scanner.dart b/lib/src/string_scanner.dart
index 8334ccb..b9714ff 100644
--- a/lib/src/string_scanner.dart
+++ b/lib/src/string_scanner.dart
@@ -32,14 +32,21 @@
}
_position = position;
+ _lastMatch = null;
}
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 get lastMatch {
+ // Lazily unset [_lastMatch] so that we avoid extra assignments in
+ // character-by-character methods that are used in core loops.
+ if (_position != _lastMatchPosition) _lastMatch = null;
+ return _lastMatch;
+ }
Match _lastMatch;
+ int _lastMatchPosition;
/// The portion of the string that hasn't yet been scanned.
String get rest => string.substring(position);
@@ -118,7 +125,10 @@
/// Returns whether or not [pattern] matched.
bool scan(Pattern pattern) {
var success = matches(pattern);
- if (success) _position = _lastMatch.end;
+ if (success) {
+ _position = _lastMatch.end;
+ _lastMatchPosition = _position;
+ }
return success;
}
@@ -159,6 +169,7 @@
/// This doesn't move the scan pointer forward.
bool matches(Pattern pattern) {
_lastMatch = pattern.matchAsPrefix(string, position);
+ _lastMatchPosition = _position;
return _lastMatch != null;
}
@@ -181,7 +192,7 @@
///
/// 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.
+ /// position; if only [position] is passed, [length] defaults to 0.
///
/// 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}) {
@@ -191,7 +202,7 @@
if (position == null) {
position = match == null ? this.position : match.start;
}
- if (length == null) length = match == null ? 1 : match.end - match.start;
+ if (length == null) length = match == null ? 0 : match.end - match.start;
var sourceFile = new SourceFile(string, url: sourceUrl);
var span = sourceFile.span(position, position + length);
diff --git a/pubspec.yaml b/pubspec.yaml
index 7f7e33a..48ed59d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: string_scanner
-version: 0.1.5
+version: 1.0.0
author: "Dart Team <misc@dartlang.org>"
homepage: https://github.com/dart-lang/string_scanner
description: >
diff --git a/test/error_test.dart b/test/error_test.dart
index ff45194..166077d 100644
--- a/test/error_test.dart
+++ b/test/error_test.dart
@@ -59,11 +59,11 @@
});
group("with position and/or length", () {
- test('defaults to length 1', () {
+ test('defaults to length 0', () {
var scanner = new StringScanner('foo bar baz');
scanner.expect('foo ');
expect(() => scanner.error('oh no!', position: 1),
- throwsStringScannerException('o'));
+ throwsStringScannerException(''));
});
test('defaults to the current position', () {
diff --git a/test/string_scanner_test.dart b/test/string_scanner_test.dart
index 0b4d482..55ffa0a 100644
--- a/test/string_scanner_test.dart
+++ b/test/string_scanner_test.dart
@@ -261,6 +261,32 @@
});
});
+ group('after a scan', () {
+ var scanner;
+ setUp(() {
+ scanner = new StringScanner('foo bar');
+ expect(scanner.scan('foo'), isTrue);
+ });
+
+ test('readChar returns the first character and unsets the last match', () {
+ expect(scanner.readChar(), equals($space));
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(4));
+ });
+
+ test('a matching scanChar returns true and unsets the last match', () {
+ expect(scanner.scanChar($space), isTrue);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(4));
+ });
+
+ test('a matching expectChar returns true and unsets the last match', () {
+ scanner.expectChar($space);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(4));
+ });
+ });
+
group('at the end of a string', () {
var scanner;
setUp(() {
@@ -346,6 +372,13 @@
expect(scanner.rest, equals('bar'));
});
+ test('setting and resetting position clears lastMatch', () {
+ var oldPosition = scanner.position;
+ scanner.position = 1;
+ scanner.position = oldPosition;
+ expect(scanner.lastMatch, isNull);
+ });
+
test('setting position beyond the string throws an ArgumentError', () {
expect(() {
scanner.position = 8;