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
This commit is contained in:
dualfroz committed 2026-09-04 03:38:24 +02:00
1 parent dc67483b42
commit 286b7d7b78
2 files changed
+39

No files matched your search

+13
View File
@@ -4703,6 +4703,13 @@ 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)
# only pre-skip whitespace here (so a parse action attached
# directly to this Or gets the whitespace-skipped loc) if every
# alternative would itself have pre-skipped it; otherwise leave
# this to each alternative, same condition already used in
# parseImpl below to decide whether to pre-skip before trying
# the alternatives
self.callPreparse = all(e.callPreparse for e in self.exprs)
else:
self._may_return_empty = True
@@ -4863,6 +4870,12 @@ 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)
# only pre-skip whitespace here (so a parse action attached
# directly to this MatchFirst gets the whitespace-skipped loc)
# if every alternative would itself have pre-skipped it; when an
# alternative depends on seeing unskipped whitespace (e.g. a
# leading White()), leave preparse to that alternative instead
self.callPreparse = all(e.callPreparse for e in self.exprs)
else:
self._may_return_empty = True
+26
View File
@@ -8910,6 +8910,32 @@ class Test02_WithoutPackrat(ppt.TestParseResultsAsserts, TestCase):
"failed testing parse actions being run inside a NotAny",
)
def testParseActionLocOnOrAndMatchFirst(self):
# see Issue #557
# a parse action attached directly to an Or ('^') or MatchFirst ('|')
# expression should receive the whitespace-skipped loc of the
# matched tokens, just as it would if the same alternatives were
# combined with And ('+')
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(