LibRegex: Track optional capture groups for match_length_minimum

Backreferences can match the empty string when the referenced group
didn't participate in the match, so we shouldn't add their length to the
match_length_minimum, as it makes us skip valid matches.
This commit is contained in:
aplefull 2026-02-17 12:58:33 +01:00 committed by Ali Mohammad Pur
parent 53a98f26d4
commit 6f1b7c8d50
3 changed files with 43 additions and 1 deletions

View file

@ -182,6 +182,7 @@ ALWAYS_INLINE void Parser::reset()
m_parser_state.error = Error::NoError;
m_parser_state.error_token = { TokenType::Eof, 0, {} };
m_parser_state.capture_group_minimum_lengths.clear();
m_parser_state.optional_capture_groups.clear();
m_parser_state.capture_groups_count = 0;
m_parser_state.named_capture_groups_count = 0;
m_parser_state.named_capture_groups.clear();
@ -1011,6 +1012,7 @@ bool ECMA262Parser::parse_disjunction(ByteCode& stack, size_t& match_length_mini
{
size_t total_match_length_minimum = NumericLimits<size_t>::max();
Vector<ByteCode> alternatives;
size_t initial_capture_groups_count = m_parser_state.capture_groups_count;
TemporaryChange<size_t> alternative_id_change { m_current_alternative_id, 1 };
@ -1033,6 +1035,7 @@ bool ECMA262Parser::parse_disjunction(ByteCode& stack, size_t& match_length_mini
if (alternatives.size() > 1) {
m_parser_state.greedy_lookaround = false;
mark_capture_groups_as_optional_from(initial_capture_groups_count);
}
Optimizer::append_alternation(stack, alternatives.span());
match_length_minimum = total_match_length_minimum;
@ -1060,6 +1063,8 @@ bool ECMA262Parser::parse_term(ByteCode& stack, size_t& match_length_minimum, Pa
ByteCode atom_stack;
size_t minimum_atom_length = 0;
size_t initial_capture_groups_count = m_parser_state.capture_groups_count;
auto parse_with_quantifier = [&] {
bool did_parse_one = false;
if (m_should_use_browser_extended_grammar)
@ -1078,6 +1083,9 @@ bool ECMA262Parser::parse_term(ByteCode& stack, size_t& match_length_minimum, Pa
if (!parse_with_quantifier())
return false;
if (minimum_atom_length == 0)
mark_capture_groups_as_optional_from(initial_capture_groups_count);
stack.extend(move(atom_stack));
match_length_minimum += minimum_atom_length;
return true;
@ -1128,6 +1136,7 @@ bool ECMA262Parser::parse_assertion(ByteCode& stack, [[maybe_unused]] size_t& ma
}
if (should_parse_forward_assertion && try_skip("!"sv)) {
enter_capture_group_scope();
size_t initial_capture_groups_count = m_parser_state.capture_groups_count;
ScopeGuard quit_scope {
[this] {
exit_capture_group_scope();
@ -1137,6 +1146,7 @@ bool ECMA262Parser::parse_assertion(ByteCode& stack, [[maybe_unused]] size_t& ma
return false;
stack.insert_bytecode_lookaround(move(assertion_stack), ByteCode::LookAroundType::NegatedLookAhead);
clear_all_capture_groups_in_scope(stack);
mark_capture_groups_as_optional_from(initial_capture_groups_count);
return true;
}
@ -1161,6 +1171,7 @@ bool ECMA262Parser::parse_assertion(ByteCode& stack, [[maybe_unused]] size_t& ma
}
if (try_skip("<!"sv)) {
enter_capture_group_scope();
size_t initial_capture_groups_count = m_parser_state.capture_groups_count;
ScopeGuard quit_scope {
[this] {
exit_capture_group_scope();
@ -1170,6 +1181,7 @@ bool ECMA262Parser::parse_assertion(ByteCode& stack, [[maybe_unused]] size_t& ma
return false;
stack.insert_bytecode_lookaround(move(assertion_stack), ByteCode::LookAroundType::NegatedLookBehind, length_dummy);
clear_all_capture_groups_in_scope(stack);
mark_capture_groups_as_optional_from(initial_capture_groups_count);
return true;
}
@ -1207,6 +1219,7 @@ bool ECMA262Parser::parse_quantifiable_assertion(ByteCode& stack, size_t&, Parse
}
if (try_skip("!"sv)) {
enter_capture_group_scope();
size_t initial_capture_groups_count = m_parser_state.capture_groups_count;
ScopeGuard quit_scope {
[this] {
exit_capture_group_scope();
@ -1217,6 +1230,7 @@ bool ECMA262Parser::parse_quantifiable_assertion(ByteCode& stack, size_t&, Parse
stack.insert_bytecode_lookaround(move(assertion_stack), ByteCode::LookAroundType::NegatedLookAhead);
clear_all_capture_groups_in_scope(stack);
mark_capture_groups_as_optional_from(initial_capture_groups_count);
return true;
}
@ -1636,7 +1650,8 @@ bool ECMA262Parser::parse_atom_escape(ByteCode& stack, size_t& match_length_mini
// See if this is a "back"-reference (we've already parsed the group it refers to)
auto maybe_length = m_parser_state.capture_group_minimum_lengths.get(escape.value());
if (maybe_length.has_value()) {
match_length_minimum += maybe_length.value();
if (!m_parser_state.optional_capture_groups.contains(escape.value()))
match_length_minimum += maybe_length.value();
stack.insert_bytecode_compare_values({ { CharacterCompareType::Reference, (ByteCodeValueType)escape.value() } });
return true;
}
@ -1679,6 +1694,8 @@ bool ECMA262Parser::parse_atom_escape(ByteCode& stack, size_t& match_length_mini
auto maybe_length = m_parser_state.capture_group_minimum_lengths.get(group_index);
if (maybe_length.has_value()) {
// Backward reference
if (!m_parser_state.optional_capture_groups.contains(group_index))
match_length_minimum += maybe_length.value();
stack.insert_bytecode_compare_values({ { CharacterCompareType::NamedReference, static_cast<ByteCodeValueType>(group_index) } });
} else {
// Self-reference or forward reference

View file

@ -14,6 +14,7 @@
#include <AK/FlyString.h>
#include <AK/Forward.h>
#include <AK/HashMap.h>
#include <AK/HashTable.h>
#include <AK/Types.h>
#include <AK/Vector.h>
#include <LibUnicode/Forward.h>
@ -123,6 +124,7 @@ protected:
bool in_negated_character_class { false };
AllOptions regex_options;
HashMap<size_t, size_t> capture_group_minimum_lengths;
HashTable<size_t> optional_capture_groups;
OrderedHashMap<FlyString, Vector<NamedCaptureGroup>> named_capture_groups;
struct UnresolvedNamedReference {
@ -323,6 +325,12 @@ private:
stack.insert_bytecode_clear_capture_group(index);
}
void mark_capture_groups_as_optional_from(size_t first_group)
{
for (size_t i = first_group + 1; i <= m_parser_state.capture_groups_count; ++i)
m_parser_state.optional_capture_groups.set(i);
}
// ECMA-262's flavour of regex is a bit weird in that it allows backrefs to reference "future" captures, and such backrefs
// always match the empty string. So we have to know how many capturing parenthesis there are, but we don't want to always
// parse it twice, so we'll just do so when it's actually needed.

View file

@ -1485,6 +1485,23 @@ TEST_CASE(zero_width_backreference)
EXPECT_EQ(result.matches.first().view.to_byte_string(), "b"sv);
EXPECT_EQ(result.capture_group_matches.first()[0].view.to_byte_string(), ""sv);
}
{
Regex<ECMA262> re("(x)?\\1y"sv);
auto result = re.match("y"sv);
EXPECT_EQ(result.success, true);
EXPECT_EQ(result.matches.first().view, "y"sv);
EXPECT(result.capture_group_matches.first()[0].view.is_null());
}
{
Regex<ECMA262> re("(?!(y)y)(\\1)z"sv, ECMAScriptFlags::Global);
auto result = re.match("xyyz"sv);
EXPECT_EQ(result.success, true);
EXPECT_EQ(result.matches.first().view, "z"sv);
EXPECT(result.capture_group_matches.first()[0].view.is_null());
EXPECT_EQ(result.capture_group_matches.first()[1].view.to_byte_string(), ""sv);
}
}
TEST_CASE(account_for_opcode_size_calculating_incoming_jump_edges)