Skip to content

Experimental command to format code blocks embedded in docstrings #7598

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Jul 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 2 additions & 6 deletions analysis/src/Commands.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
2 changes: 1 addition & 1 deletion analysis/src/CreateInterface.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
5 changes: 1 addition & 4 deletions analysis/src/Xform.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
20 changes: 13 additions & 7 deletions compiler/ml/location.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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 "@[<v>@, %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

Expand Down Expand Up @@ -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
Expand Down
24 changes: 20 additions & 4 deletions compiler/ml/location.mli
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions compiler/syntax/src/res_diagnostics.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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 "@[<v>";
Format.fprintf formatter "@[<v>";
print (List.rev diagnostics) src;
Format.fprintf Format.err_formatter "@]@."
Format.fprintf formatter "@]@."

let unexpected token context = Unexpected {token; context}

Expand Down
7 changes: 6 additions & 1 deletion compiler/syntax/src/res_diagnostics.mli
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 2 additions & 4 deletions compiler/syntax/src/res_multi_printer.ml
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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 =
Expand All @@ -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]

Expand Down
2 changes: 0 additions & 2 deletions compiler/syntax/src/res_multi_printer.mli
Original file line number Diff line number Diff line change
@@ -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"]
8 changes: 6 additions & 2 deletions compiler/syntax/src/res_printer.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -6055,15 +6057,17 @@ 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; *)
let doc = print_structure ~state:(State.init ()) s cmt_tbl in
(* 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"
Expand Down
6 changes: 4 additions & 2 deletions compiler/syntax/src/res_printer.mli
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
val default_print_width : int

val print_type_params :
(Parsetree.core_type * Asttypes.variance) list ->
Res_comments_table.t ->
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions dune-project
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
(depends
(ocaml
(>= 4.14))
(cmarkit
(>= 0.3.0))
(cppo
(= 1.8.0))
analysis
Expand Down
14 changes: 7 additions & 7 deletions runtime/Stdlib_Result.resi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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")
Expand Down Expand Up @@ -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.
```
Expand Down
49 changes: 49 additions & 0 deletions tests/tools_tests/src/docstrings-format/FormatDocstringsTest1.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
This is the first docstring with unformatted ReScript code.

```res example
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.

```res info
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<user>)=>{
users
->Array.map(user=>{...user,active:false})->Array.filter(u=>u.age>21)
}

type status=|Loading|Success(string)|Error(option<string>)
```
*/
let testFunction3 = () => "test3"
Loading