fix(parser): do not re-lex template substitution tail after fatal error (#26230)

When the parser records a fatal error it fast-forwards the lexer to end of file. `re_lex_template_substitution_tail` did not check for that, so an incomplete template substitution would still re-lex the `}` and derive the token's start from a cursor which had already jumped to the end.

With tokens enabled, that fails the debug assertion in `finish_next_inner`, because the token being replaced is no longer the one on top of the collected token stream. Release builds silently overwrite an unrelated token instead. Tokens are only collected for Oxlint's JS plugins, which bail out if any parsing error, which is why this hadn't been noticed before.

Fix by adding the same `fatal_error` guard, added to other re-lex methods in #11023.

This does make a difference to diagnostics output for invalid code matching this pattern. TypeScript snapshot loses an "Unterminated string" diagnostic on 2 fixtures. Those errors came from the bogus re-lex reading past end of file. Both these fixtures already have an "Unexpected token" error. The additional "Unterminated string" errors were incorrect, and this fix has the side effect of removing them.
This commit is contained in:
overlookmotel committed 2026-09-02 21:41:11 +00:00
1 parent 07851b9c5d
commit 3ab9bd122c
3 files changed
+17 -14

No files matched your search

+3
View File
@@ -241,6 +241,9 @@ impl<'a, C: Config> ParserImpl<'a, C> {
/// Tell lexer to read a template substitution tail
pub(crate) fn re_lex_template_substitution_tail(&mut self) {
if self.fatal_error.is_some() {
return;
}
if self.at(Kind::RCurly) {
self.token = self.lexer.next_template_substitution_tail();
}
+14
View File
@@ -1304,4 +1304,18 @@ mod test {
]
);
}
// A fatal error fast-forwards the lexer to end of file. Re-lexing the `}` of a template
// substitution after that would derive a token position from the moved cursor, and so
// overwrite an unrelated token in the collected stream.
#[test]
fn tokens_when_template_substitution_fails() {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, "`${}`", SourceType::default())
.with_config(config::TokensParserConfig)
.parse();
assert!(ret.panicked);
assert!(ret.tokens.is_empty());
}
}
@@ -13339,13 +13339,6 @@ Expect Syntax Error: tasks/coverage/typescript/tests/cases/conformance/types/wit
5 │ f `123qdawdrqw${
╰────
× Unterminated string
╭─[typescript/tests/cases/compiler/taggedTemplatesWithIncompleteTemplateExpressions2.ts:5:20]
4 │ // Incomplete call, enough parameters.
5 │ f `123qdawdrqw${ }${
· ─
╰────
× Unexpected token
╭─[typescript/tests/cases/compiler/taggedTemplatesWithIncompleteTemplateExpressions2.ts:5:18]
4 │ // Incomplete call, enough parameters.
@@ -13359,13 +13352,6 @@ Expect Syntax Error: tasks/coverage/typescript/tests/cases/conformance/types/wit
5 │ f `123qdawdrqw${ 1 }${
╰────
× Unterminated string
╭─[typescript/tests/cases/compiler/taggedTemplatesWithIncompleteTemplateExpressions4.ts:5:27]
4 │ // Incomplete call, but too many parameters.
5 │ f `123qdawdrqw${ 1 }${ }${
· ─
╰────
× Unexpected token
╭─[typescript/tests/cases/compiler/taggedTemplatesWithIncompleteTemplateExpressions4.ts:5:24]
4 │ // Incomplete call, but too many parameters.