diff --git a/src/Ramstack.Parsing/ParseContext.cs b/src/Ramstack.Parsing/ParseContext.cs
index 23043cf..1d5b68a 100644
--- a/src/Ramstack.Parsing/ParseContext.cs
+++ b/src/Ramstack.Parsing/ParseContext.cs
@@ -263,9 +263,22 @@ public void RestorePosition(PositionBookmark bookmark)
_match.Length = 0;
}
- ///
- public readonly override string ToString() =>
- GenerateErrorMessage(_source, _diagnostics.Index, _diagnostics.ToString());
+ ///
+ /// Returns the accumulated parsing diagnostics, including line and column information.
+ ///
+ ///
+ /// The formatted diagnostic message, or an empty string if no diagnostics have been recorded.
+ ///
+ ///
+ /// The presence or absence of diagnostics does not indicate whether parsing succeeded.
+ ///
+ public readonly override string ToString()
+ {
+ var message = _diagnostics.ToString();
+ return message.Length != 0
+ ? GenerateErrorMessage(_source, _diagnostics.Index, message)
+ : "";
+ }
///
/// Generates a formatted error message that includes line and column information.
diff --git a/src/Ramstack.Parsing/Parser.Where.cs b/src/Ramstack.Parsing/Parser.Where.cs
new file mode 100644
index 0000000..efe312a
--- /dev/null
+++ b/src/Ramstack.Parsing/Parser.Where.cs
@@ -0,0 +1,64 @@
+namespace Ramstack.Parsing;
+
+partial class Parser
+{
+ ///
+ /// Creates a parser that accepts a parsed value only when it satisfies the specified predicate.
+ ///
+ /// The type of the value produced by the parser.
+ /// The parser that produces the value to check.
+ /// The condition that the parsed value must satisfy.
+ /// A non-empty description of the expected value for diagnostics.
+ ///
+ /// A parser that returns the original value on success, or an ordinary parse failure otherwise.
+ ///
+ ///
+ ///
+ /// Prefer a side-effect-free predicate: restoring input does not undo changes to external state.
+ ///
+ ///
+ /// Exceptions are not converted into predicate rejections.
+ ///
+ ///
+ public static Parser Where(this Parser parser, Func predicate, string expected)
+ {
+ Argument.ThrowIfNull(parser);
+ Argument.ThrowIfNull(predicate);
+ Argument.ThrowIfNullOrEmpty(expected);
+
+ return new WhereParser(parser, predicate, expected);
+ }
+
+ ///
+ /// Represents a parser that checks a condition on the parsed value.
+ ///
+ /// The type of the value produced by the parser.
+ /// The parser that produces the value to check.
+ /// The condition that the parsed value must satisfy.
+ /// A non-empty description of the expected value for diagnostics.
+ private sealed class WhereParser(Parser parser, Func predicate, string expected) : Parser
+ {
+ ///
+ 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.
+ //
+ }
+}
diff --git a/src/Ramstack.Parsing/Parser`1.cs b/src/Ramstack.Parsing/Parser`1.cs
index 5a4cfd4..d078cd4 100644
--- a/src/Ramstack.Parsing/Parser`1.cs
+++ b/src/Ramstack.Parsing/Parser`1.cs
@@ -88,10 +88,13 @@ public ParseResult Parse(ReadOnlySpan source)
};
}
+ var message = context.ToString();
return new ParseResult
{
Length = 0,
- ErrorMessage = context.ToString()
+ ErrorMessage = message.Length == 0
+ ? "Parsing failed."
+ : message
};
}
catch (FatalErrorException exception)
diff --git a/tests/Ramstack.Parsing.Tests/ParsersTests.As.cs b/tests/Ramstack.Parsing.Tests/ParsersTests.As.cs
index f3b8769..e4e855e 100644
--- a/tests/Ramstack.Parsing.Tests/ParsersTests.As.cs
+++ b/tests/Ramstack.Parsing.Tests/ParsersTests.As.cs
@@ -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'"));
+ }
}
diff --git a/tests/Ramstack.Parsing.Tests/ParsersTests.Eof.cs b/tests/Ramstack.Parsing.Tests/ParsersTests.Eof.cs
index e0fe46e..dffad86 100644
--- a/tests/Ramstack.Parsing.Tests/ParsersTests.Eof.cs
+++ b/tests/Ramstack.Parsing.Tests/ParsersTests.Eof.cs
@@ -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"));
+ }
}
diff --git a/tests/Ramstack.Parsing.Tests/ParsersTests.Where.cs b/tests/Ramstack.Parsing.Tests/ParsersTests.Where.cs
new file mode 100644
index 0000000..97a6240
--- /dev/null
+++ b/tests/Ramstack.Parsing.Tests/ParsersTests.Where.cs
@@ -0,0 +1,430 @@
+using static Ramstack.Parsing.Parser;
+
+namespace Ramstack.Parsing;
+
+partial class ParsersTests
+{
+ [TestCase("0", 0)]
+ [TestCase("49", 49)]
+ [TestCase("123", 123)]
+ public void Where_AcceptedValue_ReturnsValue(string input, int expected)
+ {
+ var parser = L('!')
+ .Then(
+ Literal
+ .Number()
+ .ThenIgnore(S))
+ .Where(
+ static n => n is >= 0 and <= 255,
+ "octet");
+
+ var context = new ParseContext($"!{input} \t?");
+
+ Assert.That(parser.TryParse(ref context, out var value), Is.True);
+ Assert.That(value, Is.EqualTo(expected));
+ Assert.That(context.Position, Is.EqualTo(input.Length + 3));
+ Assert.That(context.MatchedSegment.ToString(), Is.EqualTo(input));
+ Assert.That(context.Remaining.ToString(), Is.EqualTo("?"));
+ }
+
+ [TestCase("-1")]
+ [TestCase("256")]
+ public void Where_RejectedValue_RestoresPosition(string input)
+ {
+ var parser = L('!')
+ .Then(S)
+ .Then(
+ Literal
+ .Number()
+ .ThenIgnore(S))
+ .Where(
+ static n => n is >= 0 and <= 255,
+ "octet");
+
+ var context = new ParseContext($"! \t{input} ?");
+
+ Assert.That(parser.TryParse(ref context, out var value), Is.False);
+ Assert.That(value, Is.Zero);
+ Assert.That(context.Position, Is.Zero);
+ Assert.That(context.MatchedSegment.Length, Is.Zero);
+ Assert.That(context.MatchedSegment.ToString(), Is.Empty);
+ Assert.That(context.Remaining.ToString(), Is.EqualTo(context.Source.ToString()));
+ Assert.That(context.ToString(), Is.EqualTo("(1:1) Expected octet"));
+ }
+
+ [TestCase("-1")]
+ [TestCase("256")]
+ public void Where_RejectedValueAfterPrefix_RestoresPosition(string input)
+ {
+ var parser = Literal
+ .Number()
+ .ThenIgnore(S)
+ .Where(
+ n => n is >= 0 and <= 255,
+ "octet");
+
+ var context = new ParseContext($"!{input} ?");
+
+ Assert.That(L('!').TryParse(ref context, out _), Is.True);
+ Assert.That(context.Position, Is.EqualTo(1));
+ Assert.That(context.MatchedSegment.Index, Is.EqualTo(0));
+ Assert.That(context.MatchedSegment.Length, Is.EqualTo(1));
+
+ Assert.That(parser.TryParse(ref context, out var value), Is.False);
+ Assert.That(value, Is.Zero);
+
+ Assert.That(context.Position, Is.EqualTo(1));
+ Assert.That(context.MatchedSegment.Length, Is.Zero);
+ Assert.That(context.Remaining.ToString(), Is.EqualTo($"{input} ?"));
+ Assert.That(context.ToString(), Is.EqualTo("(1:2) Expected octet"));
+ }
+
+ [Test]
+ public void Where_PrimitiveFailure_PreservesInnerState()
+ {
+ var calls = 0;
+ var parser = L('c').Where(_ =>
+ {
+ calls++;
+ return true;
+ }, "validated character");
+
+ var context = new ParseContext("!abx");
+
+ Assert.That(L('!').TryParse(ref context, out _), Is.True);
+
+ Assert.That(parser.TryParse(ref context, out var value), Is.False);
+
+ Assert.That(value, Is.EqualTo('\0'));
+ Assert.That(calls, Is.Zero);
+
+ Assert.That(context.Position, Is.EqualTo(1));
+ Assert.That(context.MatchedSegment.Index, Is.EqualTo(0));
+ Assert.That(context.MatchedSegment.Length, Is.EqualTo(1));
+ Assert.That(context.ToString(), Is.EqualTo("(1:2) Expected 'c'"));
+ }
+
+ [Test]
+ public void Where_CompositeFailure_PreservesInnerState()
+ {
+ var calls = 0;
+ var parser = L("ab").Then(L('c')).Where(_ =>
+ {
+ calls++;
+ return true;
+ }, "validated character");
+
+ var context = new ParseContext("!abx");
+
+ Assert.That(L('!').TryParse(ref context, out _), Is.True);
+
+ Assert.That(parser.TryParse(ref context, out var value), Is.False);
+
+ Assert.That(value, Is.EqualTo('\0'));
+ Assert.That(calls, Is.Zero);
+
+ Assert.That(context.Position, Is.EqualTo(1));
+ Assert.That(context.MatchedSegment.Length, Is.Zero);
+ Assert.That(context.ToString(), Is.EqualTo("(1:4) Expected 'c'"));
+ }
+
+ [Test]
+ public void Where_RejectedAlternative_ParsesFallbackFromOriginalPosition()
+ {
+ var number = Literal.Number();
+ var parser = L('!')
+ .Then(number
+ .Where(n => n <= 100, "small integer")
+ .Or(number));
+
+ var context = new ParseContext("!150?");
+
+ Assert.That(parser.TryParse(ref context, out var value), Is.True);
+ Assert.That(value, Is.EqualTo(150));
+ Assert.That(context.Position, Is.EqualTo(4));
+ Assert.That((ValueTuple)context.MatchedSegment, Is.EqualTo((1, 3)));
+ }
+
+ [Test]
+ public void Where_RejectedFirstAlternative_ParsesFallback()
+ {
+ var parserA = L('a').Do(_ => 1).Where(n => n == 2, "two");
+ var parserB = L('a').Do(_ => 2);
+
+ var parser = parserA.Or(parserB);
+ var result = parser.Parse("a");
+
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.Value, Is.EqualTo(2));
+ }
+
+ [Test]
+ public void Where_OuterRejection_DoesNotRetryInnerAlternative()
+ {
+ var parserA = L('a').Do(_ => 1);
+ var parserB = L('a').Do(_ => 2);
+
+ var parser = parserA.Or(parserB).Where(n => n == 2, "two");
+ var result = parser.Parse("a");
+
+ Assert.That(result.Success, Is.False);
+ Assert.That(result.Value, Is.Zero);
+ Assert.That(result.ErrorMessage, Is.EqualTo("(1:1) Expected two"));
+ }
+
+ [Test]
+ public void Where_OptionalRejection_LeavesInputForNextParser()
+ {
+ var parser = Seq(
+ L('a').Where(_ => false, "accepted character").Optional(),
+ L('a').Optional());
+
+ var context = new ParseContext("a");
+
+ Assert.That(parser.TryParse(ref context, out var value), Is.True);
+ Assert.That(value.Value1.HasValue, Is.False);
+ Assert.That(value.Value2.HasValue, Is.True);
+ Assert.That(value.Value2.Value, Is.EqualTo('a'));
+ }
+
+ [Test]
+ public void Where_RepetitionRejection_StopsBeforeRejectedValue()
+ {
+ var parser = Character
+ .Digit.Where(
+ static c => c != '3',
+ "digit other than three")
+ .ZeroOrMore();
+
+ var context = new ParseContext("123");
+
+ Assert.That(parser.TryParse(ref context, out var value), Is.True);
+ Assert.That(value, Is.EqualTo(['1', '2']));
+
+ Assert.That(context.Position, Is.EqualTo(2));
+ Assert.That(context.MatchedSegment.ToString(), Is.EqualTo("12"));
+ Assert.That(context.Remaining.ToString(), Is.EqualTo("3"));
+
+ Assert.That(
+ parser.Void().Then(L('3')).ThenIgnore(Eof).TryParse("123", out _),
+ Is.True);
+ }
+
+ [TestCase("42", true, 42)]
+ [TestCase("256", false, 256)]
+ public void Where_VoidAndSuppressedDiagnostics_PreserveRequiredTransformations(string input, bool accepted, int expected)
+ {
+ var mapCalls = 0;
+ var doCalls = 0;
+ var predicateCalls = 0;
+
+ var parser = Character
+ .Digit.OneOrMore()
+ .Map(m =>
+ {
+ mapCalls++;
+ return m.ToString();
+ })
+ .Do(text =>
+ {
+ doCalls++;
+ return int.Parse(text, System.Globalization.CultureInfo.InvariantCulture);
+ })
+ .Where(n =>
+ {
+ predicateCalls++;
+ Assert.That(n, Is.EqualTo(expected));
+ return n is >= 0 and <= 255;
+ }, "octet")
+ .Void();
+
+ var context = new ParseContext(input)
+ {
+ DiagnosticState = DiagnosticState.Suppressed
+ };
+
+ Assert.That(parser.TryParse(ref context, out _), Is.EqualTo(accepted));
+
+ Assert.That(mapCalls, Is.EqualTo(1));
+ Assert.That(doCalls, Is.EqualTo(1));
+ Assert.That(predicateCalls, Is.EqualTo(1));
+
+ Assert.That(context.Position, Is.EqualTo(accepted ? input.Length : 0));
+ Assert.That(context.MatchedSegment.Length, Is.EqualTo(accepted ? input.Length : 0));
+ Assert.That(context.DiagnosticState, Is.EqualTo(DiagnosticState.Suppressed));
+ Assert.That(context.ToString(), Is.Empty);
+ }
+
+ [TestCase("42", true, null)]
+ [TestCase("256", false, "(1:1) Expected octet")]
+ [TestCase("abc", false, "(1:1) Expected integer")]
+ [TestCase("42xyz", false, "(1:3) Expected end of input")]
+ public void Where_CompleteInput_ReportsPredicateInnerOrEofFailure(string input, bool accepted, string? error)
+ {
+ var parser = Literal
+ .Number("integer")
+ .Where(
+ n => n is >= 0 and <= 255,
+ "octet")
+ .ThenIgnore(Eof);
+
+ var result = parser.Parse(input);
+ var voidResult = parser.Void().Parse(input);
+
+ Assert.That(result.Success, Is.EqualTo(accepted));
+ Assert.That(result.ErrorMessage, Is.EqualTo(error));
+
+ Assert.That(voidResult.Success, Is.EqualTo(accepted));
+ Assert.That(voidResult.ErrorMessage, Is.EqualTo(error));
+
+ Assert.That(parser.TryParse(input, out _), Is.EqualTo(accepted));
+ }
+
+ [Test]
+ public void Where_AlternativeFailures_MergeExpectationsAtSamePosition()
+ {
+ var parser = L('a').Where(_ => false, "accepted character").Or(L('b'));
+
+ Assert.That(parser.Parse("a").ErrorMessage, Is.EqualTo("(1:1) Expected accepted character or 'b'"));
+ }
+
+ [Test]
+ public void Where_FartherInnerDiagnostic_TakesPrecedenceOverPredicateRejection()
+ {
+ var parser = L('a').ThenIgnore(L('b').Optional()).Where(_ => false, "accepted character");
+
+ Assert.That(parser.Parse("a?").ErrorMessage, Is.EqualTo("(1:2) Expected 'b'"));
+ }
+
+ [Test]
+ public void Where_FartherExistingDiagnostic_IsPreservedAfterRollback()
+ {
+ var parser =
+ L("ab").Then(L('c'))
+ .Or(L('a').Where(_ => false, "accepted character"));
+
+ Assert.That(parser.Parse("ab?").ErrorMessage, Is.EqualTo("(1:3) Expected 'c'"));
+ }
+
+ [TestCase("ab")]
+ [TestCase("ax")]
+ public void Where_NamedParser_ReplacesInnerAndPredicateExpectations(string input)
+ {
+ var parser = L('a')
+ .Then(L('b'))
+ .Where(_ => false, "accepted character")
+ .As("rule");
+
+ Assert.That(parser.Parse(input).ErrorMessage, Is.EqualTo("(1:1) Expected rule"));
+ Assert.That(parser.Void().Parse(input).ErrorMessage, Is.EqualTo("(1:1) Expected rule"));
+ Assert.That(parser.Void().As("renamed").Parse(input).ErrorMessage, Is.EqualTo("(1:1) Expected renamed"));
+ }
+
+ [TestCase("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "(1:1) Expected identifier of at most 64 characters", 1)]
+ [TestCase("int", "(1:1) Expected non-reserved identifier", 2)]
+ [TestCase("foo__bar", "(1:1) Expected identifier without double underscore", 3)]
+ [TestCase("foo_bar", null, 3)]
+ public void Where_ChainedChecks_ReportFirstFailingStage(string input, string? error, int executedStages)
+ {
+ var stageCalls = new int[3];
+ var reserved = new HashSet { "class", "int", "return" };
+
+ // An unnamed lexeme would leave the trailing expectation of ZeroOrMore
+ // at the end of input, and by the farthest-error policy it would shadow
+ // the expected messages of the chain.
+ var parser = Seq(
+ Set("A-Za-z_"),
+ Set("A-Za-z0-9_").ZeroOrMore())
+ .Text()
+ .As("identifier")
+ .Where(s =>
+ {
+ stageCalls[0]++;
+ return s.Length <= 64;
+ }, "identifier of at most 64 characters")
+ .Where(s =>
+ {
+ stageCalls[1]++;
+ return !reserved.Contains(s);
+ }, "non-reserved identifier")
+ .Where(s =>
+ {
+ stageCalls[2]++;
+ return !s.Contains("__", StringComparison.Ordinal);
+ }, "identifier without double underscore");
+
+ var result = parser.Parse(input);
+
+ Assert.That(result.Success, Is.EqualTo(error is null), result.ErrorMessage);
+ Assert.That(result.ErrorMessage, Is.EqualTo(error));
+
+ var expectedCalls = Enumerable
+ .Range(0, stageCalls.Length)
+ .Select(i => i < executedStages ? 1 : 0)
+ .ToArray();
+
+ Assert.That(stageCalls, Is.EqualTo(expectedCalls));
+ }
+
+ [Test]
+ public void Where_ChainedChecks_KeepBaseValidatorMessages()
+ {
+ var octet = Literal
+ .Number("integer")
+ .Where(n => n is >= 0 and <= 255, "octet");
+
+ var nonZeroOctet = octet
+ .Where(n => n != 0, "non-zero octet");
+
+ Assert.That(octet.Parse("0").Success, Is.True);
+ Assert.That(nonZeroOctet.Parse("7").Success, Is.True);
+ Assert.That(nonZeroOctet.Parse("0").ErrorMessage, Is.EqualTo("(1:1) Expected non-zero octet"));
+ Assert.That(nonZeroOctet.Parse("256").ErrorMessage, Is.EqualTo("(1:1) Expected octet"));
+ }
+
+ [Test]
+ public void Where_PredicateThrows_UsesCallbackExceptionContract()
+ {
+ var exception = new InvalidOperationException("predicate failure");
+ var fallbackCalls = 0;
+
+ var parser = L('a')
+ .Where(_ => throw exception, "accepted character")
+ .Or(L('a').Do(c =>
+ {
+ fallbackCalls++;
+ return c;
+ }));
+
+ var result = parser.Parse("a");
+
+ Assert.That(result.Success, Is.False);
+ Assert.That(result.Exception, Is.SameAs(exception));
+ Assert.That(result.ErrorMessage, Does.StartWith("(1:2) ").And.Contain("predicate failure"));
+
+ Assert.That(Assert.Throws(() => parser.TryParse("a", out _)), Is.SameAs(exception));
+ Assert.That(parser.Void().Parse("a").Exception, Is.SameAs(exception));
+ Assert.That(Assert.Throws(() => parser.Void().TryParse("a", out _)), Is.SameAs(exception));
+ Assert.That(fallbackCalls, Is.Zero);
+ }
+
+ [Test]
+ public void Where_FatalErrorInPredicate_UsesFatalErrorContract()
+ {
+ var parser = L('a').Where(c =>
+ {
+ if (c == 'a')
+ FatalError("forced failure");
+
+ return true;
+ }, "accepted character");
+
+ 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);
+ }
+}
diff --git a/tests/Ramstack.Parsing.Tests/ParsersTests.cs b/tests/Ramstack.Parsing.Tests/ParsersTests.cs
index 50dbd77..0707516 100644
--- a/tests/Ramstack.Parsing.Tests/ParsersTests.cs
+++ b/tests/Ramstack.Parsing.Tests/ParsersTests.cs
@@ -3,6 +3,75 @@ namespace Ramstack.Parsing;
[TestFixture]
public partial class ParsersTests
{
+ [TestCase("", 0)]
+ [TestCase("a\nb", 2)]
+ public void ToString_NoDiagnostics_ReturnsEmptyString(string source, int position)
+ {
+ var context = new ParseContext(source);
+ context.Advance(position);
+
+ Assert.That(context.ToString(), Is.Empty);
+ }
+
+ [Test]
+ public void ToString_RecordedDiagnostics_PreservesErrorPositionAfterRollback()
+ {
+ var context = new ParseContext("a\nb");
+ var bookmark = context.BookmarkPosition();
+
+ context.Advance(2);
+ context.ReportExpected("digit");
+ context.RestorePosition(bookmark);
+
+ Assert.That(context.Position, Is.Zero);
+ Assert.That(context.ToString(), Is.EqualTo("(2:1) Expected digit"));
+ }
+
+ [Test]
+ public void ToString_SuccessfulOptionalMatch_PreservesDiagnostics()
+ {
+ var parser = Parser.L('a').Optional();
+ var context = new ParseContext("b");
+
+ Assert.That(parser.TryParse(ref context, out var value), Is.True);
+ Assert.That(value.HasValue, Is.False);
+ Assert.That(context.ToString(), Is.EqualTo("(1:1) Expected 'a'"));
+
+ var result = parser.Parse("b");
+
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.Value.HasValue, Is.False);
+ Assert.That(result.Length, Is.Zero);
+ Assert.That(result.ErrorMessage, Is.Null);
+ }
+
+ [Test]
+ public void Parse_FailedMatchWithoutDiagnostics_ReturnsGenericFailure()
+ {
+ var result = new NonReportingParser().Parse("x");
+
+ Assert.That(result.Success, Is.False);
+ Assert.That(result.Length, Is.Zero);
+ Assert.That(result.ErrorMessage, Is.EqualTo("Parsing failed."));
+ Assert.That(result.ToString(), Is.EqualTo("Parsing failed."));
+ Assert.That(result.Exception, Is.Null);
+ }
+
+ #region Inner type: NonReportingParser
+
+ private sealed class NonReportingParser : Parser
+ {
+ public override bool TryParse(ref ParseContext context, out char value)
+ {
+ value = '\0';
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Inner type: BoundedParser
+
// Bound parser invocations so a missing progress check fails instead of hanging the test run.
private sealed class BoundedParser(Parser parser) : Parser
{
@@ -17,4 +86,6 @@ public override bool TryParse(ref ParseContext context, [NotNullWhen(true)] out
return parser.TryParse(ref context, out value);
}
}
+
+ #endregion
}