Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion include/cons_expr/cons_expr.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ template<typename CharType> [[nodiscard]] constexpr Token<CharType> next_token(s
auto location = std::next(input.begin());
while (location != input.end()) {
if (*location == chars::ch('\\')) {
in_escape = true;
in_escape = !in_escape;
} else if (*location == chars::ch('"') && !in_escape) {
++location;
break;
Expand Down
35 changes: 35 additions & 0 deletions test/string_escape_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,41 @@ TEST_CASE("String escape edge cases", "[string][escape][edge]")
STATIC_CHECK(evaluate_expected<std::string_view>("\"\\n\"", "\n"));
}

// Regression tests for issue #7: escaped backslash before the closing quote
// caused the tokenizer to stay in escape mode and eat the terminator.
TEST_CASE("Tokenizer backslash escape state", "[string][escape][tokenizer][regression][issue-7]")
{
// Direct tokenizer test: `"a\\" "b"` must split into two string tokens.
// With the bug, `\\` left the tokenizer in escape mode, so the first `"`
// after `\\` was consumed instead of terminating the literal.
constexpr auto split_two_literals = []() constexpr {
auto token = lefticus::next_token(std::string_view(R"("a\\" "b")"));
return token.parsed == std::string_view(R"("a\\")")
&& token.remaining == std::string_view(R"("b")");
};
STATIC_CHECK(split_two_literals());

// End-to-end: evaluating the two-literal sequence should yield "b".
STATIC_CHECK(evaluate_expected<std::string_view>(R"("a\\" "b")", "b"));

// `"\\"` is a valid one-character string whose content is a single backslash.
STATIC_CHECK(evaluate_expected<std::string_view>(R"("\\")", "\\"));

// A valid string ending in `\\` followed by a newline must parse cleanly
// and unescape to `a\`. With the bug the tokenizer ran past the terminator,
// leaving a token that did not end in `"`, so the parser reported the
// literal as unterminated.
STATIC_CHECK(evaluate_expected<std::string_view>("\"a\\\\\"\n", "a\\"));

// A consecutive-escape terminator: `"\\\\"` is two backslashes in the
// source string literal. Every pair must cancel so the final `"` closes.
constexpr auto double_escaped_backslash_pair = []() constexpr {
auto token = lefticus::next_token(std::string_view(R"("\\\\")"));
return token.parsed == std::string_view(R"("\\\\")") && token.remaining.empty();
};
STATIC_CHECK(double_escaped_backslash_pair());
}

// Branch Coverage Enhancement Tests - Missing String Cases

TEST_CASE("String escape error conditions for coverage", "[string][escape][coverage]")
Expand Down
Loading