Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/FSharp.Formatting.ApiDocs/TypeFormatter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,11 @@ module internal TypeFormatter =
bracketHtmlIf (prec <= 2) (formatTypesWithPrecAsHtml ctx 2 "&#32;*&#32;" 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; !!"&#32;->&#32;" ]) retType
Expand Down
30 changes: 30 additions & 0 deletions tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 []

[<Test>]
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"

[<Test>]
[<TestCaseSource("formats")>]
let ``ApiDocs renders inherited members section in output (issue 590)`` (format: OutputFormat) =
Expand Down
15 changes: 15 additions & 0 deletions tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading