diff --git a/Libraries/LibRegex/RegexParser.cpp b/Libraries/LibRegex/RegexParser.cpp index f6030507f7..7189f7b727 100644 --- a/Libraries/LibRegex/RegexParser.cpp +++ b/Libraries/LibRegex/RegexParser.cpp @@ -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::max(); Vector alternatives; + size_t initial_capture_groups_count = m_parser_state.capture_groups_count; TemporaryChange 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("(group_index) } }); } else { // Self-reference or forward reference diff --git a/Libraries/LibRegex/RegexParser.h b/Libraries/LibRegex/RegexParser.h index b47d644935..3ae3d33a5c 100644 --- a/Libraries/LibRegex/RegexParser.h +++ b/Libraries/LibRegex/RegexParser.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -123,6 +124,7 @@ protected: bool in_negated_character_class { false }; AllOptions regex_options; HashMap capture_group_minimum_lengths; + HashTable optional_capture_groups; OrderedHashMap> 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. diff --git a/Tests/LibRegex/TestRegex.cpp b/Tests/LibRegex/TestRegex.cpp index b205723aea..922e3ca817 100644 --- a/Tests/LibRegex/TestRegex.cpp +++ b/Tests/LibRegex/TestRegex.cpp @@ -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 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 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)