From f7d0a06e958361852ed16a307e70f4bdd272af7d Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 13:38:06 +0200 Subject: [PATCH 01/25] add experimental command to format code blocks embedded in docstrings --- compiler/ml/location.ml | 20 ++- compiler/ml/location.mli | 24 ++- compiler/syntax/src/res_diagnostics.ml | 11 +- compiler/syntax/src/res_diagnostics.mli | 7 +- .../FormatDocstringsTest1.res | 48 +++++ .../FormatDocstringsTest1.resi | 48 +++++ .../FormatDocstringsTest2.res | 41 +++++ .../FormatDocstringsTestError.res | 9 + .../FormatDocstringsTest1.res.expected | 55 ++++++ .../FormatDocstringsTest1.resi.expected | 55 ++++++ .../FormatDocstringsTest2.res.expected | 47 +++++ .../FormatDocstringsTestError.res.expected | 11 ++ tests/tools_tests/test.sh | 10 ++ tools/bin/main.ml | 33 +++- tools/src/tools.ml | 166 ++++++++++++++++++ 15 files changed, 564 insertions(+), 21 deletions(-) create mode 100644 tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res create mode 100644 tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.resi create mode 100644 tests/tools_tests/src/docstrings-format/FormatDocstringsTest2.res create mode 100644 tests/tools_tests/src/docstrings-format/FormatDocstringsTestError.res create mode 100644 tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected create mode 100644 tests/tools_tests/src/expected/FormatDocstringsTest1.resi.expected create mode 100644 tests/tools_tests/src/expected/FormatDocstringsTest2.res.expected create mode 100644 tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected diff --git a/compiler/ml/location.ml b/compiler/ml/location.ml index 87592822e8..19de2b7125 100644 --- a/compiler/ml/location.ml +++ b/compiler/ml/location.ml @@ -231,24 +231,29 @@ let error_of_exn exn = (* taken from https://github.com/rescript-lang/ocaml/blob/d4144647d1bf9bc7dc3aadc24c25a7efa3a67915/parsing/location.ml#L380 *) (* This is the error report entry point. We'll replace the default reporter with this one. *) -let rec default_error_reporter ?(src = None) ppf {loc; msg; sub} = +let rec default_error_reporter ?(custom_intro = None) ?(src = None) ppf + {loc; msg; sub} = setup_colors (); (* open a vertical box. Everything in our message is indented 2 spaces *) (* If src is given, it will display a syntax error after parsing. *) let intro = - match src with - | Some _ -> "Syntax error!" - | None -> "We've found a bug for you!" + match (custom_intro, src) with + | Some intro, _ -> intro + | None, Some _ -> "Syntax error!" + | None, None -> "We've found a bug for you!" in Format.fprintf ppf "@[@, %a@, %s@,@]" (print ~src ~message_kind:`error intro) loc msg; - List.iter (Format.fprintf ppf "@,@[%a@]" (default_error_reporter ~src)) sub + List.iter + (Format.fprintf ppf "@,@[%a@]" (default_error_reporter ~custom_intro ~src)) + sub (* no need to flush here; location's report_exception (which uses this ultimately) flushes *) let error_reporter = ref default_error_reporter -let report_error ?(src = None) ppf err = !error_reporter ~src ppf err +let report_error ?(custom_intro = None) ?(src = None) ppf err = + !error_reporter ~custom_intro ~src ppf err let error_of_printer loc print x = errorf ~loc "%a@?" print x @@ -276,7 +281,8 @@ let rec report_exception_rec n ppf exn = match error_of_exn exn with | None -> reraise exn | Some `Already_displayed -> () - | Some (`Ok err) -> fprintf ppf "@[%a@]@." (report_error ~src:None) err + | Some (`Ok err) -> + fprintf ppf "@[%a@]@." (report_error ~custom_intro:None ~src:None) err with exn when n > 0 -> report_exception_rec (n - 1) ppf exn let report_exception ppf exn = report_exception_rec 5 ppf exn diff --git a/compiler/ml/location.mli b/compiler/ml/location.mli index 0df157efcc..49758de42a 100644 --- a/compiler/ml/location.mli +++ b/compiler/ml/location.mli @@ -103,12 +103,28 @@ val register_error_of_exn : (exn -> error option) -> unit a location, a message, and optionally sub-messages (each of them being located as well). *) -val report_error : ?src:string option -> formatter -> error -> unit - -val error_reporter : (?src:string option -> formatter -> error -> unit) ref +val report_error : + ?custom_intro:string option -> + ?src:string option -> + formatter -> + error -> + unit + +val error_reporter : + (?custom_intro:string option -> + ?src:string option -> + formatter -> + error -> + unit) + ref (** Hook for intercepting error reports. *) -val default_error_reporter : ?src:string option -> formatter -> error -> unit +val default_error_reporter : + ?custom_intro:string option -> + ?src:string option -> + formatter -> + error -> + unit (** Original error reporter for use in hooks. *) val report_exception : formatter -> exn -> unit diff --git a/compiler/syntax/src/res_diagnostics.ml b/compiler/syntax/src/res_diagnostics.ml index 7fd3b4df04..4b6ad8ca69 100644 --- a/compiler/syntax/src/res_diagnostics.ml +++ b/compiler/syntax/src/res_diagnostics.ml @@ -131,12 +131,13 @@ let explain t = let make ~start_pos ~end_pos category = {start_pos; end_pos; category} -let print_report diagnostics src = +let print_report ?(custom_intro = None) ?(formatter = Format.err_formatter) + diagnostics src = let rec print diagnostics src = match diagnostics with | [] -> () | d :: rest -> - Location.report_error ~src:(Some src) Format.err_formatter + Location.report_error ~custom_intro ~src:(Some src) formatter Location. { loc = @@ -147,12 +148,12 @@ let print_report diagnostics src = }; (match rest with | [] -> () - | _ -> Format.fprintf Format.err_formatter "@."); + | _ -> Format.fprintf formatter "@."); print rest src in - Format.fprintf Format.err_formatter "@["; + Format.fprintf formatter "@["; print (List.rev diagnostics) src; - Format.fprintf Format.err_formatter "@]@." + Format.fprintf formatter "@]@." let unexpected token context = Unexpected {token; context} diff --git a/compiler/syntax/src/res_diagnostics.mli b/compiler/syntax/src/res_diagnostics.mli index 4fd9155665..694788ac4b 100644 --- a/compiler/syntax/src/res_diagnostics.mli +++ b/compiler/syntax/src/res_diagnostics.mli @@ -22,4 +22,9 @@ val message : string -> category val make : start_pos:Lexing.position -> end_pos:Lexing.position -> category -> t -val print_report : t list -> string -> unit +val print_report : + ?custom_intro:string option -> + ?formatter:Format.formatter -> + t list -> + string -> + unit diff --git a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res new file mode 100644 index 0000000000..b2258d42cc --- /dev/null +++ b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res @@ -0,0 +1,48 @@ +/** +This is the first docstring with unformatted ReScript code. + +```rescript +let badly_formatted=(x,y)=>{ +let result=x+y +if result>0{Console.log("positive")}else{Console.log("negative")} +result +} +``` + +And another code block in the same docstring: + +```rescript +type user={name:string,age:int,active:bool} +let createUser=(name,age)=>{name:name,age:age,active:true} +``` +*/ +let testFunction1 = () => "test1" + +module Nested = { + /** + This is a second docstring with different formatting issues. + + But if I add another line here it should be fine. + + ```rescript + module UserService={ + let validate=user => user.age>=18 && user.name !== "" + let getName = user=>user.name + } + ``` +*/ + let testFunction2 = () => "test2" +} + +/** +Third docstring with array and option types. + +```rescript +let processUsers=(users:array)=>{ +users->Array.map(user=>{...user,active:false})->Array.filter(u=>u.age>21) +} + +type status=|Loading|Success(string)|Error(option) +``` +*/ +let testFunction3 = () => "test3" diff --git a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.resi b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.resi new file mode 100644 index 0000000000..c7b5343feb --- /dev/null +++ b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.resi @@ -0,0 +1,48 @@ +/** +This is the first docstring with unformatted ReScript code. + +```rescript +let badly_formatted=(x,y)=>{ +let result=x+y +if result>0{Console.log("positive")}else{Console.log("negative")} +result +} +``` + +And another code block in the same docstring: + +```rescript +type user={name:string,age:int,active:bool} +let createUser=(name,age)=>{name:name,age:age,active:true} +``` +*/ +let testFunction1: unit => string + +module Nested: { + /** + This is a second docstring with different formatting issues. + + But if I add another line here it should be fine. + + ```rescript + module UserService={ + let validate=user => user.age>=18 && user.name !== "" + let getName = user=>user.name + } + ``` +*/ + let testFunction2: unit => string +} + +/** +Third docstring with array and option types. + +```rescript +let processUsers=(users:array)=>{ +users->Array.map(user=>{...user,active:false})->Array.filter(u=>u.age>21) +} + +type status=|Loading|Success(string)|Error(option) +``` +*/ +let testFunction3: unit => string diff --git a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest2.res b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest2.res new file mode 100644 index 0000000000..24ece061d8 --- /dev/null +++ b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest2.res @@ -0,0 +1,41 @@ +/** +Testing JSX and more complex formatting scenarios. + +```rescript +let component=()=>{ +
+

{"Title"->React.string}

+ +
+} +``` + +Testing pattern matching and switch expressions: + +```rescript +let handleResult=(result:result)=>{ +switch result { +| Ok(value)=>Console.log(`Success: ${value}`) +| Error(error)=>Console.error(`Error: ${error}`) +} +} +``` +*/ +let testJsx = () => "jsx test" + +/** +Testing function composition and piping. + +```rescript +let processData=(data:array)=>{ +data->Array.filter(x=>x>0)->Array.map(x=>x*2)->Array.reduce(0,(acc,x)=>acc+x) +} + +let asyncExample=async()=>{ +let data=await fetchData() +let processed=await processData(data) +Console.log(processed) +} +``` +*/ +let testPipes = () => "pipes test" diff --git a/tests/tools_tests/src/docstrings-format/FormatDocstringsTestError.res b/tests/tools_tests/src/docstrings-format/FormatDocstringsTestError.res new file mode 100644 index 0000000000..dda74f297f --- /dev/null +++ b/tests/tools_tests/src/docstrings-format/FormatDocstringsTestError.res @@ -0,0 +1,9 @@ +/** +This docstring has an error. + +```rescript +let name= +let x=12 +``` +*/ +let testJsx = () => "jsx test" diff --git a/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected b/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected new file mode 100644 index 0000000000..0fbe95261e --- /dev/null +++ b/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected @@ -0,0 +1,55 @@ +/** +This is the first docstring with unformatted ReScript code. + +```rescript +let badly_formatted = (x, y) => { + let result = x + y + if result > 0 { + Console.log("positive") + } else { + Console.log("negative") + } + result +} +``` + +And another code block in the same docstring: + +```rescript +type user = {name: string, age: int, active: bool} +let createUser = (name, age) => {name, age, active: true} +``` +*/ +let testFunction1 = () => "test1" + +module Nested = { + /** + This is a second docstring with different formatting issues. + + But if I add another line here it should be fine. + + ```rescript + module UserService = { + let validate = user => user.age >= 18 && user.name !== "" + let getName = user => user.name + } + ``` +*/ + let testFunction2 = () => "test2" +} + +/** +Third docstring with array and option types. + +```rescript +let processUsers = (users: array) => { + users + ->Array.map(user => {...user, active: false}) + ->Array.filter(u => u.age > 21) +} + +type status = Loading | Success(string) | Error(option) +``` +*/ +let testFunction3 = () => "test3" + diff --git a/tests/tools_tests/src/expected/FormatDocstringsTest1.resi.expected b/tests/tools_tests/src/expected/FormatDocstringsTest1.resi.expected new file mode 100644 index 0000000000..f3833fc742 --- /dev/null +++ b/tests/tools_tests/src/expected/FormatDocstringsTest1.resi.expected @@ -0,0 +1,55 @@ +/** +This is the first docstring with unformatted ReScript code. + +```rescript +let badly_formatted = (x, y) => { + let result = x + y + if result > 0 { + Console.log("positive") + } else { + Console.log("negative") + } + result +} +``` + +And another code block in the same docstring: + +```rescript +type user = {name: string, age: int, active: bool} +let createUser = (name, age) => {name, age, active: true} +``` +*/ +let testFunction1: unit => string + +module Nested: { + /** + This is a second docstring with different formatting issues. + + But if I add another line here it should be fine. + + ```rescript + module UserService = { + let validate = user => user.age >= 18 && user.name !== "" + let getName = user => user.name + } + ``` +*/ + let testFunction2: unit => string +} + +/** +Third docstring with array and option types. + +```rescript +let processUsers = (users: array) => { + users + ->Array.map(user => {...user, active: false}) + ->Array.filter(u => u.age > 21) +} + +type status = Loading | Success(string) | Error(option) +``` +*/ +let testFunction3: unit => string + diff --git a/tests/tools_tests/src/expected/FormatDocstringsTest2.res.expected b/tests/tools_tests/src/expected/FormatDocstringsTest2.res.expected new file mode 100644 index 0000000000..0738800e2f --- /dev/null +++ b/tests/tools_tests/src/expected/FormatDocstringsTest2.res.expected @@ -0,0 +1,47 @@ +/** +Testing JSX and more complex formatting scenarios. + +```rescript +let component = () => { +
+

{"Title"->React.string}

+ +
+} +``` + +Testing pattern matching and switch expressions: + +```rescript +let handleResult = (result: result) => { + switch result { + | Ok(value) => Console.log(`Success: ${value}`) + | Error(error) => Console.error(`Error: ${error}`) + } +} +``` +*/ +let testJsx = () => "jsx test" + +/** +Testing function composition and piping. + +```rescript +let processData = (data: array) => { + data + ->Array.filter(x => x > 0) + ->Array.map(x => x * 2) + ->Array.reduce(0, (acc, x) => acc + x) +} + +let asyncExample = async () => { + let data = await fetchData() + let processed = await processData(data) + Console.log(processed) +} +``` +*/ +let testPipes = () => "pipes test" + diff --git a/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected b/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected new file mode 100644 index 0000000000..fbb25e256b --- /dev/null +++ b/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected @@ -0,0 +1,11 @@ + + Syntax error in code block in docstring + /Users/zth/OSS/rescript-compiler/tests/tools_tests/src/docstrings-format/FormatDocstringsTestError.res:2:10-3:3 + + 1 │ + 2 │ let name=  + 3 │ let x=12 + + This let-binding misses an expression + + diff --git a/tests/tools_tests/test.sh b/tests/tools_tests/test.sh index cd1b32571a..4c8de617a8 100755 --- a/tests/tools_tests/test.sh +++ b/tests/tools_tests/test.sh @@ -16,6 +16,16 @@ for file in ppx/*.res; do fi done +# Test format-docstrings command +for file in src/docstrings-format/*.{res,resi}; do + output="src/expected/$(basename $file).expected" + ../../_build/install/default/bin/rescript-tools format-docstrings "$file" --stdout > $output + # # CI. We use LF, and the CI OCaml fork prints CRLF. Convert. + if [ "$RUNNER_OS" == "Windows" ]; then + perl -pi -e 's/\r\n/\n/g' -- $output + fi +done + warningYellow='\033[0;33m' successGreen='\033[0;32m' reset='\033[0m' diff --git a/tools/bin/main.ml b/tools/bin/main.ml index 2d97dea930..6b0a2a7249 100644 --- a/tools/bin/main.ml +++ b/tools/bin/main.ml @@ -7,6 +7,15 @@ Usage: rescript-tools doc Example: rescript-tools doc ./path/to/EntryPointLib.res|} +let formatDocstringsHelp = + {|ReScript Tools + +Format ReScript code blocks in docstrings + +Usage: rescript-tools format-docstrings [--stdout] + +Example: rescript-tools format-docstrings ./path/to/MyModule.res|} + let help = {|ReScript Tools @@ -14,10 +23,11 @@ Usage: rescript-tools [command] Commands: -doc Generate documentation -reanalyze Reanalyze --v, --version Print version --h, --help Print help|} +doc Generate documentation +format-docstrings [--stdout] Format ReScript code blocks in docstrings +reanalyze Reanalyze +-v, --version Print version +-h, --help Print help|} let logAndExit = function | Ok log -> @@ -43,6 +53,21 @@ let main () = in logAndExit (Tools.extractDocs ~entryPointFile:path ~debug:false) | _ -> logAndExit (Error docHelp)) + | "format-docstrings" :: rest -> ( + match rest with + | ["-h"] | ["--help"] -> logAndExit (Ok formatDocstringsHelp) + | [path; "--stdout"] -> ( + match + Tools.FormatDocstrings.formatDocstrings ~outputMode:`Stdout + ~entryPointFile:path + with + | Ok content -> print_endline content + | Error e -> logAndExit (Error e)) + | [path] -> + Tools.FormatDocstrings.formatDocstrings ~outputMode:`File + ~entryPointFile:path + |> logAndExit + | _ -> logAndExit (Error formatDocstringsHelp)) | "reanalyze" :: _ -> let len = Array.length Sys.argv in for i = 1 to len - 2 do diff --git a/tools/src/tools.ml b/tools/src/tools.ml index 2db1be7e6a..cddaf635ed 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -676,3 +676,169 @@ let extractEmbedded ~extensionPoints ~filename = ("loc", Some (Analysis.Utils.cmtLocToRange loc |> stringifyRange)); ]) |> List.rev |> array + +module FormatDocstrings = struct + let mapRescriptCodeBlocks ~colIndent ~(mapper : string -> int -> string) + (doc : string) = + let indent = String.make colIndent ' ' in + let len = String.length doc in + let buf = Buffer.create len in + let addIndent () = Buffer.add_string buf indent in + let currentCodeBlockContents = ref None in + let lines = String.split_on_char '\n' doc in + let lineCount = ref (-1) in + let rec processLines lines = + let currentLine = !lineCount in + lineCount := currentLine + 1; + match (lines, !currentCodeBlockContents) with + | l :: rest, None -> + if String.trim l = "```rescript" then ( + currentCodeBlockContents := Some []; + processLines rest) + else ( + Buffer.add_string buf l; + Buffer.add_char buf '\n'; + processLines rest) + | l :: rest, Some codeBlockContents -> + if String.trim l = "```" then ( + let codeBlockContents = + codeBlockContents |> List.rev |> String.concat "\n" + in + let mappedCodeBlockContents = + mapper codeBlockContents currentLine + |> String.split_on_char '\n' + |> List.map (fun line -> indent ^ line) + |> String.concat "\n" + in + addIndent (); + Buffer.add_string buf "```rescript\n"; + Buffer.add_string buf mappedCodeBlockContents; + Buffer.add_char buf '\n'; + addIndent (); + Buffer.add_string buf "```"; + Buffer.add_char buf '\n'; + currentCodeBlockContents := None; + processLines rest) + else ( + currentCodeBlockContents := Some (l :: codeBlockContents); + processLines rest) + | [], Some codeBlockContents -> + (* EOF, broken, do not format*) + let codeBlockContents = + codeBlockContents |> List.rev |> String.concat "\n" + in + addIndent (); + Buffer.add_string buf "```rescript\n"; + Buffer.add_string buf codeBlockContents + | [], None -> () + in + processLines lines; + + (* Normalize newlines at start/end of the content. *) + "\n" ^ indent ^ (buf |> Buffer.contents |> String.trim) ^ indent ^ "\n" + + let formatRescriptCodeBlocks content ~displayFilename ~addError + ~(payloadLoc : Location.t) = + let newContent = + mapRescriptCodeBlocks + ~colIndent:(payloadLoc.loc_start.pos_cnum - payloadLoc.loc_start.pos_bol) + ~mapper:(fun code currentLine -> + (* TODO: Figure out the line offsets here so the error messages line up as intended. *) + let newlinesNeeded = + payloadLoc.loc_start.pos_lnum + currentLine - 5 + in + let codeOffset = String.make newlinesNeeded '\n' in + let codeWithOffset = codeOffset ^ code in + let formatted_code = + let {Res_driver.parsetree; comments; invalid; diagnostics} = + Res_driver.parse_implementation_from_source ~for_printer:true + ~display_filename:displayFilename ~source:codeWithOffset + in + if invalid then ( + let buf = Buffer.create 32 in + let formatter = Format.formatter_of_buffer buf in + Res_diagnostics.print_report ~formatter + ~custom_intro:(Some "Syntax error in code block in docstring") + diagnostics codeWithOffset; + addError (Buffer.contents buf); + code) + else + Res_printer.print_implementation ~width:80 parsetree ~comments + |> String.trim + in + formatted_code) + content + in + newContent + + let formatDocstrings ~outputMode ~entryPointFile = + let path = + match Filename.is_relative entryPointFile with + | true -> Unix.realpath entryPointFile + | false -> entryPointFile + in + let errors = ref [] in + let addError error = errors := error :: !errors in + + let makeMapper ~displayFilename = + { + Ast_mapper.default_mapper with + attribute = + (fun mapper ((name, payload) as attr) -> + match (name, Ast_payload.is_single_string payload, payload) with + | ( {txt = "res.doc"}, + Some (contents, None), + PStr [{pstr_desc = Pstr_eval ({pexp_loc}, _)}] ) -> + let formatted_contents = + formatRescriptCodeBlocks ~addError ~displayFilename + ~payloadLoc:pexp_loc contents + in + if formatted_contents <> contents then + ( name, + PStr + [ + Ast_helper.Str.eval + (Ast_helper.Exp.constant + (Pconst_string (formatted_contents, None))); + ] ) + else attr + | _ -> Ast_mapper.default_mapper.attribute mapper attr); + } + in + let formatted_content, source = + if Filename.check_suffix path ".res" then + let parser = + Res_driver.parsing_engine.parse_implementation ~for_printer:true + in + let {Res_driver.parsetree = structure; comments; source; filename} = + parser ~filename:path + in + + let mapper = makeMapper ~displayFilename:filename in + let astMapped = mapper.structure mapper structure in + (Res_printer.print_implementation ~width:80 astMapped ~comments, source) + else + let parser = + Res_driver.parsing_engine.parse_interface ~for_printer:true + in + let {Res_driver.parsetree = signature; comments; source; filename} = + parser ~filename:path + in + let mapper = makeMapper ~displayFilename:filename in + let astMapped = mapper.signature mapper signature in + (Res_printer.print_interface ~width:80 astMapped ~comments, source) + in + let errors = !errors in + if not (List.is_empty errors) then ( + errors |> String.concat "\n" |> print_endline; + Error (Printf.sprintf "Error formatting docstrings.")) + else if formatted_content <> source then ( + match outputMode with + | `Stdout -> Ok formatted_content + | `File -> + let oc = open_out path in + Printf.fprintf oc "%s" formatted_content; + close_out oc; + Ok "Formatted docstrings successfully") + else Ok "No formatting needed" +end From c1b9e3e8d4aa04fc1241d3d0d640c93c59d71e76 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 14:13:18 +0200 Subject: [PATCH 02/25] normalize whitespace better --- tools/src/tools.ml | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/tools/src/tools.ml b/tools/src/tools.ml index cddaf635ed..9eafd5104e 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -735,7 +735,16 @@ module FormatDocstrings = struct processLines lines; (* Normalize newlines at start/end of the content. *) - "\n" ^ indent ^ (buf |> Buffer.contents |> String.trim) ^ indent ^ "\n" + let initialWhitespace = + let rec findFirstNonWhitespace i = + if i >= String.length doc then "" + else if not (String.contains " \t\n\r" doc.[i]) then String.sub doc 0 i + else findFirstNonWhitespace (i + 1) + in + findFirstNonWhitespace 0 + in + + initialWhitespace ^ (buf |> Buffer.contents |> String.trim) ^ indent ^ "\n" let formatRescriptCodeBlocks content ~displayFilename ~addError ~(payloadLoc : Location.t) = @@ -763,7 +772,8 @@ module FormatDocstrings = struct addError (Buffer.contents buf); code) else - Res_printer.print_implementation ~width:80 parsetree ~comments + Res_printer.print_implementation + ~width:Res_multi_printer.default_print_width parsetree ~comments |> String.trim in formatted_code) @@ -816,7 +826,9 @@ module FormatDocstrings = struct let mapper = makeMapper ~displayFilename:filename in let astMapped = mapper.structure mapper structure in - (Res_printer.print_implementation ~width:80 astMapped ~comments, source) + ( Res_printer.print_implementation + ~width:Res_multi_printer.default_print_width astMapped ~comments, + source ) else let parser = Res_driver.parsing_engine.parse_interface ~for_printer:true @@ -826,7 +838,9 @@ module FormatDocstrings = struct in let mapper = makeMapper ~displayFilename:filename in let astMapped = mapper.signature mapper signature in - (Res_printer.print_interface ~width:80 astMapped ~comments, source) + ( Res_printer.print_interface + ~width:Res_multi_printer.default_print_width astMapped ~comments, + source ) in let errors = !errors in if not (List.is_empty errors) then ( From 76ffef7ecaffa8f49d2f5fa1297a7af2756207de Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 14:51:17 +0200 Subject: [PATCH 03/25] fix offset lines calculation --- .../src/expected/FormatDocstringsTestError.res.expected | 9 +++++---- tools/src/tools.ml | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected b/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected index fbb25e256b..7e5153c4ca 100644 --- a/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected +++ b/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected @@ -1,10 +1,11 @@ Syntax error in code block in docstring - /Users/zth/OSS/rescript-compiler/tests/tools_tests/src/docstrings-format/FormatDocstringsTestError.res:2:10-3:3 + /Users/zth/OSS/rescript-compiler/tests/tools_tests/src/docstrings-format/FormatDocstringsTestError.res:5:10-6:3 - 1 │ - 2 │ let name=  - 3 │ let x=12 + 3 │ + 4 │ + 5 │ let name=  + 6 │ let x=12 This let-binding misses an expression diff --git a/tools/src/tools.ml b/tools/src/tools.ml index 9eafd5104e..fab85d7609 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -752,12 +752,12 @@ module FormatDocstrings = struct mapRescriptCodeBlocks ~colIndent:(payloadLoc.loc_start.pos_cnum - payloadLoc.loc_start.pos_bol) ~mapper:(fun code currentLine -> - (* TODO: Figure out the line offsets here so the error messages line up as intended. *) + let codeLines = String.split_on_char '\n' code in + let n = List.length codeLines in let newlinesNeeded = - payloadLoc.loc_start.pos_lnum + currentLine - 5 + max 0 (payloadLoc.loc_start.pos_lnum + currentLine - n) in - let codeOffset = String.make newlinesNeeded '\n' in - let codeWithOffset = codeOffset ^ code in + let codeWithOffset = String.make newlinesNeeded '\n' ^ code in let formatted_code = let {Res_driver.parsetree; comments; invalid; diagnostics} = Res_driver.parse_implementation_from_source ~for_printer:true From 3beb72635211cfb767c8c23404dcc9619417c130 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 14:51:25 +0200 Subject: [PATCH 04/25] fix tests --- .../src/docstrings-format/FormatDocstringsTest1.res | 3 ++- .../src/docstrings-format/FormatDocstringsTest1.resi | 3 ++- .../src/docstrings-format/FormatDocstringsTest2.res | 3 ++- .../src/expected/FormatDocstringsTest2.res.expected | 4 +--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res index b2258d42cc..7afcf0423c 100644 --- a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res +++ b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res @@ -39,7 +39,8 @@ Third docstring with array and option types. ```rescript let processUsers=(users:array)=>{ -users->Array.map(user=>{...user,active:false})->Array.filter(u=>u.age>21) +users +->Array.map(user=>{...user,active:false})->Array.filter(u=>u.age>21) } type status=|Loading|Success(string)|Error(option) diff --git a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.resi b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.resi index c7b5343feb..da9106fdc9 100644 --- a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.resi +++ b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.resi @@ -39,7 +39,8 @@ Third docstring with array and option types. ```rescript let processUsers=(users:array)=>{ -users->Array.map(user=>{...user,active:false})->Array.filter(u=>u.age>21) +users +->Array.map(user=>{...user,active:false})->Array.filter(u=>u.age>21) } type status=|Loading|Success(string)|Error(option) diff --git a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest2.res b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest2.res index 24ece061d8..b316d3743f 100644 --- a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest2.res +++ b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest2.res @@ -28,7 +28,8 @@ Testing function composition and piping. ```rescript let processData=(data:array)=>{ -data->Array.filter(x=>x>0)->Array.map(x=>x*2)->Array.reduce(0,(acc,x)=>acc+x) +data +->Array.filter(x=>x>0)->Array.map(x=>x*2)->Array.reduce(0,(acc,x)=>acc+x) } let asyncExample=async()=>{ diff --git a/tests/tools_tests/src/expected/FormatDocstringsTest2.res.expected b/tests/tools_tests/src/expected/FormatDocstringsTest2.res.expected index 0738800e2f..61d79bf980 100644 --- a/tests/tools_tests/src/expected/FormatDocstringsTest2.res.expected +++ b/tests/tools_tests/src/expected/FormatDocstringsTest2.res.expected @@ -5,9 +5,7 @@ Testing JSX and more complex formatting scenarios. let component = () => {

