Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions compiler/ml/ast_payload.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve source offsets while decoding raw templates

When malformed embedded JavaScript occurs after a decoded escape, the Flow parser reports positions in the shortened source, but check_flow_errors later projects those positions onto the original ReScript text using only a constant delimiter offset. For example, an error following \`` or ${` is highlighted one or more columns early, while an error after a removed backslash-newline continuation can be reported on the wrong line. The decoder needs to retain an original-offset mapping for diagnostics, or error locations must otherwise be translated before reporting.

Useful? React with 👍 / 👎.

@cknitt cknitt Sep 5, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. I checked the ReScript 9.1.4 implementation: parseTemplateStringLiteral already decoded these escapes before the Flow parser ran, while check_flow_errors projected the resulting locations using only the same fixed delimiter offset. So diagnostic drift after a removed escape or line continuation is pre-existing behavior rather than something introduced by this change. Preserving exact positions would require carrying a source-offset map through decoding (including Unicode and removed line terminators), which is a substantially broader diagnostics change and outside the scope of this PR. I’d prefer to keep this PR focused on restoring the raw-template escape behavior.

Some (source, Bs_flow_ast_utils.flow_deli_offset (Some "js"), expression)
| PStr
[
{
Expand Down
33 changes: 33 additions & 0 deletions compiler/ml/string_literal.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
6 changes: 6 additions & 0 deletions compiler/ml/string_literal.mli
Original file line number Diff line number Diff line change
Expand Up @@ -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. *)
Expand Down
8 changes: 8 additions & 0 deletions tests/ounit_tests/ounit_string_literal_tests.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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😀" );
Expand Down
2 changes: 1 addition & 1 deletion tests/tests/src/gbk.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ console.log("你好");

console.log("你好你好");

console.log("\\u4f60\\u597d");
console.log("\u4f60\u597d");

/* Not a pure module */
3 changes: 3 additions & 0 deletions tests/tests/src/raw_output_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ function mk(fn) {

console.log(1);

var issue6236 = `${"hello"}`;
;

export {
mk,
}
Expand Down
6 changes: 6 additions & 0 deletions tests/tests/src/raw_output_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -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"}\`;
`)
2 changes: 1 addition & 1 deletion tests/tests/src/unsafe_ppx_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading