From ebc77bc5c827750aacd3b8e9d3529527342a71c7 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Wed, 9 Sep 2020 17:39:39 +0100 Subject: [PATCH 1/3] various fixes --- docs/apidocs.fsx | 23 ++++ src/FSharp.Formatting.ApiDocs/GenerateHtml.fs | 14 +++ .../GenerateModel.fs | 113 ++++++++++++------ tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs | 17 ++- .../files/crefLib2/Library2.fs | 43 ++++++- 5 files changed, 167 insertions(+), 43 deletions(-) diff --git a/docs/apidocs.fsx b/docs/apidocs.fsx index c44b73737..3a857c87f 100644 --- a/docs/apidocs.fsx +++ b/docs/apidocs.fsx @@ -107,6 +107,29 @@ type SomeType() = (** +Generic types are referred to by .NET compiled name, e.g. +*) +/// . + +(** +Like types, members are referred to by xml doc sig. These must currently be precise as the F# +compiler doesn't elaborate these references from simpler names: +*) + +type Class2() = + member this.Other = "more" + member this.OtherMethod0() = "more" + member this.OtherMethod1(c: string) = "more" + member this.OtherMethod2(c: string, o: obj) = "more" + +/// +/// and +/// and +/// and +let f () = "result" + +(* + ## Go to Source links 'fsdocs' normally automatically adds GitHub links to each functions, values and class members for further reference. diff --git a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs index 17b203e56..ff739dee6 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs @@ -205,6 +205,20 @@ type HtmlRender(model: ApiDocModel) = td [Class "fsdocs-xmldoc" ] [ p [] [yield! sourceLink e.SourceLocation embed e.Comment.Summary; ] + + match e.Comment.Remarks with + | Some r -> + p [Class "fsdocs-remarks"] [embed r] + | None -> () + + for e in e.Comment.Notes do + h5 [Class "fsdocs-note-header"] [!! "Note"] + p [Class "fsdocs-note"] [embed e] + + for e in e.Comment.Examples do + h5 [Class "fsdocs-example-header"] [!! "Example"] + p [Class "fsdocs-example"] [embed e] + ] ] ] diff --git a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs index 5c040e246..5a948bb76 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs @@ -340,9 +340,13 @@ type ApiDocMember (displayName: string, attributes: ApiDocAttribute list, entity member x.UrlBaseName = entityUrlBaseName /// The URL of the best link documentation for the item relative to "reference" directory (without the http://site.io/reference) - member x.Url(root, collectionName, qualify) = + static member GetUrl(entityUrlBaseName, displayName, root, collectionName, qualify) = sprintf "%sreference/%s%s.html#%s" root (if qualify then collectionName + "/" else "") entityUrlBaseName displayName + /// The URL of the best link documentation for the item relative to "reference" directory (without the http://site.io/reference) + member x.Url(root, collectionName, qualify) = + ApiDocMember.GetUrl(entityUrlBaseName, displayName, root, collectionName, qualify) + /// The declared attributes of the member member x.Attributes = attributes @@ -411,9 +415,13 @@ type ApiDocEntity /// The URL base name of the primary documentation for the entity (without the http://site.io/reference) member x.UrlBaseName : string = urlBaseName + /// Compute the URL of the best link for the entity relative to "reference" directory (without the http://site.io/reference) + static member GetUrl(urlBaseName, root, collectionName, qualify) = + sprintf "%sreference/%s%s.html" root (if qualify then collectionName + "/" else "") urlBaseName + /// The URL of the best link for the entity relative to "reference" directory (without the http://site.io/reference) member x.Url(root, collectionName, qualify) = - sprintf "%sreference/%s%s.html" root (if qualify then collectionName + "/" else "") urlBaseName + ApiDocEntity.GetUrl(urlBaseName, root, collectionName, qualify) /// The name of the file generated for this entity member x.OutputFile(collectionName, qualify) = @@ -501,7 +509,7 @@ type ApiDocNamespace(name: string, modifiers, substitutions: Substitutions, nsdo member x.Name : string = name /// The hash label for the URL with the overall namespaces file - member x.UrlHash = name.Replace(".", "-").ToLower() + member x.UrlHash = urlBaseName /// The base name for the generated file member x.UrlBaseName = urlBaseName @@ -623,8 +631,8 @@ type internal CrossReferenceResolver (root, collectionName, qualify) = |> Seq.distinctBy fst |> Seq.toList let usedNames = Dictionary<_, _>() - let registeredEntitiesToUrlBaseName = Dictionary() - let xmlDocNameToEntity = Dictionary() + let registeredSymbolsToUrlBaseName = Dictionary() + let xmlDocNameToSymbol = Dictionary() let niceNameEntityLookup = Dictionary<_, _>() let nameGen (name:string) = @@ -638,14 +646,21 @@ type internal CrossReferenceResolver (root, collectionName, qualify) = usedNames.Add(found, true) found + let registerMember (memb: FSharpMemberOrFunctionOrValue) = + let xmlsig = getXmlDocSigForMember memb + + if (not (System.String.IsNullOrEmpty xmlsig)) then + assert (xmlsig.StartsWith("M:") || xmlsig.StartsWith("P:") || xmlsig.StartsWith("F:") || xmlsig.StartsWith("E:")) + xmlDocNameToSymbol.[xmlsig] <- memb + let rec registerEntity (entity: FSharpEntity) = let newName = nameGen (sprintf "%s.%s" entity.AccessPath entity.CompiledName) - registeredEntitiesToUrlBaseName.[entity] <- newName + registeredSymbolsToUrlBaseName.[entity] <- newName let xmlsig = getXmlDocSigForType entity if (not (System.String.IsNullOrEmpty xmlsig)) then assert (xmlsig.StartsWith("T:")) - xmlDocNameToEntity.[xmlsig.Substring(2)] <- entity + xmlDocNameToSymbol.[xmlsig] <- entity if (not(niceNameEntityLookup.ContainsKey(entity.LogicalName))) then niceNameEntityLookup.[entity.LogicalName] <- System.Collections.Generic.List<_>() niceNameEntityLookup.[entity.LogicalName].Add(entity) @@ -653,8 +668,11 @@ type internal CrossReferenceResolver (root, collectionName, qualify) = for nested in entity.NestedEntities do registerEntity nested + for memb in entity.TryGetMembersFunctionsAndValues do + registerMember memb + let getUrlBaseNameForRegisteredEntity (entity:FSharpEntity) = - match registeredEntitiesToUrlBaseName.TryGetValue (entity) with + match registeredSymbolsToUrlBaseName.TryGetValue (entity) with | true, v -> v | _ -> failwithf "The entity %s was not registered before!" (sprintf "%s.%s" entity.AccessPath entity.CompiledName) @@ -696,10 +714,13 @@ type internal CrossReferenceResolver (root, collectionName, qualify) = sprintf "https://docs.microsoft.com/dotnet/api/%s" docs let internalCrossReference urlBaseName = - sprintf "%sreference/%s%s.html" root (if qualify then collectionName + "/" else "") urlBaseName + ApiDocEntity.GetUrl(urlBaseName, root, collectionName, qualify) + + let internalCrossReferenceForMember entityUrlBaseName (memb: FSharpMemberOrFunctionOrValue) = + ApiDocMember.GetUrl(entityUrlBaseName, memb.DisplayName, root, collectionName, qualify) - let tryResolveCrossReferenceForEntity entity = - match registeredEntitiesToUrlBaseName.TryGetValue (entity) with + let tryResolveCrossReferenceForEntity (entity: FSharpEntity) = + match registeredSymbolsToUrlBaseName.TryGetValue (entity) with | true, _v -> let urlBaseName = getUrlBaseNameForRegisteredEntity entity Some @@ -718,15 +739,17 @@ type internal CrossReferenceResolver (root, collectionName, qualify) = NiceName = simple HasModuleSuffix = false} - let resolveCrossReferenceForTypeByName typeName = - match xmlDocNameToEntity.TryGetValue(typeName) with - | true, entity -> + let resolveCrossReferenceForTypeByXmlSig (typeXmlSig: string) = + assert (typeXmlSig.StartsWith("T:")) + match xmlDocNameToSymbol.TryGetValue(typeXmlSig) with + | true, (:? FSharpEntity as entity) -> let urlBaseName = getUrlBaseNameForRegisteredEntity entity { IsInternal = true ReferenceLink = internalCrossReference urlBaseName - NiceName = entity.LogicalName + NiceName = entity.DisplayName HasModuleSuffix=entity.HasFSharpModuleSuffix } | _ -> + let typeName = typeXmlSig.Substring(2) match niceNameEntityLookup.TryGetValue(typeName) with | true, entities -> match Seq.toList entities with @@ -734,7 +757,7 @@ type internal CrossReferenceResolver (root, collectionName, qualify) = let urlBaseName = getUrlBaseNameForRegisteredEntity entity { IsInternal = true ReferenceLink = internalCrossReference urlBaseName - NiceName = entity.LogicalName + NiceName = entity.DisplayName HasModuleSuffix=entity.HasFSharpModuleSuffix } | _ -> failwith "unreachable" | _ -> @@ -745,6 +768,27 @@ type internal CrossReferenceResolver (root, collectionName, qualify) = NiceName = simple HasModuleSuffix = false} + let tryResolveCrossReferenceForMemberByXmlSig (memberXmlSig: string) = + assert (memberXmlSig.StartsWith("M:") || memberXmlSig.StartsWith("P:") || memberXmlSig.StartsWith("F:") || 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 + HasModuleSuffix=false } + |> 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 + match tryGetTypeFromMemberName memberName with + | Some typeName -> + let reference = resolveCrossReferenceForTypeByXmlSig ("T:" + typeName) + Some { reference with NiceName = getMemberName 2 reference.HasModuleSuffix memberName } + | None -> + Log.errorf "Assumed '%s' was a member but we cannot extract a type!" memberXmlSig + None + member _.ResolveCref (cref:string) = if (cref.Length < 2) then invalidArg "cref" (sprintf "the given cref: '%s' is invalid!" cref) let memberName = cref.Substring(2) @@ -752,7 +796,7 @@ type internal CrossReferenceResolver (root, collectionName, qualify) = match cref with // Type | _ when cref.StartsWith("T:") -> - let reference = resolveCrossReferenceForTypeByName memberName + let reference = resolveCrossReferenceForTypeByXmlSig cref // A reference to something in this component let simple = getMemberName 1 reference.HasModuleSuffix noParen Some { reference with NiceName = simple } @@ -761,18 +805,9 @@ type internal CrossReferenceResolver (root, collectionName, qualify) = | _ when cref.StartsWith("!:") -> Log.warnf "Compiler was unable to resolve %s" cref None - // ApiDocMember | _ when cref.[1] = ':' -> - match tryGetTypeFromMemberName memberName with - | Some typeName -> - let reference = resolveCrossReferenceForTypeByName typeName - // A reference to something in this component - let simple = getMemberName 2 reference.HasModuleSuffix noParen - Some { reference with NiceName = simple } - | None -> - Log.warnf "Assumed '%s' was a member but we cannot extract a type!" cref - None + tryResolveCrossReferenceForMemberByXmlSig cref // No idea | _ -> Log.warnf "Unresolved reference '%s'!" cref @@ -1403,7 +1438,7 @@ module internal SymbolReader = // not part of the XML doc standard let nsels = - let ds = doc.Descendants(XName.Get "namespacedoc") + let ds = doc.Elements(XName.Get "namespacedoc") if Seq.length ds > 0 then Some (Seq.toList ds) else @@ -1411,7 +1446,7 @@ module internal SymbolReader = let summary = if summaryExpected then - let summaries = doc.Descendants(XName.Get "summary") |> Seq.toList + let summaries = doc.Elements(XName.Get "summary") |> Seq.toList let html = new StringBuilder() for (id, e) in List.indexed summaries do let n = if id = 0 then "summary" else "summary-" + string id @@ -1423,7 +1458,7 @@ module internal SymbolReader = readXmlElementAsHtml false urlMap cmds html doc ApiDocHtml(html.ToString()) - let paramNodes = doc.Descendants(XName.Get "param") |> Seq.toList + let paramNodes = doc.Elements(XName.Get "param") |> Seq.toList let parameters = [ for e in paramNodes do let paramName = e.Attribute(XName.Get "name").Value @@ -1432,13 +1467,13 @@ module internal SymbolReader = let paramHtml = ApiDocHtml(phtml.ToString()) paramName, paramHtml ] - for e in doc.Descendants(XName.Get "exclude") do + for e in doc.Elements(XName.Get "exclude") do cmds.["exclude"] <- e.Value - for e in doc.Descendants(XName.Get "omit") do + for e in doc.Elements(XName.Get "omit") do cmds.["omit"] <- e.Value - for e in doc.Descendants(XName.Get "category") do + for e in doc.Elements(XName.Get "category") do match e.Attribute(XName.Get "index") with | null -> () | a -> @@ -1446,7 +1481,7 @@ module internal SymbolReader = cmds.["category"] <- e.Value let remarks = - let remarkNodes = doc.Descendants(XName.Get "remarks") |> Seq.toList + let remarkNodes = doc.Elements(XName.Get "remarks") |> Seq.toList if Seq.length remarkNodes > 0 then let html = new StringBuilder() for (id, e) in List.indexed remarkNodes do @@ -1459,7 +1494,7 @@ module internal SymbolReader = let returns = let html = new StringBuilder() - let returnNodes = doc.Descendants(XName.Get "returns") |> Seq.toList + let returnNodes = doc.Elements(XName.Get "returns") |> Seq.toList if returnNodes.Length > 0 then for (id, e) in List.indexed returnNodes do let n = if id = 0 then "returns" else "returns-" + string id @@ -1470,7 +1505,7 @@ module internal SymbolReader = None let exceptions = - let exceptionNodes = doc.Descendants(XName.Get "exception") |> Seq.toList + let exceptionNodes = doc.Elements(XName.Get "exception") |> Seq.toList [ for e in exceptionNodes do let cref = e.Attribute(XName.Get "cref") if cref <> null then @@ -1495,7 +1530,7 @@ module internal SymbolReader = ] let examples = - let exampleNodes = doc.Descendants(XName.Get "example") |> Seq.toList + let exampleNodes = doc.Elements(XName.Get "example") |> Seq.toList [ for (id, e) in List.indexed exampleNodes do let html = new StringBuilder() let n = if id = 0 then "example" else "example-" + string id @@ -1504,7 +1539,7 @@ module internal SymbolReader = ApiDocHtml(html.ToString()) ] let notes = - let noteNodes = doc.Descendants(XName.Get "note") |> Seq.toList + let noteNodes = doc.Elements(XName.Get "note") |> Seq.toList // 'note' is not part of the XML doc standard but is supported by Sandcastle and other tools [ for (id, e) in List.indexed noteNodes do let html = new StringBuilder() @@ -1840,7 +1875,7 @@ module internal SymbolReader = // so we need to add them to the XmlMemberMap separately let registerTypeProviderXmlDocs (ctx:ReadingContext) (typ:FSharpEntity) = let xmlDoc = registerXmlDoc ctx typ.XmlDocSig (String.concat "" typ.XmlDoc) - xmlDoc.Descendants(XName.Get "param") + xmlDoc.Elements(XName.Get "param") |> Seq.choose (fun p -> let nameAttr = p.Attribute(XName.Get "name") if nameAttr = null then None diff --git a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs index 2ffab7cb2..06998f7fe 100644 --- a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs +++ b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs @@ -249,15 +249,30 @@ let ``ApiDocs test that cref generation works``() = files.["creflib4-class2.html"] |> shouldContainText "creflib1-class1.html" /// + no crash on unresolved reference. files.["creflib4-class2.html"] |> shouldContainText "Unknown__Reference" + /// reference to a member works. files.["creflib4-class3.html"] |> shouldContainText "Class2.Other" files.["creflib4-class3.html"] |> shouldContainText "creflib4-class2.html" + /// references to members work and give correct links + files.["creflib2-class3.html"] |> shouldContainText """Class2.Other""" + files.["creflib2-class3.html"] |> shouldContainText """and Class2.Method0""" + files.["creflib2-class3.html"] |> shouldContainText """and Class2.Method1""" + files.["creflib2-class3.html"] |> shouldContainText """and Class2.Method2""" + + files.["creflib2-class3.html"] |> shouldContainText """and GenericClass2""" + files.["creflib2-class3.html"] |> shouldContainText """and GenericClass2.Property""" + files.["creflib2-class3.html"] |> shouldContainText """and GenericClass2.NonGenericMethod""" + files.["creflib2-class3.html"] |> shouldContainText """and GenericClass2.GenericMethod""" + + /// references to non-existent members where the type resolves give an approximation + files.["creflib2-class3.html"] |> shouldContainText """and Class2.NotExistsProperty""" + files.["creflib2-class3.html"] |> shouldContainText """and Class2.NotExistsMethod""" + /// reference to a corelib class works. files.["creflib4-class4.html"] |> shouldContainText "Assembly" files.["creflib4-class4.html"] |> shouldContainText "https://docs.microsoft.com/dotnet/api/system.reflection.assembly" - // F# tests (at least we not not crash for them, compiler doesn't resolve anything) // reference class in same assembly files.["creflib2-class1.html"] |> shouldContainText "Class2" diff --git a/tests/FSharp.ApiDocs.Tests/files/crefLib2/Library2.fs b/tests/FSharp.ApiDocs.Tests/files/crefLib2/Library2.fs index dbd6620c1..f5d75d9aa 100644 --- a/tests/FSharp.ApiDocs.Tests/files/crefLib2/Library2.fs +++ b/tests/FSharp.ApiDocs.Tests/files/crefLib2/Library2.fs @@ -1,4 +1,4 @@ -namespace crefLib2 +namespace crefLib2 /// /// @@ -10,7 +10,7 @@ type Class1() = member this.X = "F#" /// -/// +/// /// type Class2() = /// @@ -18,6 +18,32 @@ type Class2() = /// member this.Other = "more" + /// + /// This is a method in Class2 called Method0 + /// + member this.Method0() = "more" + + /// + /// This is a method in Class2 called Method1 + /// + member this.Method1(_c: string) = "more" + + /// + /// This is a method in Class2 called Method2 + /// + member this.Method2(_c: string, _o: obj) = "more" + + +type GenericClass2<'T>() = + /// This is a property in GenericClass2 called Property + member this.Property = "more" + + /// This is a method in GenericClass2 called NonGenericMethod + member this.NonGenericMethod(_c: 'T) = "more" + + /// This is a method in GenericClass2 called GenericMethod + member this.GenericMethod(_c: 'T, _o: 'U) = "more" + /// /// Test @@ -25,6 +51,17 @@ type Class2() = type Class3() = /// /// + /// and + /// and + /// and + /// and + /// and + /// and + /// and + /// and + /// and + /// and + /// and /// member this.X = "F#" @@ -33,7 +70,7 @@ type Class3() = /// type Class4() = /// - /// + /// /// member this.X = "F#" From 787de2933d0bd00f04e4d81a7b44c876f29d485c Mon Sep 17 00:00:00 2001 From: Don Syme Date: Wed, 9 Sep 2020 17:42:54 +0100 Subject: [PATCH 2/3] various fixes --- docs/apidocs.fsx | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/docs/apidocs.fsx b/docs/apidocs.fsx index 3a857c87f..ae9a13744 100644 --- a/docs/apidocs.fsx +++ b/docs/apidocs.fsx @@ -107,26 +107,38 @@ type SomeType() = (** -Generic types are referred to by .NET compiled name, e.g. -*) -/// . - -(** Like types, members are referred to by xml doc sig. These must currently be precise as the F# compiler doesn't elaborate these references from simpler names: *) type Class2() = - member this.Other = "more" - member this.OtherMethod0() = "more" - member this.OtherMethod1(c: string) = "more" - member this.OtherMethod2(c: string, o: obj) = "more" - -/// -/// and -/// and -/// and -let f () = "result" + member this.Property = "more" + member this.Method0() = "more" + member this.Method1(c: string) = "more" + member this.Method2(c: string, o: obj) = "more" + +/// +/// and +/// and +/// and +let referringFunction1 () = "result" + +(** +Generic types are referred to by .NET compiled name, e.g. +*) + +type GenericClass2<'T>() = + member this.Property = "more" + + member this.NonGenericMethod(_c: 'T) = "more" + + member this.GenericMethod(_c: 'T, _o: 'U) = "more" + +/// See +/// and +/// and +/// and +let referringFunction2 () = "result" (* From 32b959f0f77e73a6ffa34e120497dfe4798524db Mon Sep 17 00:00:00 2001 From: Don Syme Date: Wed, 9 Sep 2020 17:45:13 +0100 Subject: [PATCH 3/3] bump version --- RELEASE_NOTES.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e23ec44f5..0fc516cb3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,11 @@ +## 7.2.7 + +* [ApiDocs: examples not showing for types and modules](https://github.com/fsprojects/FSharp.Formatting/issues/599) + +* [ApiDocs: cref to members are not resolving to best possible link](https://github.com/fsprojects/FSharp.Formatting/issues/598) + +* [ApiDocs: namespace docs are showing in module/type summaries as well](https://github.com/fsprojects/FSharp.Formatting/issues/597) + ## 7.2.6 - In ApiDocsModel, separate out the parameter, summary, remarks sections etc.