Add an "ErrorListener" to collect errors during parsing
diff --git a/pkgs/yaml/lib/src/error_listener.dart b/pkgs/yaml/lib/src/error_listener.dart
new file mode 100644
index 0000000..d156655
--- /dev/null
+++ b/pkgs/yaml/lib/src/error_listener.dart
@@ -0,0 +1,33 @@
+import 'package:source_span/source_span.dart';
+
+import 'yaml_exception.dart';
+
+/// A listener that is notified of [YamlError]s during scanning/parsing.
+abstract class ErrorListener {
+ /// This method is invoked when an [error] has been found in the YAML.
+ void onError(YamlError error);
+}
+
+/// An error found in the YAML.
+class YamlError {
+ /// A message describing the exception.
+ final String message;
+
+ /// The span associated with this exception.
+ final FileSpan span;
+
+ YamlError(this.message, this.span);
+}
+
+extension YamlErrorExtensions on YamlError {
+ /// Creates a [YamlException] from a [YamlError].
+ YamlException toException() => YamlException(message, span);
+}
+
+/// An [ErrorListener] that collects all errors into [errors].
+class ErrorCollector extends ErrorListener {
+ final List<YamlError> errors = [];
+
+ @override
+ void onError(YamlError error) => errors.add(error);
+}
diff --git a/pkgs/yaml/lib/src/loader.dart b/pkgs/yaml/lib/src/loader.dart
index dafc59b..0056a64 100644
--- a/pkgs/yaml/lib/src/loader.dart
+++ b/pkgs/yaml/lib/src/loader.dart
@@ -4,6 +4,7 @@
import 'package:charcode/ascii.dart';
import 'package:source_span/source_span.dart';
+import 'package:yaml/src/error_listener.dart';
import 'equality.dart';
import 'event.dart';
@@ -30,8 +31,10 @@
FileSpan _span;
/// Creates a loader that loads [source].
- factory Loader(String source, {Uri? sourceUrl, bool recover = false}) {
- var parser = Parser(source, sourceUrl: sourceUrl, recover: recover);
+ factory Loader(String source,
+ {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) {
+ var parser = Parser(source,
+ sourceUrl: sourceUrl, recover: recover, errorListener: errorListener);
var event = parser.parse();
assert(event.type == EventType.streamStart);
return Loader._(parser, event.span);
diff --git a/pkgs/yaml/lib/src/parser.dart b/pkgs/yaml/lib/src/parser.dart
index 695c1d0..2f759e9 100644
--- a/pkgs/yaml/lib/src/parser.dart
+++ b/pkgs/yaml/lib/src/parser.dart
@@ -4,6 +4,7 @@
import 'package:source_span/source_span.dart';
import 'package:string_scanner/string_scanner.dart';
+import 'package:yaml/src/error_listener.dart';
import 'event.dart';
import 'scanner.dart';
@@ -35,8 +36,12 @@
bool get isDone => _state == _State.END;
/// Creates a parser that parses [source].
- Parser(String source, {Uri? sourceUrl, bool recover = false})
- : _scanner = Scanner(source, sourceUrl: sourceUrl, recover: recover);
+ Parser(String source,
+ {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener})
+ : _scanner = Scanner(source,
+ sourceUrl: sourceUrl,
+ recover: recover,
+ errorListener: errorListener);
/// Consumes and returns the next event.
Event parse() {
diff --git a/pkgs/yaml/lib/src/scanner.dart b/pkgs/yaml/lib/src/scanner.dart
index 171364c..260da1c 100644
--- a/pkgs/yaml/lib/src/scanner.dart
+++ b/pkgs/yaml/lib/src/scanner.dart
@@ -5,6 +5,7 @@
import 'package:collection/collection.dart';
import 'package:source_span/source_span.dart';
import 'package:string_scanner/string_scanner.dart';
+import 'package:yaml/src/error_listener.dart';
import 'style.dart';
import 'token.dart';
@@ -92,6 +93,9 @@
/// Whether this scanner should attempt to recover when parsing invalid YAML.
final bool _recover;
+ /// A listener to report YAML errors to.
+ final ErrorListener? _errorListener;
+
/// The underlying [SpanScanner] used to read characters from the source text.
///
/// This is also used to track line and column information and to generate
@@ -291,8 +295,10 @@
}
/// Creates a scanner that scans [source].
- Scanner(String source, {Uri? sourceUrl, bool recover = false})
+ Scanner(String source,
+ {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener})
: _recover = recover,
+ _errorListener = errorListener,
_scanner = SpanScanner.eager(source, sourceUrl: sourceUrl);
/// Consumes and returns the next token.
@@ -490,11 +496,12 @@
if (key.line == _scanner.line) continue;
if (key.required) {
+ final error = _reportError("Expected ':'.", _scanner.emptySpan);
if (_recover) {
_tokens.insert(key.tokenNumber - _tokensParsed,
Token(TokenType.key, key.location.pointSpan() as FileSpan));
} else {
- throw YamlException("Expected ':'.", _scanner.emptySpan);
+ throw error.toException();
}
}
@@ -1633,6 +1640,13 @@
_scanner.readChar();
}
}
+
+ /// Reports an error to [_errorListener] and returns the [YamlError].
+ YamlError _reportError(String message, FileSpan span) {
+ final error = YamlError("Expected ':'.", _scanner.emptySpan);
+ _errorListener?.onError(error);
+ return error;
+ }
}
/// A record of the location of a potential simple key.
diff --git a/pkgs/yaml/lib/yaml.dart b/pkgs/yaml/lib/yaml.dart
index 988de2d..dec4bc0 100644
--- a/pkgs/yaml/lib/yaml.dart
+++ b/pkgs/yaml/lib/yaml.dart
@@ -2,6 +2,7 @@
// 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.
+import 'src/error_listener.dart';
import 'src/loader.dart';
import 'src/style.dart';
import 'src/yaml_document.dart';
@@ -33,16 +34,30 @@
///
/// If [recover] is true, will attempt to recover from parse errors and may return
/// invalid or synthetic nodes.
-dynamic loadYaml(String yaml, {Uri? sourceUrl, bool recover = false}) =>
- loadYamlNode(yaml, sourceUrl: sourceUrl, recover: recover).value;
+///
+/// If [errorListener] is supplied, its onError method will be called for each
+/// error. If [recover] is false, parsing will end early so only the first error
+/// may be emitted.
+dynamic loadYaml(String yaml,
+ {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) =>
+ loadYamlNode(yaml,
+ sourceUrl: sourceUrl,
+ recover: recover,
+ errorListener: errorListener)
+ .value;
/// Loads a single document from a YAML string as a [YamlNode].
///
/// This is just like [loadYaml], except that where [loadYaml] would return a
/// normal Dart value this returns a [YamlNode] instead. This allows the caller
/// to be confident that the return value will always be a [YamlNode].
-YamlNode loadYamlNode(String yaml, {Uri? sourceUrl, bool recover = false}) =>
- loadYamlDocument(yaml, sourceUrl: sourceUrl, recover: recover).contents;
+YamlNode loadYamlNode(String yaml,
+ {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) =>
+ loadYamlDocument(yaml,
+ sourceUrl: sourceUrl,
+ recover: recover,
+ errorListener: errorListener)
+ .contents;
/// Loads a single document from a YAML string as a [YamlDocument].
///
@@ -50,8 +65,9 @@
/// normal Dart value this returns a [YamlDocument] instead. This allows the
/// caller to access document metadata.
YamlDocument loadYamlDocument(String yaml,
- {Uri? sourceUrl, bool recover = false}) {
- var loader = Loader(yaml, sourceUrl: sourceUrl, recover: recover);
+ {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) {
+ var loader = Loader(yaml,
+ sourceUrl: sourceUrl, recover: recover, errorListener: errorListener);
var document = loader.load();
if (document == null) {
return YamlDocument.internal(YamlScalar.internalWithSpan(null, loader.span),
diff --git a/pkgs/yaml/test/utils.dart b/pkgs/yaml/test/utils.dart
index ce05481..0904458 100644
--- a/pkgs/yaml/test/utils.dart
+++ b/pkgs/yaml/test/utils.dart
@@ -4,6 +4,7 @@
import 'package:test/test.dart';
import 'package:yaml/src/equality.dart' as equality;
+import 'package:yaml/src/error_listener.dart' show YamlError;
import 'package:yaml/yaml.dart';
/// A matcher that validates that a closure or Future throws a [YamlException].
@@ -22,6 +23,13 @@
return map;
}
+/// Asserts that an error has the given message and starts at the given line/col.
+void expectErrorAtLineCol(YamlError error, String message, int line, int col) {
+ expect(error.message, equals(message));
+ expect(error.span.start.line, equals(line));
+ expect(error.span.start.column, equals(col));
+}
+
/// Asserts that a string containing a single YAML document produces a given
/// value when loaded.
void expectYamlLoads(expected, String source) {
diff --git a/pkgs/yaml/test/yaml_test.dart b/pkgs/yaml/test/yaml_test.dart
index 014da96..dffb9d5 100644
--- a/pkgs/yaml/test/yaml_test.dart
+++ b/pkgs/yaml/test/yaml_test.dart
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:test/test.dart';
+import 'package:yaml/src/error_listener.dart';
import 'package:yaml/yaml.dart';
import 'utils.dart';
@@ -62,13 +63,18 @@
});
group('recovers', () {
+ var collector = ErrorCollector();
+ setUp(() {
+ collector = ErrorCollector();
+ });
+
test('from incomplete leading keys', () {
final yaml = cleanUpLiteral(r'''
dependencies:
zero
one: any
''');
- var result = loadYaml(yaml, recover: true);
+ var result = loadYaml(yaml, recover: true, errorListener: collector);
expect(
result,
deepEquals({
@@ -77,6 +83,10 @@
'one': 'any',
}
}));
+ expect(collector.errors.length, equals(1));
+ // These errors are reported at the start of the next token (after the
+ // whitespace/newlines).
+ expectErrorAtLineCol(collector.errors[0], "Expected ':'.", 2, 2);
// Skipped because this case is not currently handled. If it's the first
// package without the colon, because the value is indented from the line
// above, the whole `zero\n one` is treated as a scalar value.
@@ -92,7 +102,7 @@
1.2.3
six: 5.4.3
''');
- var result = loadYaml(yaml, recover: true);
+ var result = loadYaml(yaml, recover: true, errorListener: collector);
expect(
result,
deepEquals({
@@ -105,6 +115,12 @@
'six': '5.4.3',
}
}));
+
+ expect(collector.errors.length, equals(2));
+ // These errors are reported at the start of the next token (after the
+ // whitespace/newlines).
+ expectErrorAtLineCol(collector.errors[0], "Expected ':'.", 3, 2);
+ expectErrorAtLineCol(collector.errors[1], "Expected ':'.", 5, 2);
});
test('from incomplete trailing keys', () {
final yaml = cleanUpLiteral(r'''