{"Title"->React.string}

- +
} ``` From a18e342bbc4e5831a4557e253adba127f3950e1c Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 14:53:03 +0200 Subject: [PATCH 05/25] format Stdlib_Result.resi with the new docstrings formatter --- runtime/Stdlib_Result.resi | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/runtime/Stdlib_Result.resi b/runtime/Stdlib_Result.resi index 6a6a0c2ecc..08644f11c7 100644 --- a/runtime/Stdlib_Result.resi +++ b/runtime/Stdlib_Result.resi @@ -83,10 +83,10 @@ let getOrThrow: result<'a, 'b> => 'a ```rescript let ok = Ok(42) -Result.mapOr(ok, 0, (x) => x / 2) == 21 +Result.mapOr(ok, 0, x => x / 2) == 21 let error = Error("Invalid data") -Result.mapOr(error, 0, (x) => x / 2) == 0 +Result.mapOr(error, 0, x => x / 2) == 0 ``` */ let mapOr: (result<'a, 'c>, 'b, 'a => 'b) => 'b @@ -102,7 +102,7 @@ ordinary value. ## Examples ```rescript -let f = (x) => sqrt(Int.toFloat(x)) +let f = x => sqrt(Int.toFloat(x)) Result.map(Ok(64), f) == Ok(8.0) @@ -119,8 +119,8 @@ unchanged. Function `f` takes a value of the same type as `n` and returns a ## Examples ```rescript -let recip = (x) => - if (x !== 0.0) { +let recip = x => + if x !== 0.0 { Ok(1.0 /. x) } else { Error("Divide by zero") @@ -219,11 +219,11 @@ let mod10cmp = (a, b) => Int.compare(mod(a, 10), mod(b, 10)) Result.compare(Ok(39), Ok(57), mod10cmp) == 1. -Result.compare(Ok(57), Ok(39), mod10cmp) == (-1.) +Result.compare(Ok(57), Ok(39), mod10cmp) == -1. Result.compare(Ok(39), Error("y"), mod10cmp) == 1. -Result.compare(Error("x"), Ok(57), mod10cmp) == (-1.) +Result.compare(Error("x"), Ok(57), mod10cmp) == -1. Result.compare(Error("x"), Error("y"), mod10cmp) == 0. ``` From 803aae5ab73e4ecc0e81cc17d788b24dd8d7438e Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 16:45:05 +0200 Subject: [PATCH 06/25] just output the filename without the full path for now --- .../src/expected/FormatDocstringsTestError.res.expected | 2 +- tools/src/tools.ml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected b/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected index 7e5153c4ca..b2bf2a2ada 100644 --- a/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected +++ b/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected @@ -1,6 +1,6 @@ Syntax error in code block in docstring - /Users/zth/OSS/rescript-compiler/tests/tools_tests/src/docstrings-format/FormatDocstringsTestError.res:5:10-6:3 + FormatDocstringsTestError.res:5:10-6:3 3 │ 4 │ diff --git a/tools/src/tools.ml b/tools/src/tools.ml index fab85d7609..2a26a873d5 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -823,7 +823,7 @@ module FormatDocstrings = struct let {Res_driver.parsetree = structure; comments; source; filename} = parser ~filename:path in - + let filename = Filename.basename filename in let mapper = makeMapper ~displayFilename:filename in let astMapped = mapper.structure mapper structure in ( Res_printer.print_implementation From f32fa155ffe108c805a5da0ee00a1004e1324f46 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 16:52:41 +0200 Subject: [PATCH 07/25] fix --- tools/src/tools.ml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/src/tools.ml b/tools/src/tools.ml index 2a26a873d5..6e9153d7ec 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -843,7 +843,7 @@ module FormatDocstrings = struct source ) in let errors = !errors in - if not (List.is_empty errors) then ( + if List.length errors > 0 then ( errors |> String.concat "\n" |> print_endline; Error (Printf.sprintf "Error formatting docstrings.")) else if formatted_content <> source then ( From b9bb6d9c1822e6b775177c3a943212d81c0190fb Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 18:33:59 +0200 Subject: [PATCH 08/25] disable color in docstring format tests since it breaks in different environments in CI --- .../src/expected/FormatDocstringsTestError.res.expected | 8 ++++---- tests/tools_tests/test.sh | 2 +- tools/bin/main.ml | 5 +++++ tools/src/tools.ml | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected b/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected index b2bf2a2ada..c5b52e6828 100644 --- a/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected +++ b/tests/tools_tests/src/expected/FormatDocstringsTestError.res.expected @@ -2,10 +2,10 @@ Syntax error in code block in docstring FormatDocstringsTestError.res:5:10-6:3 - 3 │ - 4 │ - 5 │ let name=  - 6 │ let x=12 + 3 │ + 4 │ + 5 │ let name= + 6 │ let x=12 This let-binding misses an expression diff --git a/tests/tools_tests/test.sh b/tests/tools_tests/test.sh index 4c8de617a8..5fb9a6a6a0 100755 --- a/tests/tools_tests/test.sh +++ b/tests/tools_tests/test.sh @@ -19,7 +19,7 @@ done # Test format-docstrings command for file in src/docstrings-format/*.{res,resi}; do output="src/expected/$(basename $file).expected" - ../../_build/install/default/bin/rescript-tools format-docstrings "$file" --stdout > $output + DISABLE_COLOR=true ../../_build/install/default/bin/rescript-tools format-docstrings "$file" --stdout > $output # # CI. We use LF, and the CI OCaml fork prints CRLF. Convert. if [ "$RUNNER_OS" == "Windows" ]; then perl -pi -e 's/\r\n/\n/g' -- $output diff --git a/tools/bin/main.ml b/tools/bin/main.ml index 6b0a2a7249..04fdbc4877 100644 --- a/tools/bin/main.ml +++ b/tools/bin/main.ml @@ -54,6 +54,11 @@ let main () = logAndExit (Tools.extractDocs ~entryPointFile:path ~debug:false) | _ -> logAndExit (Error docHelp)) | "format-docstrings" :: rest -> ( + (try + match Sys.getenv "DISABLE_COLOR" with + | "true" -> Clflags.color := Some Misc.Color.Never + | _ -> () + with Not_found -> ()); match rest with | ["-h"] | ["--help"] -> logAndExit (Ok formatDocstringsHelp) | [path; "--stdout"] -> ( diff --git a/tools/src/tools.ml b/tools/src/tools.ml index 6e9153d7ec..fb4df29425 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -764,7 +764,7 @@ module FormatDocstrings = struct ~display_filename:displayFilename ~source:codeWithOffset in if invalid then ( - let buf = Buffer.create 32 in + let buf = Buffer.create 1000 in let formatter = Format.formatter_of_buffer buf in Res_diagnostics.print_report ~formatter ~custom_intro:(Some "Syntax error in code block in docstring") From 02eb87b67d91c7f6a1da09e5cb08e30ba08198eb Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 18:40:49 +0200 Subject: [PATCH 09/25] better approach than a new env variable --- tests/tools_tests/test.sh | 2 +- tools/bin/main.ml | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/tools_tests/test.sh b/tests/tools_tests/test.sh index 5fb9a6a6a0..4c8de617a8 100755 --- a/tests/tools_tests/test.sh +++ b/tests/tools_tests/test.sh @@ -19,7 +19,7 @@ done # Test format-docstrings command for file in src/docstrings-format/*.{res,resi}; do output="src/expected/$(basename $file).expected" - DISABLE_COLOR=true ../../_build/install/default/bin/rescript-tools format-docstrings "$file" --stdout > $output + ../../_build/install/default/bin/rescript-tools format-docstrings "$file" --stdout > $output # # CI. We use LF, and the CI OCaml fork prints CRLF. Convert. if [ "$RUNNER_OS" == "Windows" ]; then perl -pi -e 's/\r\n/\n/g' -- $output diff --git a/tools/bin/main.ml b/tools/bin/main.ml index 04fdbc4877..5ae8f0ef9b 100644 --- a/tools/bin/main.ml +++ b/tools/bin/main.ml @@ -54,14 +54,10 @@ let main () = logAndExit (Tools.extractDocs ~entryPointFile:path ~debug:false) | _ -> logAndExit (Error docHelp)) | "format-docstrings" :: rest -> ( - (try - match Sys.getenv "DISABLE_COLOR" with - | "true" -> Clflags.color := Some Misc.Color.Never - | _ -> () - with Not_found -> ()); match rest with | ["-h"] | ["--help"] -> logAndExit (Ok formatDocstringsHelp) | [path; "--stdout"] -> ( + Clflags.color := Some Misc.Color.Never; match Tools.FormatDocstrings.formatDocstrings ~outputMode:`Stdout ~entryPointFile:path From af1fb336a5a1037cf482887b3e6007cd97f2f7d1 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 19:32:18 +0200 Subject: [PATCH 10/25] more tweaks to not make more changes to docstrings than needed --- tools/src/tools.ml | 134 ++++++++++++++++++++++++++------------------- 1 file changed, 79 insertions(+), 55 deletions(-) diff --git a/tools/src/tools.ml b/tools/src/tools.ml index fb4df29425..c9d753ecbb 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -686,65 +686,87 @@ module FormatDocstrings = struct let addIndent () = Buffer.add_string buf indent in let currentCodeBlockContents = ref None in let lines = String.split_on_char '\n' doc in - let lineCount = ref (-1) in - let rec processLines lines = - let currentLine = !lineCount in - lineCount := currentLine + 1; - match (lines, !currentCodeBlockContents) with - | l :: rest, None -> - if String.trim l = "```rescript" then ( - currentCodeBlockContents := Some []; - processLines rest) - else ( - Buffer.add_string buf l; - Buffer.add_char buf '\n'; - processLines rest) - | l :: rest, Some codeBlockContents -> - if String.trim l = "```" then ( + let isSingleLine = + match lines with + | [_] -> true + | _ -> false + in + if isSingleLine then + (* No code blocks in single line comments... *) + doc + else + let lineCount = ref (-1) in + let rec processLines lines = + let currentLine = !lineCount in + lineCount := currentLine + 1; + match (lines, !currentCodeBlockContents) with + | l :: rest, None -> + if String.trim l = "```rescript" then ( + currentCodeBlockContents := Some []; + processLines rest) + else ( + Buffer.add_string buf l; + Buffer.add_char buf '\n'; + processLines rest) + | l :: rest, Some codeBlockContents -> + if String.trim l = "```" then ( + let codeBlockContents = + codeBlockContents |> List.rev |> String.concat "\n" + in + let mappedCodeBlockContents = + mapper codeBlockContents currentLine + |> String.split_on_char '\n' + |> List.map (fun line -> indent ^ line) + |> String.concat "\n" + in + addIndent (); + Buffer.add_string buf "```rescript\n"; + Buffer.add_string buf mappedCodeBlockContents; + Buffer.add_char buf '\n'; + addIndent (); + Buffer.add_string buf "```"; + Buffer.add_char buf '\n'; + currentCodeBlockContents := None; + processLines rest) + else ( + currentCodeBlockContents := Some (l :: codeBlockContents); + processLines rest) + | [], Some codeBlockContents -> + (* EOF, broken, do not format*) let codeBlockContents = codeBlockContents |> List.rev |> String.concat "\n" in - let mappedCodeBlockContents = - mapper codeBlockContents currentLine - |> String.split_on_char '\n' - |> List.map (fun line -> indent ^ line) - |> String.concat "\n" - in addIndent (); Buffer.add_string buf "```rescript\n"; - Buffer.add_string buf mappedCodeBlockContents; - Buffer.add_char buf '\n'; - addIndent (); - Buffer.add_string buf "```"; - Buffer.add_char buf '\n'; - currentCodeBlockContents := None; - processLines rest) - else ( - currentCodeBlockContents := Some (l :: codeBlockContents); - processLines rest) - | [], Some codeBlockContents -> - (* EOF, broken, do not format*) - let codeBlockContents = - codeBlockContents |> List.rev |> String.concat "\n" + Buffer.add_string buf codeBlockContents + | [], None -> () + in + processLines lines; + + (* Normalize newlines at start/end of the content. *) + let initialWhitespace = + let rec findFirstNonWhitespace i = + if i >= String.length doc then "" + else if not (String.contains " \t\n\r" doc.[i]) then + String.sub doc 0 i + else findFirstNonWhitespace (i + 1) in - addIndent (); - Buffer.add_string buf "```rescript\n"; - Buffer.add_string buf codeBlockContents - | [], None -> () - in - processLines lines; - - (* Normalize newlines at start/end of the content. *) - let initialWhitespace = - let rec findFirstNonWhitespace i = - if i >= String.length doc then "" - else if not (String.contains " \t\n\r" doc.[i]) then String.sub doc 0 i - else findFirstNonWhitespace (i + 1) + findFirstNonWhitespace 0 + in + + let endingWhitespace = + let rec findLastWhitespace i = + if i < 0 then "" + else if not (String.contains " \t\n\r" doc.[i]) then + String.sub doc (i + 1) (String.length doc - i - 1) + else findLastWhitespace (i - 1) + in + findLastWhitespace (String.length doc - 1) in - findFirstNonWhitespace 0 - in - initialWhitespace ^ (buf |> Buffer.contents |> String.trim) ^ indent ^ "\n" + initialWhitespace + ^ (buf |> Buffer.contents |> String.trim) + ^ endingWhitespace let formatRescriptCodeBlocks content ~displayFilename ~addError ~(payloadLoc : Location.t) = @@ -844,8 +866,10 @@ module FormatDocstrings = struct in let errors = !errors in if List.length errors > 0 then ( - errors |> String.concat "\n" |> print_endline; - Error (Printf.sprintf "Error formatting docstrings.")) + errors |> List.rev |> String.concat "\n" |> print_endline; + Error + (Printf.sprintf "%s: Error formatting docstrings." + (Filename.basename path))) else if formatted_content <> source then ( match outputMode with | `Stdout -> Ok formatted_content @@ -853,6 +877,6 @@ module FormatDocstrings = struct let oc = open_out path in Printf.fprintf oc "%s" formatted_content; close_out oc; - Ok "Formatted docstrings successfully") - else Ok "No formatting needed" + Ok (Filename.basename path ^ ": formatted successfully")) + else Ok (Filename.basename path ^ ": needed no formatting") end From a63d3e5dd7dbdd7192a8e66c973f1d3bb1e25024 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 19:37:04 +0200 Subject: [PATCH 11/25] do not edit anything if we didnt have any code blocks --- tools/src/tools.ml | 142 +++++++++++++++++++++------------------------ 1 file changed, 67 insertions(+), 75 deletions(-) diff --git a/tools/src/tools.ml b/tools/src/tools.ml index c9d753ecbb..bec527a1a8 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -686,94 +686,86 @@ module FormatDocstrings = struct let addIndent () = Buffer.add_string buf indent in let currentCodeBlockContents = ref None in let lines = String.split_on_char '\n' doc in - let isSingleLine = - match lines with - | [_] -> true - | _ -> false - in - if isSingleLine then - (* No code blocks in single line comments... *) - doc - else - let lineCount = ref (-1) in - let rec processLines lines = - let currentLine = !lineCount in - lineCount := currentLine + 1; - match (lines, !currentCodeBlockContents) with - | l :: rest, None -> - if String.trim l = "```rescript" then ( - currentCodeBlockContents := Some []; - processLines rest) - else ( - Buffer.add_string buf l; - Buffer.add_char buf '\n'; - processLines rest) - | l :: rest, Some codeBlockContents -> - if String.trim l = "```" then ( - let codeBlockContents = - codeBlockContents |> List.rev |> String.concat "\n" - in - let mappedCodeBlockContents = - mapper codeBlockContents currentLine - |> String.split_on_char '\n' - |> List.map (fun line -> indent ^ line) - |> String.concat "\n" - in - addIndent (); - Buffer.add_string buf "```rescript\n"; - Buffer.add_string buf mappedCodeBlockContents; - Buffer.add_char buf '\n'; - addIndent (); - Buffer.add_string buf "```"; - Buffer.add_char buf '\n'; - currentCodeBlockContents := None; - processLines rest) - else ( - currentCodeBlockContents := Some (l :: codeBlockContents); - processLines rest) - | [], Some codeBlockContents -> - (* EOF, broken, do not format*) + let lineCount = ref (-1) in + let rec processLines lines = + let currentLine = !lineCount in + lineCount := currentLine + 1; + match (lines, !currentCodeBlockContents) with + | l :: rest, None -> + if String.trim l = "```rescript" then ( + currentCodeBlockContents := Some []; + processLines rest) + else ( + Buffer.add_string buf l; + Buffer.add_char buf '\n'; + processLines rest) + | l :: rest, Some codeBlockContents -> + if String.trim l = "```" then ( let codeBlockContents = codeBlockContents |> List.rev |> String.concat "\n" in + let mappedCodeBlockContents = + mapper codeBlockContents currentLine + |> String.split_on_char '\n' + |> List.map (fun line -> indent ^ line) + |> String.concat "\n" + in addIndent (); Buffer.add_string buf "```rescript\n"; - Buffer.add_string buf codeBlockContents - | [], None -> () - in - processLines lines; - - (* Normalize newlines at start/end of the content. *) - let initialWhitespace = - let rec findFirstNonWhitespace i = - if i >= String.length doc then "" - else if not (String.contains " \t\n\r" doc.[i]) then - String.sub doc 0 i - else findFirstNonWhitespace (i + 1) + Buffer.add_string buf mappedCodeBlockContents; + Buffer.add_char buf '\n'; + addIndent (); + Buffer.add_string buf "```"; + Buffer.add_char buf '\n'; + currentCodeBlockContents := None; + processLines rest) + else ( + currentCodeBlockContents := Some (l :: codeBlockContents); + processLines rest) + | [], Some codeBlockContents -> + (* EOF, broken, do not format*) + let codeBlockContents = + codeBlockContents |> List.rev |> String.concat "\n" in - findFirstNonWhitespace 0 + addIndent (); + Buffer.add_string buf "```rescript\n"; + Buffer.add_string buf codeBlockContents + | [], None -> () + in + processLines lines; + + (* Normalize newlines at start/end of the content. *) + let initialWhitespace = + let rec findFirstNonWhitespace i = + if i >= String.length doc then "" + else if not (String.contains " \t\n\r" doc.[i]) then String.sub doc 0 i + else findFirstNonWhitespace (i + 1) in + findFirstNonWhitespace 0 + in - let endingWhitespace = - let rec findLastWhitespace i = - if i < 0 then "" - else if not (String.contains " \t\n\r" doc.[i]) then - String.sub doc (i + 1) (String.length doc - i - 1) - else findLastWhitespace (i - 1) - in - findLastWhitespace (String.length doc - 1) + let endingWhitespace = + let rec findLastWhitespace i = + if i < 0 then "" + else if not (String.contains " \t\n\r" doc.[i]) then + String.sub doc (i + 1) (String.length doc - i - 1) + else findLastWhitespace (i - 1) in + findLastWhitespace (String.length doc - 1) + in - initialWhitespace - ^ (buf |> Buffer.contents |> String.trim) - ^ endingWhitespace + initialWhitespace + ^ (buf |> Buffer.contents |> String.trim) + ^ endingWhitespace let formatRescriptCodeBlocks content ~displayFilename ~addError ~(payloadLoc : Location.t) = + let hadCodeBlocks = ref false in let newContent = mapRescriptCodeBlocks ~colIndent:(payloadLoc.loc_start.pos_cnum - payloadLoc.loc_start.pos_bol) ~mapper:(fun code currentLine -> + hadCodeBlocks := true; let codeLines = String.split_on_char '\n' code in let n = List.length codeLines in let newlinesNeeded = @@ -801,7 +793,7 @@ module FormatDocstrings = struct formatted_code) content in - newContent + (newContent, !hadCodeBlocks) let formatDocstrings ~outputMode ~entryPointFile = let path = @@ -821,17 +813,17 @@ module FormatDocstrings = struct | ( {txt = "res.doc"}, Some (contents, None), PStr [{pstr_desc = Pstr_eval ({pexp_loc}, _)}] ) -> - let formatted_contents = + let formattedContents, hadCodeBlocks = formatRescriptCodeBlocks ~addError ~displayFilename ~payloadLoc:pexp_loc contents in - if formatted_contents <> contents then + if hadCodeBlocks && formattedContents <> contents then ( name, PStr [ Ast_helper.Str.eval (Ast_helper.Exp.constant - (Pconst_string (formatted_contents, None))); + (Pconst_string (formattedContents, None))); ] ) else attr | _ -> Ast_mapper.default_mapper.attribute mapper attr); From 90b113335e0e131824a6caf2dad9e49a967310c8 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 5 Jul 2025 18:45:49 +0200 Subject: [PATCH 12/25] use cmarkit for transforming codeblocks --- .../FormatDocstringsTest1.res.expected | 2 +- .../FormatDocstringsTest1.resi.expected | 2 +- tools/src/dune | 2 +- tools/src/tools.ml | 124 +++++------------- 4 files changed, 36 insertions(+), 94 deletions(-) diff --git a/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected b/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected index 0fbe95261e..ff7e4f8265 100644 --- a/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected +++ b/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected @@ -33,7 +33,7 @@ module Nested = { let validate = user => user.age >= 18 && user.name !== "" let getName = user => user.name } - ``` + ``` */ let testFunction2 = () => "test2" } diff --git a/tests/tools_tests/src/expected/FormatDocstringsTest1.resi.expected b/tests/tools_tests/src/expected/FormatDocstringsTest1.resi.expected index f3833fc742..5a46d5fe2a 100644 --- a/tests/tools_tests/src/expected/FormatDocstringsTest1.resi.expected +++ b/tests/tools_tests/src/expected/FormatDocstringsTest1.resi.expected @@ -33,7 +33,7 @@ module Nested: { let validate = user => user.age >= 18 && user.name !== "" let getName = user => user.name } - ``` + ``` */ let testFunction2: unit => string } diff --git a/tools/src/dune b/tools/src/dune index f124a3737a..ec90d74ab0 100644 --- a/tools/src/dune +++ b/tools/src/dune @@ -2,4 +2,4 @@ (name tools) (flags (-w "+6+26+27+32+33+39")) - (libraries analysis)) + (libraries analysis cmarkit)) diff --git a/tools/src/tools.ml b/tools/src/tools.ml index bec527a1a8..b8edc844fb 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -678,101 +678,33 @@ let extractEmbedded ~extensionPoints ~filename = |> List.rev |> array module FormatDocstrings = struct - let mapRescriptCodeBlocks ~colIndent ~(mapper : string -> int -> string) - (doc : string) = - let indent = String.make colIndent ' ' in - let len = String.length doc in - let buf = Buffer.create len in - let addIndent () = Buffer.add_string buf indent in - let currentCodeBlockContents = ref None in - let lines = String.split_on_char '\n' doc in - let lineCount = ref (-1) in - let rec processLines lines = - let currentLine = !lineCount in - lineCount := currentLine + 1; - match (lines, !currentCodeBlockContents) with - | l :: rest, None -> - if String.trim l = "```rescript" then ( - currentCodeBlockContents := Some []; - processLines rest) - else ( - Buffer.add_string buf l; - Buffer.add_char buf '\n'; - processLines rest) - | l :: rest, Some codeBlockContents -> - if String.trim l = "```" then ( - let codeBlockContents = - codeBlockContents |> List.rev |> String.concat "\n" - in - let mappedCodeBlockContents = - mapper codeBlockContents currentLine - |> String.split_on_char '\n' - |> List.map (fun line -> indent ^ line) - |> String.concat "\n" - in - addIndent (); - Buffer.add_string buf "```rescript\n"; - Buffer.add_string buf mappedCodeBlockContents; - Buffer.add_char buf '\n'; - addIndent (); - Buffer.add_string buf "```"; - Buffer.add_char buf '\n'; - currentCodeBlockContents := None; - processLines rest) - else ( - currentCodeBlockContents := Some (l :: codeBlockContents); - processLines rest) - | [], Some codeBlockContents -> - (* EOF, broken, do not format*) - let codeBlockContents = - codeBlockContents |> List.rev |> String.concat "\n" - in - addIndent (); - Buffer.add_string buf "```rescript\n"; - Buffer.add_string buf codeBlockContents - | [], None -> () - in - processLines lines; - - (* Normalize newlines at start/end of the content. *) - let initialWhitespace = - let rec findFirstNonWhitespace i = - if i >= String.length doc then "" - else if not (String.contains " \t\n\r" doc.[i]) then String.sub doc 0 i - else findFirstNonWhitespace (i + 1) - in - findFirstNonWhitespace 0 - in - - let endingWhitespace = - let rec findLastWhitespace i = - if i < 0 then "" - else if not (String.contains " \t\n\r" doc.[i]) then - String.sub doc (i + 1) (String.length doc - i - 1) - else findLastWhitespace (i - 1) - in - findLastWhitespace (String.length doc - 1) - in - - initialWhitespace - ^ (buf |> Buffer.contents |> String.trim) - ^ endingWhitespace - let formatRescriptCodeBlocks content ~displayFilename ~addError ~(payloadLoc : Location.t) = + let open Cmarkit in + (* Detect ReScript code blocks. *) let hadCodeBlocks = ref false in - let newContent = - mapRescriptCodeBlocks - ~colIndent:(payloadLoc.loc_start.pos_cnum - payloadLoc.loc_start.pos_bol) - ~mapper:(fun code currentLine -> + let block _m = function + | Block.Code_block (codeBlock, meta) -> ( + match Block.Code_block.info_string codeBlock with + | Some ((("rescript" | "res"), _) as info_string) -> hadCodeBlocks := true; - let codeLines = String.split_on_char '\n' code in - let n = List.length codeLines in + + let currentLine = + meta |> Meta.textloc |> Textloc.first_line |> fst |> Int.add 1 + in + let layout = Block.Code_block.layout codeBlock in + let code = Block.Code_block.code codeBlock in + let codeText = + code |> List.map Block_line.to_string |> String.concat "\n" + in + + let n = List.length code in let newlinesNeeded = max 0 (payloadLoc.loc_start.pos_lnum + currentLine - n) in - let codeWithOffset = String.make newlinesNeeded '\n' ^ code in - let formatted_code = + let codeWithOffset = String.make newlinesNeeded '\n' ^ codeText in + + let formattedCode = let {Res_driver.parsetree; comments; invalid; diagnostics} = Res_driver.parse_implementation_from_source ~for_printer:true ~display_filename:displayFilename ~source:codeWithOffset @@ -788,10 +720,20 @@ module FormatDocstrings = struct else Res_printer.print_implementation ~width:Res_multi_printer.default_print_width parsetree ~comments - |> String.trim + |> String.trim |> Block_line.list_of_string + in + + let mappedCodeBlock = + Block.Code_block.make ~layout ~info_string formattedCode in - formatted_code) - content + Mapper.ret (Block.Code_block (mappedCodeBlock, meta)) + | _ -> Mapper.default) + | _ -> Mapper.default + in + let mapper = Mapper.make ~block () in + let newContent = + content |> Doc.of_string ~locs:true |> Mapper.map_doc mapper + |> Cmarkit_commonmark.of_doc in (newContent, !hadCodeBlocks) From 9c635e1ecc5eb712860ba7a858a3ad639d021271 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 5 Jul 2025 18:52:43 +0200 Subject: [PATCH 13/25] add to opam file --- tools.opam | 1 + 1 file changed, 1 insertion(+) diff --git a/tools.opam b/tools.opam index 48333848f1..4e31f92595 100644 --- a/tools.opam +++ b/tools.opam @@ -8,6 +8,7 @@ homepage: "https://github.com/rescript-lang/rescript-compiler" bug-reports: "https://github.com/rescript-lang/rescript-compiler/issues" depends: [ "ocaml" {>= "4.14"} + "cmarkit" {>= "0.3.0"} "cppo" {= "1.8.0"} "analysis" "dune" {>= "3.17"} From 4054a521e15afd21eadef98448cb036e3e1c3a76 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 5 Jul 2025 20:42:06 +0200 Subject: [PATCH 14/25] add to dune-project --- dune-project | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dune-project b/dune-project index 61fbc62cd2..d8e0553c80 100644 --- a/dune-project +++ b/dune-project @@ -34,6 +34,8 @@ (depends (ocaml (>= 4.14)) + (cmarkit + (>= 0.3.0)) (cppo (= 1.8.0)) analysis From 11da9c9577d47c6b31cc408afc8b3fda4c513433 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 5 Jul 2025 20:42:24 +0200 Subject: [PATCH 15/25] make markdown code block formatter work on markdown files as well --- .../FormatDocstringsTest1.res | 2 +- .../docstrings-format/FormatRescriptBlocks.md | 20 +++++++++++++ .../FormatDocstringsTest1.res.expected | 2 +- .../expected/FormatRescriptBlocks.md.expected | 25 ++++++++++++++++ tests/tools_tests/test.sh | 4 +-- tools/bin/main.ml | 8 ++--- tools/src/tools.ml | 29 +++++++++++++++---- 7 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 tests/tools_tests/src/docstrings-format/FormatRescriptBlocks.md create mode 100644 tests/tools_tests/src/expected/FormatRescriptBlocks.md.expected diff --git a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res index 7afcf0423c..ff72fc5795 100644 --- a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res +++ b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res @@ -30,7 +30,7 @@ module Nested = { let getName = user=>user.name } ``` -*/ + */ let testFunction2 = () => "test2" } diff --git a/tests/tools_tests/src/docstrings-format/FormatRescriptBlocks.md b/tests/tools_tests/src/docstrings-format/FormatRescriptBlocks.md new file mode 100644 index 0000000000..e602b42168 --- /dev/null +++ b/tests/tools_tests/src/docstrings-format/FormatRescriptBlocks.md @@ -0,0 +1,20 @@ +# Format ReScript code blocks + +This markdown file should be formatted. + +This is the first docstring with unformatted ReScript code. + +```rescript +let badly_formatted=(x,y)=>{ +let result=x+y +if result>0{Console.log("positive")}else{Console.log("negative")} +result +} +``` + +And another code block in the same docstring: + +```res +type user={name:string,age:int,active:bool} +let createUser=(name,age)=>{name:name,age:age,active:true} +``` diff --git a/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected b/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected index ff7e4f8265..18641065d9 100644 --- a/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected +++ b/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected @@ -34,7 +34,7 @@ module Nested = { let getName = user => user.name } ``` -*/ + */ let testFunction2 = () => "test2" } diff --git a/tests/tools_tests/src/expected/FormatRescriptBlocks.md.expected b/tests/tools_tests/src/expected/FormatRescriptBlocks.md.expected new file mode 100644 index 0000000000..43a09d84c8 --- /dev/null +++ b/tests/tools_tests/src/expected/FormatRescriptBlocks.md.expected @@ -0,0 +1,25 @@ +# Format ReScript code blocks + +This markdown file should be formatted. + +This is the first docstring with unformatted ReScript code. + +```rescript +let badly_formatted = (x, y) => { + let result = x + y + if result > 0 { + Console.log("positive") + } else { + Console.log("negative") + } + result +} +``` + +And another code block in the same docstring: + +```res +type user = {name: string, age: int, active: bool} +let createUser = (name, age) => {name, age, active: true} +``` + diff --git a/tests/tools_tests/test.sh b/tests/tools_tests/test.sh index 4c8de617a8..7289d651c8 100755 --- a/tests/tools_tests/test.sh +++ b/tests/tools_tests/test.sh @@ -17,9 +17,9 @@ for file in ppx/*.res; do done # Test format-docstrings command -for file in src/docstrings-format/*.{res,resi}; do +for file in src/docstrings-format/*.{res,resi,md}; do output="src/expected/$(basename $file).expected" - ../../_build/install/default/bin/rescript-tools format-docstrings "$file" --stdout > $output + ../../_build/install/default/bin/rescript-tools format-codeblocks "$file" --stdout > $output # # CI. We use LF, and the CI OCaml fork prints CRLF. Convert. if [ "$RUNNER_OS" == "Windows" ]; then perl -pi -e 's/\r\n/\n/g' -- $output diff --git a/tools/bin/main.ml b/tools/bin/main.ml index 5ae8f0ef9b..15b8e762c4 100644 --- a/tools/bin/main.ml +++ b/tools/bin/main.ml @@ -24,7 +24,7 @@ Usage: rescript-tools [command] Commands: doc Generate documentation -format-docstrings [--stdout] Format ReScript code blocks in docstrings +format-codeblocks [--stdout] Format ReScript code blocks reanalyze Reanalyze -v, --version Print version -h, --help Print help|} @@ -53,19 +53,19 @@ let main () = in logAndExit (Tools.extractDocs ~entryPointFile:path ~debug:false) | _ -> logAndExit (Error docHelp)) - | "format-docstrings" :: rest -> ( + | "format-codeblocks" :: rest -> ( match rest with | ["-h"] | ["--help"] -> logAndExit (Ok formatDocstringsHelp) | [path; "--stdout"] -> ( Clflags.color := Some Misc.Color.Never; match - Tools.FormatDocstrings.formatDocstrings ~outputMode:`Stdout + Tools.FormatCodeblocks.formatCodeBlocksInFile ~outputMode:`Stdout ~entryPointFile:path with | Ok content -> print_endline content | Error e -> logAndExit (Error e)) | [path] -> - Tools.FormatDocstrings.formatDocstrings ~outputMode:`File + Tools.FormatCodeblocks.formatCodeBlocksInFile ~outputMode:`File ~entryPointFile:path |> logAndExit | _ -> logAndExit (Error formatDocstringsHelp)) diff --git a/tools/src/tools.ml b/tools/src/tools.ml index b8edc844fb..67ad2a031c 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -677,9 +677,9 @@ let extractEmbedded ~extensionPoints ~filename = ]) |> List.rev |> array -module FormatDocstrings = struct +module FormatCodeblocks = struct let formatRescriptCodeBlocks content ~displayFilename ~addError - ~(payloadLoc : Location.t) = + ~markdownBlockStartLine = let open Cmarkit in (* Detect ReScript code blocks. *) let hadCodeBlocks = ref false in @@ -700,7 +700,7 @@ module FormatDocstrings = struct let n = List.length code in let newlinesNeeded = - max 0 (payloadLoc.loc_start.pos_lnum + currentLine - n) + max 0 (markdownBlockStartLine + currentLine - n) in let codeWithOffset = String.make newlinesNeeded '\n' ^ codeText in @@ -737,7 +737,7 @@ module FormatDocstrings = struct in (newContent, !hadCodeBlocks) - let formatDocstrings ~outputMode ~entryPointFile = + let formatCodeBlocksInFile ~outputMode ~entryPointFile = let path = match Filename.is_relative entryPointFile with | true -> Unix.realpath entryPointFile @@ -757,7 +757,7 @@ module FormatDocstrings = struct PStr [{pstr_desc = Pstr_eval ({pexp_loc}, _)}] ) -> let formattedContents, hadCodeBlocks = formatRescriptCodeBlocks ~addError ~displayFilename - ~payloadLoc:pexp_loc contents + ~markdownBlockStartLine:pexp_loc.loc_start.pos_lnum contents in if hadCodeBlocks && formattedContents <> contents then ( name, @@ -772,7 +772,24 @@ module FormatDocstrings = struct } in let formatted_content, source = - if Filename.check_suffix path ".res" then + if Filename.check_suffix path ".md" then + let content = + let ic = open_in path in + let n = in_channel_length ic in + let s = Bytes.create n in + really_input ic s 0 n; + close_in ic; + Bytes.to_string s + in + let displayFilename = Filename.basename path in + let formattedContents, hadCodeBlocks = + formatRescriptCodeBlocks ~addError ~displayFilename + ~markdownBlockStartLine:1 content + in + if hadCodeBlocks && formattedContents <> content then + (formattedContents, content) + else (content, content) + else if Filename.check_suffix path ".res" then let parser = Res_driver.parsing_engine.parse_implementation ~for_printer:true in From d0a7b3ed130cc00da565f0899b909fcc9e76e382 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 5 Jul 2025 20:48:13 +0200 Subject: [PATCH 16/25] restructure args --- tools/bin/main.ml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tools/bin/main.ml b/tools/bin/main.ml index 15b8e762c4..1b8305369c 100644 --- a/tools/bin/main.ml +++ b/tools/bin/main.ml @@ -56,18 +56,18 @@ let main () = | "format-codeblocks" :: rest -> ( match rest with | ["-h"] | ["--help"] -> logAndExit (Ok formatDocstringsHelp) - | [path; "--stdout"] -> ( + | path :: args -> ( + let isStdout = List.mem "--stdout" args in + let outputMode = if isStdout then `Stdout else `File in Clflags.color := Some Misc.Color.Never; match - Tools.FormatCodeblocks.formatCodeBlocksInFile ~outputMode:`Stdout - ~entryPointFile:path + ( Tools.FormatCodeblocks.formatCodeBlocksInFile ~outputMode + ~entryPointFile:path, + outputMode ) with - | Ok content -> print_endline content - | Error e -> logAndExit (Error e)) - | [path] -> - Tools.FormatCodeblocks.formatCodeBlocksInFile ~outputMode:`File - ~entryPointFile:path - |> logAndExit + | Ok content, `Stdout -> print_endline content + | result, `File -> logAndExit result + | Error e, _ -> logAndExit (Error e)) | _ -> logAndExit (Error formatDocstringsHelp)) | "reanalyze" :: _ -> let len = Array.length Sys.argv in From e95cbbeac5fd0b1c767e0feaef8c9c557d7f968a Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 5 Jul 2025 21:02:21 +0200 Subject: [PATCH 17/25] refactor --- tools/src/tools.ml | 60 +++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/tools/src/tools.ml b/tools/src/tools.ml index 67ad2a031c..b70644f030 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -771,7 +771,7 @@ module FormatCodeblocks = struct | _ -> Ast_mapper.default_mapper.attribute mapper attr); } in - let formatted_content, source = + let content = if Filename.check_suffix path ".md" then let content = let ic = open_in path in @@ -787,8 +787,8 @@ module FormatCodeblocks = struct ~markdownBlockStartLine:1 content in if hadCodeBlocks && formattedContents <> content then - (formattedContents, content) - else (content, content) + Ok (formattedContents, content) + else Ok (content, content) else if Filename.check_suffix path ".res" then let parser = Res_driver.parsing_engine.parse_implementation ~for_printer:true @@ -799,10 +799,11 @@ module FormatCodeblocks = struct let filename = Filename.basename filename in let mapper = makeMapper ~displayFilename:filename in let astMapped = mapper.structure mapper structure in - ( Res_printer.print_implementation - ~width:Res_multi_printer.default_print_width astMapped ~comments, - source ) - else + Ok + ( Res_printer.print_implementation + ~width:Res_multi_printer.default_print_width astMapped ~comments, + source ) + else if Filename.check_suffix path ".resi" then let parser = Res_driver.parsing_engine.parse_interface ~for_printer:true in @@ -811,23 +812,32 @@ module FormatCodeblocks = struct in let mapper = makeMapper ~displayFilename:filename in let astMapped = mapper.signature mapper signature in - ( Res_printer.print_interface - ~width:Res_multi_printer.default_print_width astMapped ~comments, - source ) + Ok + ( Res_printer.print_interface + ~width:Res_multi_printer.default_print_width astMapped ~comments, + source ) + else + Error + (Printf.sprintf + "File extension not supported. This command accepts .res, .resi, \ + and .md files") in - let errors = !errors in - if List.length errors > 0 then ( - errors |> List.rev |> String.concat "\n" |> print_endline; - Error - (Printf.sprintf "%s: Error formatting docstrings." - (Filename.basename path))) - else if formatted_content <> source then ( - match outputMode with - | `Stdout -> Ok formatted_content - | `File -> - let oc = open_out path in - Printf.fprintf oc "%s" formatted_content; - close_out oc; - Ok (Filename.basename path ^ ": formatted successfully")) - else Ok (Filename.basename path ^ ": needed no formatting") + match content with + | Error e -> Error e + | Ok (formatted_content, source) -> + let errors = !errors in + if List.length errors > 0 then ( + errors |> List.rev |> String.concat "\n" |> print_endline; + Error + (Printf.sprintf "%s: Error formatting docstrings." + (Filename.basename path))) + else if formatted_content <> source then ( + match outputMode with + | `Stdout -> Ok formatted_content + | `File -> + let oc = open_out path in + Printf.fprintf oc "%s" formatted_content; + close_out oc; + Ok (Filename.basename path ^ ": formatted successfully")) + else Ok (Filename.basename path ^ ": needed no formatting") end From 254ae5c39bc57ef0ae403cb1750faeaf06713b98 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 5 Jul 2025 21:32:03 +0200 Subject: [PATCH 18/25] add transform for assertEqual -> == in code blocks --- tools/bin/main.ml | 3 +- tools/src/tools.ml | 77 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/tools/bin/main.ml b/tools/bin/main.ml index 1b8305369c..48475485eb 100644 --- a/tools/bin/main.ml +++ b/tools/bin/main.ml @@ -58,11 +58,12 @@ let main () = | ["-h"] | ["--help"] -> logAndExit (Ok formatDocstringsHelp) | path :: args -> ( let isStdout = List.mem "--stdout" args in + let transformAssertEqual = List.mem "--transform-assert-equal" args in let outputMode = if isStdout then `Stdout else `File in Clflags.color := Some Misc.Color.Never; match ( Tools.FormatCodeblocks.formatCodeBlocksInFile ~outputMode - ~entryPointFile:path, + ~transformAssertEqual ~entryPointFile:path, outputMode ) with | Ok content, `Stdout -> print_endline content diff --git a/tools/src/tools.ml b/tools/src/tools.ml index b70644f030..23ab1ac97a 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -678,8 +678,56 @@ let extractEmbedded ~extensionPoints ~filename = |> List.rev |> array module FormatCodeblocks = struct - let formatRescriptCodeBlocks content ~displayFilename ~addError - ~markdownBlockStartLine = + module Transform = struct + type transform = AssertEqualFnToEquals (** assertEqual(a, b) -> a == b *) + + (** Transforms for the code blocks themselves. *) + let transform ~transforms ast = + match transforms with + | [] -> ast + | transforms -> + let hasTransform transform = transforms |> List.mem transform in + let mapper = + { + Ast_mapper.default_mapper with + expr = + (fun mapper exp -> + match exp.pexp_desc with + | Pexp_apply + { + funct = + { + pexp_desc = + Pexp_ident + ({txt = Lident "assertEqual"} as identTxt); + } as ident; + partial = false; + args = [(Nolabel, _arg1); (Nolabel, _arg2)] as args; + } + when hasTransform AssertEqualFnToEquals -> + { + exp with + pexp_desc = + Pexp_apply + { + funct = + { + ident with + pexp_desc = + Pexp_ident {identTxt with txt = Lident "=="}; + }; + args; + partial = false; + transformed_jsx = false; + }; + } + | _ -> Ast_mapper.default_mapper.expr mapper exp); + } + in + mapper.structure mapper ast + end + let formatRescriptCodeBlocks content ~transformAssertEqual ~displayFilename + ~addError ~markdownBlockStartLine = let open Cmarkit in (* Detect ReScript code blocks. *) let hadCodeBlocks = ref false in @@ -718,6 +766,12 @@ module FormatCodeblocks = struct addError (Buffer.contents buf); code) else + let parsetree = + if transformAssertEqual then + Transform.transform ~transforms:[AssertEqualFnToEquals] + parsetree + else parsetree + in Res_printer.print_implementation ~width:Res_multi_printer.default_print_width parsetree ~comments |> String.trim |> Block_line.list_of_string @@ -737,7 +791,7 @@ module FormatCodeblocks = struct in (newContent, !hadCodeBlocks) - let formatCodeBlocksInFile ~outputMode ~entryPointFile = + let formatCodeBlocksInFile ~outputMode ~transformAssertEqual ~entryPointFile = let path = match Filename.is_relative entryPointFile with | true -> Unix.realpath entryPointFile @@ -746,7 +800,7 @@ module FormatCodeblocks = struct let errors = ref [] in let addError error = errors := error :: !errors in - let makeMapper ~displayFilename = + let makeMapper ~transformAssertEqual ~displayFilename = { Ast_mapper.default_mapper with attribute = @@ -756,7 +810,8 @@ module FormatCodeblocks = struct Some (contents, None), PStr [{pstr_desc = Pstr_eval ({pexp_loc}, _)}] ) -> let formattedContents, hadCodeBlocks = - formatRescriptCodeBlocks ~addError ~displayFilename + formatRescriptCodeBlocks ~transformAssertEqual ~addError + ~displayFilename ~markdownBlockStartLine:pexp_loc.loc_start.pos_lnum contents in if hadCodeBlocks && formattedContents <> contents then @@ -783,8 +838,8 @@ module FormatCodeblocks = struct in let displayFilename = Filename.basename path in let formattedContents, hadCodeBlocks = - formatRescriptCodeBlocks ~addError ~displayFilename - ~markdownBlockStartLine:1 content + formatRescriptCodeBlocks ~transformAssertEqual ~addError + ~displayFilename ~markdownBlockStartLine:1 content in if hadCodeBlocks && formattedContents <> content then Ok (formattedContents, content) @@ -797,7 +852,9 @@ module FormatCodeblocks = struct parser ~filename:path in let filename = Filename.basename filename in - let mapper = makeMapper ~displayFilename:filename in + let mapper = + makeMapper ~transformAssertEqual ~displayFilename:filename + in let astMapped = mapper.structure mapper structure in Ok ( Res_printer.print_implementation @@ -810,7 +867,9 @@ module FormatCodeblocks = struct let {Res_driver.parsetree = signature; comments; source; filename} = parser ~filename:path in - let mapper = makeMapper ~displayFilename:filename in + let mapper = + makeMapper ~transformAssertEqual ~displayFilename:filename + in let astMapped = mapper.signature mapper signature in Ok ( Res_printer.print_interface From 8be709d0381dfb42f0571e3a31ff4a5ae079dbb3 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 9 Jul 2025 13:31:55 +0200 Subject: [PATCH 19/25] change printer to make width optional --- analysis/src/Commands.ml | 8 ++------ analysis/src/CreateInterface.ml | 2 +- analysis/src/Xform.ml | 5 +---- compiler/syntax/src/res_multi_printer.ml | 6 ++---- compiler/syntax/src/res_multi_printer.mli | 2 -- compiler/syntax/src/res_printer.ml | 8 ++++++-- compiler/syntax/src/res_printer.mli | 6 ++++-- tools/src/tools.ml | 13 +++---------- 8 files changed, 19 insertions(+), 31 deletions(-) diff --git a/analysis/src/Commands.ml b/analysis/src/Commands.ml index 464b3fa53d..9d529dc6c3 100644 --- a/analysis/src/Commands.ml +++ b/analysis/src/Commands.ml @@ -282,17 +282,13 @@ let format ~path = ~filename:path in if List.length diagnostics > 0 then "" - else - Res_printer.print_implementation - ~width:Res_multi_printer.default_print_width ~comments structure + else Res_printer.print_implementation ~comments structure else if Filename.check_suffix path ".resi" then let {Res_driver.parsetree = signature; comments; diagnostics} = Res_driver.parsing_engine.parse_interface ~for_printer:true ~filename:path in if List.length diagnostics > 0 then "" - else - Res_printer.print_interface ~width:Res_multi_printer.default_print_width - ~comments signature + else Res_printer.print_interface ~comments signature else "" let diagnosticSyntax ~path = diff --git a/analysis/src/CreateInterface.ml b/analysis/src/CreateInterface.ml index 6aa1ea1993..5cfd2c2d88 100644 --- a/analysis/src/CreateInterface.ml +++ b/analysis/src/CreateInterface.ml @@ -102,7 +102,7 @@ let printSignature ~extractor ~signature = Printtyp.reset_names (); let sigItemToString (item : Outcometree.out_sig_item) = item |> Res_outcome_printer.print_out_sig_item_doc - |> Res_doc.to_string ~width:Res_multi_printer.default_print_width + |> Res_doc.to_string ~width:Res_printer.default_print_width in let genSigStrForInlineAttr lines attributes id vd = diff --git a/analysis/src/Xform.ml b/analysis/src/Xform.ml index 4d62a9d68f..ddf783c559 100644 --- a/analysis/src/Xform.ml +++ b/analysis/src/Xform.ml @@ -855,7 +855,6 @@ let parseImplementation ~filename = let structure = [Ast_helper.Str.eval ~loc:expr.pexp_loc expr] in structure |> Res_printer.print_implementation - ~width:Res_multi_printer.default_print_width ~comments:(comments |> filterComments ~loc:expr.pexp_loc) |> Utils.indent range.start.character in @@ -864,14 +863,12 @@ let parseImplementation ~filename = let structure = [item] in structure |> Res_printer.print_implementation - ~width:Res_multi_printer.default_print_width ~comments:(comments |> filterComments ~loc:item.pstr_loc) |> Utils.indent range.start.character in let printStandaloneStructure ~(loc : Location.t) structure = structure |> Res_printer.print_implementation - ~width:Res_multi_printer.default_print_width ~comments:(comments |> filterComments ~loc) in (structure, printExpr, printStructureItem, printStandaloneStructure) @@ -891,7 +888,7 @@ let parseInterface ~filename = (item : Parsetree.signature_item) = let signature_item = [item] in signature_item - |> Res_printer.print_interface ~width:Res_multi_printer.default_print_width + |> Res_printer.print_interface ~comments:(comments |> filterComments ~loc:item.psig_loc) |> Utils.indent range.start.character in diff --git a/compiler/syntax/src/res_multi_printer.ml b/compiler/syntax/src/res_multi_printer.ml index ff877538c2..711241ade5 100644 --- a/compiler/syntax/src/res_multi_printer.ml +++ b/compiler/syntax/src/res_multi_printer.ml @@ -1,5 +1,3 @@ -let default_print_width = 100 - (* print res files to res syntax *) let print_res ~ignore_parse_errors ~is_interface ~filename = if is_interface then ( @@ -9,7 +7,7 @@ let print_res ~ignore_parse_errors ~is_interface ~filename = if parse_result.invalid then ( Res_diagnostics.print_report parse_result.diagnostics parse_result.source; if not ignore_parse_errors then exit 1); - Res_printer.print_interface ~width:default_print_width + Res_printer.print_interface ~width:Res_printer.default_print_width ~comments:parse_result.comments parse_result.parsetree) else let parse_result = @@ -18,7 +16,7 @@ let print_res ~ignore_parse_errors ~is_interface ~filename = if parse_result.invalid then ( Res_diagnostics.print_report parse_result.diagnostics parse_result.source; if not ignore_parse_errors then exit 1); - Res_printer.print_implementation ~width:default_print_width + Res_printer.print_implementation ~width:Res_printer.default_print_width ~comments:parse_result.comments parse_result.parsetree [@@raises exit] diff --git a/compiler/syntax/src/res_multi_printer.mli b/compiler/syntax/src/res_multi_printer.mli index c6b1d07cb8..bb66106900 100644 --- a/compiler/syntax/src/res_multi_printer.mli +++ b/compiler/syntax/src/res_multi_printer.mli @@ -1,5 +1,3 @@ -val default_print_width : int [@@live] - (* Interface to print source code to res. * Takes a filename called "input" and returns the corresponding formatted res syntax *) val print : ?ignore_parse_errors:bool -> string -> string [@@dead "+print"] diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 7e2c76a32b..3941c57691 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -5,6 +5,8 @@ module Token = Res_token module Parens = Res_parens module ParsetreeViewer = Res_parsetree_viewer +let default_print_width = 100 + type callback_style = (* regular arrow function, example: `let f = x => x + 1` *) | NoCallback @@ -6055,7 +6057,8 @@ let print_typ_expr t = print_typ_expr ~state:(State.init ()) t let print_expression e = print_expression ~state:(State.init ()) e let print_pattern p = print_pattern ~state:(State.init ()) p -let print_implementation ~width (s : Parsetree.structure) ~comments = +let print_implementation ?(width = default_print_width) + (s : Parsetree.structure) ~comments = let cmt_tbl = CommentTable.make () in CommentTable.walk_structure s cmt_tbl comments; (* CommentTable.log cmt_tbl; *) @@ -6063,7 +6066,8 @@ let print_implementation ~width (s : Parsetree.structure) ~comments = (* Doc.debug doc; *) Doc.to_string ~width doc ^ "\n" -let print_interface ~width (s : Parsetree.signature) ~comments = +let print_interface ?(width = default_print_width) (s : Parsetree.signature) + ~comments = let cmt_tbl = CommentTable.make () in CommentTable.walk_signature s cmt_tbl comments; Doc.to_string ~width (print_signature ~state:(State.init ()) s cmt_tbl) ^ "\n" diff --git a/compiler/syntax/src/res_printer.mli b/compiler/syntax/src/res_printer.mli index c3b95b8e24..33461d7714 100644 --- a/compiler/syntax/src/res_printer.mli +++ b/compiler/syntax/src/res_printer.mli @@ -1,3 +1,5 @@ +val default_print_width : int + val print_type_params : (Parsetree.core_type * Asttypes.variance) list -> Res_comments_table.t -> @@ -18,9 +20,9 @@ val print_structure : Parsetree.structure -> Res_comments_table.t -> Res_doc.t [@@live] val print_implementation : - width:int -> Parsetree.structure -> comments:Res_comment.t list -> string + ?width:int -> Parsetree.structure -> comments:Res_comment.t list -> string val print_interface : - width:int -> Parsetree.signature -> comments:Res_comment.t list -> string + ?width:int -> Parsetree.signature -> comments:Res_comment.t list -> string val print_ident_like : ?allow_uident:bool -> ?allow_hyphen:bool -> string -> Res_doc.t diff --git a/tools/src/tools.ml b/tools/src/tools.ml index 23ab1ac97a..0a66695ac3 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -772,8 +772,7 @@ module FormatCodeblocks = struct parsetree else parsetree in - Res_printer.print_implementation - ~width:Res_multi_printer.default_print_width parsetree ~comments + Res_printer.print_implementation parsetree ~comments |> String.trim |> Block_line.list_of_string in @@ -856,10 +855,7 @@ module FormatCodeblocks = struct makeMapper ~transformAssertEqual ~displayFilename:filename in let astMapped = mapper.structure mapper structure in - Ok - ( Res_printer.print_implementation - ~width:Res_multi_printer.default_print_width astMapped ~comments, - source ) + Ok (Res_printer.print_implementation astMapped ~comments, source) else if Filename.check_suffix path ".resi" then let parser = Res_driver.parsing_engine.parse_interface ~for_printer:true @@ -871,10 +867,7 @@ module FormatCodeblocks = struct makeMapper ~transformAssertEqual ~displayFilename:filename in let astMapped = mapper.signature mapper signature in - Ok - ( Res_printer.print_interface - ~width:Res_multi_printer.default_print_width astMapped ~comments, - source ) + Ok (Res_printer.print_interface astMapped ~comments, source) else Error (Printf.sprintf From 89a90ddaa03cb9ff8dd1e609e2be5a7908fe5dd6 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 9 Jul 2025 13:34:10 +0200 Subject: [PATCH 20/25] explicit cmarkit --- tools/src/tools.ml | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/tools/src/tools.ml b/tools/src/tools.ml index 0a66695ac3..ca06194c73 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -728,22 +728,23 @@ module FormatCodeblocks = struct end let formatRescriptCodeBlocks content ~transformAssertEqual ~displayFilename ~addError ~markdownBlockStartLine = - let open Cmarkit in (* Detect ReScript code blocks. *) let hadCodeBlocks = ref false in let block _m = function - | Block.Code_block (codeBlock, meta) -> ( - match Block.Code_block.info_string codeBlock with + | Cmarkit.Block.Code_block (codeBlock, meta) -> ( + match Cmarkit.Block.Code_block.info_string codeBlock with | Some ((("rescript" | "res"), _) as info_string) -> hadCodeBlocks := true; let currentLine = - meta |> Meta.textloc |> Textloc.first_line |> fst |> Int.add 1 + meta |> Cmarkit.Meta.textloc |> Cmarkit.Textloc.first_line |> fst in - let layout = Block.Code_block.layout codeBlock in - let code = Block.Code_block.code codeBlock in + (* Account for 0-based line numbers *) + let currentLine = currentLine + 1 in + let layout = Cmarkit.Block.Code_block.layout codeBlock in + let code = Cmarkit.Block.Code_block.code codeBlock in let codeText = - code |> List.map Block_line.to_string |> String.concat "\n" + code |> List.map Cmarkit.Block_line.to_string |> String.concat "\n" in let n = List.length code in @@ -773,19 +774,21 @@ module FormatCodeblocks = struct else parsetree in Res_printer.print_implementation parsetree ~comments - |> String.trim |> Block_line.list_of_string + |> String.trim |> Cmarkit.Block_line.list_of_string in let mappedCodeBlock = - Block.Code_block.make ~layout ~info_string formattedCode + Cmarkit.Block.Code_block.make ~layout ~info_string formattedCode in - Mapper.ret (Block.Code_block (mappedCodeBlock, meta)) - | _ -> Mapper.default) - | _ -> Mapper.default + Cmarkit.Mapper.ret (Cmarkit.Block.Code_block (mappedCodeBlock, meta)) + | _ -> Cmarkit.Mapper.default) + | _ -> Cmarkit.Mapper.default in - let mapper = Mapper.make ~block () in + let mapper = Cmarkit.Mapper.make ~block () in let newContent = - content |> Doc.of_string ~locs:true |> Mapper.map_doc mapper + content + |> Cmarkit.Doc.of_string ~locs:true + |> Cmarkit.Mapper.map_doc mapper |> Cmarkit_commonmark.of_doc in (newContent, !hadCodeBlocks) From 0462aa7639e2d04fa9bd8ad048d54cf8f87ce931 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 9 Jul 2025 13:36:24 +0200 Subject: [PATCH 21/25] add note about --transform-assert-equal --- tools/bin/main.ml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/bin/main.ml b/tools/bin/main.ml index 48475485eb..cd139b9257 100644 --- a/tools/bin/main.ml +++ b/tools/bin/main.ml @@ -24,7 +24,9 @@ Usage: rescript-tools [command] Commands: doc Generate documentation -format-codeblocks [--stdout] Format ReScript code blocks +format-codeblocks Format ReScript code blocks + [--stdout] Output to stdout + [--transform-assert-equal] Transform `assertEqual` to `==` reanalyze Reanalyze -v, --version Print version -h, --help Print help|} From be8657f2569151010996c259a0f3e8f2a0092dfa Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 9 Jul 2025 13:40:17 +0200 Subject: [PATCH 22/25] support more rescript lang tags --- .../src/docstrings-format/FormatDocstringsTest1.res | 4 ++-- .../src/expected/FormatDocstringsTest1.res.expected | 4 ++-- tools/src/tools.ml | 12 +++++++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res index ff72fc5795..f9c0a27339 100644 --- a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res +++ b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res @@ -1,7 +1,7 @@ /** This is the first docstring with unformatted ReScript code. -```rescript +```res example let badly_formatted=(x,y)=>{ let result=x+y if result>0{Console.log("positive")}else{Console.log("negative")} @@ -24,7 +24,7 @@ module Nested = { But if I add another line here it should be fine. - ```rescript + ```res info module UserService={ let validate=user => user.age>=18 && user.name !== "" let getName = user=>user.name diff --git a/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected b/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected index 18641065d9..99c9a398f3 100644 --- a/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected +++ b/tests/tools_tests/src/expected/FormatDocstringsTest1.res.expected @@ -1,7 +1,7 @@ /** This is the first docstring with unformatted ReScript code. -```rescript +```res example let badly_formatted = (x, y) => { let result = x + y if result > 0 { @@ -28,7 +28,7 @@ module Nested = { But if I add another line here it should be fine. - ```rescript + ```res info module UserService = { let validate = user => user.age >= 18 && user.name !== "" let getName = user => user.name diff --git a/tools/src/tools.ml b/tools/src/tools.ml index ca06194c73..d83a3d7a42 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -726,6 +726,16 @@ module FormatCodeblocks = struct in mapper.structure mapper ast end + + let isResLang lang = + match String.lowercase_ascii lang with + | "res" | "rescript" | "resi" -> true + | lang -> + (* Cover ```res example, and similar *) + String.starts_with ~prefix:"res " lang + || String.starts_with ~prefix:"rescript " lang + || String.starts_with ~prefix:"resi " lang + let formatRescriptCodeBlocks content ~transformAssertEqual ~displayFilename ~addError ~markdownBlockStartLine = (* Detect ReScript code blocks. *) @@ -733,7 +743,7 @@ module FormatCodeblocks = struct let block _m = function | Cmarkit.Block.Code_block (codeBlock, meta) -> ( match Cmarkit.Block.Code_block.info_string codeBlock with - | Some ((("rescript" | "res"), _) as info_string) -> + | Some ((lang, _) as info_string) when isResLang lang -> hadCodeBlocks := true; let currentLine = From 977c5595cc2ec71ae9a2b772a1f77d2b1514379c Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 9 Jul 2025 13:46:55 +0200 Subject: [PATCH 23/25] support resi code blocks as well --- .../FormatDocstringsTest2.res | 14 +++++ .../FormatDocstringsTest2.res.expected | 13 +++++ tools/src/tools.ml | 54 ++++++++++++------- 3 files changed, 61 insertions(+), 20 deletions(-) diff --git a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest2.res b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest2.res index b316d3743f..3afb1906c8 100644 --- a/tests/tools_tests/src/docstrings-format/FormatDocstringsTest2.res +++ b/tests/tools_tests/src/docstrings-format/FormatDocstringsTest2.res @@ -40,3 +40,17 @@ Console.log(processed) ``` */ let testPipes = () => "pipes test" + +/** +Testing resi code blocks. + +```resi +type x=int +let x:int => + string +module M:{let ff: string => int +} + +``` + */ +let testResi = () => true diff --git a/tests/tools_tests/src/expected/FormatDocstringsTest2.res.expected b/tests/tools_tests/src/expected/FormatDocstringsTest2.res.expected index 61d79bf980..53f7fe5619 100644 --- a/tests/tools_tests/src/expected/FormatDocstringsTest2.res.expected +++ b/tests/tools_tests/src/expected/FormatDocstringsTest2.res.expected @@ -43,3 +43,16 @@ let asyncExample = async () => { */ let testPipes = () => "pipes test" +/** +Testing resi code blocks. + +```resi +type x = int +let x: int => string +module M: { + let ff: string => int +} +``` + */ +let testResi = () => true + diff --git a/tools/src/tools.ml b/tools/src/tools.ml index d83a3d7a42..05cc125284 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -762,29 +762,43 @@ module FormatCodeblocks = struct max 0 (markdownBlockStartLine + currentLine - n) in let codeWithOffset = String.make newlinesNeeded '\n' ^ codeText in - + let reportParseError diagnostics = + let buf = Buffer.create 1000 in + let formatter = Format.formatter_of_buffer buf in + Res_diagnostics.print_report ~formatter + ~custom_intro:(Some "Syntax error in code block in docstring") + diagnostics codeWithOffset; + addError (Buffer.contents buf) + in let formattedCode = - let {Res_driver.parsetree; comments; invalid; diagnostics} = - Res_driver.parse_implementation_from_source ~for_printer:true - ~display_filename:displayFilename ~source:codeWithOffset - in - if invalid then ( - let buf = Buffer.create 1000 in - let formatter = Format.formatter_of_buffer buf in - Res_diagnostics.print_report ~formatter - ~custom_intro:(Some "Syntax error in code block in docstring") - diagnostics codeWithOffset; - addError (Buffer.contents buf); - code) + if lang |> String.split_on_char ' ' |> List.hd = "resi" then + let {Res_driver.parsetree; comments; invalid; diagnostics} = + Res_driver.parse_interface_from_source ~for_printer:true + ~display_filename:displayFilename ~source:codeWithOffset + in + if invalid then ( + reportParseError diagnostics; + code) + else + Res_printer.print_interface parsetree ~comments + |> String.trim |> Cmarkit.Block_line.list_of_string else - let parsetree = - if transformAssertEqual then - Transform.transform ~transforms:[AssertEqualFnToEquals] - parsetree - else parsetree + let {Res_driver.parsetree; comments; invalid; diagnostics} = + Res_driver.parse_implementation_from_source ~for_printer:true + ~display_filename:displayFilename ~source:codeWithOffset in - Res_printer.print_implementation parsetree ~comments - |> String.trim |> Cmarkit.Block_line.list_of_string + if invalid then ( + reportParseError diagnostics; + code) + else + let parsetree = + if transformAssertEqual then + Transform.transform ~transforms:[AssertEqualFnToEquals] + parsetree + else parsetree + in + Res_printer.print_implementation parsetree ~comments + |> String.trim |> Cmarkit.Block_line.list_of_string in let mappedCodeBlock = From c48d4f95e9e7ae169e43465125660198df51b4cb Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 9 Jul 2025 13:50:13 +0200 Subject: [PATCH 24/25] add missing help text --- tools/bin/main.ml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/bin/main.ml b/tools/bin/main.ml index cd139b9257..da44211ac6 100644 --- a/tools/bin/main.ml +++ b/tools/bin/main.ml @@ -12,7 +12,7 @@ let formatDocstringsHelp = Format ReScript code blocks in docstrings -Usage: rescript-tools format-docstrings [--stdout] +Usage: rescript-tools format-docstrings [--stdout] [--transform-assert-equal] Example: rescript-tools format-docstrings ./path/to/MyModule.res|} From 5a540c7a32ea6a58210a7d0b5f549c3983513c94 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 9 Jul 2025 20:35:42 +0200 Subject: [PATCH 25/25] changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ff23e2329..de6f499274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ # 12.0.0-beta.1 (Unreleased) +#### :rocket: New Feature + +- Add experimental command to `rescript-tools` for formatting all ReScript code blocks in markdown. Either in a markdown file directly, or inside of docstrings in ReScript code. https://github.com/rescript-lang/rescript/pull/7598 + # 12.0.0-alpha.15 #### :boom: Breaking Change