From 18dd806296e4f79b98742e997d9152e389265b50 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 2 Oct 2026 20:44:00 +0200 Subject: [PATCH 1/9] Keep regexp statements parseable after formatting Signed-off-by: Florian Hammerschmidt --- CHANGELOG.md | 1 + compiler/syntax/src/res_parens.ml | 23 ++++++ compiler/syntax/src/res_parens.mli | 1 + compiler/syntax/src/res_printer.ml | 21 +++++- .../regexp_statement_formatting/Main.res | 44 +++++++++++ .../regexp_statement_formatting/input.js | 52 +++++++++++++ .../regexp_statement_formatting/rescript.json | 4 + .../printer/expected/regexpStatements.res.txt | 69 ++++++++++++++++++ .../data/printer/regexpStatements.res | 73 +++++++++++++++++++ tests/syntax_tests/res_test.ml | 29 ++++++++ 10 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 tests/build_tests/regexp_statement_formatting/Main.res create mode 100644 tests/build_tests/regexp_statement_formatting/input.js create mode 100644 tests/build_tests/regexp_statement_formatting/rescript.json create mode 100644 tests/syntax_tests/data/printer/expected/regexpStatements.res.txt create mode 100644 tests/syntax_tests/data/printer/regexpStatements.res diff --git a/CHANGELOG.md b/CHANGELOG.md index 54012b56111..70ffc1f23fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ #### :bug: Bug fix +- Preserve parentheses around statements that start with a regexp literal, so formatting cannot turn them into division continuing the previous statement. - Make rewatch compile independent modules after an unrelated failure and recompile blocked dependents when a changed interface survives a failed implementation, including across full watcher rebuilds. https://github.com/rescript-lang/rescript/pull/8667 #### :memo: Documentation diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index 4b8e6710277..853aa463ca2 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -19,6 +19,29 @@ let expr_with_coercion_kind coercion_kind expr = let expr expr = expr_with_coercion_kind Parenthesized expr let expr_allowing_coercion expr = expr_with_coercion_kind Nothing expr +(* A leading regexp can be read as division continuing the previous statement. + Follow the left edge of pipes, calls and ternaries, including placeholder + sugar, so parentheses protect the whole statement even when it wraps. *) +let rec starts_with_regexp expr = + match expr.Parsetree.pexp_desc with + | Pexp_regexp _ -> true + | Pexp_apply {args = [(Nolabel, lhs); (Nolabel, _rhs)]} + when Parsetree_viewer.is_binary_expression expr -> + starts_with_regexp lhs + | Pexp_apply {funct} -> starts_with_regexp funct + | Pexp_ternary (condition, _consequent, _alternate) -> + starts_with_regexp condition + | Pexp_object_get (obj, _) | Pexp_object_set (obj, _, _) -> + starts_with_regexp obj + | Pexp_fun _ when Parsetree_viewer.is_underscore_apply_sugar expr -> + starts_with_regexp (Parsetree_viewer.rewrite_underscore_apply expr) + | _ -> false + +let statement_expr e = + match expr e with + | Nothing when starts_with_regexp e -> Parenthesized + | kind -> kind + let expr_record_row_rhs ~optional e = let kind = expr e in match kind with diff --git a/compiler/syntax/src/res_parens.mli b/compiler/syntax/src/res_parens.mli index 8d304823f41..1ea522fc675 100644 --- a/compiler/syntax/src/res_parens.mli +++ b/compiler/syntax/src/res_parens.mli @@ -1,6 +1,7 @@ type kind = Parenthesized | Braced of Location.t | Nothing val expr : Parsetree.expression -> kind +val statement_expr : Parsetree.expression -> kind (* Unlike [expr], this does not request parentheses for a top-level coercion. Use only where the surrounding grammar delimits the expression, such as call diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 6e9ff38af30..3a55ff60dac 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -28,6 +28,12 @@ let add_parens doc = Doc.rparen; ]) +(* Keep the delimiters on the expression's first and last lines. The parser + excludes parentheses from expression locations, so breaking just inside + them would create blank lines or move comments on the next formatting pass. *) +let add_statement_parens doc = + Doc.group (Doc.concat [Doc.lparen; doc; Doc.rparen]) + let add_braces ?(force_break = false) doc = Doc.breakable_group ~force_break (Doc.concat @@ -688,7 +694,10 @@ and print_structure_item ~state (si : Parsetree.structure_item) cmt_tbl = match Parens.structure_expr expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces - | Nothing -> doc + | Nothing -> ( + match Parens.statement_expr expr with + | Parenthesized -> add_statement_parens doc + | Braced _ | Nothing -> doc) in Doc.concat [print_attributes ~state attrs cmt_tbl; expr_doc] | Pstr_attribute attr -> @@ -5580,7 +5589,10 @@ and print_expression_block ~state ~braces expr cmt_tbl = match Parens.expr expr1 with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr1 braces - | Nothing -> doc + | Nothing -> ( + match Parens.statement_expr expr1 with + | Parenthesized when acc <> [] -> add_statement_parens doc + | Parenthesized | Braced _ | Nothing -> doc) in let loc = expr1.pexp_loc in collect_rows ((loc, expr_doc) :: acc) expr2 @@ -5622,7 +5634,10 @@ and print_expression_block ~state ~braces expr cmt_tbl = match Parens.expr expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces - | Nothing -> doc + | Nothing -> ( + match Parens.statement_expr expr with + | Parenthesized when acc <> [] -> add_statement_parens doc + | Parenthesized | Braced _ | Nothing -> doc) in List.rev ((expr.pexp_loc, expr_doc) :: acc) in diff --git a/tests/build_tests/regexp_statement_formatting/Main.res b/tests/build_tests/regexp_statement_formatting/Main.res new file mode 100644 index 00000000000..222a536b36b --- /dev/null +++ b/tests/build_tests/regexp_statement_formatting/Main.res @@ -0,0 +1,44 @@ +let afterLet = s => { + let t = s + (/a/->RegExp.test(t)) +} + +let afterExpression = s => { + ignore(s) + (/a/->RegExp.test(s)) +} + +let intermediate = () => { + let seen = ref(false) + (/a/->RegExp.test("a")->(result => seen := result)) + seen.contents +} + +let bare = () => { + ignore() + (/a/) +} + +let dotAndWhitespace = s => { + ignore(s) + (/./s->RegExp.test(s)->ignore) + (/ a/->RegExp.test(s)) +} + +let ternaryAndBinary = s => { + let t = s + (/a/->RegExp.test(t) && true ? 1 : 0) +} + +let division = a => { + let b = a / 2 / 3 + b +} + +let floatDivision = a => { + let b = a /. 2. /. 3. + b +} + +let s = "a" +(/a/->RegExp.test(s)->ignore) diff --git a/tests/build_tests/regexp_statement_formatting/input.js b/tests/build_tests/regexp_statement_formatting/input.js new file mode 100644 index 00000000000..c96cd178e68 --- /dev/null +++ b/tests/build_tests/regexp_statement_formatting/input.js @@ -0,0 +1,52 @@ +// @ts-check + +import assert from "node:assert/strict"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { pathToFileURL } from "node:url"; +import { setup } from "#dev/process"; + +const { bsc } = setup(import.meta.dirname); +const formatted = await bsc(["-format", "Main.res"]); +assert.equal(formatted.status, 0, formatted.stderr); + +const generatedDir = await fs.mkdtemp( + path.join(import.meta.dirname, "generated-"), +); +try { + const source = path.join(generatedDir, "Main.res"); + await fs.writeFile(source, formatted.stdout); + const reformatted = await bsc(["-format", source]); + assert.equal(reformatted.status, 0, reformatted.stderr); + assert.equal(reformatted.stdout, formatted.stdout); + + const compiled = await bsc([ + "-check-lam", + "-bs-project-root", + import.meta.dirname, + "-bs-package-name", + "regexp-statement-formatting", + "-bs-package-output", + `esmodule:${path.basename(generatedDir)}:.mjs`, + "-o", + path.join(generatedDir, "Main.cmj"), + source, + ]); + assert.equal(compiled.status, 0, compiled.stderr); + + const output = await import( + pathToFileURL(path.join(generatedDir, "Main.mjs")).href + ); + assert.equal(output.afterLet("a"), true); + assert.equal(output.afterLet("b"), false); + assert.equal(output.afterExpression("a"), true); + assert.equal(output.intermediate(), true); + assert.equal(output.bare().test("a"), true); + assert.equal(output.dotAndWhitespace(" a"), true); + assert.equal(output.ternaryAndBinary("a"), 1); + assert.equal(output.ternaryAndBinary("b"), 0); + assert.equal(output.division(24), 4); + assert.equal(output.floatDivision(24), 4); +} finally { + await fs.rm(generatedDir, { recursive: true, force: true }); +} diff --git a/tests/build_tests/regexp_statement_formatting/rescript.json b/tests/build_tests/regexp_statement_formatting/rescript.json new file mode 100644 index 00000000000..0517d76218d --- /dev/null +++ b/tests/build_tests/regexp_statement_formatting/rescript.json @@ -0,0 +1,4 @@ +{ + "name": "regexp-statement-formatting", + "sources": [] +} diff --git a/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt b/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt new file mode 100644 index 00000000000..0e24b692c55 --- /dev/null +++ b/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt @@ -0,0 +1,69 @@ +let afterLet = s => { + let t = s + (/a/->RegExp.test(t)) +} + +let afterExpression = s => { + Console.log(s) + (/a/->RegExp.test(s)) +} + +let intermediate = () => { + Console.log("before") + (/a/->ignore) + Console.log("after") +} + +let bare = () => { + Console.log("before") + (/a/g) +} + +let dotAndWhitespace = s => { + Console.log(s) + (/./s->RegExp.test(s)->ignore) + (/ a/->RegExp.test(s)) +} + +let ternaryAndBinary = s => { + let t = s + (/a/->RegExp.test(t) && true ? Some(t) : None) +} + +let placeholder = () => { + Console.log("before") + (/a/->use("a")) +} + +let objectAccess = () => { + Console.log("before") + (/a/g["lastIndex"]) +} + +let comments = s => { + let t = s // binding + // before regexp + /* inside */ (/a/ /* after regexp */->RegExp.test(t)) // after statement +} + +let first = s => { + /a/->RegExp.test(s) +} + +let division = a => { + let b = a / 2 / 3 + b +} + +let floatDivision = a => { + let b = a /. 2. /. 3. + b +} + +let s = "a" +(/a/->RegExp.test(s)->ignore) + +module Nested = { + let s = "a" + (/a/->RegExp.test(s)->ignore) +} diff --git a/tests/syntax_tests/data/printer/regexpStatements.res b/tests/syntax_tests/data/printer/regexpStatements.res new file mode 100644 index 00000000000..ebed5046b72 --- /dev/null +++ b/tests/syntax_tests/data/printer/regexpStatements.res @@ -0,0 +1,73 @@ +let afterLet = s => { + let t = s + (/a/)->RegExp.test(t) +} + +let afterExpression = s => { + Console.log(s); + /a/->RegExp.test(s) +} + +let intermediate = () => { + Console.log("before") + (/a/)->ignore + Console.log("after") +} + +let bare = () => { + Console.log("before") + (/a/g) +} + +let dotAndWhitespace = s => { + Console.log(s) + (/./s)->RegExp.test(s)->ignore + (/ a/)->RegExp.test(s) +} + +let ternaryAndBinary = s => { + let t = s + (/a/)->RegExp.test(t) && true ? Some(t) : None +} + +let placeholder = () => { + Console.log("before") + (/a/)->use(_, "a") +} + +let objectAccess = () => { + Console.log("before") + (/a/g)["lastIndex"] +} + +let comments = s => { + let t = s // binding + // before regexp + (/* inside */ /a/ /* after regexp */)->RegExp.test(t) // after statement +} + +let first = s => { + /a/->RegExp.test(s) +} + +let division = a => { + let b = a + /2 + / 3 + b +} + +let floatDivision = a => { + let b = a + /.2. + /. 3. + b +} + +let s = "a"; +/a/->RegExp.test(s)->ignore + +module Nested = { + let s = "a" + (/a/)->RegExp.test(s)->ignore +} diff --git a/tests/syntax_tests/res_test.ml b/tests/syntax_tests/res_test.ml index dadc9069bda..f82db1ecdb2 100644 --- a/tests/syntax_tests/res_test.ml +++ b/tests/syntax_tests/res_test.ml @@ -75,6 +75,35 @@ let () = [20; 40; 80; 100; 120]; print_endline "✅ callback trailing comments are stable at multiple widths" +let () = + let filename = Filename.concat data_dir "printer/regexpStatements.res" in + let source = Ext_io.load_file filename in + let parse source = + let result = + Res_driver.parse_implementation_from_source ~display_filename:filename + ~source + in + if result.invalid then + failwith ("Regexp statements failed to parse:\n" ^ source); + result + in + let original = parse source in + let comment_texts result = + List.map Res_comment.txt result.Res_driver.comments + in + List.iter + (fun width -> + let format result = + Res_printer.print_implementation ~width result.Res_driver.parsetree + ~comments:result.comments + in + let printed = format original in + let reparsed = parse printed in + assert (comment_texts original = comment_texts reparsed); + assert (printed = format reparsed)) + [20; 40; 80; 100; 120]; + print_endline "✅ regexp statements round-trip at multiple widths" + module Outcome_printer_tests = struct let signature_to_outcome structure = Lazy.force Res_outcome_printer.setup; From 56cae7b1e5d95c792373408be2986fee7c90d2a6 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 2 Oct 2026 20:44:36 +0200 Subject: [PATCH 2/9] Link regexp statement changelog to PR #8720 Signed-off-by: Florian Hammerschmidt --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ffc1f23fd..ab9c2ff33ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ #### :bug: Bug fix -- Preserve parentheses around statements that start with a regexp literal, so formatting cannot turn them into division continuing the previous statement. +- Preserve parentheses around statements that start with a regexp literal, so formatting cannot turn them into division continuing the previous statement. https://github.com/rescript-lang/rescript/pull/8720 - Make rewatch compile independent modules after an unrelated failure and recompile blocked dependents when a changed interface survives a failed implementation, including across full watcher rebuilds. https://github.com/rescript-lang/rescript/pull/8667 #### :memo: Documentation From f7ef23231ffbccda07e66efe64d5c92dd125fc31 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 9 Oct 2026 15:20:18 +0200 Subject: [PATCH 3/9] Preserve regexp statement parentheses for bracket accesses Signed-off-by: Florian Hammerschmidt --- compiler/syntax/src/res_parens.ml | 22 ++++++++++++-- .../regexp_statement_formatting/Main.res | 23 +++++++++++++++ .../regexp_statement_formatting/input.js | 2 ++ .../printer/expected/regexpStatements.res.txt | 29 +++++++++++++++++++ .../data/printer/regexpStatements.res | 29 +++++++++++++++++++ 5 files changed, 103 insertions(+), 2 deletions(-) diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index 5fa39c11219..a60db30d6ab 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -20,14 +20,32 @@ let expr expr = expr_with_coercion_kind Parenthesized expr let expr_allowing_coercion expr = expr_with_coercion_kind Nothing expr (* A leading regexp can be read as division continuing the previous statement. - Follow the left edge of pipes, calls and ternaries, including placeholder - sugar, so parentheses protect the whole statement even when it wraps. *) + Follow the left edge of pipes, calls, bracket accesses and ternaries, + including placeholder sugar, so parentheses protect the whole statement + even when it wraps. *) let rec starts_with_regexp expr = match expr.Parsetree.pexp_desc with | Pexp_regexp _ -> true | Pexp_apply {args = [(Nolabel, lhs); (Nolabel, _rhs)]} when Parsetree_viewer.is_binary_expression expr -> starts_with_regexp lhs + | Pexp_apply {args = [(Nolabel, parent_expr); (Nolabel, _member_expr)]} + when Parsetree_viewer.is_array_access expr -> + starts_with_regexp parent_expr + | Pexp_apply + { + funct = + { + pexp_desc = Pexp_ident {txt = Longident.Ldot (Lident "Array", "set")}; + }; + args = + [ + (Nolabel, parent_expr); + (Nolabel, _member_expr); + (Nolabel, _target_expr); + ]; + } -> + starts_with_regexp parent_expr | Pexp_apply {funct} -> starts_with_regexp funct | Pexp_ternary (condition, _consequent, _alternate) -> starts_with_regexp condition diff --git a/tests/build_tests/regexp_statement_formatting/Main.res b/tests/build_tests/regexp_statement_formatting/Main.res index 222a536b36b..9d7704653aa 100644 --- a/tests/build_tests/regexp_statement_formatting/Main.res +++ b/tests/build_tests/regexp_statement_formatting/Main.res @@ -30,6 +30,29 @@ let ternaryAndBinary = s => { (/a/->RegExp.test(t) && true ? 1 : 0) } +module Array = { + @get_index external get: (RegExp.t, string) => int = "" + @set_index external set: (RegExp.t, string, int) => unit = "" +} + +let arrayAccess = key => { + ignore(key) + (/a/g[key]) +} + +let arrayMutation = (key, value) => { + let written = ref(0) + module Array = { + let set = (regexp, key, value) => { + regexp[key] = value + written := regexp[key] + } + } + ignore(key) + (/a/g[key] = value) + written.contents +} + let division = a => { let b = a / 2 / 3 b diff --git a/tests/build_tests/regexp_statement_formatting/input.js b/tests/build_tests/regexp_statement_formatting/input.js index c96cd178e68..7a7b6cf82cd 100644 --- a/tests/build_tests/regexp_statement_formatting/input.js +++ b/tests/build_tests/regexp_statement_formatting/input.js @@ -45,6 +45,8 @@ try { assert.equal(output.dotAndWhitespace(" a"), true); assert.equal(output.ternaryAndBinary("a"), 1); assert.equal(output.ternaryAndBinary("b"), 0); + assert.equal(output.arrayAccess("lastIndex"), 0); + assert.equal(output.arrayMutation("lastIndex", 7), 7); assert.equal(output.division(24), 4); assert.equal(output.floatDivision(24), 4); } finally { diff --git a/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt b/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt index 0e24b692c55..bb05249ddc3 100644 --- a/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt +++ b/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt @@ -40,6 +40,33 @@ let objectAccess = () => { (/a/g["lastIndex"]) } +let arrayAccess = i => { + Console.log("before") + (/a/[i]) +} + +let arrayMutation = (i, value) => { + Console.log("before") + (/a/g[i] = value) + Console.log("after") +} + +let nestedArrayAccess = (i, j) => { + Console.log("before") + (/a/[i][j]->ignore) +} + +let arrayCalls = (i, value) => { + Console.log("before") + (/a/[i]) + (/a/g[i] = value) +} + +let arrayComments = i => { + Console.log("before") + /* before regexp */ (/a/ /* after regexp */[/* index */ i]) +} + let comments = s => { let t = s // binding // before regexp @@ -66,4 +93,6 @@ let s = "a" module Nested = { let s = "a" (/a/->RegExp.test(s)->ignore) + (/a/[0]) + (/a/g[0] = 1) } diff --git a/tests/syntax_tests/data/printer/regexpStatements.res b/tests/syntax_tests/data/printer/regexpStatements.res index ebed5046b72..e89a4cebeec 100644 --- a/tests/syntax_tests/data/printer/regexpStatements.res +++ b/tests/syntax_tests/data/printer/regexpStatements.res @@ -40,6 +40,33 @@ let objectAccess = () => { (/a/g)["lastIndex"] } +let arrayAccess = i => { + Console.log("before") + ((/a/)[i]) +} + +let arrayMutation = (i, value) => { + Console.log("before") + ((/a/g)[i] = value) + Console.log("after") +} + +let nestedArrayAccess = (i, j) => { + Console.log("before") + ((/a/)[i][j])->ignore +} + +let arrayCalls = (i, value) => { + Console.log("before") + Array.get(/a/, i) + Array.set(/a/g, i, value) +} + +let arrayComments = i => { + Console.log("before") + ((/* before regexp */ /a/ /* after regexp */)[/* index */ i]) +} + let comments = s => { let t = s // binding // before regexp @@ -70,4 +97,6 @@ let s = "a"; module Nested = { let s = "a" (/a/)->RegExp.test(s)->ignore + ((/a/)[0]) + ((/a/g)[0] = 1) } From c922ca108fcc022ae2d1908f89f6d2482932ff39 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 9 Oct 2026 16:26:32 +0200 Subject: [PATCH 4/9] Preserve regexp statements through field receivers Signed-off-by: Florian Hammerschmidt --- compiler/syntax/src/res_parens.ml | 7 +++-- .../regexp_statement_formatting/Main.res | 29 +++++++++++++++++++ .../regexp_statement_formatting/input.js | 6 ++++ .../printer/expected/regexpStatements.res.txt | 28 ++++++++++++++++++ .../data/printer/regexpStatements.res | 28 ++++++++++++++++++ 5 files changed, 96 insertions(+), 2 deletions(-) diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index a60db30d6ab..1317c8da3d6 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -20,7 +20,7 @@ let expr expr = expr_with_coercion_kind Parenthesized expr let expr_allowing_coercion expr = expr_with_coercion_kind Nothing expr (* A leading regexp can be read as division continuing the previous statement. - Follow the left edge of pipes, calls, bracket accesses and ternaries, + Follow the left edge of pipes, calls, field and bracket accesses and ternaries, including placeholder sugar, so parentheses protect the whole statement even when it wraps. *) let rec starts_with_regexp expr = @@ -49,7 +49,10 @@ let rec starts_with_regexp expr = | Pexp_apply {funct} -> starts_with_regexp funct | Pexp_ternary (condition, _consequent, _alternate) -> starts_with_regexp condition - | Pexp_object_get (obj, _) | Pexp_object_set (obj, _, _) -> + | Pexp_field (obj, _) + | Pexp_setfield (obj, _, _) + | Pexp_object_get (obj, _) + | Pexp_object_set (obj, _, _) -> starts_with_regexp obj | Pexp_fun _ when Parsetree_viewer.is_underscore_apply_sugar expr -> starts_with_regexp (Parsetree_viewer.rewrite_underscore_apply expr) diff --git a/tests/build_tests/regexp_statement_formatting/Main.res b/tests/build_tests/regexp_statement_formatting/Main.res index 9d7704653aa..397108fe7cb 100644 --- a/tests/build_tests/regexp_statement_formatting/Main.res +++ b/tests/build_tests/regexp_statement_formatting/Main.res @@ -53,6 +53,35 @@ let arrayMutation = (key, value) => { written.contents } +type field = {mutable value: int} +type predicate = {test: unit => bool} + +let fieldAccess = key => { + module Array = { + let get = (regexp, key) => {value: regexp->RegExp.test(key) ? 1 : 2} + } + ignore(key) + (/a/[key].value) +} + +let fieldMutation = (key, value) => { + let field = {value: 0} + module Array = { + let get = (regexp, key) => regexp->RegExp.test(key) ? field : {value: -1} + } + ignore(key) + (/a/[key].value = value) + field.value +} + +let fieldCall = key => { + module Array = { + let get = (regexp, key) => {test: () => regexp->RegExp.test(key)} + } + ignore(key) + (/a/[key].test()) +} + let division = a => { let b = a / 2 / 3 b diff --git a/tests/build_tests/regexp_statement_formatting/input.js b/tests/build_tests/regexp_statement_formatting/input.js index 7a7b6cf82cd..64701fb04d0 100644 --- a/tests/build_tests/regexp_statement_formatting/input.js +++ b/tests/build_tests/regexp_statement_formatting/input.js @@ -47,6 +47,12 @@ try { assert.equal(output.ternaryAndBinary("b"), 0); assert.equal(output.arrayAccess("lastIndex"), 0); assert.equal(output.arrayMutation("lastIndex", 7), 7); + assert.equal(output.fieldAccess("a"), 1); + assert.equal(output.fieldAccess("b"), 2); + assert.equal(output.fieldMutation("a", 7), 7); + assert.equal(output.fieldMutation("b", 7), 0); + assert.equal(output.fieldCall("a"), true); + assert.equal(output.fieldCall("b"), false); assert.equal(output.division(24), 4); assert.equal(output.floatDivision(24), 4); } finally { diff --git a/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt b/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt index bb05249ddc3..bc8d1c227ac 100644 --- a/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt +++ b/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt @@ -67,6 +67,32 @@ let arrayComments = i => { /* before regexp */ (/a/ /* after regexp */[/* index */ i]) } +let fieldAccess = i => { + Console.log("before") + (/a/[i].foo) +} + +let fieldMutation = (i, value) => { + Console.log("before") + (/a/[i].foo = value) + Console.log("after") +} + +let fieldChains = (i, j, value) => { + Console.log("before") + (/a/[i].foo.bar) + (/a/[i].foo.bar = value) + (/a/[i].foo()) + (/a/[i].foo->ignore) + (/a/[i].foo[j]) + (/a/[i].foo["bar"]) +} + +let fieldComments = (i, value) => { + Console.log("before") + (/* before field regexp */ /a/ /* after field regexp */[/* field index */ i].foo /* after field */ = value) +} + let comments = s => { let t = s // binding // before regexp @@ -95,4 +121,6 @@ module Nested = { (/a/->RegExp.test(s)->ignore) (/a/[0]) (/a/g[0] = 1) + (/a/[0].foo) + (/a/g[0].foo = 1) } diff --git a/tests/syntax_tests/data/printer/regexpStatements.res b/tests/syntax_tests/data/printer/regexpStatements.res index e89a4cebeec..f7da69eacfc 100644 --- a/tests/syntax_tests/data/printer/regexpStatements.res +++ b/tests/syntax_tests/data/printer/regexpStatements.res @@ -67,6 +67,32 @@ let arrayComments = i => { ((/* before regexp */ /a/ /* after regexp */)[/* index */ i]) } +let fieldAccess = i => { + Console.log("before") + ((/a/)[i]).foo +} + +let fieldMutation = (i, value) => { + Console.log("before") + ((/a/)[i]).foo = value + Console.log("after") +} + +let fieldChains = (i, j, value) => { + Console.log("before") + ((/a/)[i]).foo.bar + ((/a/)[i]).foo.bar = value + ((/a/)[i]).foo() + ((/a/)[i]).foo->ignore + ((/a/)[i]).foo[j] + ((/a/)[i]).foo["bar"] +} + +let fieldComments = (i, value) => { + Console.log("before") + ((/* before field regexp */ /a/ /* after field regexp */)[/* field index */ i]).foo /* after field */ = value +} + let comments = s => { let t = s // binding // before regexp @@ -99,4 +125,6 @@ module Nested = { (/a/)->RegExp.test(s)->ignore ((/a/)[0]) ((/a/g)[0] = 1) + ((/a/)[0]).foo + ((/a/g)[0]).foo = 1 } From 0361c7ba6bd600ee783eb7170b8b3df6dc6b7276 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Mon, 12 Oct 2026 21:59:55 +0200 Subject: [PATCH 5/9] Refine regexp statement protection for printed contexts Signed-off-by: Florian Hammerschmidt --- compiler/syntax/src/res_parens.ml | 107 +++++++++++------- compiler/syntax/src/res_parens.mli | 2 +- compiler/syntax/src/res_printer.ml | 46 ++++---- .../printer/expected/regexpStatements.res.txt | 23 ++++ .../data/printer/regexpStatements.res | 23 ++++ tests/syntax_tests/res_test.ml | 77 ++++++++++++- 6 files changed, 209 insertions(+), 69 deletions(-) diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index 86226479daf..57dac1a0959 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -19,50 +19,6 @@ let expr_with_coercion_kind coercion_kind expr = let expr expr = expr_with_coercion_kind Parenthesized expr let expr_allowing_coercion expr = expr_with_coercion_kind Nothing expr -(* A leading regexp can be read as division continuing the previous statement. - Follow the left edge of pipes, calls, field and bracket accesses and ternaries, - including placeholder sugar, so parentheses protect the whole statement - even when it wraps. *) -let rec starts_with_regexp expr = - match expr.Parsetree.pexp_desc with - | Pexp_regexp _ -> true - | Pexp_apply {args = [(Nolabel, lhs); (Nolabel, _rhs)]} - when Parsetree_viewer.is_binary_expression expr -> - starts_with_regexp lhs - | Pexp_apply {args = [(Nolabel, parent_expr); (Nolabel, _member_expr)]} - when Parsetree_viewer.is_array_access expr -> - starts_with_regexp parent_expr - | Pexp_apply - { - funct = - { - pexp_desc = Pexp_ident {txt = Longident.Ldot (Lident "Array", "set")}; - }; - args = - [ - (Nolabel, parent_expr); - (Nolabel, _member_expr); - (Nolabel, _target_expr); - ]; - } -> - starts_with_regexp parent_expr - | Pexp_apply {funct} -> starts_with_regexp funct - | Pexp_ternary (condition, _consequent, _alternate) -> - starts_with_regexp condition - | Pexp_field (obj, _) - | Pexp_setfield (obj, _, _) - | Pexp_object_get (obj, _) - | Pexp_object_set (obj, _, _) -> - starts_with_regexp obj - | Pexp_fun _ when Parsetree_viewer.is_underscore_apply_sugar expr -> - starts_with_regexp (Parsetree_viewer.rewrite_underscore_apply expr) - | _ -> false - -let statement_expr e = - match expr e with - | Nothing when starts_with_regexp e -> Parenthesized - | kind -> kind - let expr_record_row_rhs ~optional e = let kind = expr e in match kind with @@ -518,3 +474,66 @@ let pattern_record_row_rhs (pattern : Parsetree.pattern) = false | Ppat_constraint _ -> true | _ -> false + +(* Follow the printed left edge, stopping wherever the printer adds delimiters + or a prefix such as an attribute or await. *) +let rec starts_with_regexp e = + if e.Parsetree.pexp_attributes <> [] || Parsetree_viewer.expr_is_await e then + false + else + match expr e with + | Parenthesized | Braced _ -> false + | Nothing -> ( + match e.pexp_desc with + | Pexp_regexp _ -> true + | Pexp_apply + { + funct = {pexp_desc = Pexp_ident {txt = Longident.Lident "##"}}; + args = [(Nolabel, parent); (Nolabel, _member)]; + } -> + starts_unwrapped (unary_expr_operand parent) parent + | Pexp_apply + { + funct = {pexp_desc = Pexp_ident {txt = Longident.Lident operator}}; + args = [(Nolabel, lhs); (Nolabel, _rhs)]; + } + when Parsetree_viewer.is_binary_expression e -> ( + match lhs.pexp_desc with + | Pexp_apply + { + funct = + {pexp_desc = Pexp_ident {txt = Longident.Lident child_operator}}; + } + when Parsetree_viewer.is_binary_expression lhs -> + (not (sub_binary_expr_operand operator child_operator)) + && starts_with_regexp lhs + | Pexp_setfield _ | Pexp_object_set _ -> false + | _ -> starts_unwrapped (binary_expr_operand ~is_lhs:true lhs) lhs) + | Pexp_apply {args = [(Nolabel, parent); (Nolabel, _member)]} + when Parsetree_viewer.is_array_access e -> + starts_unwrapped (unary_expr_operand parent) parent + | Pexp_apply + { + funct = + { + pexp_desc = + Pexp_ident {txt = Longident.Ldot (Lident "Array", "set")}; + }; + args = [(Nolabel, parent); (Nolabel, _member); (Nolabel, _target)]; + } -> + starts_unwrapped (unary_expr_operand parent) parent + | Pexp_apply {funct} -> starts_unwrapped (call_expr funct) funct + | Pexp_ternary (condition, _consequent, _alternate) -> + starts_unwrapped (ternary_operand condition) condition + | Pexp_field (obj, _) | Pexp_setfield (obj, _, _) -> + starts_unwrapped (field_expr obj) obj + | Pexp_object_get (obj, _) | Pexp_object_set (obj, _, _) -> + starts_unwrapped (unary_expr_operand obj) obj + | Pexp_fun {body} when Parsetree_viewer.is_underscore_apply_sugar e -> + starts_with_regexp body + | _ -> false) + +and starts_unwrapped kind e = + match kind with + | Nothing -> starts_with_regexp e + | Parenthesized | Braced _ -> false diff --git a/compiler/syntax/src/res_parens.mli b/compiler/syntax/src/res_parens.mli index ff02fc17f8d..427b92b88e3 100644 --- a/compiler/syntax/src/res_parens.mli +++ b/compiler/syntax/src/res_parens.mli @@ -1,7 +1,7 @@ type kind = Parenthesized | Braced of Location.t | Nothing val expr : Parsetree.expression -> kind -val statement_expr : Parsetree.expression -> kind +val starts_with_regexp : Parsetree.expression -> bool (* Unlike [expr], this does not request parentheses for a top-level coercion. Use only where the surrounding grammar delimits the expression, such as call diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 09e9139662f..303fe116ce3 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -716,13 +716,29 @@ let rec print_structure ~state (s : Parsetree.structure) t = match s with | [] -> print_comments_inside_file t | structure -> + let previous_can_continue = ref false in print_list ~get_loc:(fun s -> s.Parsetree.pstr_loc) ~nodes:structure - ~print:(print_structure_item ~state) + ~print:(fun item cmt_tbl -> + let protect_regexp = !previous_can_continue in + (previous_can_continue := + match item.Parsetree.pstr_desc with + | Pstr_value _ | Pstr_eval _ -> true + | _ -> false); + print_structure_item ~state ~protect_regexp item cmt_tbl) t -and print_structure_item ~state (si : Parsetree.structure_item) cmt_tbl = +and print_statement_expr ~protect_regexp expr kind doc = + match kind with + | Parens.Parenthesized -> add_parens doc + | Braced braces -> print_braces doc expr braces + | Nothing when protect_regexp && Parens.starts_with_regexp expr -> + add_statement_parens doc + | Nothing -> doc + +and print_structure_item ~state ?(protect_regexp = false) + (si : Parsetree.structure_item) cmt_tbl = let clear_pending () = state.State.pending_inline_record_defs_for_external <- None in @@ -743,13 +759,7 @@ and print_structure_item ~state (si : Parsetree.structure_item) cmt_tbl = clear_pending (); let expr_doc = let doc = print_expression_with_comments ~state expr cmt_tbl in - match Parens.structure_expr expr with - | Parens.Parenthesized -> add_parens doc - | Braced braces -> print_braces doc expr braces - | Nothing -> ( - match Parens.statement_expr expr with - | Parenthesized -> add_statement_parens doc - | Braced _ | Nothing -> doc) + print_statement_expr ~protect_regexp expr (Parens.structure_expr expr) doc in Doc.concat [print_attributes ~state attrs cmt_tbl; expr_doc] | Pstr_attribute attr -> @@ -5614,13 +5624,8 @@ and print_expression_block ~state ~braces expr cmt_tbl = | Pexp_sequence (expr1, expr2) -> let expr_doc = let doc = print_expression ~state expr1 cmt_tbl in - match Parens.expr expr1 with - | Parens.Parenthesized -> add_parens doc - | Braced braces -> print_braces doc expr1 braces - | Nothing -> ( - match Parens.statement_expr expr1 with - | Parenthesized when acc <> [] -> add_statement_parens doc - | Parenthesized | Braced _ | Nothing -> doc) + print_statement_expr ~protect_regexp:(acc <> []) expr1 + (Parens.expr expr1) doc in let loc = expr1.pexp_loc in collect_rows ((loc, expr_doc) :: acc) expr2 @@ -5659,13 +5664,8 @@ and print_expression_block ~state ~braces expr cmt_tbl = | _ -> let expr_doc = let doc = print_expression ~state expr cmt_tbl in - match Parens.expr expr with - | Parens.Parenthesized -> add_parens doc - | Braced braces -> print_braces doc expr braces - | Nothing -> ( - match Parens.statement_expr expr with - | Parenthesized when acc <> [] -> add_statement_parens doc - | Parenthesized | Braced _ | Nothing -> doc) + print_statement_expr ~protect_regexp:(acc <> []) expr (Parens.expr expr) + doc in List.rev ((expr.pexp_loc, expr_doc) :: acc) in diff --git a/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt b/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt index bc8d1c227ac..fcf39fa8a6d 100644 --- a/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt +++ b/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt @@ -1,3 +1,11 @@ +/first/->ignore + +type firstType = int +/afterType/->ignore + +open FirstModule +/afterOpen/->ignore + let afterLet = s => { let t = s (/a/->RegExp.test(t)) @@ -124,3 +132,18 @@ module Nested = { (/a/[0].foo) (/a/g[0].foo = 1) } + +let parenthesizedOperands = () => { + Console.log("before") + (/a/ ? 1 : 2) + 1 + (@attr /a/)->f + (/a/->f + 1) * 2 +} + +module FirstNested = { + /first/->ignore + type t = int + /afterType/->ignore + open FirstModule + /afterOpen/->ignore +} diff --git a/tests/syntax_tests/data/printer/regexpStatements.res b/tests/syntax_tests/data/printer/regexpStatements.res index f7da69eacfc..da90a65c98f 100644 --- a/tests/syntax_tests/data/printer/regexpStatements.res +++ b/tests/syntax_tests/data/printer/regexpStatements.res @@ -1,3 +1,11 @@ +/first/->ignore + +type firstType = int +/afterType/->ignore + +open FirstModule +/afterOpen/->ignore + let afterLet = s => { let t = s (/a/)->RegExp.test(t) @@ -128,3 +136,18 @@ module Nested = { ((/a/)[0]).foo ((/a/g)[0]).foo = 1 } + +let parenthesizedOperands = () => { + Console.log("before") + (/a/ ? 1 : 2) + 1 + (@attr /a/)->f + ((/a/->f) + 1) * 2 +} + +module FirstNested = { + /first/->ignore + type t = int + /afterType/->ignore + open FirstModule + /afterOpen/->ignore +} diff --git a/tests/syntax_tests/res_test.ml b/tests/syntax_tests/res_test.ml index f82db1ecdb2..a687afc5249 100644 --- a/tests/syntax_tests/res_test.ml +++ b/tests/syntax_tests/res_test.ml @@ -100,10 +100,85 @@ let () = let printed = format original in let reparsed = parse printed in assert (comment_texts original = comment_texts reparsed); - assert (printed = format reparsed)) + let reprinted = format reparsed in + if printed <> reprinted then + failwith + (Printf.sprintf + "Regexp formatting at width %d:\nFIRST:\n%s\nSECOND:\n%s" width + printed reprinted)) [20; 40; 80; 100; 120]; print_endline "✅ regexp statements round-trip at multiple widths" +(* PPX-generated ## accesses print their receiver before the operator. *) +let () = + let source = "let t = 0\n(/a/[\"member\"])" in + let parsed = + Res_driver.parse_implementation_from_source ~display_filename:"ppx.res" + ~source + in + assert (not parsed.invalid); + let structure = + List.map + (fun item -> + match item.Parsetree.pstr_desc with + | Pstr_eval + (({pexp_desc = Pexp_object_get (receiver, _)} as expr), attrs) -> + let funct = + Ast_helper.Exp.ident (Location.mknoloc (Longident.Lident "##")) + in + let member = + Ast_helper.Exp.ident (Location.mknoloc (Longident.Lident "member")) + in + { + item with + pstr_desc = + Pstr_eval + ( { + expr with + pexp_desc = + (Ast_helper.Exp.apply funct + [(Nolabel, receiver); (Nolabel, member)]) + .pexp_desc; + }, + attrs ); + } + | _ -> item) + parsed.parsetree + in + let printed = + Res_printer.print_implementation ~width:100 structure ~comments:[] + in + assert (printed = "let t = 0\n(/a/[\"member\"])\n"); + let reparsed = + Res_driver.parse_implementation_from_source ~display_filename:"ppx.res" + ~source:printed + in + assert (not reparsed.invalid) + +let () = + List.iter + (fun (source, expected) -> + let parsed = + Res_driver.parse_implementation_from_source + ~display_filename:"regexp.res" ~source + in + assert (not parsed.invalid); + let printed = + Res_printer.print_implementation ~width:100 parsed.parsetree + ~comments:parsed.comments + in + if printed <> expected then + failwith ("Unexpected regexp parentheses:\n" ^ printed)) + [ + ("/a/->f", "/a/->f\n"); + ("type t = int\n/a/->f", "type t = int\n/a/->f\n"); + ("open M\n/a/->f", "open M\n/a/->f\n"); + ( "let f = () => {ignore(); (/a/ ? 1 : 2) + 1}", + "let f = () => {\n ignore()\n (/a/ ? 1 : 2) + 1\n}\n" ); + ( "let f = () => {ignore(); (@attr /a/)->f}", + "let f = () => {\n ignore()\n (@attr /a/)->f\n}\n" ); + ] + module Outcome_printer_tests = struct let signature_to_outcome structure = Lazy.force Res_outcome_printer.setup; From 75bb2286cf7adab44233dbfbf4cd85e3ef3635b8 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Mon, 12 Oct 2026 22:26:11 +0200 Subject: [PATCH 6/9] Preserve regexp statement attributes and simplify protection Signed-off-by: Florian Hammerschmidt --- compiler/syntax/src/res_parens.ml | 102 ++++++++---------- compiler/syntax/src/res_printer.ml | 12 ++- .../regexp_statement_formatting/Main.res | 11 +- .../regexp_statement_formatting/input.js | 2 + .../printer/expected/regexpStatements.res.txt | 10 ++ .../expr/expected/markerAttributes.res.txt | 6 +- .../printer/expr/expected/ternary.res.txt | 2 +- .../data/printer/regexpStatements.res | 8 ++ tests/syntax_tests/res_test.ml | 24 ----- 9 files changed, 91 insertions(+), 86 deletions(-) diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index 57dac1a0959..84b4c00f2cb 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -291,6 +291,7 @@ let ternary_operand expr = Pexp_constraint ({pexp_desc = Pexp_pack _}, {ptyp_desc = Ptyp_package _}); } -> Nothing + | _ when expr.pexp_attributes <> [] -> Parenthesized | {pexp_desc = Pexp_ternary _} -> Parenthesized | {pexp_desc = Pexp_constraint _ | Pexp_coerce _} -> Parenthesized | _ when Res_parsetree_viewer.is_fun_expr expr -> ( @@ -475,63 +476,54 @@ let pattern_record_row_rhs (pattern : Parsetree.pattern) = | Ppat_constraint _ -> true | _ -> false -(* Follow the printed left edge, stopping wherever the printer adds delimiters - or a prefix such as an attribute or await. *) +(* Follow the printed left edge, stopping where operand rules add delimiters + or attributes add a prefix. *) let rec starts_with_regexp e = - if e.Parsetree.pexp_attributes <> [] || Parsetree_viewer.expr_is_await e then - false - else - match expr e with - | Parenthesized | Braced _ -> false - | Nothing -> ( - match e.pexp_desc with - | Pexp_regexp _ -> true - | Pexp_apply - { - funct = {pexp_desc = Pexp_ident {txt = Longident.Lident "##"}}; - args = [(Nolabel, parent); (Nolabel, _member)]; - } -> - starts_unwrapped (unary_expr_operand parent) parent - | Pexp_apply - { - funct = {pexp_desc = Pexp_ident {txt = Longident.Lident operator}}; - args = [(Nolabel, lhs); (Nolabel, _rhs)]; - } - when Parsetree_viewer.is_binary_expression e -> ( - match lhs.pexp_desc with - | Pexp_apply - { - funct = - {pexp_desc = Pexp_ident {txt = Longident.Lident child_operator}}; - } - when Parsetree_viewer.is_binary_expression lhs -> - (not (sub_binary_expr_operand operator child_operator)) - && starts_with_regexp lhs - | Pexp_setfield _ | Pexp_object_set _ -> false - | _ -> starts_unwrapped (binary_expr_operand ~is_lhs:true lhs) lhs) - | Pexp_apply {args = [(Nolabel, parent); (Nolabel, _member)]} - when Parsetree_viewer.is_array_access e -> - starts_unwrapped (unary_expr_operand parent) parent - | Pexp_apply + match e.Parsetree.pexp_desc with + | _ when e.pexp_attributes <> [] -> false + | Pexp_regexp _ -> true + | Pexp_apply + { + funct = { - funct = - { - pexp_desc = - Pexp_ident {txt = Longident.Ldot (Lident "Array", "set")}; - }; - args = [(Nolabel, parent); (Nolabel, _member); (Nolabel, _target)]; - } -> - starts_unwrapped (unary_expr_operand parent) parent - | Pexp_apply {funct} -> starts_unwrapped (call_expr funct) funct - | Pexp_ternary (condition, _consequent, _alternate) -> - starts_unwrapped (ternary_operand condition) condition - | Pexp_field (obj, _) | Pexp_setfield (obj, _, _) -> - starts_unwrapped (field_expr obj) obj - | Pexp_object_get (obj, _) | Pexp_object_set (obj, _, _) -> - starts_unwrapped (unary_expr_operand obj) obj - | Pexp_fun {body} when Parsetree_viewer.is_underscore_apply_sugar e -> - starts_with_regexp body - | _ -> false) + pexp_desc = + Pexp_ident {txt = Lident "##" | Ldot (Lident "Array", "get")}; + }; + args = [(Nolabel, parent); (Nolabel, _)]; + } + | Pexp_apply + { + funct = {pexp_desc = Pexp_ident {txt = Ldot (Lident "Array", "set")}}; + args = [(Nolabel, parent); (Nolabel, _); (Nolabel, _)]; + } + | Pexp_object_get (parent, _) + | Pexp_object_set (parent, _, _) -> + starts_unwrapped (unary_expr_operand parent) parent + | Pexp_apply + { + funct = {pexp_desc = Pexp_ident {txt = Longident.Lident operator}}; + args = [(Nolabel, lhs); (Nolabel, _rhs)]; + } + when Parsetree_viewer.is_binary_expression e -> ( + match lhs.pexp_desc with + | Pexp_apply + { + funct = + {pexp_desc = Pexp_ident {txt = Longident.Lident child_operator}}; + } + when Parsetree_viewer.is_binary_expression lhs -> + (not (sub_binary_expr_operand operator child_operator)) + && starts_with_regexp lhs + | Pexp_setfield _ | Pexp_object_set _ -> false + | _ -> starts_unwrapped (binary_expr_operand ~is_lhs:true lhs) lhs) + | Pexp_apply {funct} -> starts_unwrapped (call_expr funct) funct + | Pexp_ternary (condition, _consequent, _alternate) -> + starts_unwrapped (ternary_operand condition) condition + | Pexp_field (obj, _) | Pexp_setfield (obj, _, _) -> + starts_unwrapped (field_expr obj) obj + | Pexp_fun {body} when Parsetree_viewer.is_underscore_apply_sugar e -> + starts_with_regexp body + | _ -> false and starts_unwrapped kind e = match kind with diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 303fe116ce3..d5d2528a068 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -3838,6 +3838,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = in let should_print_its_own_attributes = match e.pexp_desc with + | Pexp_fun _ when Parsetree_viewer.is_underscore_apply_sugar e -> false | Pexp_apply _ | Pexp_fun _ | Pexp_setfield _ | Pexp_ifthenelse _ | Pexp_ternary _ -> true @@ -5565,6 +5566,13 @@ and print_exp_fun_parameter ~state parameter cmt_tbl = and print_expression_block ~state ~braces expr cmt_tbl = let expr = Parsetree_viewer.unwrap_braces expr in + (* Attributes can print on earlier lines than the expression itself. Include + them when preserving blank lines between statements. *) + let statement_loc (expr : Parsetree.expression) = + match expr.pexp_attributes with + | ({loc}, _) :: _ -> {expr.pexp_loc with loc_start = loc.loc_start} + | [] -> expr.pexp_loc + in let rec collect_rows acc expr = match expr.Parsetree.pexp_desc with | Parsetree.Pexp_letmodule (mod_name, mod_expr, expr2) -> @@ -5627,7 +5635,7 @@ and print_expression_block ~state ~braces expr cmt_tbl = print_statement_expr ~protect_regexp:(acc <> []) expr1 (Parens.expr expr1) doc in - let loc = expr1.pexp_loc in + let loc = statement_loc expr1 in collect_rows ((loc, expr_doc) :: acc) expr2 | Pexp_let (rec_flag, value_bindings, expr2) -> ( let loc = @@ -5667,7 +5675,7 @@ and print_expression_block ~state ~braces expr cmt_tbl = print_statement_expr ~protect_regexp:(acc <> []) expr (Parens.expr expr) doc in - List.rev ((expr.pexp_loc, expr_doc) :: acc) + List.rev ((statement_loc expr, expr_doc) :: acc) in let rows = collect_rows [] expr in let block = diff --git a/tests/build_tests/regexp_statement_formatting/Main.res b/tests/build_tests/regexp_statement_formatting/Main.res index 397108fe7cb..eeec1e908e8 100644 --- a/tests/build_tests/regexp_statement_formatting/Main.res +++ b/tests/build_tests/regexp_statement_formatting/Main.res @@ -79,7 +79,16 @@ let fieldCall = key => { let get = (regexp, key) => {test: () => regexp->RegExp.test(key)} } ignore(key) - (/a/[key].test()) + @warning("-26") + /a/[key].test() +} + +let attributedPlaceholder = () => { + module Array = { + let get = (regexp, _index) => (value, suffix) => regexp->RegExp.test(value ++ suffix) + } + ignore() + @warning("-26") /^a$/[0](_, "a") } let division = a => { diff --git a/tests/build_tests/regexp_statement_formatting/input.js b/tests/build_tests/regexp_statement_formatting/input.js index 64701fb04d0..51f5bceb0b6 100644 --- a/tests/build_tests/regexp_statement_formatting/input.js +++ b/tests/build_tests/regexp_statement_formatting/input.js @@ -53,6 +53,8 @@ try { assert.equal(output.fieldMutation("b", 7), 0); assert.equal(output.fieldCall("a"), true); assert.equal(output.fieldCall("b"), false); + assert.equal(output.attributedPlaceholder()(""), true); + assert.equal(output.attributedPlaceholder()("b"), false); assert.equal(output.division(24), 4); assert.equal(output.floatDivision(24), 4); } finally { diff --git a/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt b/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt index fcf39fa8a6d..6d1d49c92c3 100644 --- a/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt +++ b/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt @@ -147,3 +147,13 @@ module FirstNested = { open FirstModule /afterOpen/->ignore } + +let attributes = i => { + Console.log("before") + @attr /a/[i](_, "a") + @attr + /a/[i].foo() + @attr + (/a/ ? 1 : 2) + (@attr /a/) ? 1 : 2 +} diff --git a/tests/syntax_tests/data/printer/expr/expected/markerAttributes.res.txt b/tests/syntax_tests/data/printer/expr/expected/markerAttributes.res.txt index 999ae68d6e3..081808d521e 100644 --- a/tests/syntax_tests/data/printer/expr/expected/markerAttributes.res.txt +++ b/tests/syntax_tests/data/printer/expr/expected/markerAttributes.res.txt @@ -13,7 +13,7 @@ let a = (@JSX x) == y let a = (@JSX x) && y let a = !(@JSX x) let a = (@JSX x).field -let a = @JSX x ? a : b +let a = (@JSX x) ? a : b let b = @res.ternary x let b = (@res.ternary x) + 1 @@ -27,8 +27,8 @@ let b = !(@res.ternary x) let b = -(@res.ternary x) let b = (@res.ternary x).field let b = (@res.ternary x)[0] -let b = @res.ternary x ? a : b -let b = c ? @res.ternary x : b +let b = (@res.ternary x) ? a : b +let b = c ? (@res.ternary x) : b let b = f(@res.ternary x) let b = [@res.ternary x, y] let b = {a: @res.ternary x} diff --git a/tests/syntax_tests/data/printer/expr/expected/ternary.res.txt b/tests/syntax_tests/data/printer/expr/expected/ternary.res.txt index 6de00d07868..cf3ea657fb0 100644 --- a/tests/syntax_tests/data/printer/expr/expected/ternary.res.txt +++ b/tests/syntax_tests/data/printer/expr/expected/ternary.res.txt @@ -258,7 +258,7 @@ a => a ? a : a a => a ? aasdasdasdasdasdasdaaasdasdasdasdasdasdasdasdasdasdasdasdasdaaaaaaaaa : a let x = @attrOnTernary (truth ? true : false) -let x = @attrOnCondition truth ? true : false +let x = (@attrOnCondition truth) ? true : false x ? y => () : onChange let y = () : onChange} /> diff --git a/tests/syntax_tests/data/printer/regexpStatements.res b/tests/syntax_tests/data/printer/regexpStatements.res index da90a65c98f..3f85439dbba 100644 --- a/tests/syntax_tests/data/printer/regexpStatements.res +++ b/tests/syntax_tests/data/printer/regexpStatements.res @@ -151,3 +151,11 @@ module FirstNested = { open FirstModule /afterOpen/->ignore } + +let attributes = i => { + Console.log("before") + (@attr ((/a/)[i])(_, "a")) + (@attr (((/a/)[i]).foo())) + (@attr (/a/ ? 1 : 2)) + ((@attr /a/) ? 1 : 2) +} diff --git a/tests/syntax_tests/res_test.ml b/tests/syntax_tests/res_test.ml index a687afc5249..56d7be97a5e 100644 --- a/tests/syntax_tests/res_test.ml +++ b/tests/syntax_tests/res_test.ml @@ -155,30 +155,6 @@ let () = in assert (not reparsed.invalid) -let () = - List.iter - (fun (source, expected) -> - let parsed = - Res_driver.parse_implementation_from_source - ~display_filename:"regexp.res" ~source - in - assert (not parsed.invalid); - let printed = - Res_printer.print_implementation ~width:100 parsed.parsetree - ~comments:parsed.comments - in - if printed <> expected then - failwith ("Unexpected regexp parentheses:\n" ^ printed)) - [ - ("/a/->f", "/a/->f\n"); - ("type t = int\n/a/->f", "type t = int\n/a/->f\n"); - ("open M\n/a/->f", "open M\n/a/->f\n"); - ( "let f = () => {ignore(); (/a/ ? 1 : 2) + 1}", - "let f = () => {\n ignore()\n (/a/ ? 1 : 2) + 1\n}\n" ); - ( "let f = () => {ignore(); (@attr /a/)->f}", - "let f = () => {\n ignore()\n (@attr /a/)->f\n}\n" ); - ] - module Outcome_printer_tests = struct let signature_to_outcome structure = Lazy.force Res_outcome_printer.setup; From cdf2c08374b325003a725a42c39b22683d7bc89e Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Mon, 12 Oct 2026 22:36:41 +0200 Subject: [PATCH 7/9] Preserve comments on attributed statements Signed-off-by: Florian Hammerschmidt --- compiler/syntax/src/res_printer.ml | 24 ++++++++++++------- .../printer/expected/regexpStatements.res.txt | 13 ++++++++++ .../data/printer/regexpStatements.res | 12 ++++++++++ 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index d5d2528a068..01015564805 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -5566,12 +5566,19 @@ and print_exp_fun_parameter ~state parameter cmt_tbl = and print_expression_block ~state ~braces expr cmt_tbl = let expr = Parsetree_viewer.unwrap_braces expr in - (* Attributes can print on earlier lines than the expression itself. Include - them when preserving blank lines between statements. *) - let statement_loc (expr : Parsetree.expression) = - match expr.pexp_attributes with - | ({loc}, _) :: _ -> {expr.pexp_loc with loc_start = loc.loc_start} - | [] -> expr.pexp_loc + (* Include attributes and leading comments when preserving blank lines, but + look up comments using the original expression location. *) + let statement_row (expr : Parsetree.expression) doc = + let loc = + match get_first_leading_comment cmt_tbl expr.pexp_loc with + | Some comment -> + {(Comment.loc comment) with loc_end = expr.pexp_loc.loc_end} + | None -> ( + match expr.pexp_attributes with + | ({loc}, _) :: _ -> {expr.pexp_loc with loc_start = loc.loc_start} + | [] -> expr.pexp_loc) + in + (loc, print_comments doc cmt_tbl expr.pexp_loc) in let rec collect_rows acc expr = match expr.Parsetree.pexp_desc with @@ -5635,8 +5642,7 @@ and print_expression_block ~state ~braces expr cmt_tbl = print_statement_expr ~protect_regexp:(acc <> []) expr1 (Parens.expr expr1) doc in - let loc = statement_loc expr1 in - collect_rows ((loc, expr_doc) :: acc) expr2 + collect_rows (statement_row expr1 expr_doc :: acc) expr2 | Pexp_let (rec_flag, value_bindings, expr2) -> ( let loc = let loc = @@ -5675,7 +5681,7 @@ and print_expression_block ~state ~braces expr cmt_tbl = print_statement_expr ~protect_regexp:(acc <> []) expr (Parens.expr expr) doc in - List.rev ((statement_loc expr, expr_doc) :: acc) + List.rev (statement_row expr expr_doc :: acc) in let rows = collect_rows [] expr in let block = diff --git a/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt b/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt index 6d1d49c92c3..4c0a909a380 100644 --- a/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt +++ b/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt @@ -157,3 +157,16 @@ let attributes = i => { (/a/ ? 1 : 2) (@attr /a/) ? 1 : 2 } + +let attributeComments = i => { + Console.log("before") + /* Keep the comment before an attributed statement. */ + + @attr + /a/[i].foo() // intermediate statement + Console.log("after") + // Keep the comment before the final attributed statement. + + @attr + /a/[i].foo() /* final statement */ +} diff --git a/tests/syntax_tests/data/printer/regexpStatements.res b/tests/syntax_tests/data/printer/regexpStatements.res index 3f85439dbba..a1ceb42d3cc 100644 --- a/tests/syntax_tests/data/printer/regexpStatements.res +++ b/tests/syntax_tests/data/printer/regexpStatements.res @@ -159,3 +159,15 @@ let attributes = i => { (@attr (/a/ ? 1 : 2)) ((@attr /a/) ? 1 : 2) } + +let attributeComments = i => { + Console.log("before") + /* Keep the comment before an attributed statement. */ + + @attr + /a/[i].foo() // intermediate statement + Console.log("after") + // Keep the comment before the final attributed statement. + @attr + /a/[i].foo() /* final statement */ +} From 74d239b609c4f4e91aa40049b9488f34de7ce81c Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Tue, 13 Oct 2026 14:13:35 +0200 Subject: [PATCH 8/9] Document ternary attribute scope preservation Signed-off-by: Florian Hammerschmidt --- changes/regexp-statement-formatting.fix.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changes/regexp-statement-formatting.fix.md b/changes/regexp-statement-formatting.fix.md index 08066b47394..0f27031ec83 100644 --- a/changes/regexp-statement-formatting.fix.md +++ b/changes/regexp-statement-formatting.fix.md @@ -1 +1,2 @@ - Preserve parentheses around statements that start with a regexp literal, so formatting cannot turn them into division continuing the previous statement. https://github.com/rescript-lang/rescript/pull/8720 +- Preserve the scope of attributes on ternary operands when formatting. https://github.com/rescript-lang/rescript/pull/8720 From f76591a43dd549a74e33c97691d092dd9d15155b Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Wed, 14 Oct 2026 21:45:12 +0200 Subject: [PATCH 9/9] Limit formatter parentheses to ambiguous statement boundaries Signed-off-by: Florian Hammerschmidt --- changes/regexp-statement-formatting.fix.md | 2 +- compiler/syntax/src/res_parens.ml | 7 ++-- compiler/syntax/src/res_parens.mli | 2 +- compiler/syntax/src/res_printer.ml | 34 +++++++-------- .../regexp_statement_formatting/Main.res | 10 +++++ .../regexp_statement_formatting/input.js | 4 ++ .../printer/expected/regexpStatements.res.txt | 29 +++++++++++++ .../expr/expected/markerAttributes.res.txt | 2 +- .../data/printer/regexpStatements.res | 27 ++++++++++++ tests/syntax_tests/res_test.ml | 41 +++++++++++++++++++ 10 files changed, 136 insertions(+), 22 deletions(-) diff --git a/changes/regexp-statement-formatting.fix.md b/changes/regexp-statement-formatting.fix.md index 0f27031ec83..a4f2519654b 100644 --- a/changes/regexp-statement-formatting.fix.md +++ b/changes/regexp-statement-formatting.fix.md @@ -1,2 +1,2 @@ - Preserve parentheses around statements that start with a regexp literal, so formatting cannot turn them into division continuing the previous statement. https://github.com/rescript-lang/rescript/pull/8720 -- Preserve the scope of attributes on ternary operands when formatting. https://github.com/rescript-lang/rescript/pull/8720 +- Preserve the scope of attributes on ternary conditions when formatting. https://github.com/rescript-lang/rescript/pull/8720 diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index 84b4c00f2cb..612e8e3926f 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -280,7 +280,7 @@ let field_expr expr = | _ when Parsetree_viewer.expr_is_await expr -> Parenthesized | _ -> Nothing) -let ternary_operand expr = +let ternary_operand ?(is_condition = false) expr = let opt_braces, _ = Parsetree_viewer.process_braces expr in match opt_braces with | Some braces_loc -> Braced braces_loc @@ -291,7 +291,8 @@ let ternary_operand expr = Pexp_constraint ({pexp_desc = Pexp_pack _}, {ptyp_desc = Ptyp_package _}); } -> Nothing - | _ when expr.pexp_attributes <> [] -> Parenthesized + (* A leading condition attribute otherwise attaches to the whole statement. *) + | _ when is_condition && expr.pexp_attributes <> [] -> Parenthesized | {pexp_desc = Pexp_ternary _} -> Parenthesized | {pexp_desc = Pexp_constraint _ | Pexp_coerce _} -> Parenthesized | _ when Res_parsetree_viewer.is_fun_expr expr -> ( @@ -518,7 +519,7 @@ let rec starts_with_regexp e = | _ -> starts_unwrapped (binary_expr_operand ~is_lhs:true lhs) lhs) | Pexp_apply {funct} -> starts_unwrapped (call_expr funct) funct | Pexp_ternary (condition, _consequent, _alternate) -> - starts_unwrapped (ternary_operand condition) condition + starts_unwrapped (ternary_operand ~is_condition:true condition) condition | Pexp_field (obj, _) | Pexp_setfield (obj, _, _) -> starts_unwrapped (field_expr obj) obj | Pexp_fun {body} when Parsetree_viewer.is_underscore_apply_sugar e -> diff --git a/compiler/syntax/src/res_parens.mli b/compiler/syntax/src/res_parens.mli index 427b92b88e3..31d8d74ab1c 100644 --- a/compiler/syntax/src/res_parens.mli +++ b/compiler/syntax/src/res_parens.mli @@ -21,7 +21,7 @@ val assert_or_await_expr_rhs : ?in_await:bool -> Parsetree.expression -> kind val field_expr : Parsetree.expression -> kind -val ternary_operand : Parsetree.expression -> kind +val ternary_operand : ?is_condition:bool -> Parsetree.expression -> kind val jsx_prop_expr : Parsetree.expression -> kind val jsx_child_expr : Parsetree.expression -> kind diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 01015564805..04ad65ebe72 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -758,6 +758,7 @@ and print_structure_item ~state ?(protect_regexp = false) | Pstr_eval (expr, attrs) -> clear_pending (); let expr_doc = + let protect_regexp = protect_regexp && attrs = [] in let doc = print_expression_with_comments ~state expr cmt_tbl in print_statement_expr ~protect_regexp expr (Parens.structure_expr expr) doc in @@ -3586,7 +3587,8 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = Doc.group (Doc.concat [ - print_ternary_operand ~state condition1 cmt_tbl; + print_ternary_operand ~is_condition:true ~state condition1 + cmt_tbl; Doc.indent (Doc.concat [ @@ -3604,8 +3606,8 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = [ Doc.line; Doc.text ": "; - print_ternary_operand ~state condition - cmt_tbl; + print_ternary_operand ~is_condition:true + ~state condition cmt_tbl; Doc.line; Doc.text "? "; print_ternary_operand ~state consequent @@ -3947,9 +3949,9 @@ and print_pexp_fun ~state ~in_callback e cmt_tbl = | NoCallback -> Doc.group doc | FitsOnOneLine | ArgumentsFitOnOneLine -> doc -and print_ternary_operand ~state expr cmt_tbl = +and print_ternary_operand ?(is_condition = false) ~state expr cmt_tbl = let doc = print_expression_with_comments ~state expr cmt_tbl in - match Parens.ternary_operand expr with + match Parens.ternary_operand ~is_condition expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc @@ -5580,7 +5582,7 @@ and print_expression_block ~state ~braces expr cmt_tbl = in (loc, print_comments doc cmt_tbl expr.pexp_loc) in - let rec collect_rows acc expr = + let rec collect_rows ~protect_regexp acc expr = match expr.Parsetree.pexp_desc with | Parsetree.Pexp_letmodule (mod_name, mod_expr, expr2) -> let name = @@ -5608,7 +5610,7 @@ and print_expression_block ~state ~braces expr cmt_tbl = ] in let loc = {expr.pexp_loc with loc_end = mod_expr.pmod_loc.loc_end} in - collect_rows ((loc, let_module_doc) :: acc) expr2 + collect_rows ~protect_regexp:false ((loc, let_module_doc) :: acc) expr2 | Pexp_letexception (extension_constructor, expr2) -> let loc = let loc = @@ -5623,7 +5625,7 @@ and print_expression_block ~state ~braces expr cmt_tbl = let let_exception_doc = print_exception_def ~state extension_constructor cmt_tbl in - collect_rows ((loc, let_exception_doc) :: acc) expr2 + collect_rows ~protect_regexp:false ((loc, let_exception_doc) :: acc) expr2 | Pexp_open (override_flag, longident_loc, expr2) -> let open_doc = Doc.concat @@ -5635,14 +5637,15 @@ and print_expression_block ~state ~braces expr cmt_tbl = ] in let loc = {expr.pexp_loc with loc_end = longident_loc.loc.loc_end} in - collect_rows ((loc, open_doc) :: acc) expr2 + collect_rows ~protect_regexp:false ((loc, open_doc) :: acc) expr2 | Pexp_sequence (expr1, expr2) -> let expr_doc = let doc = print_expression ~state expr1 cmt_tbl in - print_statement_expr ~protect_regexp:(acc <> []) expr1 - (Parens.expr expr1) doc + print_statement_expr ~protect_regexp expr1 (Parens.expr expr1) doc in - collect_rows (statement_row expr1 expr_doc :: acc) expr2 + collect_rows ~protect_regexp:true + (statement_row expr1 expr_doc :: acc) + expr2 | Pexp_let (rec_flag, value_bindings, expr2) -> ( let loc = let loc = @@ -5674,16 +5677,15 @@ and print_expression_block ~state ~braces expr cmt_tbl = match expr2.pexp_desc with | Pexp_construct ({txt = Longident.Lident "()"}, _) -> List.rev ((loc, let_doc) :: acc) - | _ -> collect_rows ((loc, let_doc) :: acc) expr2) + | _ -> collect_rows ~protect_regexp:true ((loc, let_doc) :: acc) expr2) | _ -> let expr_doc = let doc = print_expression ~state expr cmt_tbl in - print_statement_expr ~protect_regexp:(acc <> []) expr (Parens.expr expr) - doc + print_statement_expr ~protect_regexp expr (Parens.expr expr) doc in List.rev (statement_row expr expr_doc :: acc) in - let rows = collect_rows [] expr in + let rows = collect_rows ~protect_regexp:false [] expr in let block = print_list ~get_loc:fst ~nodes:rows ~print:(fun (_, doc) _ -> doc) diff --git a/tests/build_tests/regexp_statement_formatting/Main.res b/tests/build_tests/regexp_statement_formatting/Main.res index eeec1e908e8..89864faa172 100644 --- a/tests/build_tests/regexp_statement_formatting/Main.res +++ b/tests/build_tests/regexp_statement_formatting/Main.res @@ -8,6 +8,16 @@ let afterExpression = s => { (/a/->RegExp.test(s)) } +let afterOpen = s => { + open RegExp + /a/->test(s) +} + +let afterModule = s => { + module RE = RegExp + /a/->RE.test(s) +} + let intermediate = () => { let seen = ref(false) (/a/->RegExp.test("a")->(result => seen := result)) diff --git a/tests/build_tests/regexp_statement_formatting/input.js b/tests/build_tests/regexp_statement_formatting/input.js index 51f5bceb0b6..cb2ed453d7c 100644 --- a/tests/build_tests/regexp_statement_formatting/input.js +++ b/tests/build_tests/regexp_statement_formatting/input.js @@ -40,6 +40,10 @@ try { assert.equal(output.afterLet("a"), true); assert.equal(output.afterLet("b"), false); assert.equal(output.afterExpression("a"), true); + assert.equal(output.afterOpen("a"), true); + assert.equal(output.afterOpen("b"), false); + assert.equal(output.afterModule("a"), true); + assert.equal(output.afterModule("b"), false); assert.equal(output.intermediate(), true); assert.equal(output.bare().test("a"), true); assert.equal(output.dotAndWhitespace(" a"), true); diff --git a/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt b/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt index 4c0a909a380..a5193971eca 100644 --- a/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt +++ b/tests/syntax_tests/data/printer/expected/regexpStatements.res.txt @@ -170,3 +170,32 @@ let attributeComments = i => { @attr /a/[i].foo() /* final statement */ } + +let afterBlockOpen = () => { + let before = 0 + open Foo + /a/->ignore +} + +let afterBlockModule = () => { + let before = 0 + module M = Foo + /a/->ignore + (/b/->ignore) +} + +let afterOpenExpression = () => { + open Foo + Console.log("before") + (/a/->ignore) +} + +let beforeStatementAttribute = 0 +@foo +/a/->ignore +/**Keep the documentation attribute.*/ +/b/->ignore + +let ternaryAttributes = (@condition truth) ? @yes first : @no second +let nestedTernaryAttributes = truth ? first : (@condition other) ? @yes second : @no third +(@condition truth) ? @yes first : @no second diff --git a/tests/syntax_tests/data/printer/expr/expected/markerAttributes.res.txt b/tests/syntax_tests/data/printer/expr/expected/markerAttributes.res.txt index 081808d521e..64b38878f1a 100644 --- a/tests/syntax_tests/data/printer/expr/expected/markerAttributes.res.txt +++ b/tests/syntax_tests/data/printer/expr/expected/markerAttributes.res.txt @@ -28,7 +28,7 @@ let b = -(@res.ternary x) let b = (@res.ternary x).field let b = (@res.ternary x)[0] let b = (@res.ternary x) ? a : b -let b = c ? (@res.ternary x) : b +let b = c ? @res.ternary x : b let b = f(@res.ternary x) let b = [@res.ternary x, y] let b = {a: @res.ternary x} diff --git a/tests/syntax_tests/data/printer/regexpStatements.res b/tests/syntax_tests/data/printer/regexpStatements.res index a1ceb42d3cc..3e794934856 100644 --- a/tests/syntax_tests/data/printer/regexpStatements.res +++ b/tests/syntax_tests/data/printer/regexpStatements.res @@ -171,3 +171,30 @@ let attributeComments = i => { @attr /a/[i].foo() /* final statement */ } + +let afterBlockOpen = () => { + let before = 0 + open Foo + (/a/->ignore) +} + +let afterBlockModule = () => { + let before = 0 + module M = Foo + (/a/->ignore) + (/b/->ignore) +} + +let afterOpenExpression = () => { + open Foo + Console.log("before") + (/a/->ignore) +} + +let beforeStatementAttribute = 0 +@foo (/a/->ignore) +@res.doc("Keep the documentation attribute.") (/b/->ignore) + +let ternaryAttributes = (@condition truth) ? (@yes first) : (@no second) +let nestedTernaryAttributes = truth ? first : ((@condition other) ? (@yes second) : (@no third)) +(@condition truth) ? (@yes first) : (@no second) diff --git a/tests/syntax_tests/res_test.ml b/tests/syntax_tests/res_test.ml index 56d7be97a5e..de7113a3710 100644 --- a/tests/syntax_tests/res_test.ml +++ b/tests/syntax_tests/res_test.ml @@ -109,6 +109,47 @@ let () = [20; 40; 80; 100; 120]; print_endline "✅ regexp statements round-trip at multiple widths" +let () = + let without_locations tree = + let mapper = + {Ast_mapper.default_mapper with location = (fun _ _ -> Location.none)} + in + mapper.structure mapper tree + in + let parse source = + let result = + Res_driver.parse_implementation_from_source + ~display_filename:"statement-boundaries.res" ~source + in + assert (not result.invalid); + result + in + List.iter + (fun source -> + let original = parse source in + List.iter + (fun width -> + let printed = + Res_printer.print_implementation ~width original.parsetree + ~comments:original.comments + in + let reparsed = parse printed in + assert ( + without_locations original.parsetree + = without_locations reparsed.parsetree)) + [20; 40; 80; 100; 120]) + [ + "(@condition truth) ? (@yes first) : (@no second)"; + "let result = (@condition truth) ? (@yes first) : (@no second)"; + "let result = truth ? first : ((@condition other) ? (@yes second) : (@no \ + third))"; + "let f = () => {\nlet before = 0\nopen Foo\n(/a/->ignore)\n}"; + "let f = () => {\nmodule M = Foo\n(/a/->ignore)\n(/b/->ignore)\n}"; + "let f = () => {\nopen Foo\nignore()\n(/a/->ignore)\n}"; + "let before = 0\n@foo (/a/->ignore)"; + ]; + print_endline "✅ statement boundary formatting preserves attribute scope" + (* PPX-generated ## accesses print their receiver before the operator. *) let () = let source = "let t = 0\n(/a/[\"member\"])" in