From 30ba11f32fcd44ecbb41a6b40a3c1d1b11c3d505 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Thu, 10 Sep 2026 13:33:08 +0200 Subject: [PATCH] Keep comments of type abbreviations to tuples, lists and BCL types The compiler reports the members of the abbreviated type as members of the abbreviation itself, e.g. Item1 and Item2 for `type P = int * int`. Those members were registered in the cross-reference resolver and read as members of the abbreviation. With warn-on-missing-docs on, looking up such a member hit its declaring entity (System.Tuple, FSharpList, System.String), which is never registered, and the resolver threw "The entity ... was not registered before". The exception escaped the whole entity read, so the abbreviation's own comment was dropped. An abbreviation declares no members, so the symbol reader no longer lists the target type's members on its page, and the resolver no longer registers them. Resolving a member whose declaring entity is not in the documentation set now falls back to an external link instead of throwing. Fixes #1314 --- RELEASE_NOTES.md | 1 + .../CrossReferenceResolver.fs | 28 +++++++++++------ src/FSharp.Formatting.ApiDocs/SymbolReader.fs | 7 ++++- tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs | 31 +++++++++++++++++++ .../files/FsLib2/Library2.fs | 11 +++++++ 5 files changed, 68 insertions(+), 10 deletions(-) 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