From 91a638d1092854af5d11469c8c42866f679f889d Mon Sep 17 00:00:00 2001 From: Boshen Date: Thu, 29 Oct 2026 16:30:15 +0800 Subject: [PATCH] fix(parser): normalize raw template line endings with invalid escapes (#28285) Template raw values must normalize CR and CRLF even when an invalid escape leaves the cooked value undefined. AI-assisted implementation. --- .gitattributes | 4 ++++ crates/oxc_parser/src/js/expression.rs | 3 ++- .../misc/pass/tagged-template-invalid-escape-cr.js | 1 + .../misc/pass/tagged-template-invalid-escape-crlf.js | 10 ++++++++++ tasks/coverage/snapshots/codegen_misc.snap | 4 ++-- tasks/coverage/snapshots/formatter_misc.snap | 4 ++-- tasks/coverage/snapshots/lexer_misc.snap | 4 ++-- tasks/coverage/snapshots/parser_misc.snap | 4 ++-- tasks/coverage/snapshots/semantic_misc.snap | 4 ++-- tasks/coverage/snapshots/transformer_misc.snap | 4 ++-- 10 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 tasks/coverage/misc/pass/tagged-template-invalid-escape-cr.js create mode 100644 tasks/coverage/misc/pass/tagged-template-invalid-escape-crlf.js diff --git a/.gitattributes b/.gitattributes index ea926c4b67..7d035b40f1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16,3 +16,7 @@ apps/oxlint/test/fixtures/** text=auto eol=lf crates/oxc_linter/src/generated/rules_enum.rs linguist-generated=true merge=ours crates/oxc_linter/src/generated/rule_runner_impls.rs linguist-generated=true merge=ours crates/oxc_linter/src/generated/assert_layouts.rs linguist-generated=true + +# Preserve the line terminators exercised by these parser regression fixtures. +tasks/coverage/misc/pass/tagged-template-invalid-escape-cr.js -text whitespace=cr-at-eol +tasks/coverage/misc/pass/tagged-template-invalid-escape-crlf.js -text whitespace=cr-at-eol diff --git a/crates/oxc_parser/src/js/expression.rs b/crates/oxc_parser/src/js/expression.rs index 11cc75ff06..abf0ee4650 100644 --- a/crates/oxc_parser/src/js/expression.rs +++ b/crates/oxc_parser/src/js/expression.rs @@ -646,7 +646,8 @@ impl<'a, C: Config> ParserImpl<'a, C> { let cooked = if self.cur_token().escaped() { // `cooked = None` when template literal has invalid escape sequence let cooked = self.cur_template_string(); - if cooked.is_some() && raw.contains('\r') { + // TRV normalizes line endings even when an invalid escape makes TV undefined. + if raw.contains('\r') { raw = Str::from_str_in(&raw.cow_replace("\r\n", "\n").cow_replace('\r', "\n"), self); } diff --git a/tasks/coverage/misc/pass/tagged-template-invalid-escape-cr.js b/tasks/coverage/misc/pass/tagged-template-invalid-escape-cr.js new file mode 100644 index 0000000000..97fd800e03 --- /dev/null +++ b/tasks/coverage/misc/pass/tagged-template-invalid-escape-cr.js @@ -0,0 +1 @@ +function tag(strings) { return strings; } tag`\xZ `; tag`head\xZ ${0}middle\u{} ${1}tail\8 `; \ No newline at end of file diff --git a/tasks/coverage/misc/pass/tagged-template-invalid-escape-crlf.js b/tasks/coverage/misc/pass/tagged-template-invalid-escape-crlf.js new file mode 100644 index 0000000000..2ed0f14072 --- /dev/null +++ b/tasks/coverage/misc/pass/tagged-template-invalid-escape-crlf.js @@ -0,0 +1,10 @@ +function tag(strings) { + return strings; +} + +tag`\xZ +`; +tag`head\xZ +${0}middle\u{} +${1}tail\8 +`; diff --git a/tasks/coverage/snapshots/codegen_misc.snap b/tasks/coverage/snapshots/codegen_misc.snap index 9cae631203..35f18bed42 100644 --- a/tasks/coverage/snapshots/codegen_misc.snap +++ b/tasks/coverage/snapshots/codegen_misc.snap @@ -1,3 +1,3 @@ codegen_misc Summary: -AST Parsed : 115/115 (100.00%) -Positive Passed: 115/115 (100.00%) +AST Parsed : 117/117 (100.00%) +Positive Passed: 117/117 (100.00%) diff --git a/tasks/coverage/snapshots/formatter_misc.snap b/tasks/coverage/snapshots/formatter_misc.snap index 34b4ca8ea1..68fb647891 100644 --- a/tasks/coverage/snapshots/formatter_misc.snap +++ b/tasks/coverage/snapshots/formatter_misc.snap @@ -1,3 +1,3 @@ formatter_misc Summary: -AST Parsed : 115/115 (100.00%) -Positive Passed: 115/115 (100.00%) +AST Parsed : 117/117 (100.00%) +Positive Passed: 117/117 (100.00%) diff --git a/tasks/coverage/snapshots/lexer_misc.snap b/tasks/coverage/snapshots/lexer_misc.snap index 818f2769e5..34a7837e6b 100644 --- a/tasks/coverage/snapshots/lexer_misc.snap +++ b/tasks/coverage/snapshots/lexer_misc.snap @@ -1,3 +1,3 @@ lexer_misc Summary: -AST Parsed : 370/370 (100.00%) -Positive Passed: 370/370 (100.00%) +AST Parsed : 372/372 (100.00%) +Positive Passed: 372/372 (100.00%) diff --git a/tasks/coverage/snapshots/parser_misc.snap b/tasks/coverage/snapshots/parser_misc.snap index b297e23470..03c9ff6242 100644 --- a/tasks/coverage/snapshots/parser_misc.snap +++ b/tasks/coverage/snapshots/parser_misc.snap @@ -1,6 +1,6 @@ parser_misc Summary: -AST Parsed : 115/115 (100.00%) -Positive Passed: 115/115 (100.00%) +AST Parsed : 117/117 (100.00%) +Positive Passed: 117/117 (100.00%) Negative Passed: 257/257 (100.00%) × TS(1275): 'accessor' modifier can only appear on a property declaration. diff --git a/tasks/coverage/snapshots/semantic_misc.snap b/tasks/coverage/snapshots/semantic_misc.snap index e4e8729835..ed1fa84f36 100644 --- a/tasks/coverage/snapshots/semantic_misc.snap +++ b/tasks/coverage/snapshots/semantic_misc.snap @@ -1,6 +1,6 @@ semantic_misc Summary: -AST Parsed : 115/115 (100.00%) -Positive Passed: 109/115 (94.78%) +AST Parsed : 117/117 (100.00%) +Positive Passed: 111/117 (94.87%) semantic Error: tasks/coverage/misc/pass/declare-let-private.ts Bindings mismatch: after transform: ScopeId(0): ["private"] diff --git a/tasks/coverage/snapshots/transformer_misc.snap b/tasks/coverage/snapshots/transformer_misc.snap index 6dc6c9cb37..5e1acaa010 100644 --- a/tasks/coverage/snapshots/transformer_misc.snap +++ b/tasks/coverage/snapshots/transformer_misc.snap @@ -1,3 +1,3 @@ transformer_misc Summary: -AST Parsed : 115/115 (100.00%) -Positive Passed: 115/115 (100.00%) +AST Parsed : 117/117 (100.00%) +Positive Passed: 117/117 (100.00%)