diff --git a/src/Ramstack.Parsing/Parser`1.cs b/src/Ramstack.Parsing/Parser`1.cs index ceb75e0..5a4cfd4 100644 --- a/src/Ramstack.Parsing/Parser`1.cs +++ b/src/Ramstack.Parsing/Parser`1.cs @@ -34,6 +34,18 @@ protected Parser(string? name) => /// /// if the parser succeeded; otherwise, . /// + /// + /// + /// Any failed match returns , including a forced termination + /// through or . + /// + /// + /// Exceptions thrown by user-provided functions, such as the callbacks passed to + /// Map, Do, or Fold, are not suppressed and propagate to the caller. + /// Use to capture them in + /// instead. + /// + /// public bool TryParse(ReadOnlySpan source, [NotNullWhen(true)] out T? value) { try @@ -46,9 +58,9 @@ public bool TryParse(ReadOnlySpan source, [NotNullWhen(true)] out T? value if (TryParse(ref context, out value)) return true; } - catch + catch (FatalErrorException) { - // Ignore exceptions + // A forced parse error is an ordinary failed match for the boolean API. } value = default; diff --git a/tests/Ramstack.Parsing.Tests/ParsersTests.Exceptions.cs b/tests/Ramstack.Parsing.Tests/ParsersTests.Exceptions.cs new file mode 100644 index 0000000..c838763 --- /dev/null +++ b/tests/Ramstack.Parsing.Tests/ParsersTests.Exceptions.cs @@ -0,0 +1,82 @@ +using static Ramstack.Parsing.Parser; + +namespace Ramstack.Parsing; + +partial class ParsersTests +{ + [Test] + public void TryParse_FatalErrorParser_ReturnsFalse() + { + var parser = Fail("failure message"); + + Assert.That(parser.TryParse("x", out var value), Is.False); + Assert.That(value, Is.EqualTo('\0')); + } + + [Test] + public void TryParse_FatalErrorCallback_ReturnsFalse() + { + var parser = L('a').Do(c => + { + if (c == 'a') + FatalError("forced failure"); + + return c; + }); + + Assert.That(parser.TryParse("a", out var value), Is.False); + Assert.That(value, Is.EqualTo('\0')); + + var result = parser.Parse("a"); + + Assert.That(result.ErrorMessage, Is.EqualTo("(1:2) forced failure")); + Assert.That(result.Exception, Is.Null); + } + + [Test] + public void TryParse_FailedMatch_ReturnsFalse() + { + var parser = L('a'); + + Assert.That(parser.TryParse("b", out var value), Is.False); + Assert.That(value, Is.EqualTo('\0')); + } + + [Test] + public void TryParse_UserCallbackThrows_PropagatesException() + { + var parser = L('a').Map(_ => throw new InvalidOperationException("callback failure")); + + Assert.That( + () => parser.TryParse("a", out _), + Throws + .TypeOf() + .With.Message.EqualTo("callback failure")); + } + + [Test] + public void TryParse_UninitializedDeferredParser_ReturnsFalse() + { + var parser = Deferred(); + + Assert.That(parser.TryParse("x", out var value), Is.False); + Assert.That(value, Is.EqualTo('\0')); + + Assert.That( + parser.Parse("x").ErrorMessage, + Is.EqualTo("(1:1) The deferred parser has not been initialized.")); + } + + [Test] + public void Parse_UserCallbackThrows_ReportsException() + { + var parser = L('a').Map(_ => throw new InvalidOperationException("callback failure")); + + var result = parser.Parse("a"); + + Assert.That(result.Success, Is.False); + Assert.That(result.Exception, Is.TypeOf()); + Assert.That(result.Exception!.Message, Is.EqualTo("callback failure")); + Assert.That(result.ErrorMessage, Does.Contain("callback failure")); + } +}