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
19 changes: 16 additions & 3 deletions src/Ramstack.Parsing/ParseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,22 @@ public void RestorePosition(PositionBookmark bookmark)
_match.Length = 0;
}

/// <inheritdoc />
public readonly override string ToString() =>
GenerateErrorMessage(_source, _diagnostics.Index, _diagnostics.ToString());
/// <summary>
/// Returns the accumulated parsing diagnostics, including line and column information.
/// </summary>
/// <returns>
/// The formatted diagnostic message, or an empty string if no diagnostics have been recorded.
/// </returns>
/// <remarks>
/// The presence or absence of diagnostics does not indicate whether parsing succeeded.
/// </remarks>
public readonly override string ToString()
{
var message = _diagnostics.ToString();
return message.Length != 0
? GenerateErrorMessage(_source, _diagnostics.Index, message)
: "";
}

/// <summary>
/// Generates a formatted error message that includes line and column information.
Expand Down
64 changes: 64 additions & 0 deletions src/Ramstack.Parsing/Parser.Where.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
namespace Ramstack.Parsing;

partial class Parser
{
/// <summary>
/// Creates a parser that accepts a parsed value only when it satisfies the specified predicate.
/// </summary>
/// <typeparam name="T">The type of the value produced by the parser.</typeparam>
/// <param name="parser">The parser that produces the value to check.</param>
/// <param name="predicate">The condition that the parsed value must satisfy.</param>
/// <param name="expected">A non-empty description of the expected value for diagnostics.</param>
/// <returns>
/// A parser that returns the original value on success, or an ordinary parse failure otherwise.
/// </returns>
/// <remarks>
/// <para>
/// Prefer a side-effect-free predicate: restoring input does not undo changes to external state.
/// </para>
/// <para>
/// Exceptions are not converted into predicate rejections.
/// </para>
/// </remarks>
public static Parser<T> Where<T>(this Parser<T> parser, Func<T, bool> predicate, string expected)
{
Argument.ThrowIfNull(parser);
Argument.ThrowIfNull(predicate);
Argument.ThrowIfNullOrEmpty(expected);

return new WhereParser<T>(parser, predicate, expected);
}

/// <summary>
/// Represents a parser that checks a condition on the parsed value.
/// </summary>
/// <typeparam name="T">The type of the value produced by the parser.</typeparam>
/// <param name="parser">The parser that produces the value to check.</param>
/// <param name="predicate">The condition that the parsed value must satisfy.</param>
/// <param name="expected">A non-empty description of the expected value for diagnostics.</param>
private sealed class WhereParser<T>(Parser<T> parser, Func<T, bool> predicate, string expected) : Parser<T>
{
/// <inheritdoc />
public override bool TryParse(ref ParseContext context, [NotNullWhen(true)] out T? value)
{
var bookmark = context.BookmarkPosition();

if (!parser.TryParse(ref context, out value))
return false;

if (predicate(value))
return true;

context.ReportExpected(
bookmark.Position, expected);

context.RestorePosition(bookmark);
value = default;
return false;
}

//
// NOTE: The default void wrapper preserves the value needed by the predicate.
//
}
}
5 changes: 4 additions & 1 deletion src/Ramstack.Parsing/Parser`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,13 @@ public ParseResult<T> Parse(ReadOnlySpan<char> source)
};
}

var message = context.ToString();
return new ParseResult<T>
{
Length = 0,
ErrorMessage = context.ToString()
ErrorMessage = message.Length == 0
? "Parsing failed."
: message
};
}
catch (FatalErrorException exception)
Expand Down
8 changes: 8 additions & 0 deletions tests/Ramstack.Parsing.Tests/ParsersTests.As.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,12 @@ public void AsTest()
Any.As("character").Parse("").ErrorMessage,
Is.EqualTo("(1:1) Expected character"));
}

[Test]
public void As_Success_PreservesLaterDiagnostics()
{
var parser = L('a').As("letter").ThenIgnore(L('b'));

Assert.That(parser.Parse("ax").ErrorMessage, Is.EqualTo("(1:2) Expected 'b'"));
}
}
8 changes: 8 additions & 0 deletions tests/Ramstack.Parsing.Tests/ParsersTests.Eof.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,12 @@ public void EofTest(string input)
Assert.That(Eol.Map(m => (m.Index, m.Length)).Parse(input).Value, Is.EqualTo((0, 0)));
Assert.That(Eol.Map(m => m.ToString()).Parse(input).Value, Is.Empty);
}

[Test]
public void Eof_TrailingInput_ReportsEndOfInput()
{
var parser = L('4').ThenIgnore(Eof);

Assert.That(parser.Parse("4x").ErrorMessage, Is.EqualTo("(1:2) Expected end of input"));
}
}
Loading
Loading