From 17d8b224749f0b9aad1fa25df90d1d759055443e Mon Sep 17 00:00:00 2001 From: Cristiano Calcagno Date: Mon, 17 Aug 2026 14:41:33 +0200 Subject: [PATCH] Preserve structure in docgen function details Function labels in Outcometree were strings, with optionality encoded by a leading question mark. That forced printers to decode the spelling and left downstream consumers without the label structure already known by the type system. Store Noloc.arg_label directly on Otyp_arrow, update both printers to match it exhaustively, and remove the unproduced Octy_arrow constructor. The doc generator previously walked Types.type_expr independently and flattened every reachable constructor into one list. Nested arrows became outer parameters, tuples and type variables disappeared, labels and optionality were lost, and non-function values acquired fabricated zero-parameter signatures. Build details from the normalized Outcometree instead: parameters retain their metadata, constructors, variables, tuples, and functions form recursive nodes, uncommon forms remain visible through a rendered fallback, and only top-level arrows receive signature details. Update the published RescriptTools.Docgen types and snapshots for the intentionally breaking JSON shape, and correct the implementation's stale alias tag to match the signature tag declared by its interface. The documentation site drops value details before publishing its data, but third-party consumers of rescript-tools doc need the changelog warning. Focused fixtures cover labeled and optional parameters, generic variables, callbacks, tuple returns, returned functions, fallback rendering, and non-function values. Compiler, tools, analysis, syntax, roundtrip, and full test suites remain green. Signed-off-by: Cristiano Calcagno --- CHANGELOG.md | 1 + compiler/ml/oprint.ml | 11 +- compiler/ml/outcometree.ml | 3 +- compiler/ml/printtyp.ml | 3 +- compiler/syntax/src/res_outcome_printer.ml | 35 ++- .../runtime/RescriptTools_Docgen.res | 18 +- .../runtime/RescriptTools_Docgen.resi | 16 +- .../src/DocgenSignatureDetails.res | 17 ++ .../src/DocgenSignatureDetails.resi | 17 ++ .../src/expected/DocExtraction2.res.json | 45 +++- .../src/expected/DocExtraction2.resi.json | 45 +++- .../src/expected/DocExtractionRes.res.json | 126 +++++---- .../expected/DocgenSignatureDetails.res.json | 243 ++++++++++++++++++ .../expected/DocgenSignatureDetails.resi.json | 243 ++++++++++++++++++ tests/tools_tests/src/expected/ModC.res.json | 9 +- tests/tools_tests/src/expected/ModC.resi.json | 9 +- tools/src/tools.ml | 162 +++++++----- 17 files changed, 824 insertions(+), 179 deletions(-) create mode 100644 tests/tools_tests/src/DocgenSignatureDetails.res create mode 100644 tests/tools_tests/src/DocgenSignatureDetails.resi create mode 100644 tests/tools_tests/src/expected/DocgenSignatureDetails.res.json create mode 100644 tests/tools_tests/src/expected/DocgenSignatureDetails.resi.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d1c3012c010..ff63df08f19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Remove runtime APIs that were deprecated for removal in ReScript 13, including the `Char` module, unsafe `Obj` operations, legacy `Pervasives` helpers, and `Array.unsafe_get`. https://github.com/rescript-lang/rescript/pull/8564 - Remove the deprecated `Js` namespace and its runtime modules. https://github.com/rescript-lang/rescript/pull/8531 - Move Belt into the separately installed `@rescript/belt` package. Projects using Belt must install the package and list it in their `rescript.json` dependencies. https://github.com/rescript-lang/rescript/pull/8554 +- Correct the structured function details produced by `rescript-tools doc` and exposed by `RescriptTools.Docgen`: parameters now retain labels and optionality, nested functions, tuples, variables, and generic arguments retain their type structure, return types are identified correctly, and non-function values no longer receive fake function details. This changes the published docgen detail schema. https://github.com/rescript-lang/rescript/pull/8576 #### :eyeglasses: Spec Compliance diff --git a/compiler/ml/oprint.ml b/compiler/ml/oprint.ml index 6d0b764d500..795a1b30fb0 100644 --- a/compiler/ml/oprint.ml +++ b/compiler/ml/oprint.ml @@ -253,9 +253,10 @@ and print_out_type_1 ppf = function pp_open_box ppf 0; List.iter (fun (lab, ty1) -> - if lab <> "" then ( - pp_print_string ppf lab; - pp_print_char ppf ':'); + (match lab with + | Asttypes.Noloc.Nolabel -> () + | Asttypes.Noloc.Labelled label -> fprintf ppf "%s:" label + | Asttypes.Noloc.Optional label -> fprintf ppf "?%s:" label); print_out_type_2 ppf ty1; pp_print_string ppf " ->"; pp_print_space ppf ()) @@ -406,10 +407,6 @@ let rec print_out_class_type ppf = function | tyl -> fprintf ppf "@[<1>[%a]@]@ " (print_typlist !out_type ",") tyl in fprintf ppf "@[%a%a@]" pr_tyl tyl print_ident id - | Octy_arrow (lab, ty, cty) -> - fprintf ppf "@[%s%a ->@ %a@]" - (if lab <> "" then lab ^ ":" else "") - print_out_type_2 ty print_out_class_type cty | Octy_signature (self_ty, csil) -> let pr_param ppf = function | Some ty -> fprintf ppf "@ @[(%a)@]" !out_type ty diff --git a/compiler/ml/outcometree.ml b/compiler/ml/outcometree.ml index d2e02e036e9..4d6e4190eb4 100644 --- a/compiler/ml/outcometree.ml +++ b/compiler/ml/outcometree.ml @@ -53,7 +53,7 @@ type out_type = | Otyp_abstract | Otyp_open | Otyp_alias of out_type * string - | Otyp_arrow of (string * out_type) list * out_type + | Otyp_arrow of (Asttypes.Noloc.arg_label * out_type) list * out_type | Otyp_class of bool * out_ident * out_type list | Otyp_constr of out_ident * out_type list | Otyp_manifest of out_type * out_type @@ -74,7 +74,6 @@ and out_variant = type out_class_type = | Octy_constr of out_ident * out_type list - | Octy_arrow of string * out_type * out_class_type | Octy_signature of out_type option * out_class_sig_item list and out_class_sig_item = | Ocsg_constraint of out_type * out_type diff --git a/compiler/ml/printtyp.ml b/compiler/ml/printtyp.ml index 624a3b75a0e..3f79ca004a8 100644 --- a/compiler/ml/printtyp.ml +++ b/compiler/ml/printtyp.ml @@ -616,7 +616,6 @@ let rec tree_of_typexp ?(printing_context : printing_context option) sch ty = let args = List.map (fun (arg : Types.arg) -> - let lab = string_of_label arg.lbl in let t1 = if is_optional arg.lbl then match (repr arg.typ).desc with @@ -626,7 +625,7 @@ let rec tree_of_typexp ?(printing_context : printing_context option) sch ty = | _ -> Otyp_stuff "" else tree_of_typexp ?printing_context sch arg.typ in - (lab, t1)) + (Asttypes.to_noloc arg.lbl, t1)) params in Otyp_arrow (args, tree_of_typexp ?printing_context sch ret) diff --git a/compiler/syntax/src/res_outcome_printer.ml b/compiler/syntax/src/res_outcome_printer.ml index ac723459952..684b3006689 100644 --- a/compiler/syntax/src/res_outcome_printer.ml +++ b/compiler/syntax/src/res_outcome_printer.ml @@ -238,29 +238,26 @@ let rec print_out_type_doc (out_type : Outcometree.out_type) = and print_out_arrow_type typ = let typ_args, typ = collect_arrow_args typ in + let print_labeled_arg label optional_indicator typ = + Doc.group + (Doc.concat + [ + Doc.text ("~" ^ label ^ ": "); + print_out_type_doc typ; + optional_indicator; + ]) + in let args = Doc.join ~sep:(Doc.concat [Doc.comma; Doc.line]) (List.map (fun (lbl, typ) -> - let lbl_len = String.length lbl in - if lbl_len = 0 then print_out_type_doc typ - else - let lbl, optional_indicator = - (* the ocaml compiler hardcodes the optional label inside the string of the label in printtyp.ml *) - match String.unsafe_get lbl 0 with - | '?' -> - ( (String.sub [@doesNotRaise]) lbl 1 (lbl_len - 1), - Doc.text "=?" ) - | _ -> (lbl, Doc.nil) - in - Doc.group - (Doc.concat - [ - Doc.text ("~" ^ lbl ^ ": "); - print_out_type_doc typ; - optional_indicator; - ])) + match lbl with + | Asttypes.Noloc.Nolabel -> print_out_type_doc typ + | Asttypes.Noloc.Labelled label -> + print_labeled_arg label Doc.nil typ + | Asttypes.Noloc.Optional label -> + print_labeled_arg label (Doc.text "=?") typ) typ_args) in let args_doc = @@ -268,7 +265,7 @@ and print_out_arrow_type typ = match typ_args with | [(_, (Otyp_tuple _ | Otyp_arrow _))] -> true (* single argument should not be wrapped *) - | [("", _)] -> false + | [(Asttypes.Noloc.Nolabel, _)] -> false | _ -> true in if needs_parens then diff --git a/packages/@rescript/runtime/RescriptTools_Docgen.res b/packages/@rescript/runtime/RescriptTools_Docgen.res index 6dd74547473..8dd1cc2e880 100644 --- a/packages/@rescript/runtime/RescriptTools_Docgen.res +++ b/packages/@rescript/runtime/RescriptTools_Docgen.res @@ -17,13 +17,21 @@ type constructor = { payload?: constructorPayload, } -type rec typeInSignature = { - path: string, - genericTypeParameters: array, +@tag("kind") +type rec typeInSignature = + | @as("constructor") Constructor({path: string, genericTypeParameters: array}) + | @as("variable") Variable({name: string, weak: bool}) + | @as("tuple") Tuple({elements: array}) + | @as("function") Function({parameters: array, returnType: typeInSignature}) + | @as("rendered") Rendered({signature: string}) +and signatureParameter = { + label?: string, + optional: bool, + @as("type") type_: typeInSignature, } type signatureDetails = { - parameters: array, + parameters: array, returnType: typeInSignature, } @@ -31,7 +39,7 @@ type signatureDetails = { type detail = | @as("record") Record({items: array}) | @as("variant") Variant({items: array}) - | @as("alias") Signature({details: signatureDetails}) + | @as("signature") Signature({details: signatureDetails}) type source = { filepath: string, diff --git a/packages/@rescript/runtime/RescriptTools_Docgen.resi b/packages/@rescript/runtime/RescriptTools_Docgen.resi index 2c8b1d4ad10..ae20015090f 100644 --- a/packages/@rescript/runtime/RescriptTools_Docgen.resi +++ b/packages/@rescript/runtime/RescriptTools_Docgen.resi @@ -17,13 +17,21 @@ type constructor = { payload?: constructorPayload, } -type rec typeInSignature = { - path: string, - genericTypeParameters: array, +@tag("kind") +type rec typeInSignature = + | @as("constructor") Constructor({path: string, genericTypeParameters: array}) + | @as("variable") Variable({name: string, weak: bool}) + | @as("tuple") Tuple({elements: array}) + | @as("function") Function({parameters: array, returnType: typeInSignature}) + | @as("rendered") Rendered({signature: string}) +and signatureParameter = { + label?: string, + optional: bool, + @as("type") type_: typeInSignature, } type signatureDetails = { - parameters: array, + parameters: array, returnType: typeInSignature, } diff --git a/tests/tools_tests/src/DocgenSignatureDetails.res b/tests/tools_tests/src/DocgenSignatureDetails.res new file mode 100644 index 00000000000..b52a9a8a4a6 --- /dev/null +++ b/tests/tools_tests/src/DocgenSignatureDetails.res @@ -0,0 +1,17 @@ +let labeledOptional: (~required: 'a, ~optional: array<'a>=?) => result<'a, string> = ( + ~required, + ~optional=?, +) => { + ignore(optional) + Ok(required) +} + +let takesCallback: ('a => string) => bool = _callback => true + +let returnsTuple: int => (string, int) = value => ("value", value) + +let returnsFunction: int => string => bool = _value => _text => true + +let takesVariant: [#enabled | #count(int)] => unit = _variant => () + +let constant = 42 diff --git a/tests/tools_tests/src/DocgenSignatureDetails.resi b/tests/tools_tests/src/DocgenSignatureDetails.resi new file mode 100644 index 00000000000..d6c6a598c41 --- /dev/null +++ b/tests/tools_tests/src/DocgenSignatureDetails.resi @@ -0,0 +1,17 @@ +/** Labeled and optional parameters keep their parameter metadata. */ +let labeledOptional: (~required: 'a, ~optional: array<'a>=?) => result<'a, string> + +/** A callback remains one parameter instead of becoming outer parameters. */ +let takesCallback: ('a => string) => bool + +/** A tuple remains one return-type node. */ +let returnsTuple: int => (string, int) + +/** A returned function remains nested in the return type. */ +let returnsFunction: int => string => bool + +/** Less common type forms remain visible through an explicit fallback. */ +let takesVariant: [#enabled | #count(int)] => unit + +/** Non-functions do not receive function signature details. */ +let constant: int diff --git a/tests/tools_tests/src/expected/DocExtraction2.res.json b/tests/tools_tests/src/expected/DocExtraction2.res.json index 79eac3729cf..1221c0b1b39 100644 --- a/tests/tools_tests/src/expected/DocExtraction2.res.json +++ b/tests/tools_tests/src/expected/DocExtraction2.res.json @@ -29,8 +29,21 @@ "detail": { "kind": "signature", "details": { - "parameters": [ { "path": "unit", "genericTypeParameters": [] } ], - "returnType": { "path": "t", "genericTypeParameters": [] } + "parameters": [ + { + "optional": false, + "type": { + "kind": "constructor", + "path": "unit", + "genericTypeParameters": [] + } + } + ], + "returnType": { + "kind": "constructor", + "path": "t", + "genericTypeParameters": [] + } } } }, @@ -72,9 +85,20 @@ "kind": "signature", "details": { "parameters": [ - { "path": "unit", "genericTypeParameters": [] } + { + "optional": false, + "type": { + "kind": "constructor", + "path": "unit", + "genericTypeParameters": [] + } + } ], - "returnType": { "path": "t", "genericTypeParameters": [] } + "returnType": { + "kind": "constructor", + "path": "t", + "genericTypeParameters": [] + } } } } @@ -95,8 +119,17 @@ "detail": { "kind": "signature", "details": { - "parameters": [], - "returnType": { "path": "unit", "genericTypeParameters": [] } + "parameters": [ + { + "optional": false, + "type": { "kind": "variable", "name": "a", "weak": false } + } + ], + "returnType": { + "kind": "constructor", + "path": "unit", + "genericTypeParameters": [] + } } } } diff --git a/tests/tools_tests/src/expected/DocExtraction2.resi.json b/tests/tools_tests/src/expected/DocExtraction2.resi.json index 79eac3729cf..1221c0b1b39 100644 --- a/tests/tools_tests/src/expected/DocExtraction2.resi.json +++ b/tests/tools_tests/src/expected/DocExtraction2.resi.json @@ -29,8 +29,21 @@ "detail": { "kind": "signature", "details": { - "parameters": [ { "path": "unit", "genericTypeParameters": [] } ], - "returnType": { "path": "t", "genericTypeParameters": [] } + "parameters": [ + { + "optional": false, + "type": { + "kind": "constructor", + "path": "unit", + "genericTypeParameters": [] + } + } + ], + "returnType": { + "kind": "constructor", + "path": "t", + "genericTypeParameters": [] + } } } }, @@ -72,9 +85,20 @@ "kind": "signature", "details": { "parameters": [ - { "path": "unit", "genericTypeParameters": [] } + { + "optional": false, + "type": { + "kind": "constructor", + "path": "unit", + "genericTypeParameters": [] + } + } ], - "returnType": { "path": "t", "genericTypeParameters": [] } + "returnType": { + "kind": "constructor", + "path": "t", + "genericTypeParameters": [] + } } } } @@ -95,8 +119,17 @@ "detail": { "kind": "signature", "details": { - "parameters": [], - "returnType": { "path": "unit", "genericTypeParameters": [] } + "parameters": [ + { + "optional": false, + "type": { "kind": "variable", "name": "a", "weak": false } + } + ], + "returnType": { + "kind": "constructor", + "path": "unit", + "genericTypeParameters": [] + } } } } diff --git a/tests/tools_tests/src/expected/DocExtractionRes.res.json b/tests/tools_tests/src/expected/DocExtractionRes.res.json index 5d3d3c2bdc2..da2ef13a178 100644 --- a/tests/tools_tests/src/expected/DocExtractionRes.res.json +++ b/tests/tools_tests/src/expected/DocExtractionRes.res.json @@ -48,8 +48,21 @@ "detail": { "kind": "signature", "details": { - "parameters": [ { "path": "string", "genericTypeParameters": [] } ], - "returnType": { "path": "t", "genericTypeParameters": [] } + "parameters": [ + { + "optional": false, + "type": { + "kind": "constructor", + "path": "string", + "genericTypeParameters": [] + } + } + ], + "returnType": { + "kind": "constructor", + "path": "t", + "genericTypeParameters": [] + } } } }, @@ -67,8 +80,21 @@ "detail": { "kind": "signature", "details": { - "parameters": [ { "path": "t", "genericTypeParameters": [] } ], - "returnType": { "path": "t", "genericTypeParameters": [] } + "parameters": [ + { + "optional": false, + "type": { + "kind": "constructor", + "path": "t", + "genericTypeParameters": [] + } + } + ], + "returnType": { + "kind": "constructor", + "path": "t", + "genericTypeParameters": [] + } } } }, @@ -82,13 +108,6 @@ "filepath": "src/DocExtractionRes.res", "line": 26, "col": 5 - }, - "detail": { - "kind": "signature", - "details": { - "parameters": [], - "returnType": { "path": "int", "genericTypeParameters": [] } - } } }, { @@ -211,11 +230,19 @@ "details": { "parameters": [ { - "path": "SomeInnerModule.status", - "genericTypeParameters": [] + "optional": false, + "type": { + "kind": "constructor", + "path": "SomeInnerModule.status", + "genericTypeParameters": [] + } } ], - "returnType": { "path": "bool", "genericTypeParameters": [] } + "returnType": { + "kind": "constructor", + "path": "bool", + "genericTypeParameters": [] + } } } }, @@ -312,9 +339,20 @@ "kind": "signature", "details": { "parameters": [ - { "path": "unit", "genericTypeParameters": [] } + { + "optional": false, + "type": { + "kind": "constructor", + "path": "unit", + "genericTypeParameters": [] + } + } ], - "returnType": { "path": "t", "genericTypeParameters": [] } + "returnType": { + "kind": "constructor", + "path": "t", + "genericTypeParameters": [] + } } } } @@ -357,8 +395,21 @@ "detail": { "kind": "signature", "details": { - "parameters": [ { "path": "t", "genericTypeParameters": [] } ], - "returnType": { "path": "t", "genericTypeParameters": [] } + "parameters": [ + { + "optional": false, + "type": { + "kind": "constructor", + "path": "t", + "genericTypeParameters": [] + } + } + ], + "returnType": { + "kind": "constructor", + "path": "t", + "genericTypeParameters": [] + } } } } @@ -402,9 +453,20 @@ "kind": "signature", "details": { "parameters": [ - { "path": "int", "genericTypeParameters": [] } + { + "optional": false, + "type": { + "kind": "constructor", + "path": "int", + "genericTypeParameters": [] + } + } ], - "returnType": { "path": "int", "genericTypeParameters": [] } + "returnType": { + "kind": "constructor", + "path": "int", + "genericTypeParameters": [] + } } } } @@ -432,13 +494,6 @@ "filepath": "src/DocExtractionRes.res", "line": 138, "col": 3 - }, - "detail": { - "kind": "signature", - "details": { - "parameters": [], - "returnType": { "path": "int", "genericTypeParameters": [] } - } } } ] @@ -464,13 +519,6 @@ "filepath": "src/DocExtractionRes.res", "line": 142, "col": 7 - }, - "detail": { - "kind": "signature", - "details": { - "parameters": [], - "returnType": { "path": "int", "genericTypeParameters": [] } - } } } ], @@ -508,16 +556,6 @@ "filepath": "src/DocExtractionRes.res", "line": 147, "col": 9 - }, - "detail": { - "kind": "signature", - "details": { - "parameters": [], - "returnType": { - "path": "int", - "genericTypeParameters": [] - } - } } } ], diff --git a/tests/tools_tests/src/expected/DocgenSignatureDetails.res.json b/tests/tools_tests/src/expected/DocgenSignatureDetails.res.json new file mode 100644 index 00000000000..381618d2709 --- /dev/null +++ b/tests/tools_tests/src/expected/DocgenSignatureDetails.res.json @@ -0,0 +1,243 @@ +{ + "name": "DocgenSignatureDetails", + "docstrings": [], + "source": { + "filepath": "src/DocgenSignatureDetails.resi", + "line": 1, + "col": 1 + }, + "items": [ + { + "id": "DocgenSignatureDetails.labeledOptional", + "kind": "value", + "name": "labeledOptional", + "signature": "let labeledOptional: (\n ~required: 'a,\n ~optional: array<'a>=?,\n) => result<'a, string>", + "docstrings": [ + "Labeled and optional parameters keep their parameter metadata." + ], + "source": { + "filepath": "src/DocgenSignatureDetails.resi", + "line": 2, + "col": 1 + }, + "detail": { + "kind": "signature", + "details": { + "parameters": [ + { + "label": "required", + "optional": false, + "type": { "kind": "variable", "name": "a", "weak": false } + }, + { + "label": "optional", + "optional": true, + "type": { + "kind": "constructor", + "path": "array", + "genericTypeParameters": [ + { "kind": "variable", "name": "a", "weak": false } + ] + } + } + ], + "returnType": { + "kind": "constructor", + "path": "result", + "genericTypeParameters": [ + { "kind": "variable", "name": "a", "weak": false }, + { + "kind": "constructor", + "path": "string", + "genericTypeParameters": [] + } + ] + } + } + } + }, + { + "id": "DocgenSignatureDetails.takesCallback", + "kind": "value", + "name": "takesCallback", + "signature": "let takesCallback: ('a => string) => bool", + "docstrings": [ + "A callback remains one parameter instead of becoming outer parameters." + ], + "source": { + "filepath": "src/DocgenSignatureDetails.resi", + "line": 5, + "col": 1 + }, + "detail": { + "kind": "signature", + "details": { + "parameters": [ + { + "optional": false, + "type": { + "kind": "function", + "parameters": [ + { + "optional": false, + "type": { + "kind": "variable", + "name": "a", + "weak": false + } + } + ], + "returnType": { + "kind": "constructor", + "path": "string", + "genericTypeParameters": [] + } + } + } + ], + "returnType": { + "kind": "constructor", + "path": "bool", + "genericTypeParameters": [] + } + } + } + }, + { + "id": "DocgenSignatureDetails.returnsTuple", + "kind": "value", + "name": "returnsTuple", + "signature": "let returnsTuple: int => (string, int)", + "docstrings": [ "A tuple remains one return-type node." ], + "source": { + "filepath": "src/DocgenSignatureDetails.resi", + "line": 8, + "col": 1 + }, + "detail": { + "kind": "signature", + "details": { + "parameters": [ + { + "optional": false, + "type": { + "kind": "constructor", + "path": "int", + "genericTypeParameters": [] + } + } + ], + "returnType": { + "kind": "tuple", + "elements": [ + { + "kind": "constructor", + "path": "string", + "genericTypeParameters": [] + }, + { + "kind": "constructor", + "path": "int", + "genericTypeParameters": [] + } + ] + } + } + } + }, + { + "id": "DocgenSignatureDetails.returnsFunction", + "kind": "value", + "name": "returnsFunction", + "signature": "let returnsFunction: int => string => bool", + "docstrings": [ + "A returned function remains nested in the return type." + ], + "source": { + "filepath": "src/DocgenSignatureDetails.resi", + "line": 11, + "col": 1 + }, + "detail": { + "kind": "signature", + "details": { + "parameters": [ + { + "optional": false, + "type": { + "kind": "constructor", + "path": "int", + "genericTypeParameters": [] + } + } + ], + "returnType": { + "kind": "function", + "parameters": [ + { + "optional": false, + "type": { + "kind": "constructor", + "path": "string", + "genericTypeParameters": [] + } + } + ], + "returnType": { + "kind": "constructor", + "path": "bool", + "genericTypeParameters": [] + } + } + } + } + }, + { + "id": "DocgenSignatureDetails.takesVariant", + "kind": "value", + "name": "takesVariant", + "signature": "let takesVariant: [#count(int) | #enabled] => unit", + "docstrings": [ + "Less common type forms remain visible through an explicit fallback." + ], + "source": { + "filepath": "src/DocgenSignatureDetails.resi", + "line": 14, + "col": 1 + }, + "detail": { + "kind": "signature", + "details": { + "parameters": [ + { + "optional": false, + "type": { + "kind": "rendered", + "signature": "[#count(int) | #enabled]" + } + } + ], + "returnType": { + "kind": "constructor", + "path": "unit", + "genericTypeParameters": [] + } + } + } + }, + { + "id": "DocgenSignatureDetails.constant", + "kind": "value", + "name": "constant", + "signature": "let constant: int", + "docstrings": [ + "Non-functions do not receive function signature details." + ], + "source": { + "filepath": "src/DocgenSignatureDetails.resi", + "line": 17, + "col": 1 + } + } + ] +} diff --git a/tests/tools_tests/src/expected/DocgenSignatureDetails.resi.json b/tests/tools_tests/src/expected/DocgenSignatureDetails.resi.json new file mode 100644 index 00000000000..381618d2709 --- /dev/null +++ b/tests/tools_tests/src/expected/DocgenSignatureDetails.resi.json @@ -0,0 +1,243 @@ +{ + "name": "DocgenSignatureDetails", + "docstrings": [], + "source": { + "filepath": "src/DocgenSignatureDetails.resi", + "line": 1, + "col": 1 + }, + "items": [ + { + "id": "DocgenSignatureDetails.labeledOptional", + "kind": "value", + "name": "labeledOptional", + "signature": "let labeledOptional: (\n ~required: 'a,\n ~optional: array<'a>=?,\n) => result<'a, string>", + "docstrings": [ + "Labeled and optional parameters keep their parameter metadata." + ], + "source": { + "filepath": "src/DocgenSignatureDetails.resi", + "line": 2, + "col": 1 + }, + "detail": { + "kind": "signature", + "details": { + "parameters": [ + { + "label": "required", + "optional": false, + "type": { "kind": "variable", "name": "a", "weak": false } + }, + { + "label": "optional", + "optional": true, + "type": { + "kind": "constructor", + "path": "array", + "genericTypeParameters": [ + { "kind": "variable", "name": "a", "weak": false } + ] + } + } + ], + "returnType": { + "kind": "constructor", + "path": "result", + "genericTypeParameters": [ + { "kind": "variable", "name": "a", "weak": false }, + { + "kind": "constructor", + "path": "string", + "genericTypeParameters": [] + } + ] + } + } + } + }, + { + "id": "DocgenSignatureDetails.takesCallback", + "kind": "value", + "name": "takesCallback", + "signature": "let takesCallback: ('a => string) => bool", + "docstrings": [ + "A callback remains one parameter instead of becoming outer parameters." + ], + "source": { + "filepath": "src/DocgenSignatureDetails.resi", + "line": 5, + "col": 1 + }, + "detail": { + "kind": "signature", + "details": { + "parameters": [ + { + "optional": false, + "type": { + "kind": "function", + "parameters": [ + { + "optional": false, + "type": { + "kind": "variable", + "name": "a", + "weak": false + } + } + ], + "returnType": { + "kind": "constructor", + "path": "string", + "genericTypeParameters": [] + } + } + } + ], + "returnType": { + "kind": "constructor", + "path": "bool", + "genericTypeParameters": [] + } + } + } + }, + { + "id": "DocgenSignatureDetails.returnsTuple", + "kind": "value", + "name": "returnsTuple", + "signature": "let returnsTuple: int => (string, int)", + "docstrings": [ "A tuple remains one return-type node." ], + "source": { + "filepath": "src/DocgenSignatureDetails.resi", + "line": 8, + "col": 1 + }, + "detail": { + "kind": "signature", + "details": { + "parameters": [ + { + "optional": false, + "type": { + "kind": "constructor", + "path": "int", + "genericTypeParameters": [] + } + } + ], + "returnType": { + "kind": "tuple", + "elements": [ + { + "kind": "constructor", + "path": "string", + "genericTypeParameters": [] + }, + { + "kind": "constructor", + "path": "int", + "genericTypeParameters": [] + } + ] + } + } + } + }, + { + "id": "DocgenSignatureDetails.returnsFunction", + "kind": "value", + "name": "returnsFunction", + "signature": "let returnsFunction: int => string => bool", + "docstrings": [ + "A returned function remains nested in the return type." + ], + "source": { + "filepath": "src/DocgenSignatureDetails.resi", + "line": 11, + "col": 1 + }, + "detail": { + "kind": "signature", + "details": { + "parameters": [ + { + "optional": false, + "type": { + "kind": "constructor", + "path": "int", + "genericTypeParameters": [] + } + } + ], + "returnType": { + "kind": "function", + "parameters": [ + { + "optional": false, + "type": { + "kind": "constructor", + "path": "string", + "genericTypeParameters": [] + } + } + ], + "returnType": { + "kind": "constructor", + "path": "bool", + "genericTypeParameters": [] + } + } + } + } + }, + { + "id": "DocgenSignatureDetails.takesVariant", + "kind": "value", + "name": "takesVariant", + "signature": "let takesVariant: [#count(int) | #enabled] => unit", + "docstrings": [ + "Less common type forms remain visible through an explicit fallback." + ], + "source": { + "filepath": "src/DocgenSignatureDetails.resi", + "line": 14, + "col": 1 + }, + "detail": { + "kind": "signature", + "details": { + "parameters": [ + { + "optional": false, + "type": { + "kind": "rendered", + "signature": "[#count(int) | #enabled]" + } + } + ], + "returnType": { + "kind": "constructor", + "path": "unit", + "genericTypeParameters": [] + } + } + } + }, + { + "id": "DocgenSignatureDetails.constant", + "kind": "value", + "name": "constant", + "signature": "let constant: int", + "docstrings": [ + "Non-functions do not receive function signature details." + ], + "source": { + "filepath": "src/DocgenSignatureDetails.resi", + "line": 17, + "col": 1 + } + } + ] +} diff --git a/tests/tools_tests/src/expected/ModC.res.json b/tests/tools_tests/src/expected/ModC.res.json index cc4cce09bba..db9cdf11e52 100644 --- a/tests/tools_tests/src/expected/ModC.res.json +++ b/tests/tools_tests/src/expected/ModC.res.json @@ -16,14 +16,7 @@ "name": "name", "signature": "let name: string", "docstrings": [], - "source": { "filepath": "src/ModC.resi", "line": 5, "col": 3 }, - "detail": { - "kind": "signature", - "details": { - "parameters": [], - "returnType": { "path": "string", "genericTypeParameters": [] } - } - } + "source": { "filepath": "src/ModC.resi", "line": 5, "col": 3 } } ] } diff --git a/tests/tools_tests/src/expected/ModC.resi.json b/tests/tools_tests/src/expected/ModC.resi.json index cc4cce09bba..db9cdf11e52 100644 --- a/tests/tools_tests/src/expected/ModC.resi.json +++ b/tests/tools_tests/src/expected/ModC.resi.json @@ -16,14 +16,7 @@ "name": "name", "signature": "let name: string", "docstrings": [], - "source": { "filepath": "src/ModC.resi", "line": 5, "col": 3 }, - "detail": { - "kind": "signature", - "details": { - "parameters": [], - "returnType": { "path": "string", "genericTypeParameters": [] } - } - } + "source": { "filepath": "src/ModC.resi", "line": 5, "col": 3 } } ] } diff --git a/tools/src/tools.ml b/tools/src/tools.ml index d7787073952..f893f81f986 100644 --- a/tools/src/tools.ml +++ b/tools/src/tools.ml @@ -20,8 +20,19 @@ type constructor_doc = { items: constructor_payload option; } -type type_doc = {path: string; generic_parameters: type_doc list} -type value_signature = {parameters: type_doc list; return_type: type_doc} +type type_doc = + | Constructor of {path: string; generic_parameters: type_doc list} + | Variable of {name: string; weak: bool} + | Tuple of type_doc list + | Function of value_signature + | Rendered of string + +and signature_parameter = {label: string option; optional: bool; typ: type_doc} + +and value_signature = { + parameters: signature_parameter list; + return_type: type_doc; +} type source = {filepath: string; line: int; col: int} @@ -103,13 +114,45 @@ let stringify_constructor_payload (constructor_payload : constructor_payload) = ("fields", `List (field_docs |> List.map stringify_field_doc)); ] -let rec stringify_type_doc (td : type_doc) = - let ps = - match td.generic_parameters with - | [] -> `List [] - | ts -> ts |> List.map stringify_type_doc |> fun ts -> `List ts - in - `Assoc [("path", `String td.path); ("genericTypeParameters", ps)] +let rec stringify_type_doc = function + | Constructor {path; generic_parameters} -> + `Assoc + [ + ("kind", `String "constructor"); + ("path", `String path); + ( "genericTypeParameters", + `List (List.map stringify_type_doc generic_parameters) ); + ] + | Variable {name; weak} -> + `Assoc + [ + ("kind", `String "variable"); + ("name", `String name); + ("weak", `Bool weak); + ] + | Tuple elements -> + `Assoc + [ + ("kind", `String "tuple"); + ("elements", `List (List.map stringify_type_doc elements)); + ] + | Function signature -> + `Assoc (("kind", `String "function") :: stringify_value_signature signature) + | Rendered signature -> + `Assoc [("kind", `String "rendered"); ("signature", `String signature)] + +and stringify_signature_parameter {label; optional; typ} = + `Assoc + ((match label with + | Some label -> [("label", `String label)] + | None -> []) + @ [("optional", `Bool optional); ("type", stringify_type_doc typ)]) + +and stringify_value_signature {parameters; return_type} = + [ + ("parameters", `List (List.map stringify_signature_parameter parameters)); + ("returnType", stringify_type_doc return_type); + ] let stringify_detail (detail : doc_item_detail) = match detail with @@ -147,18 +190,10 @@ let stringify_detail (detail : doc_item_detail) = | None -> []))) ); ] | Signature {parameters; return_type} -> - let ps = - match parameters with - | [] -> `List [] - | ps -> ps |> List.map stringify_type_doc |> fun ps -> `List ps - in `Assoc [ ("kind", `String "signature"); - ( "details", - `Assoc - [("parameters", ps); ("returnType", stringify_type_doc return_type)] - ); + ("details", `Assoc (stringify_value_signature {parameters; return_type})); ] let stringify_source source = @@ -309,60 +344,51 @@ let type_detail typ ~env ~full ~state = }) | _ -> None -(* split a list into two parts all the items except the last one and the last item *) -let split_last l = - let rec splitLast' acc = function - | [] -> failwith "splitLast: empty list" - | [x] -> (List.rev acc, x) - | x :: xs -> splitLast' (x :: acc) xs - in - splitLast' [] l +let rec string_of_out_ident = function + | Outcometree.Oide_ident name -> name + | Oide_dot (path, name) -> string_of_out_ident path ^ "." ^ name + | Oide_apply (functor_, argument) -> + string_of_out_ident functor_ ^ "(" ^ string_of_out_ident argument ^ ")" + +let render_out_type typ = + Res_doc.to_string ~width:80 (Res_outcome_printer.print_out_type_doc typ) + +let rec type_doc_of_out_type (typ : Outcometree.out_type) = + match typ with + | Otyp_constr (path, generic_parameters) -> + Constructor + { + path = string_of_out_ident path; + generic_parameters = List.map type_doc_of_out_type generic_parameters; + } + | Otyp_var (weak, name) -> Variable {name; weak} + | Otyp_tuple elements -> Tuple (List.map type_doc_of_out_type elements) + | Otyp_arrow (parameters, return_type) -> + Function (value_signature_of_arrow parameters return_type) + | _ -> Rendered (render_out_type typ) -let path_to_string path = - let buf = Buffer.create 64 in - let rec aux = function - | Path.Pident id -> Buffer.add_string buf (Ident.name id) - | Path.Pdot (p, s, _) -> - aux p; - Buffer.add_char buf '.'; - Buffer.add_string buf s - | Path.Papply (p1, p2) -> - aux p1; - Buffer.add_char buf '('; - aux p2; - Buffer.add_char buf ')' +and signature_parameter_of_out_type (label, typ) = + let label, optional = + match label with + | Asttypes.Noloc.Nolabel -> (None, false) + | Asttypes.Noloc.Labelled label -> (Some label, false) + | Asttypes.Noloc.Optional label -> (Some label, true) in - aux path; - Buffer.contents buf + {label; optional; typ = type_doc_of_out_type typ} + +and value_signature_of_arrow parameters return_type = + { + parameters = List.map signature_parameter_of_out_type parameters; + return_type = type_doc_of_out_type return_type; + } let value_detail (typ : Types.type_expr) = - let rec collect_signature_types (typ : Types.type_expr) = - match typ.desc with - | Tlink t | Tsubst t | Tpoly (t, []) -> collect_signature_types t - | Tconstr (path, ts, _) -> ( - let p = path_to_string path in - match ts with - | [] -> [{path = p; generic_parameters = []}] - | ts -> - let ts = - ts - |> List.concat_map (fun (t : Types.type_expr) -> - collect_signature_types t) - in - [{path = p; generic_parameters = ts}]) - | Tarrow (params, ret) -> - List.concat_map - (fun ({typ} : Types.arg) -> collect_signature_types typ) - params - @ collect_signature_types ret - | Tvar None -> [{path = "_"; generic_parameters = []}] - | _ -> [] - in - match collect_signature_types typ with - | [] -> None - | ts -> - let parameters, return_type = split_last ts in - Some (Signature {parameters; return_type}) + Printtyp.reset_names (); + Printtyp.reset_and_mark_loops typ; + match Printtyp.tree_of_typexp false typ with + | Otyp_arrow (parameters, return_type) -> + Some (Signature (value_signature_of_arrow parameters return_type)) + | _ -> None let make_id module_path ~identifier = identifier :: module_path |> List.rev |> Shared_types.ident