From 4ea3b8890b0bb85ad227546d16d81e37db8fb9d3 Mon Sep 17 00:00:00 2001 From: David Dawkins Date: Sun, 15 Jan 2023 15:45:07 +0000 Subject: [PATCH 1/4] Documentation and tests for when cref omits arguments for a member (issue #789) --- docs/apidocs.fsx | 38 ++++++++++++++ .../GenerateModel.fs | 51 ++++++++++++++++--- tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs | 16 ++++++ .../files/TestLib3/SeeAlso.fs | 20 ++++++++ .../files/TestLib3/TestLib3.fsproj | 1 + 5 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 tests/FSharp.ApiDocs.Tests/files/TestLib3/SeeAlso.fs diff --git a/docs/apidocs.fsx b/docs/apidocs.fsx index cb1eba3dd..8894523d7 100644 --- a/docs/apidocs.fsx +++ b/docs/apidocs.fsx @@ -169,6 +169,44 @@ type GenericClass2<'T>() = /// and let referringFunction2 () = "result" +(** +### Cross-referencing with <seealso> + +Use `` within `` to create cross-references. + +For example: +*) + +module Forest = + + /// + /// Find at most limit foxes in current forest + /// + /// See also: + /// + let findFoxes (limit : int) = [] + + /// + /// Find at most limit squirrels in current forest + /// + /// See also: + /// + let findSquirrels (limit : int) = [] + + +(** You can find the correct value for `cref` in the generated `.xml` documentation file (this will be generated alongside the assembly's `.dll``). + +You can also omit the `cref`'s arguments, and `fsdocs` will make an attempt to find the first member that matches. + +For example: +``` + /// See also: +``` + +If the member cannot be found, a link to the containing module/type will be used instead. +*) + + (** ### Classic XMl Doc Comments: Excluding APIs from the docs diff --git a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs index 61fdaa015..1fa4f81f9 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs @@ -858,6 +858,15 @@ type internal CrossReferenceResolver(root, collectionName, qualify, extensions) else None + let tryGetShortMemberNameFromMemberName (memberName: string) = + let sub = removeParen memberName + let lastPeriod = sub.LastIndexOf(".") + + if lastPeriod > 0 then + Some(memberName.Substring(lastPeriod + 1)) + else + None + let getMemberName keepParts hasModuleSuffix (memberNameNoParen: string) = let splits = memberNameNoParen.Split('.') |> Array.toList @@ -992,6 +1001,32 @@ type internal CrossReferenceResolver(root, collectionName, qualify, extensions) let simple = getMemberName 1 false typeName externalDocsLink false simple typeName typeName + // If there's a quicker way to search an FSharpEntity for members then this linear + // search can disappear - see usage below in tryResolveCrossReferenceForMemberByXmlSig + // It's like writing C#... + let findIList (list: IList<'T>) (p: ('T -> bool)) = + let n = list.Count + let mutable i = 0 + let mutable result: 'T option = None + + while i < n do + let item = list.[i] + + if p item then + i <- n + result <- Some item + + i <- i + 1 + + result + + let mfvToCref (mfv: FSharpMemberOrFunctionOrValue) = + let entityUrlBaseName = getUrlBaseNameForRegisteredEntity mfv.DeclaringEntity.Value + + { IsInternal = true + ReferenceLink = internalCrossReferenceForMember entityUrlBaseName mfv + NiceName = mfv.DeclaringEntity.Value.DisplayName + "." + mfv.DisplayName } + let tryResolveCrossReferenceForMemberByXmlSig (memberXmlSig: string) = assert (memberXmlSig.StartsWith("M:") @@ -1000,13 +1035,7 @@ type internal CrossReferenceResolver(root, collectionName, qualify, extensions) || memberXmlSig.StartsWith("E:")) match xmlDocNameToSymbol.TryGetValue(memberXmlSig) with - | true, (:? FSharpMemberOrFunctionOrValue as memb) when memb.DeclaringEntity.IsSome -> - let entityUrlBaseName = getUrlBaseNameForRegisteredEntity memb.DeclaringEntity.Value - - { IsInternal = true - ReferenceLink = internalCrossReferenceForMember entityUrlBaseName memb - NiceName = memb.DeclaringEntity.Value.DisplayName + "." + memb.DisplayName } - |> Some + | true, (:? FSharpMemberOrFunctionOrValue as memb) when memb.DeclaringEntity.IsSome -> memb |> mfvToCref |> Some | _ -> // If we can't find the exact symbol for the member, don't despair, look for the type let memberName = memberXmlSig.Substring(2) |> removeParen @@ -1019,10 +1048,16 @@ type internal CrossReferenceResolver(root, collectionName, qualify, extensions) | true, (:? FSharpEntity as entity) -> let urlBaseName = getUrlBaseNameForRegisteredEntity entity - Some + // See if we find the member that was intended, otherwise default to containing entity + tryGetShortMemberNameFromMemberName memberName + |> Option.bind (fun shortName -> + findIList (entity.MembersFunctionsAndValues) (fun mfv -> mfv.DisplayName = shortName)) + |> Option.map mfvToCref + |> Option.defaultValue { IsInternal = true ReferenceLink = internalCrossReference urlBaseName NiceName = getMemberName 2 entity.HasFSharpModuleSuffix memberName } + |> Some | _ -> // A reference to something external, currently assumed to be in .NET let simple = getMemberName 2 false memberName diff --git a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs index 280e02652..1fff95f02 100644 --- a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs +++ b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs @@ -127,6 +127,22 @@ let generateApiDocs (libraries: string list) (format: OutputFormat) useMdComment do FSharp.Formatting.TestHelpers.enableLogging () +[] +[] +let ``ApiDocs seealso can find members`` (format: OutputFormat) = + let library = testBin "TestLib3.dll" |> fullpath + + let files = generateApiDocs [ library ] format false "TestLib3" + + let (textA, textB) = + if format = OutputFormat.Html then + "seealso.html#disposeOnUnmount", "seealso.html#unsubscribeOnUnmount" + else + "seealso#disposeOnUnmount", "seealso#unsubscribeOnUnmount" + + files.[(sprintf "test-seealso.%s" format.Extension)] |> shouldContainText textA + + files.[(sprintf "test-seealso.%s" format.Extension)] |> shouldContainText textB [] [] diff --git a/tests/FSharp.ApiDocs.Tests/files/TestLib3/SeeAlso.fs b/tests/FSharp.ApiDocs.Tests/files/TestLib3/SeeAlso.fs new file mode 100644 index 000000000..ac5bdb072 --- /dev/null +++ b/tests/FSharp.ApiDocs.Tests/files/TestLib3/SeeAlso.fs @@ -0,0 +1,20 @@ +/// +/// DOM low-level helper functions +/// +module Test.SeeAlso + +/// +/// Dispose all given items when the parent SutilElement is unmounted. Each item should implement System.IDisposable. +/// +/// See also: +/// +let disposeOnUnmount (ds : System.IDisposable list) = + ignore ds + +/// +/// Call each function of type `(unit -> unit)` when the element is unmounted +/// +/// See also: +/// +let unsubscribeOnUnmount (ds : (unit->unit) list) = + ignore ds diff --git a/tests/FSharp.ApiDocs.Tests/files/TestLib3/TestLib3.fsproj b/tests/FSharp.ApiDocs.Tests/files/TestLib3/TestLib3.fsproj index 9cfdaae8f..66bd10ad0 100644 --- a/tests/FSharp.ApiDocs.Tests/files/TestLib3/TestLib3.fsproj +++ b/tests/FSharp.ApiDocs.Tests/files/TestLib3/TestLib3.fsproj @@ -5,6 +5,7 @@ ..\bin\$(Configuration) + From 0ae2a0906963a21307c29455d9782118b6323fae Mon Sep 17 00:00:00 2001 From: David Dawkins Date: Sun, 15 Jan 2023 15:49:42 +0000 Subject: [PATCH 2/4] Formatting --- tests/FSharp.ApiDocs.Tests/files/TestLib3/SeeAlso.fs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/FSharp.ApiDocs.Tests/files/TestLib3/SeeAlso.fs b/tests/FSharp.ApiDocs.Tests/files/TestLib3/SeeAlso.fs index ac5bdb072..f707e43e0 100644 --- a/tests/FSharp.ApiDocs.Tests/files/TestLib3/SeeAlso.fs +++ b/tests/FSharp.ApiDocs.Tests/files/TestLib3/SeeAlso.fs @@ -8,13 +8,11 @@ module Test.SeeAlso /// /// See also: /// -let disposeOnUnmount (ds : System.IDisposable list) = - ignore ds +let disposeOnUnmount (ds: System.IDisposable list) = ignore ds /// /// Call each function of type `(unit -> unit)` when the element is unmounted /// /// See also: /// -let unsubscribeOnUnmount (ds : (unit->unit) list) = - ignore ds +let unsubscribeOnUnmount (ds: (unit -> unit) list) = ignore ds From 1389ee7f16bc4ec2fd120f1c8c487fd1f93be7b9 Mon Sep 17 00:00:00 2001 From: David Dawkins Date: Mon, 16 Jan 2023 00:08:36 +0000 Subject: [PATCH 3/4] Release notes 17.2.2 --- RELEASE_NOTES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d94b8a827..f59b7b34d 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,7 @@ +## 17.2.2 + +* Improvement for `` [#789](https://github.com/fsprojects/FSharp.Formatting/issues/789) + ## 17.2.1 * Fix support for `` [#786](https://github.com/fsprojects/FSharp.Formatting/issues/786) From 8706dc8a562d615e31f4534d23e820898e27f0ba Mon Sep 17 00:00:00 2001 From: David Dawkins Date: Mon, 16 Jan 2023 09:28:54 +0000 Subject: [PATCH 4/4] Remove findIList Refactor Option.defaultValue to explicit match --- .../GenerateModel.fs | 36 ++++++------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs index 1fa4f81f9..6a4de3f14 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs @@ -1001,25 +1001,6 @@ type internal CrossReferenceResolver(root, collectionName, qualify, extensions) let simple = getMemberName 1 false typeName externalDocsLink false simple typeName typeName - // If there's a quicker way to search an FSharpEntity for members then this linear - // search can disappear - see usage below in tryResolveCrossReferenceForMemberByXmlSig - // It's like writing C#... - let findIList (list: IList<'T>) (p: ('T -> bool)) = - let n = list.Count - let mutable i = 0 - let mutable result: 'T option = None - - while i < n do - let item = list.[i] - - if p item then - i <- n - result <- Some item - - i <- i + 1 - - result - let mfvToCref (mfv: FSharpMemberOrFunctionOrValue) = let entityUrlBaseName = getUrlBaseNameForRegisteredEntity mfv.DeclaringEntity.Value @@ -1051,13 +1032,16 @@ type internal CrossReferenceResolver(root, collectionName, qualify, extensions) // See if we find the member that was intended, otherwise default to containing entity tryGetShortMemberNameFromMemberName memberName |> Option.bind (fun shortName -> - findIList (entity.MembersFunctionsAndValues) (fun mfv -> mfv.DisplayName = shortName)) - |> Option.map mfvToCref - |> Option.defaultValue - { IsInternal = true - ReferenceLink = internalCrossReference urlBaseName - NiceName = getMemberName 2 entity.HasFSharpModuleSuffix memberName } - |> Some + entity.MembersFunctionsAndValues + |> Seq.tryFind (fun mfv -> mfv.DisplayName = shortName)) + |> function + | Some mb -> Some(mfvToCref mb) + | None -> + Some + { IsInternal = true + ReferenceLink = internalCrossReference urlBaseName + NiceName = getMemberName 2 entity.HasFSharpModuleSuffix memberName } + | _ -> // A reference to something external, currently assumed to be in .NET let simple = getMemberName 2 false memberName