From 75c70ffa3fcb7c35ddef565f3d28bb3555e243df Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 13:38:06 +0200 Subject: [PATCH 01/11] 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 7b8e1466a07c524d8d346b18b9579cc5bc73e49d Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 14:13:18 +0200 Subject: [PATCH 02/11] 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 6fc54b1c3ba444c20eeb5bf5f37bf85762a4512a Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 14:51:17 +0200 Subject: [PATCH 03/11] 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 500e58e7b76add761d4ba39dc244e94e1e6d5bba Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 14:51:25 +0200 Subject: [PATCH 04/11] 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 a36fada7e6fe5763af13a736b7b79c1f9627b24e Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 14:53:03 +0200 Subject: [PATCH 05/11] 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 69e3919c27c3ad7de0afc6973c184d0020d4d245 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 16:45:05 +0200 Subject: [PATCH 06/11] 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 dc2cc936a8aaf103d8b802743ba1eddf66def7f9 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 16:52:41 +0200 Subject: [PATCH 07/11] 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 6cde0e3dd51605e2ad76711ad54b783d31bdafa6 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 18:33:59 +0200 Subject: [PATCH 08/11] 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 3a586b71238c7acad3a71d9c3e8229df80fb3b7f Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 18:40:49 +0200 Subject: [PATCH 09/11] 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 7738da9d3b8f37065e75b1f5378e27ad74597cee Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 19:32:18 +0200 Subject: [PATCH 10/11] 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 a35a86763b4e72126964eef24516858075085d55 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Fri, 4 Jul 2025 19:37:04 +0200 Subject: [PATCH 11/11] 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);