From 0d6f24d7cc79a49c048443e75b4a6092d503cd3a Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 03:38:24 +0200 Subject: [PATCH] fix: skip leading whitespace before parse-action loc on Or and MatchFirst Parse actions attached directly to an Or ('^') or MatchFirst ('|') expression received the raw, un-whitespace-skipped loc instead of the whitespace-skipped location of the matched tokens. And ('+') already handled this correctly by setting callPreparse = True in its __init__; Or and MatchFirst never did the equivalent correction, so they always fell through to the callPreparse = False default set by the shared ParseExpression base class. Set callPreparse on Or and MatchFirst using the same "do all alternatives pre-skip whitespace" condition Or.parseImpl already uses internally, so tokens_start is corrected only when it is safe to do so without changing which alternative matches (e.g. alternatives such as White() or AtStringStart() that intentionally disable callPreparse still prevent the pre-skip, exactly as before). Fixes #557 --- pyparsing/core.py | 7 +++++++ tests/test_unit.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/pyparsing/core.py b/pyparsing/core.py index 7ed0af9..10f612e 100644 --- a/pyparsing/core.py +++ b/pyparsing/core.py @@ -4703,6 +4703,9 @@ class Or(ParseExpression): if self.exprs: self._may_return_empty = any(e.mayReturnEmpty for e in self.exprs) self.skipWhitespace = all(e.skipWhitespace for e in self.exprs) + # pre-skip whitespace here only if every alternative would have + # done so itself, matching the condition used in parseImpl below + self.callPreparse = all(e.callPreparse for e in self.exprs) else: self._may_return_empty = True @@ -4863,6 +4866,10 @@ class MatchFirst(ParseExpression): if self.exprs: self._may_return_empty = any(e.mayReturnEmpty for e in self.exprs) self.skipWhitespace = all(e.skipWhitespace for e in self.exprs) + # pre-skip whitespace here only if every alternative would have + # done so itself; an alternative starting with White() must not + # lose the whitespace it needs to see + self.callPreparse = all(e.callPreparse for e in self.exprs) else: self._may_return_empty = True diff --git a/tests/test_unit.py b/tests/test_unit.py index 4bd5e70..f6c3cf9 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -8910,6 +8910,29 @@ class Test02_WithoutPackrat(ppt.TestParseResultsAsserts, TestCase): "failed testing parse actions being run inside a NotAny", ) + def testParseActionLocOnOrAndMatchFirst(self): + # a parse action on Or ('^') or MatchFirst ('|') should receive the + # whitespace-skipped loc, just as And ('+') does + src = " abc" + expected_loc = src.index("abc") + word = pp.Word(pp.alphas) + + for label, expr in ( + ("Or", word ^ pp.Literal("xyz")), + ("MatchFirst", word | pp.Literal("xyz")), + ): + with self.subTest(label=label): + seen_locs = [] + expr = expr.copy().set_parse_action( + lambda s, loc, toks: seen_locs.append(loc) + ) + expr.parse_string(src) + self.assertEqual( + [expected_loc], + seen_locs, + f"{label} parse action received wrong loc (whitespace not skipped)", + ) + def testParseResultsNameBelowUngroupedName(self): rule_num = pp.Regex("[0-9]+")("LIT_NUM*") list_num = pp.Group(