From 5e5f9b04191ce5df4348ddf506ab6327e9d52edb Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 5 Sep 2026 20:28:38 +0200 Subject: [PATCH 1/2] Fix escapes in raw template payloads Signed-off-by: Christoph Knittel --- CHANGELOG.md | 1 + compiler/ml/ast_payload.ml | 4 +-- compiler/ml/string_literal.ml | 33 +++++++++++++++++++ compiler/ml/string_literal.mli | 6 ++++ .../ounit_tests/ounit_string_literal_tests.ml | 8 +++++ tests/tests/src/gbk.mjs | 2 +- tests/tests/src/raw_output_test.mjs | 3 ++ tests/tests/src/raw_output_test.res | 6 ++++ tests/tests/src/unsafe_ppx_test.mjs | 2 +- 9 files changed, 61 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11ec53da431..73dc07d57a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ #### :bug: Bug fix +- Fix escaped backticks and interpolation openers in backquoted `%raw`, `%ffi`, and `%re` payloads leaking into emitted JavaScript. https://github.com/rescript-lang/rescript/issues/6236 - Fix the side-effect analysis treating bigint exponentiation and bounds-checked array and string reads as pure, which let dead-code elimination drop an unused one that throws: `let _ = 2n ** -1n` no longer raised. https://github.com/rescript-lang/rescript/pull/8617 - Preserve record field `@as` annotations when formatting object types containing spreads. https://github.com/rescript-lang/rescript/pull/8619 - Fix excessive parentheses and indentation in function assignments to refs, align record and array assignment formatting across refs and fields, and preserve function return-type parentheses and consistent JSX fragment layout in callbacks. https://github.com/rescript-lang/rescript/pull/8611 diff --git a/compiler/ml/ast_payload.ml b/compiler/ml/ast_payload.ml index 649bb0526b4..d960f838f13 100644 --- a/compiler/ml/ast_payload.ml +++ b/compiler/ml/ast_payload.ml @@ -193,8 +193,8 @@ let raw_as_string_exp_exn ~(kind : Js_raw_info.raw_kind) ?is_function (x : t) : _ ); }; ] -> - Some - (source.txt, Bs_flow_ast_utils.flow_deli_offset (Some "js"), expression) + let source = String_literal.decode_raw_template_source source.txt in + Some (source, Bs_flow_ast_utils.flow_deli_offset (Some "js"), expression) | PStr [ { diff --git a/compiler/ml/string_literal.ml b/compiler/ml/string_literal.ml index 4f7183b96e2..24dd0c8afa3 100644 --- a/compiler/ml/string_literal.ml +++ b/compiler/ml/string_literal.ml @@ -219,6 +219,39 @@ let decode_js_escapes = let decode_js_template_escapes = decode_js_escapes_with ~normalize_template_line_endings:true +let decode_raw_template_source source = + let length = String.length source in + let buffer = Buffer.create length in + let rec loop index = + if index < length then + match String.unsafe_get source index with + | '\\' when index + 1 < length -> ( + match String.unsafe_get source (index + 1) with + | ('\\' | '$' | '`') as escaped -> + Buffer.add_char buffer escaped; + loop (index + 2) + | '\n' -> loop (index + 2) + | '\r' -> + if index + 2 < length && String.unsafe_get source (index + 2) = '\n' + then loop (index + 3) + else loop (index + 2) + | '\226' + when index + 3 < length + && String.unsafe_get source (index + 2) = '\128' + && (String.unsafe_get source (index + 3) = '\168' + || String.unsafe_get source (index + 3) = '\169') -> + loop (index + 4) + | escaped -> + Buffer.add_char buffer '\\'; + Buffer.add_char buffer escaped; + loop (index + 2)) + | character -> + Buffer.add_char buffer character; + loop (index + 1) + in + loop 0; + Buffer.contents buffer + type encode_js_mode = String | Template let encode_js mode s = diff --git a/compiler/ml/string_literal.mli b/compiler/ml/string_literal.mli index 78fcb617b89..46477407b5c 100644 --- a/compiler/ml/string_literal.mli +++ b/compiler/ml/string_literal.mli @@ -55,6 +55,12 @@ val decode_js_template_escapes : string -> string option as required by JavaScript template-literal semantics. Returns [None] for malformed escapes, malformed UTF-8, or an unescaped interpolation opener. *) +val decode_raw_template_source : string -> string +(** Remove the escapes that protect the surrounding ReScript template syntax + from JavaScript source embedded in a [raw], [ffi], or [re] extension. + Backslashes belonging to the embedded JavaScript, such as [\\n], are + preserved. *) + val encode_js_string : string -> string (** Encode a semantic UTF-8 string as a canonical JavaScript string-literal body. *) diff --git a/tests/ounit_tests/ounit_string_literal_tests.ml b/tests/ounit_tests/ounit_string_literal_tests.ml index a8fdb3e542a..76a40d7b879 100644 --- a/tests/ounit_tests/ounit_string_literal_tests.ml +++ b/tests/ounit_tests/ounit_string_literal_tests.ml @@ -407,6 +407,14 @@ let suites = ( "template segments reject interpolation openers" >:: fun _ -> assert_invalid_template "${value}"; assert_template_decoded ~encoded:"\\${value}" ~expected:"${value}" ); + ( "raw templates decode only their surrounding syntax escapes" + >:: fun _ -> + OUnit.assert_equal ~printer:(Printf.sprintf "%S") + {e|`${"hello"}` with \n and \t|e} + (String_literal.decode_raw_template_source + {e|\`\${"hello"}\` with \n and \t|e}); + OUnit.assert_equal ~printer:(Printf.sprintf "%S") "continued" + (String_literal.decode_raw_template_source "con\\\r\ntinued") ); ( "ordinary literals become semantic strings" >:: fun _ -> assert_parsed_string ~source:{|\x61\n\uD83D\uDE00|} ~expected_semantic:"a\n😀" ); diff --git a/tests/tests/src/gbk.mjs b/tests/tests/src/gbk.mjs index 0db86aecac6..3078134d373 100644 --- a/tests/tests/src/gbk.mjs +++ b/tests/tests/src/gbk.mjs @@ -9,6 +9,6 @@ console.log("你好"); console.log("你好你好"); -console.log("\\u4f60\\u597d"); +console.log("\u4f60\u597d"); /* Not a pure module */ diff --git a/tests/tests/src/raw_output_test.mjs b/tests/tests/src/raw_output_test.mjs index 2ff4d352b14..c61d251d39d 100644 --- a/tests/tests/src/raw_output_test.mjs +++ b/tests/tests/src/raw_output_test.mjs @@ -9,6 +9,9 @@ function mk(fn) { console.log(1); +var issue6236 = `${"hello"}`; +; + export { mk, } diff --git a/tests/tests/src/raw_output_test.res b/tests/tests/src/raw_output_test.res index 356ebb7ee35..fd4b7f4599d 100644 --- a/tests/tests/src/raw_output_test.res +++ b/tests/tests/src/raw_output_test.res @@ -12,3 +12,9 @@ let mk = fn => fn() mk(%raw(`(_)=> console.log('should works')`)) Console.log((() => 1)()) + +// GitHub issue #6236: escapes needed for the surrounding ReScript template +// must not survive in the emitted raw JavaScript. +%%raw(` +var issue6236 = \`\${"hello"}\`; +`) diff --git a/tests/tests/src/unsafe_ppx_test.mjs b/tests/tests/src/unsafe_ppx_test.mjs index fb22a30a3f9..4e94c29395c 100644 --- a/tests/tests/src/unsafe_ppx_test.mjs +++ b/tests/tests/src/unsafe_ppx_test.mjs @@ -5,7 +5,7 @@ import * as Pervasives from "@rescript/runtime/lib/es6/Pervasives.mjs"; import * as Test_utils from "./test_utils.mjs"; import * as Ffi_js_test from "./ffi_js_test.mjs"; -let x = "\\x01\\x02\\x03"; +let x = "\x01\x02\x03"; let max = Math.max; From 6d73272f225416f07ea54a56e34c9c417017610b Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 5 Sep 2026 20:29:15 +0200 Subject: [PATCH 2/2] Link changelog to pull request Signed-off-by: Christoph Knittel --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73dc07d57a7..15a6487f9e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,7 @@ #### :bug: Bug fix -- Fix escaped backticks and interpolation openers in backquoted `%raw`, `%ffi`, and `%re` payloads leaking into emitted JavaScript. https://github.com/rescript-lang/rescript/issues/6236 +- Fix escaped backticks and interpolation openers in backquoted `%raw`, `%ffi`, and `%re` payloads leaking into emitted JavaScript. https://github.com/rescript-lang/rescript/pull/8630 - Fix the side-effect analysis treating bigint exponentiation and bounds-checked array and string reads as pure, which let dead-code elimination drop an unused one that throws: `let _ = 2n ** -1n` no longer raised. https://github.com/rescript-lang/rescript/pull/8617 - Preserve record field `@as` annotations when formatting object types containing spreads. https://github.com/rescript-lang/rescript/pull/8619 - Fix excessive parentheses and indentation in function assignments to refs, align record and array assignment formatting across refs and fields, and preserve function return-type parentheses and consistent JSX fragment layout in callbacks. https://github.com/rescript-lang/rescript/pull/8611