From 0f246f8bc9fc3491728650aeeb65a5466a422c07 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Wed, 13 Jan 2021 14:40:27 +0000 Subject: [PATCH 1/5] Attempt to recover from invalid YAML --- lib/src/loader.dart | 4 +-- lib/src/parser.dart | 4 +-- lib/src/scanner.dart | 15 ++++++++--- lib/yaml.dart | 16 ++++++----- test/yaml_test.dart | 63 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 13 deletions(-) diff --git a/lib/src/loader.dart b/lib/src/loader.dart index 7333227..dafc59b 100644 --- a/lib/src/loader.dart +++ b/lib/src/loader.dart @@ -30,8 +30,8 @@ class Loader { FileSpan _span; /// Creates a loader that loads [source]. - factory Loader(String source, {Uri? sourceUrl}) { - var parser = Parser(source, sourceUrl: sourceUrl); + factory Loader(String source, {Uri? sourceUrl, bool recover = false}) { + var parser = Parser(source, sourceUrl: sourceUrl, recover: recover); var event = parser.parse(); assert(event.type == EventType.streamStart); return Loader._(parser, event.span); diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 3443e2e..695c1d0 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -35,8 +35,8 @@ class Parser { bool get isDone => _state == _State.END; /// Creates a parser that parses [source]. - Parser(String source, {Uri? sourceUrl}) - : _scanner = Scanner(source, sourceUrl: sourceUrl); + Parser(String source, {Uri? sourceUrl, bool recover = false}) + : _scanner = Scanner(source, sourceUrl: sourceUrl, recover: recover); /// Consumes and returns the next event. Event parse() { diff --git a/lib/src/scanner.dart b/lib/src/scanner.dart index a9f64be..171364c 100644 --- a/lib/src/scanner.dart +++ b/lib/src/scanner.dart @@ -89,6 +89,9 @@ class Scanner { static const LETTER_CAP_X = 0x58; static const LETTER_CAP_Z = 0x5A; + /// Whether this scanner should attempt to recover when parsing invalid YAML. + final bool _recover; + /// The underlying [SpanScanner] used to read characters from the source text. /// /// This is also used to track line and column information and to generate @@ -288,8 +291,9 @@ class Scanner { } /// Creates a scanner that scans [source]. - Scanner(String source, {Uri? sourceUrl}) - : _scanner = SpanScanner.eager(source, sourceUrl: sourceUrl); + Scanner(String source, {Uri? sourceUrl, bool recover = false}) + : _recover = recover, + _scanner = SpanScanner.eager(source, sourceUrl: sourceUrl); /// Consumes and returns the next token. Token scan() { @@ -486,7 +490,12 @@ class Scanner { if (key.line == _scanner.line) continue; if (key.required) { - throw YamlException("Expected ':'.", _scanner.emptySpan); + if (_recover) { + _tokens.insert(key.tokenNumber - _tokensParsed, + Token(TokenType.key, key.location.pointSpan() as FileSpan)); + } else { + throw YamlException("Expected ':'.", _scanner.emptySpan); + } } _simpleKeys[i] = null; diff --git a/lib/yaml.dart b/lib/yaml.dart index f80489a..988de2d 100644 --- a/lib/yaml.dart +++ b/lib/yaml.dart @@ -30,24 +30,28 @@ export 'src/yaml_node.dart' hide setSpan; /// /// If [sourceUrl] is passed, it's used as the URL from which the YAML /// originated for error reporting. -dynamic loadYaml(String yaml, {Uri? sourceUrl}) => - loadYamlNode(yaml, sourceUrl: sourceUrl).value; +/// +/// 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; /// 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}) => - loadYamlDocument(yaml, sourceUrl: sourceUrl).contents; +YamlNode loadYamlNode(String yaml, {Uri? sourceUrl, bool recover = false}) => + loadYamlDocument(yaml, sourceUrl: sourceUrl, recover: recover).contents; /// Loads a single document from a YAML string as a [YamlDocument]. /// /// This is just like [loadYaml], except that where [loadYaml] would return a /// normal Dart value this returns a [YamlDocument] instead. This allows the /// caller to access document metadata. -YamlDocument loadYamlDocument(String yaml, {Uri? sourceUrl}) { - var loader = Loader(yaml, sourceUrl: sourceUrl); +YamlDocument loadYamlDocument(String yaml, + {Uri? sourceUrl, bool recover = false}) { + var loader = Loader(yaml, sourceUrl: sourceUrl, recover: recover); var document = loader.load(); if (document == null) { return YamlDocument.internal(YamlScalar.internalWithSpan(null, loader.span), diff --git a/test/yaml_test.dart b/test/yaml_test.dart index a0fb8f1..014da96 100644 --- a/test/yaml_test.dart +++ b/test/yaml_test.dart @@ -61,6 +61,69 @@ void main() { }); }); + group('recovers', () { + test('from incomplete leading keys', () { + final yaml = cleanUpLiteral(r''' + dependencies: + zero + one: any + '''); + var result = loadYaml(yaml, recover: true); + expect( + result, + deepEquals({ + 'dependencies': { + 'zero': null, + 'one': 'any', + } + })); + // 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. + }, skip: true); + test('from incomplete keys', () { + final yaml = cleanUpLiteral(r''' + dependencies: + one: any + two + three: + four + five: + 1.2.3 + six: 5.4.3 + '''); + var result = loadYaml(yaml, recover: true); + expect( + result, + deepEquals({ + 'dependencies': { + 'one': 'any', + 'two': null, + 'three': null, + 'four': null, + 'five': '1.2.3', + 'six': '5.4.3', + } + })); + }); + test('from incomplete trailing keys', () { + final yaml = cleanUpLiteral(r''' + dependencies: + six: 5.4.3 + seven + '''); + var result = loadYaml(yaml, recover: true); + expect( + result, + deepEquals({ + 'dependencies': { + 'six': '5.4.3', + 'seven': null, + } + })); + }); + }); + test('includes source span information', () { var yaml = loadYamlNode(r''' - foo: From e0d32ae1d0e57132feefa149e6244200a97501bc Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Mon, 8 Feb 2021 12:11:41 +0000 Subject: [PATCH 2/5] Add an "ErrorListener" to collect errors during parsing --- lib/src/error_listener.dart | 33 +++++++++++++++++++++++++++++++++ lib/src/loader.dart | 7 +++++-- lib/src/parser.dart | 9 +++++++-- lib/src/scanner.dart | 18 ++++++++++++++++-- lib/yaml.dart | 28 ++++++++++++++++++++++------ test/utils.dart | 8 ++++++++ test/yaml_test.dart | 20 ++++++++++++++++++-- 7 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 lib/src/error_listener.dart diff --git a/lib/src/error_listener.dart b/lib/src/error_listener.dart new file mode 100644 index 0000000..d156655 --- /dev/null +++ b/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 errors = []; + + @override + void onError(YamlError error) => errors.add(error); +} diff --git a/lib/src/loader.dart b/lib/src/loader.dart index dafc59b..0056a64 100644 --- a/lib/src/loader.dart +++ b/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 @@ class Loader { 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/lib/src/parser.dart b/lib/src/parser.dart index 695c1d0..2f759e9 100644 --- a/lib/src/parser.dart +++ b/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 @@ class Parser { 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/lib/src/scanner.dart b/lib/src/scanner.dart index 171364c..260da1c 100644 --- a/lib/src/scanner.dart +++ b/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 @@ class Scanner { /// 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 @@ class Scanner { } /// 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 @@ class Scanner { 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 @@ class Scanner { _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/lib/yaml.dart b/lib/yaml.dart index 988de2d..dec4bc0 100644 --- a/lib/yaml.dart +++ b/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 @@ export 'src/yaml_node.dart' hide setSpan; /// /// 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 @@ YamlNode loadYamlNode(String yaml, {Uri? sourceUrl, bool recover = false}) => /// 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/test/utils.dart b/test/utils.dart index ce05481..0904458 100644 --- a/test/utils.dart +++ b/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 @@ Map deepEqualsMap([Map? from]) { 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/test/yaml_test.dart b/test/yaml_test.dart index 014da96..dffb9d5 100644 --- a/test/yaml_test.dart +++ b/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 @@ void main() { }); 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 @@ void main() { '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 @@ void main() { 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 @@ void main() { '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''' From 585bfe9085cebb48891f7b3cf7398e9aff2da161 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Mon, 8 Feb 2021 18:18:27 +0000 Subject: [PATCH 3/5] Use YamlExceptions directly + tidy up error reporting --- lib/src/error_listener.dart | 24 +++--------------------- lib/src/scanner.dart | 22 ++++++++++------------ test/utils.dart | 8 ++++---- 3 files changed, 17 insertions(+), 37 deletions(-) diff --git a/lib/src/error_listener.dart b/lib/src/error_listener.dart index d156655..c302a4d 100644 --- a/lib/src/error_listener.dart +++ b/lib/src/error_listener.dart @@ -1,33 +1,15 @@ -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); + void onError(YamlException error); } /// An [ErrorListener] that collects all errors into [errors]. class ErrorCollector extends ErrorListener { - final List errors = []; + final List errors = []; @override - void onError(YamlError error) => errors.add(error); + void onError(YamlException error) => errors.add(error); } diff --git a/lib/src/scanner.dart b/lib/src/scanner.dart index 260da1c..e6e2062 100644 --- a/lib/src/scanner.dart +++ b/lib/src/scanner.dart @@ -496,13 +496,9 @@ class Scanner { 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 error.toException(); - } + _reportError(YamlException("Expected ':'.", _scanner.emptySpan)); + _tokens.insert(key.tokenNumber - _tokensParsed, + Token(TokenType.key, key.location.pointSpan() as FileSpan)); } _simpleKeys[i] = null; @@ -1641,11 +1637,13 @@ class Scanner { } } - /// 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; + /// Reports a [YamlException] to [_errorListener] and if [_recover] is false, + /// throw the exception. + void _reportError(YamlException exception) { + _errorListener?.onError(exception); + if (!_recover) { + throw exception; + } } } diff --git a/test/utils.dart b/test/utils.dart index 0904458..f90ac5b 100644 --- a/test/utils.dart +++ b/test/utils.dart @@ -4,7 +4,6 @@ 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]. @@ -24,10 +23,11 @@ Map deepEqualsMap([Map? from]) { } /// 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) { +void expectErrorAtLineCol( + YamlException 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)); + 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 From 53d358eb3e35c48192b4568ba608cc9e11214e5c Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Thu, 11 Feb 2021 17:15:40 +0000 Subject: [PATCH 4/5] Add missing copyright --- lib/src/error_listener.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/error_listener.dart b/lib/src/error_listener.dart index c302a4d..dac7d63 100644 --- a/lib/src/error_listener.dart +++ b/lib/src/error_listener.dart @@ -1,3 +1,7 @@ +// Copyright (c) 2021, 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. + import 'yaml_exception.dart'; /// A listener that is notified of [YamlError]s during scanning/parsing. From 338b1027191adb2b9917bff1fbeca6567bec9952 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Fri, 12 Feb 2021 11:13:50 +0000 Subject: [PATCH 5/5] Disallow errorListener when not recovering, and don't call error listener if throwing --- lib/src/parser.dart | 8 +++++++- lib/src/scanner.dart | 6 +++--- lib/yaml.dart | 8 +++----- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 2f759e9..aeeda4e 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -36,9 +36,15 @@ class Parser { bool get isDone => _state == _State.END; /// Creates a parser that parses [source]. + /// + /// If [recover] is true, will attempt to recover from parse errors and may return + /// invalid or synthetic nodes. If [errorListener] is also supplied, its onError + /// method will be called for each error recovered from. It is not valid to + /// provide [errorListener] if [recover] is false. Parser(String source, {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) - : _scanner = Scanner(source, + : assert(recover || errorListener == null), + _scanner = Scanner(source, sourceUrl: sourceUrl, recover: recover, errorListener: errorListener); diff --git a/lib/src/scanner.dart b/lib/src/scanner.dart index e6e2062..5cb329a 100644 --- a/lib/src/scanner.dart +++ b/lib/src/scanner.dart @@ -1637,13 +1637,13 @@ class Scanner { } } - /// Reports a [YamlException] to [_errorListener] and if [_recover] is false, - /// throw the exception. + /// Reports a [YamlException] to [_errorListener] if [_recover] is true, + /// otherwise throws the exception. void _reportError(YamlException exception) { - _errorListener?.onError(exception); if (!_recover) { throw exception; } + _errorListener?.onError(exception); } } diff --git a/lib/yaml.dart b/lib/yaml.dart index dec4bc0..d150725 100644 --- a/lib/yaml.dart +++ b/lib/yaml.dart @@ -33,11 +33,9 @@ export 'src/yaml_node.dart' hide setSpan; /// originated for error reporting. /// /// If [recover] is true, will attempt to recover from parse errors and may return -/// invalid or synthetic nodes. -/// -/// 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. +/// invalid or synthetic nodes. If [errorListener] is also supplied, its onError +/// method will be called for each error recovered from. It is not valid to +/// provide [errorListener] if [recover] is false. dynamic loadYaml(String yaml, {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) => loadYamlNode(yaml,