Skip to content

Commit 158397b

Browse files
committed
Fix lexing for unterminated strings/heredocs etc.
When we hit EOF and still have lex modes left, it means some content was unterminated. Heredocs specifically have logic that needs to happen when the body finished lexing. If we don't reset the mode back to how it was before, it will not continue lexing at the correct place.
1 parent 24e4ae8 commit 158397b

4 files changed

Lines changed: 120 additions & 3 deletions

File tree

src/prism.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9783,6 +9783,12 @@ parser_lex(pm_parser_t *parser) {
97839783
unsigned int semantic_token_seen = parser->semantic_token_seen;
97849784
parser->semantic_token_seen = true;
97859785

9786+
// We'll jump to this label when we are about to encounter an EOF.
9787+
// If we still have lex_modes on the stack, we pop them so that cleanup
9788+
// can happen. For example, we should still continue parsing after a heredoc
9789+
// identifier, even if the heredoc body was syntax invalid.
9790+
switch_lex_mode:
9791+
97869792
switch (parser->lex_modes.current->mode) {
97879793
case PM_LEX_DEFAULT:
97889794
case PM_LEX_EMBEXPR:
@@ -9856,6 +9862,14 @@ parser_lex(pm_parser_t *parser) {
98569862
// We'll check if we're at the end of the file. If we are, then we
98579863
// need to return the EOF token.
98589864
if (parser->current.end >= parser->end) {
9865+
// We may be missing closing tokens. We should pop modes one by one
9866+
// to do the appropriate cleanup like moving next_start for heredocs.
9867+
// Only when no mode is remaining will we actually emit the EOF token.
9868+
if (parser->lex_modes.current->mode != PM_LEX_DEFAULT) {
9869+
lex_mode_pop(parser);
9870+
goto switch_lex_mode;
9871+
}
9872+
98599873
// If we hit EOF, but the EOF came immediately after a newline,
98609874
// set the start of the token to the newline. This way any EOF
98619875
// errors will be reported as happening on that line rather than
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<<A+B
2+
^ unterminated heredoc; can't find string "A" anywhere before EOF
3+
^ unexpected '+', ignoring it
4+
^ unterminated heredoc; can't find string "A" anywhere before EOF
5+
#{C
6+
^ unexpected heredoc ending; expected an argument
7+
^ unexpected heredoc ending, expecting end-of-input
8+
^ unexpected heredoc ending, ignoring it
9+
^ unexpected end-of-input, assuming it is closing the parent top level context
10+
^ expected a `}` to close the embedded expression
11+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<<A+B
2+
^ unterminated heredoc; can't find string "A" anywhere before EOF
3+
#{C + "#{"}
4+
^ unterminated string meets end of file
5+
^ unexpected end-of-input, assuming it is closing the parent top level context
6+
^ expected a `}` to close the embedded expression
7+
^ unterminated string; expected a closing delimiter for the interpolated string
8+
^ expected a `}` to close the embedded expression
9+

test/prism/lex_test.rb

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,94 @@ def test_parse_lex_file
4848
end
4949

5050
if RUBY_VERSION >= "3.3"
51-
def test_lex_compare
52-
prism = Prism.lex_compat(File.read(__FILE__), version: "current").value
53-
ripper = Ripper.lex(File.read(__FILE__))
51+
def test_lex_compat
52+
source = "foo bar"
53+
prism = Prism.lex_compat(source, version: "current").value
54+
ripper = Ripper.lex(source)
5455
assert_equal(ripper, prism)
5556
end
5657
end
58+
59+
def test_lex_interpolation_unterminated
60+
assert_lexed(<<~'RUBY'.strip, <<~'LEXED')
61+
"#{
62+
RUBY
63+
STRING_BEGIN(1,0)-(1,1)("\"")
64+
EMBEXPR_BEGIN(1,1)-(1,3)("\#{")
65+
EOF(1,3)-(1,3)("")
66+
LEXED
67+
68+
assert_lexed(<<~'RUBY', <<~'LEXED')
69+
"#{
70+
RUBY
71+
STRING_BEGIN(1,0)-(1,1)("\"")
72+
EMBEXPR_BEGIN(1,1)-(1,3)("\#{")
73+
IGNORED_NEWLINE(1,3)-(2,0)("\n")
74+
EOF(2,0)-(2,0)("")
75+
LEXED
76+
end
77+
78+
def test_lex_interpolation_unterminated_with_content
79+
# No trailing newline.
80+
# FIXME: Emits EOL twice.
81+
assert_lexed(<<~'RUBY'.strip, <<~'LEXED')
82+
"#{C
83+
RUBY
84+
STRING_BEGIN(1,0)-(1,1)("\"")
85+
EMBEXPR_BEGIN(1,1)-(1,3)("\#{")
86+
CONSTANT(1,3)-(1,4)("C")
87+
EOF(1,4)-(1,4)("")
88+
EOF(1,4)-(1,4)("")
89+
LEXED
90+
91+
# Trailing newline
92+
assert_lexed(<<~'RUBY', <<~'LEXED')
93+
"#{C
94+
RUBY
95+
STRING_BEGIN(1,0)-(1,1)("\"")
96+
EMBEXPR_BEGIN(1,1)-(1,3)("\#{")
97+
CONSTANT(1,3)-(1,4)("C")
98+
NEWLINE(1,4)-(2,0)("\n")
99+
EOF(2,0)-(2,0)("")
100+
LEXED
101+
end
102+
103+
def test_lex_heredoc_unterminated
104+
# No trailing newline
105+
assert_lexed(<<~'RUBY'.strip, <<~'LEXED')
106+
<<A+B
107+
#{C
108+
RUBY
109+
HEREDOC_START(1,0)-(1,3)("<<A")
110+
EMBEXPR_BEGIN(2,0)-(2,2)("\#{")
111+
CONSTANT(2,2)-(2,3)("C")
112+
HEREDOC_END(2,3)-(2,3)("")
113+
PLUS(1,3)-(1,4)("+")
114+
CONSTANT(1,4)-(1,5)("B")
115+
NEWLINE(1,5)-(2,0)("\n")
116+
EOF(2,3)-(2,3)("")
117+
LEXED
118+
119+
# Trailing newline
120+
assert_lexed(<<~'RUBY', <<~'LEXED')
121+
<<A+B
122+
#{C
123+
RUBY
124+
HEREDOC_START(1,0)-(1,3)("<<A")
125+
EMBEXPR_BEGIN(2,0)-(2,2)("\#{")
126+
CONSTANT(2,2)-(2,3)("C")
127+
NEWLINE(2,3)-(3,0)("\n")
128+
HEREDOC_END(3,0)-(3,0)("")
129+
PLUS(1,3)-(1,4)("+")
130+
CONSTANT(1,4)-(1,5)("B")
131+
NEWLINE(1,5)-(2,0)("\n")
132+
EOF(2,3)-(3,0)("\n")
133+
LEXED
134+
end
135+
136+
def assert_lexed(code, expected)
137+
actual = Prism.lex(code).value.map { |token| token[0].pretty_inspect }
138+
assert_equal(expected, actual.join)
139+
end
57140
end
58141
end

0 commit comments

Comments
 (0)