Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/Ramstack.Parsing/Parser`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ protected Parser(string? name) =>
/// <returns>
/// <see langword="true"/> if the parser succeeded; otherwise, <see langword="false"/>.
/// </returns>
/// <remarks>
/// <para>
/// Any failed match returns <see langword="false"/>, including a forced termination
/// through <see cref="Parser.Fail{T}(string)"/> or <see cref="Parser.FatalError(string)"/>.
/// </para>
/// <para>
/// Exceptions thrown by user-provided functions, such as the callbacks passed to
/// <c>Map</c>, <c>Do</c>, or <c>Fold</c>, are not suppressed and propagate to the caller.
/// Use <see cref="Parse(ReadOnlySpan{char})"/> to capture them in
/// <see cref="ParseResult{T}.Exception"/> instead.
/// </para>
/// </remarks>
public bool TryParse(ReadOnlySpan<char> source, [NotNullWhen(true)] out T? value)
{
try
Expand All @@ -46,9 +58,9 @@ public bool TryParse(ReadOnlySpan<char> 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;
Expand Down
82 changes: 82 additions & 0 deletions tests/Ramstack.Parsing.Tests/ParsersTests.Exceptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using static Ramstack.Parsing.Parser;

namespace Ramstack.Parsing;

partial class ParsersTests
{
[Test]
public void TryParse_FatalErrorParser_ReturnsFalse()
{
var parser = Fail<char>("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<char, char>(_ => throw new InvalidOperationException("callback failure"));

Assert.That(
() => parser.TryParse("a", out _),
Throws
.TypeOf<InvalidOperationException>()
.With.Message.EqualTo("callback failure"));
}

[Test]
public void TryParse_UninitializedDeferredParser_ReturnsFalse()
{
var parser = Deferred<char>();

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<char, char>(_ => throw new InvalidOperationException("callback failure"));

var result = parser.Parse("a");

Assert.That(result.Success, Is.False);
Assert.That(result.Exception, Is.TypeOf<InvalidOperationException>());
Assert.That(result.Exception!.Message, Is.EqualTo("callback failure"));
Assert.That(result.ErrorMessage, Does.Contain("callback failure"));
}
}
Loading