diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index 975fef31a..f77fa4195 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -3,7 +3,18 @@ open SharedTypes let showConstructor {Constructor.cname = {txt}; args; res} = txt ^ (match args with - | Args [] | InlineRecord _ -> "" + | Args [] -> "" + | InlineRecord fields -> + "({" + ^ (fields + |> List.map (fun (field : field) -> + Printf.sprintf "%s%s: %s" field.fname.txt + (if field.optional then "?" else "") + (Shared.typeToString + (if field.optional then Utils.unwrapIfOption field.typ + else field.typ))) + |> String.concat ", ") + ^ "})" | Args args -> "(" ^ (args diff --git a/analysis/src/DocExtraction.ml b/analysis/src/DocExtraction.ml index d646bd175..59320d94a 100644 --- a/analysis/src/DocExtraction.ml +++ b/analysis/src/DocExtraction.ml @@ -6,11 +6,14 @@ type fieldDoc = { deprecated: string option; } +type constructorPayload = InlineRecord of {fieldDocs: fieldDoc list} + type constructorDoc = { constructorName: string; docstrings: string list; signature: string; deprecated: string option; + items: constructorPayload option; } type docItemDetail = @@ -54,6 +57,35 @@ let stringifyDocstrings docstrings = |> List.map (fun docstring -> docstring |> String.trim |> wrapInQuotes) |> array +let stringifyFieldDoc ~indentation (fieldDoc : fieldDoc) = + let open Protocol in + stringifyObject ~indentation:(indentation + 1) + [ + ("name", Some (wrapInQuotes fieldDoc.fieldName)); + ( "deprecated", + match fieldDoc.deprecated with + | Some d -> Some (wrapInQuotes d) + | None -> None ); + ("optional", Some (string_of_bool fieldDoc.optional)); + ("docstrings", Some (stringifyDocstrings fieldDoc.docstrings)); + ("signature", Some (wrapInQuotes fieldDoc.signature)); + ] + +let stringifyConstructorPayload ~indentation + (constructorPayload : constructorPayload) = + let open Protocol in + match constructorPayload with + | InlineRecord {fieldDocs} -> + stringifyObject ~indentation:(indentation + 1) + [ + ("kind", Some (wrapInQuotes "inlineRecord")); + ( "fields", + Some + (fieldDocs + |> List.map (stringifyFieldDoc ~indentation:(indentation + 1)) + |> array) ); + ] + let stringifyDetail ?(indentation = 0) (detail : docItemDetail) = let open Protocol in match detail with @@ -62,22 +94,8 @@ let stringifyDetail ?(indentation = 0) (detail : docItemDetail) = [ ("kind", Some (wrapInQuotes "record")); ( "items", - Some - (fieldDocs - |> List.map (fun fieldDoc -> - stringifyObject ~indentation:(indentation + 1) - [ - ("name", Some (wrapInQuotes fieldDoc.fieldName)); - ( "deprecated", - match fieldDoc.deprecated with - | Some d -> Some (wrapInQuotes d) - | None -> None ); - ("optional", Some (string_of_bool fieldDoc.optional)); - ( "docstrings", - Some (stringifyDocstrings fieldDoc.docstrings) ); - ("signature", Some (wrapInQuotes fieldDoc.signature)); - ]) - |> array) ); + Some (fieldDocs |> List.map (stringifyFieldDoc ~indentation) |> array) + ); ] | Variant {constructorDocs} -> stringifyObject ~startOnNewline:true ~indentation @@ -100,6 +118,14 @@ let stringifyDetail ?(indentation = 0) (detail : docItemDetail) = Some (stringifyDocstrings constructorDoc.docstrings) ); ( "signature", Some (wrapInQuotes constructorDoc.signature) ); + ( "payload", + match constructorDoc.items with + | None -> None + | Some constructorPayload -> + Some + (stringifyConstructorPayload + ~indentation:(indentation + 1) + constructorPayload) ); ]) |> array) ); ] @@ -145,6 +171,7 @@ let rec stringifyDocItem ?(indentation = 0) ~originalEnv (item : docItem) = ("id", Some (wrapInQuotes m.id)); ("name", Some (wrapInQuotes m.name)); ("kind", Some (wrapInQuotes "module")); + ("docstrings", Some (stringifyDocstrings m.docstring)); ( "items", Some (m.items @@ -185,24 +212,20 @@ and stringifyDocsForModule ?(indentation = 0) ~originalEnv (d : docsForModule) = |> array) ); ] +let fieldToFieldDoc (field : SharedTypes.field) : fieldDoc = + { + fieldName = field.fname.txt; + docstrings = field.docstring; + optional = field.optional; + signature = Shared.typeToString field.typ; + deprecated = field.deprecated; + } + let typeDetail typ ~env ~full = let open SharedTypes in match TypeUtils.extractTypeFromResolvedType ~env ~full typ with | Some (Trecord {fields}) -> - Some - (Record - { - fieldDocs = - fields - |> List.map (fun (field : field) -> - { - fieldName = field.fname.txt; - docstrings = field.docstring; - optional = field.optional; - signature = Shared.typeToString field.typ; - deprecated = field.deprecated; - }); - }) + Some (Record {fieldDocs = fields |> List.map fieldToFieldDoc}) | Some (Tvariant {constructors}) -> Some (Variant @@ -215,6 +238,13 @@ let typeDetail typ ~env ~full = docstrings = c.docstring; signature = CompletionBackEnd.showConstructor c; deprecated = c.deprecated; + items = + (match c.args with + | InlineRecord fields -> + Some + (InlineRecord + {fieldDocs = fields |> List.map fieldToFieldDoc}) + | _ -> None); }); }) | _ -> None @@ -312,7 +342,9 @@ let extractDocs ~path ~debug = id; name = item.name; items; - docstring = item.docstring @ internalDocstrings |> List.map String.trim; + docstring = + item.docstring @ internalDocstrings + |> List.map String.trim; }) | Module (Structure m) -> (* module Whatever = {} in res or module Whatever: {} in resi. *) diff --git a/analysis/tests/src/DocExtractionRes.res b/analysis/tests/src/DocExtractionRes.res index 582e3bcbe..9ccd611c7 100644 --- a/analysis/tests/src/DocExtractionRes.res +++ b/analysis/tests/src/DocExtractionRes.res @@ -49,7 +49,12 @@ module AnotherModule = { let isGoodStatus = (status: SomeInnerModule.status) => status == Stopped /** Trying how it looks with an inline record in a variant. */ - type someVariantWithInlineRecords = | /** This has inline records...*/ SomeStuff({offline: bool}) + type someVariantWithInlineRecords = + | /** This has inline records...*/ + SomeStuff({ + offline: bool, + /** Is the user online? */ online?: bool, + }) open ReactDOM diff --git a/analysis/tests/src/expected/DocExtraction2.res.txt b/analysis/tests/src/expected/DocExtraction2.res.txt index 7645cfac0..74f2e9cc1 100644 --- a/analysis/tests/src/expected/DocExtraction2.res.txt +++ b/analysis/tests/src/expected/DocExtraction2.res.txt @@ -24,6 +24,7 @@ preferring found resi file for impl: src/DocExtraction2.resi "id": "DocExtraction2.InnerModule", "name": "InnerModule", "kind": "module", + "docstrings": [], "items": [ { "id": "DocExtraction2.InnerModule.t", diff --git a/analysis/tests/src/expected/DocExtraction2.resi.txt b/analysis/tests/src/expected/DocExtraction2.resi.txt index 51d938f0c..074ce4ccf 100644 --- a/analysis/tests/src/expected/DocExtraction2.resi.txt +++ b/analysis/tests/src/expected/DocExtraction2.resi.txt @@ -23,6 +23,7 @@ extracting docs for src/DocExtraction2.resi "id": "DocExtraction2.InnerModule", "name": "InnerModule", "kind": "module", + "docstrings": [], "items": [ { "id": "DocExtraction2.InnerModule.t", diff --git a/analysis/tests/src/expected/DocExtractionRes.res.txt b/analysis/tests/src/expected/DocExtractionRes.res.txt index fdf2b3c0f..a197ecf56 100644 --- a/analysis/tests/src/expected/DocExtractionRes.res.txt +++ b/analysis/tests/src/expected/DocExtractionRes.res.txt @@ -52,6 +52,7 @@ extracting docs for src/DocExtractionRes.res "id": "DocExtractionRes.SomeInnerModule", "name": "SomeInnerModule", "kind": "module", + "docstrings": ["Another module level docstring here."], "items": [ { "id": "DocExtractionRes.SomeInnerModule.status", @@ -99,6 +100,7 @@ extracting docs for src/DocExtractionRes.res "id": "DocExtractionRes.AnotherModule", "name": "AnotherModule", "kind": "module", + "docstrings": ["Mighty fine module here too!"], "items": [ { "id": "DocExtractionRes.LinkedModule", @@ -125,7 +127,7 @@ extracting docs for src/DocExtractionRes.res "id": "DocExtractionRes.AnotherModule.someVariantWithInlineRecords", "kind": "type", "name": "someVariantWithInlineRecords", - "signature": "type someVariantWithInlineRecords =\n | SomeStuff({offline: bool})", + "signature": "type someVariantWithInlineRecords =\n | SomeStuff({offline: bool, online?: bool})", "docstrings": ["Trying how it looks with an inline record in a variant."], "detail": { @@ -134,7 +136,21 @@ extracting docs for src/DocExtractionRes.res { "name": "SomeStuff", "docstrings": ["This has inline records..."], - "signature": "SomeStuff" + "signature": "SomeStuff({offline: bool, online?: bool})", + "payload": { + "kind": "inlineRecord", + "fields": [{ + "name": "offline", + "optional": false, + "docstrings": [], + "signature": "bool" + }, { + "name": "online", + "optional": true, + "docstrings": ["Is the user online?"], + "signature": "option" + }] + } }] } }, @@ -150,6 +166,7 @@ extracting docs for src/DocExtractionRes.res "id": "DocExtractionRes.ModuleWithThingsThatShouldNotBeExported", "name": "ModuleWithThingsThatShouldNotBeExported", "kind": "module", + "docstrings": [], "items": [ { "id": "DocExtractionRes.ModuleWithThingsThatShouldNotBeExported.t", diff --git a/tools/CHANGELOG.md b/tools/CHANGELOG.md index 6e728b690..d9dcb0c17 100644 --- a/tools/CHANGELOG.md +++ b/tools/CHANGELOG.md @@ -14,6 +14,7 @@ #### :bug: Bug Fix +- Support inline record fields in constructors. https://github.com/rescript-lang/rescript-vscode/pull/889 - Fix docstrings for module alias. Get internal docstrings of module file. https://github.com/rescript-lang/rescript-vscode/pull/878 - Fix extracted docs of types include escaped linebreaks in signature. https://github.com/rescript-lang/rescript-vscode/pull/891 diff --git a/tools/src/Tools_Docgen.res b/tools/src/Tools_Docgen.res index a7e008d32..d62458d00 100644 --- a/tools/src/Tools_Docgen.res +++ b/tools/src/Tools_Docgen.res @@ -6,11 +6,15 @@ type field = { deprecated?: string, } +@tag("kind") +type constructorPayload = | @as("inlineRecord") InlineRecord({fields: array}) + type constructor = { name: string, docstrings: array, signature: string, deprecated?: string, + payload?: constructorPayload, } @tag("kind") @@ -54,159 +58,6 @@ type rec item = items: array, }) -let decodeDocstrings = item => { - open Js.Json - switch item->Js.Dict.get("docstrings") { - | Some(Array(arr)) => - arr->Js.Array2.map(s => - switch s { - | String(s) => s - | _ => assert(false) - } - ) - | _ => [] - } -} - -let decodeStringByField = (item, field) => { - open Js.Json - switch item->Js.Dict.get(field) { - | Some(String(s)) => s - | _ => assert(false) - } -} - -let decodeDepreacted = item => { - open Js.Json - switch item->Js.Dict.get("deprecated") { - | Some(String(s)) => Some(s) - | _ => None - } -} - -let decodeRecordFields = fields => { - open Js.Json - let items = fields->Js.Array2.map(field => { - switch field { - | Object(doc) => { - let name = doc->decodeStringByField("name") - let docstrings = doc->decodeDocstrings - let signature = doc->decodeStringByField("signature") - let deprecated = doc->decodeDepreacted - let optional = switch Js.Dict.get(doc, "optional") { - | Some(Boolean(bool)) => bool - | _ => assert(false) - } - - {name, docstrings, signature, optional, ?deprecated} - } - - | _ => assert(false) - } - }) - Record({items: items}) -} - -let decodeConstructorFields = fields => { - open Js.Json - let items = fields->Js.Array2.map(field => { - switch field { - | Object(doc) => { - let name = doc->decodeStringByField("name") - let docstrings = doc->decodeDocstrings - let signature = doc->decodeStringByField("signature") - let deprecated = doc->decodeDepreacted - - {name, docstrings, signature, ?deprecated} - } - - | _ => assert(false) - } - }) - Variant({items: items}) -} - -let decodeDetail = detail => { - open Js.Json - - switch detail { - | Object(detail) => - switch (detail->Js.Dict.get("kind"), detail->Js.Dict.get("items")) { - | (Some(String(kind)), Some(Array(items))) => - switch kind { - | "record" => decodeRecordFields(items) - | "variant" => decodeConstructorFields(items) - | _ => assert(false) - } - | _ => assert(false) - } - - | _ => assert(false) - } -} - -let rec decodeValue = item => { - let id = item->decodeStringByField("id") - let signature = item->decodeStringByField("signature") - let name = item->decodeStringByField("name") - let deprecated = item->decodeDepreacted - let docstrings = item->decodeDocstrings - Value({id, docstrings, signature, name, ?deprecated}) -} -and decodeType = item => { - let id = item->decodeStringByField("id") - let signature = item->decodeStringByField("signature") - let name = item->decodeStringByField("name") - let deprecated = item->decodeDepreacted - let docstrings = item->decodeDocstrings - let detail = switch item->Js_dict.get("detail") { - | Some(field) => decodeDetail(field)->Some - | None => None - } - Type({id, docstrings, signature, name, ?deprecated, ?detail}) -} -and decodeModuleAlias = item => { - open Js.Json - let id = item->decodeStringByField("id") - let name = item->decodeStringByField("name") - let docstrings = item->decodeDocstrings - let items = switch Js.Dict.get(item, "items") { - | Some(Array(items)) => items->Js.Array2.map(item => decodeItem(item)) - | _ => assert(false) - } - ModuleAlias({id, items, name, docstrings}) -} -and decodeModule = item => { - open Js.Json - let id = item->decodeStringByField("id") - let name = item->decodeStringByField("name") - let deprecated = item->decodeDepreacted - let docstrings = item->decodeDocstrings - let items = switch Js.Dict.get(item, "items") { - | Some(Array(items)) => items->Js.Array2.map(item => decodeItem(item)) - | _ => assert(false) - } - Module({id, name, docstrings, ?deprecated, items}) -} -and decodeItem = item => { - open Js.Json - switch item { - | Object(value) => - switch Js.Dict.get(value, "kind") { - | Some(String(kind)) => - switch kind { - | "type" => decodeType(value) - | "value" => decodeValue(value) - | "module" => decodeModule(value) - | "moduleAlias" => decodeModuleAlias(value) - | _ => assert(false) - } - | _ => assert(false) - } - | _ => assert(false) - } -} - type doc = { name: string, deprecated: option, @@ -217,22 +68,4 @@ type doc = { /** `decodeFromJson(json)` parse JSON generated from `restool doc` command */ -let decodeFromJson = json => { - open Js.Json - - switch json { - | Object(mod) => { - let name = mod->decodeStringByField("name") - let deprecated = mod->decodeDepreacted - let docstrings = mod->decodeDocstrings - let items = switch Js.Dict.get(mod, "items") { - | Some(Array(items)) => items->Js.Array2.map(item => decodeItem(item)) - | _ => assert(false) - } - - {name, deprecated, docstrings, items} - } - - | _ => assert(false) - } -} +external decodeFromJson: Js.Json.t => doc = "%identity" diff --git a/tools/src/Tools_Docgen.resi b/tools/src/Tools_Docgen.resi index 513471b59..f1c338a8a 100644 --- a/tools/src/Tools_Docgen.resi +++ b/tools/src/Tools_Docgen.resi @@ -5,11 +5,16 @@ type field = { optional: bool, deprecated?: string, } + +@tag("kind") +type constructorPayload = | @as("inlineRecord") InlineRecord({fields: array}) + type constructor = { name: string, docstrings: array, signature: string, deprecated?: string, + payload?: constructorPayload, } @tag("kind") type detail = @@ -54,9 +59,4 @@ type rec item = type doc = {name: string, deprecated: option, docstrings: array, items: array} -let decodeValue: Js.Dict.t => item -let decodeType: Js.Dict.t => item -let decodeModule: Js.Dict.t => item -let decodeModuleAlias: Js.Dict.t => item -let decodeItem: Js.Json.t => item let decodeFromJson: Js.Json.t => doc