From 781743394ad4e0d6187e315d8757c301797249b4 Mon Sep 17 00:00:00 2001 From: rameel Date: Thu, 24 Sep 2026 23:34:28 +0500 Subject: [PATCH] parsers: stop TryParse from suppressing user exceptions Catch only FatalErrorException so forced parse failures still return false, while exceptions from user callbacks and library errors propagate. Parse continues to capture exceptions in ParseResult.Exception. --- src/Ramstack.Parsing/Parser`1.cs | 16 +++- .../ParsersTests.Exceptions.cs | 82 +++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 tests/Ramstack.Parsing.Tests/ParsersTests.Exceptions.cs 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")); + } +}