diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index abac577c5..3888e1c05 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -5,6 +5,7 @@ ### Fixed * Fix tooltip not being interactive: moving the mouse from a code token into its tooltip now keeps the tooltip open, allowing users to select and copy the tooltip text. [#949](https://github.com/fsprojects/FSharp.Formatting/issues/949) * `fsdocs watch` rebuilds a page when a file its script depends on changes, following `#load` transitively and `#r` to local files, wherever those files are (a dot folder, outside the input folder). The directives are read from the syntax tree, so a `#load` in a comment does not count. [#1309](https://github.com/fsprojects/FSharp.Formatting/issues/1309) +* API docs keep the comment of a type abbreviation whose target is a tuple, a list or a BCL type such as `string`, instead of dropping it with "The entity ... was not registered before". An abbreviation page no longer lists the members of the target type (`Item1`, `Item2`, ...) as its own. [#1314](https://github.com/fsprojects/FSharp.Formatting/issues/1314) ## [23.0.0-alpha.1] - 2026-09-08 diff --git a/src/FSharp.Formatting.ApiDocs/CrossReferenceResolver.fs b/src/FSharp.Formatting.ApiDocs/CrossReferenceResolver.fs index d259e3e84..85fe2f3f4 100644 --- a/src/FSharp.Formatting.ApiDocs/CrossReferenceResolver.fs +++ b/src/FSharp.Formatting.ApiDocs/CrossReferenceResolver.fs @@ -209,8 +209,12 @@ type internal CrossReferenceResolver(root, collectionName, qualify, extensions) for nested in entity.NestedEntities do registerEntity nested - for memb in entity.TryGetMembersFunctionsAndValues() do - registerMember memb + // A type abbreviation has no members of its own: the compiler reports the members of the + // abbreviated type (e.g. System.Tuple.Item1), whose declaring entity is not part of this + // documentation set. Registering them would make them look local. + if not entity.IsFSharpAbbreviation then + for memb in entity.TryGetMembersFunctionsAndValues() do + registerMember memb /// Returns the previously-assigned URL base name for a registered entity, /// raising an exception if the entity has not been registered. @@ -473,13 +477,19 @@ type internal CrossReferenceResolver(root, collectionName, qualify, extensions) match mfv.DeclaringEntity with | None -> failwith $"%s{mfv.DisplayName} does not have a DeclaringEntity" | Some declaringEntity -> - let entityUrlBaseName = getUrlBaseNameForRegisteredEntity declaringEntity - - { - IsInternal = true - ReferenceLink = internalCrossReferenceForMember entityUrlBaseName mfv - NiceName = declaringEntity.DisplayName + "." + mfv.DisplayName - } + match registeredSymbolsToUrlBaseName.TryGetValue(declaringEntity) with + | true, entityUrlBaseName -> + { + IsInternal = true + ReferenceLink = internalCrossReferenceForMember entityUrlBaseName mfv + NiceName = declaringEntity.DisplayName + "." + mfv.DisplayName + } + | _ -> + // The declaring entity is not part of this documentation set (e.g. a member of + // System.Tuple reached through a type abbreviation), so link externally instead. + let typeName = defaultArg declaringEntity.TryFullName declaringEntity.DisplayName + let memberName = typeName + "." + mfv.DisplayName + externalDocsLink true (declaringEntity.DisplayName + "." + mfv.DisplayName) typeName memberName /// Tries to resolve a cross-reference for a member given its XML doc signature /// (must start with "M:", "P:", "F:", or "E:"). diff --git a/src/FSharp.Formatting.ApiDocs/SymbolReader.fs b/src/FSharp.Formatting.ApiDocs/SymbolReader.fs index 019226f8e..655d9e7ae 100644 --- a/src/FSharp.Formatting.ApiDocs/SymbolReader.fs +++ b/src/FSharp.Formatting.ApiDocs/SymbolReader.fs @@ -1014,9 +1014,14 @@ module internal SymbolReader = readCommentsInto typ ctx xmlDocSig (fun cat catidx exclude _cmds comment -> let entityUrl = ctx.UrlMap.ResolveUrlBaseNameForEntity typ + // A type abbreviation declares no members of its own. The compiler reports the + // members of the abbreviated type (e.g. Item1/Item2 for a tuple), which belong to + // and are documented on that target type, so they must not be listed here. let rec getMembers (typ: FSharpEntity) = [ - yield! typ.MembersFunctionsAndValues + if not typ.IsFSharpAbbreviation then + yield! typ.MembersFunctionsAndValues + match typ.BaseType with | Some baseType -> let loc = typ.DeclarationLocation diff --git a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs index 05ba18177..f7ac24bbc 100644 --- a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs +++ b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs @@ -519,6 +519,37 @@ let ``ApiDocs InheritedMembers is populated for derived types (issue 590)`` () = memberNames |> shouldContain "BaseMethod" memberNames |> shouldContain "BaseStaticMethod" +[] +let ``ApiDocs reads comments of type abbreviations to tuples, lists and BCL types (issue 1314)`` () = + let libraries = [ testBin "FsLib2.dll" ] + + // warn=true is what triggers the cross-reference lookup that used to throw + 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 abbreviations = + model.Collection.Namespaces.[0].Entities + |> List.find (fun e -> e.Name = "Abbreviations") + + let find name = + abbreviations.NestedEntities |> List.find (fun e -> e.Name = name) + + let expectations = + [ + "Position01", "A position as a zero-based line and column" + "LongIdent", "A long identifier" + "FileName", "A file name" + ] + + for name, summary in expectations do + let entity = find name + entity.Comment.Summary.HtmlText |> shouldContainText summary + entity.AbbreviatedType.IsSome |> shouldEqual true + // An abbreviation declares no members; the target type's members must not leak in + entity.AllMembers |> shouldEqual [] + [] [] 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 5e2cc884b..d1980a48e 100644 --- a/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs +++ b/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs @@ -218,3 +218,14 @@ module TypeConstraintTests = member _.Value = value /// Compares with another wrapper member x.CompareTo(other: ComparisonWrapper<'T>) = compare x.Value other.Value + +/// Type abbreviations whose target is a tuple, a list or a BCL type (issue 1314) +module Abbreviations = + /// A position as a zero-based line and column + type Position01 = int * int + + /// A long identifier + type LongIdent = string list + + /// A file name + type FileName = string