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
61 changes: 25 additions & 36 deletions parser/internal/lexer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -335,17 +335,31 @@ Token Lexer::Lex() {
return SetError(start, GetPosition(), "unexpected character");
}

// Consumes characters up to and including the first occurrence of character `c`
// without interpreting backslashes as escapes.
// Returns true if `c` was found and consumed; false if end of input was
// reached.
bool Lexer::ConsumeUntilAfter(char32_t c) {
// Consumes characters up to and including the first occurrence of character
// `c`. If `is_raw` is false, backslashes are interpreted as escapes. Returns
// true if `c` was found and consumed; false if end of input or an unescaped
// newline was reached.
bool Lexer::ConsumeUntilAfter(char32_t c, bool is_raw) {
ABSL_DCHECK_NE(c, '\n');
for (int32_t pos = position_; pos < content_.size(); ++pos) {
if (content_.at(pos) == c) {
AdvanceProcessingNewLines(pos + 1);
return true;
ABSL_DCHECK_NE(c, '\r');
int32_t pos = position_;
bool escaped = false;
while (pos < content_.size()) {
char32_t cc = content_.at(pos);
if (cc == '\n' || cc == '\r') {
AdvanceProcessingNewLines(pos);
return false;
}
if (!is_raw && cc == '\\') {
escaped = !escaped;
} else {
if (cc == c && (is_raw || !escaped)) {
AdvanceProcessingNewLines(pos + 1);
return true;
}
escaped = false;
}
++pos;
}
AdvanceProcessingNewLines(content_.size());
return false;
Expand Down Expand Up @@ -376,31 +390,6 @@ bool Lexer::ConsumeUntilAfterString(std::u32string_view s) {
return false;
}

// Consumes characters up to and including the first occurrence of `c` that is
// not preceded by an odd number of backslash ('\') escape characters. Returns
// true if an unescaped `c` was found and consumed; false if reached EOF.
bool Lexer::ConsumeUntilAfterUnescaped(char32_t c) {
ABSL_DCHECK_NE(c, '\n');
ABSL_DCHECK_NE(c, '\\');
int32_t pos = position_;
bool escaped = false;
while (pos < content_.size()) {
char32_t cc = content_.at(pos);
if (cc == '\\') {
escaped = !escaped;
} else {
if (cc == c && !escaped) {
AdvanceProcessingNewLines(pos + 1);
return true;
}
escaped = false;
}
++pos;
}
AdvanceProcessingNewLines(content_.size());
return false;
}

// Consumes characters up to and including the first occurrence of substring `s`
// where the first character of `s` is not preceded by an odd number of
// backslashes. Returns true if an unescaped `s` was found and consumed; false
Expand Down Expand Up @@ -564,7 +553,7 @@ TokenType Lexer::ConsumeIntegralSuffix() {
Token Lexer::ConsumeQuotedIdent() {
int32_t start = GetPosition();
Advance(1);
if (!ConsumeUntilAfter('`')) {
if (!ConsumeUntilAfter('`', /*is_raw=*/true)) {
return SetError(start, GetPosition(), "unterminated quoted identifier");
}
return MakeToken(TokenType::kIdent, start, GetPosition());
Expand All @@ -584,7 +573,7 @@ Token Lexer::ConsumeStringLiteral(int32_t start, char32_t quote, bool is_bytes,
return MakeToken(is_bytes ? TokenType::kBytes : TokenType::kString, start,
GetPosition());
}
if (is_raw ? !ConsumeUntilAfter(quote) : !ConsumeUntilAfterUnescaped(quote)) {
if (!ConsumeUntilAfter(quote, is_raw)) {
return SetError(start, GetPosition(),
is_bytes ? "unterminated bytes literal"
: "unterminated string literal");
Expand Down
12 changes: 4 additions & 8 deletions parser/internal/lexer.h
Original file line number Diff line number Diff line change
Expand Up @@ -217,21 +217,17 @@ class Lexer final {
void AtEndTokenCreated() { done_ = true; }

// Consumes characters up to and including the first occurrence of character
// `c` without interpreting backslashes as escapes. Returns true if `c` was
// found and consumed; false if end of input was reached.
[[nodiscard]] bool ConsumeUntilAfter(char32_t c);
// `c`. If `is_raw` is false, backslashes are interpreted as escapes. Returns
// true if `c` was found and consumed; false if end of input or an unescaped
// newline was reached.
[[nodiscard]] bool ConsumeUntilAfter(char32_t c, bool is_raw);

// Consumes characters up to and including the first occurrence of substring
// `s` without interpreting backslashes as escapes (`s` must not contain
// newlines). Returns true if `s` was found and consumed; false if end of
// input was reached.
[[nodiscard]] bool ConsumeUntilAfterString(std::u32string_view s);

// Consumes characters up to and including the first occurrence of `c` that is
// not preceded by an odd number of backslash ('\') escape characters. Returns
// true if an unescaped `c` was found and consumed; false if reached EOF.
[[nodiscard]] bool ConsumeUntilAfterUnescaped(char32_t c);

// Consumes characters up to and including the first occurrence of substring
// `s` where the first character of `s` is not preceded by an odd number of
// backslashes. Returns true if an unescaped `s` was found and consumed; false
Expand Down
30 changes: 18 additions & 12 deletions parser/internal/lexer_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -334,29 +334,23 @@ TEST(LexerTest, LineOffsets) {
EXPECT_EQ(line_offsets[1], 13);
}

TEST(LexerTest, LineOffsetsInStringsAndIdentifiers) {
std::string_view source_text =
"'''multi\nline'''\n\"another\nline\"\n`ident\nhere`";
TEST(LexerTest, LineOffsetsInMultilineStrings) {
std::string_view source_text = "'''multi\nline'''\n\"\"\"another\nline\"\"\"";
ASSERT_OK_AND_ASSIGN(auto source, cel::NewSource(source_text));
Lexer lexer(*source);

EXPECT_THAT(lexer.Lex(),
IsToken(source.get(), TokenType::kString, "'''multi\nline'''"));
EXPECT_THAT(lexer.Lex(), IsToken(source.get(), TokenType::kWhitespace, "\n"));
EXPECT_THAT(lexer.Lex(),
IsToken(source.get(), TokenType::kString, "\"another\nline\""));
EXPECT_THAT(lexer.Lex(), IsToken(source.get(), TokenType::kWhitespace, "\n"));
EXPECT_THAT(lexer.Lex(),
IsToken(source.get(), TokenType::kIdent, "`ident\nhere`"));
EXPECT_THAT(lexer.Lex(), IsToken(source.get(), TokenType::kString,
"\"\"\"another\nline\"\"\""));
EXPECT_THAT(lexer.Lex(), IsToken(source.get(), TokenType::kEnd, ""));

auto line_offsets = source->line_offsets();
ASSERT_GE(line_offsets.size(), 5);
ASSERT_GE(line_offsets.size(), 3);
EXPECT_EQ(line_offsets[0], 9);
EXPECT_EQ(line_offsets[1], 17);
EXPECT_EQ(line_offsets[2], 26);
EXPECT_EQ(line_offsets[3], 32);
EXPECT_EQ(line_offsets[4], 39);
EXPECT_EQ(line_offsets[2], 28);
}

struct LexerErrorTestCase {
Expand Down Expand Up @@ -392,6 +386,12 @@ INSTANTIATE_TEST_SUITE_P(
.expected_error_location = "\n | \"unterminated"
"\n | .............^",
},
LexerErrorTestCase{
.source = "\"another\nline\"",
.expected_error_message = "unterminated string literal",
.expected_error_location = "\n | \"another"
"\n | ........^",
},
LexerErrorTestCase{
.source = "0x",
.expected_error_message =
Expand Down Expand Up @@ -439,6 +439,12 @@ INSTANTIATE_TEST_SUITE_P(
.expected_error_location = "\n | `unterminated quoted"
"\n | ....................^",
},
LexerErrorTestCase{
.source = "`ident\nhere`",
.expected_error_message = "unterminated quoted identifier",
.expected_error_location = "\n | `ident"
"\n | ......^",
},
LexerErrorTestCase{
.source = "'''unterminated multi",
.expected_error_message = "unterminated string literal",
Expand Down
108 changes: 108 additions & 0 deletions parser/internal/pratt_parser_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,36 @@ std::vector<TestCase> GetParserTestCases() {
"\x07\x08\x0c\n\r\t\x0b'\"\\? Legal escapes"^#1:string#
)",
},
TestCase{
.source = "'''hello\nworld'''",
.expected_ast = R"(
"hello\nworld"^#1:string#
)",
},
TestCase{
.source = "\"\"\"hello\nworld\"\"\"",
.expected_ast = R"(
"hello\nworld"^#1:string#
)",
},
TestCase{
.source = "r\"\"\"hello\nworld\"\"\"",
.expected_ast = R"(
"hello\nworld"^#1:string#
)",
},
TestCase{
.source = "\"\"\"hello\\\"\"\"world\"\"\"",
.expected_ast = R"(
"hello\"\"\"world"^#1:string#
)",
},
TestCase{
.source = "'''hello\\'''world'''",
.expected_ast = R"(
"hello'''world"^#1:string#
)",
},
TestCase{
.source = "a",
.expected_ast = R"(
Expand Down Expand Up @@ -1394,6 +1424,84 @@ std::vector<ErrorTestCase> GetErrorTestCases() {
" | \"unterminated\n"
" | ^",
},
ErrorTestCase{
.source = "\"\"\"hello\nworld",
.expected_error =
"ERROR: <input>:1:1: Syntax error: unterminated string literal\n"
" | \"\"\"hello\n"
" | ^",
},
ErrorTestCase{
.source = "'''hello\nworld",
.expected_error =
"ERROR: <input>:1:1: Syntax error: unterminated string literal\n"
" | '''hello\n"
" | ^",
},
ErrorTestCase{
.source = "r\"\"\"hello\nworld",
.expected_error =
"ERROR: <input>:1:1: Syntax error: unterminated string literal\n"
" | r\"\"\"hello\n"
" | ^",
},
ErrorTestCase{
.source = "\"hello\nworld\"",
.expected_error =
"ERROR: <input>:1:1: Syntax error: unterminated string literal\n"
" | \"hello\n"
" | ^\n"
"ERROR: <input>:2:1: Syntax error: unexpected token after "
"expression\n"
" | world\"\n"
" | ^",
},
ErrorTestCase{
.source = "'hello\nworld'",
.expected_error =
"ERROR: <input>:1:1: Syntax error: unterminated string literal\n"
" | 'hello\n"
" | ^\n"
"ERROR: <input>:2:1: Syntax error: unexpected token after "
"expression\n"
" | world'\n"
" | ^",
},
ErrorTestCase{
.source = "r\"hello\nworld\"",
.expected_error =
"ERROR: <input>:1:1: Syntax error: unterminated string literal\n"
" | r\"hello\n"
" | ^\n"
"ERROR: <input>:2:1: Syntax error: unexpected token after "
"expression\n"
" | world\"\n"
" | ^",
},
ErrorTestCase{
.source = "`hello\nworld`",
.expected_error =
"ERROR: <input>:1:1: Syntax error: unterminated quoted "
"identifier\n"
" | `hello\n"
" | ^\n"
"ERROR: <input>:2:1: Syntax error: unexpected token after "
"expression\n"
" | world`\n"
" | ^",
.enable_quoted_identifiers = true,
},
ErrorTestCase{
.source = "\"hello\rworld\"",
.expected_error =
"ERROR: <input>:1:1: Syntax error: unterminated string literal\n"
" | \"hello\rworld\"\n"
" | ^\n"
"ERROR: <input>:1:8: Syntax error: unexpected token after "
"expression\n"
" | \"hello\rworld\"\n"
" | .......^",
},
ErrorTestCase{
.source = "b\"unterminated",
.expected_error =
Expand Down
Loading
Loading