diff --git a/CHANGELOG.md b/CHANGELOG.md index 11ec53da43..15a6487f9e 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/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 diff --git a/compiler/ml/ast_payload.ml b/compiler/ml/ast_payload.ml index 649bb0526b..d960f838f1 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 4f7183b96e..24dd0c8afa 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 78fcb617b8..46477407b5 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 a8fdb3e542..76a40d7b87 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 0db86aecac..3078134d37 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 2ff4d352b1..c61d251d39 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 356ebb7ee3..fd4b7f4599 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 fb22a30a3f..4e94c29395 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;