diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4a88e1e3f..2e221e8d4 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,10 @@ # Changelog +## [Unreleased] + +### Fixed +* API docs keep the comment of a member whose signature has a type abbreviation of a function type in the range of a function type, such as `(string -> IsPathIgnored) * (unit -> int)` with `type IsPathIgnored = string -> bool`. Reading the member threw `ArgumentOutOfRangeException` and the comment was dropped. The abbreviation is now shown under its own name, as it is everywhere else. [#1327](https://github.com/fsprojects/FSharp.Formatting/issues/1327) + ## [23.0.0-alpha.5] - 2026-09-15 ### Added diff --git a/src/FSharp.Formatting.ApiDocs/TypeFormatter.fs b/src/FSharp.Formatting.ApiDocs/TypeFormatter.fs index f1392d732..a8cdfb055 100644 --- a/src/FSharp.Formatting.ApiDocs/TypeFormatter.fs +++ b/src/FSharp.Formatting.ApiDocs/TypeFormatter.fs @@ -219,7 +219,11 @@ module internal TypeFormatter = bracketHtmlIf (prec <= 2) (formatTypesWithPrecAsHtml ctx 2 " * " tyargs) | _ when typ.IsFunctionType -> let rec loop soFar (typ: FSharpType) = - if typ.IsFunctionType then + // IsFunctionType strips abbreviations, so an abbreviation of a function type answers + // true while its GenericArguments are its own type arguments, not a domain and a + // range. Only a bare function type is taken apart, the rest is formatted under its + // own name like the cases above do. + if typ.IsFunctionType && not typ.HasTypeDefinition then let domainTyp, retType = typ.GenericArguments.[0], typ.GenericArguments.[1] loop (soFar @ [ formatTypeWithPrecAsHtml ctx 4 domainTyp; !!" -> " ]) retType diff --git a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs index 2e13c647d..1e82b0e55 100644 --- a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs +++ b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs @@ -573,6 +573,36 @@ let ``ApiDocs reads comments of type abbreviations to tuples, lists and BCL type // An abbreviation declares no members; the target type's members must not leak in entity.AllMembers |> shouldEqual [] +[] +let ``ApiDocs reads comments of members returning a function-type abbreviation (issue 1327)`` () = + let libraries = [ testBin "FsLib2.dll" ] + let inputs = [ for lib in libraries -> ApiDocInput.FromFile(lib, mdcomments = false, warn = true) ] + + let model = + ApiDocs.GenerateModel(inputs, collectionName = "FsLib", substitutions = substitutions, libDirs = [ testBin ]) + + let entity = + model.Collection.Namespaces.[0].Entities + |> List.find (fun e -> e.Name = "FunctionTypeAbbreviations") + + let find name = + entity.AllMembers |> List.find (fun m -> m.Name = name) + + // Formatting the return type used to throw, which dropped the comment of the whole member + let makeLoader = find "makeLoader" + makeLoader.Comment.Summary.HtmlText |> shouldContainText "Returns a loader" + + let returnTypeHtml (m: ApiDocMember) = + m.ReturnInfo.ReturnType |> Option.map (fun (_, html) -> html.HtmlText) + + // The abbreviation is shown under its own name, not expanded into its target type + returnTypeHtml makeLoader |> Option.get |> shouldContainText "IsPathIgnored" + + // A generic abbreviation carries two type arguments, which are not a domain and a range + let lookupTransform = find "lookupTransform" + lookupTransform.Comment.Summary.HtmlText |> shouldContainText "Returns a lookup" + returnTypeHtml lookupTransform |> Option.get |> shouldContainText "Transform" + [] [] let ``ApiDocs renders inherited members section in output (issue 590)`` (format: OutputFormat) = diff --git a/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs b/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs index d1980a48e..f70f26de7 100644 --- a/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs +++ b/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs @@ -229,3 +229,18 @@ module Abbreviations = /// A file name type FileName = string + +/// Type abbreviations of function types used as the range of a function type (issue 1327) +module FunctionTypeAbbreviations = + /// An abbreviation for a function type + type IsPathIgnored = string -> bool + + /// A generic abbreviation for a function type + type Transform<'T, 'U> = 'T -> 'U + + /// Returns a loader for the given checker + let makeLoader (isIgnored: IsPathIgnored) : (string -> IsPathIgnored) * (unit -> int) = + (fun _ -> isIgnored), (fun () -> 0) + + /// Returns a lookup for the given transform + let lookupTransform (transform: Transform<'T, 'U>) : (string -> Transform<'T, 'U>) * int = (fun _ -> transform), 0