Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

various fixes #600

Merged
merged 3 commits into from
Sep 9, 2020
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
8 changes: 8 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
35 changes: 35 additions & 0 deletions docs/apidocs.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,41 @@ type SomeType() =

(**

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.Property = "more"
member this.Method0() = "more"
member this.Method1(c: string) = "more"
member this.Method2(c: string, o: obj) = "more"

/// <see cref="P:TheNamespace.Class2.Property" />
/// and <see cref="M:TheNamespace.Class2.OtherMethod0" />
/// and <see cref="M:TheNamespace.Class2.Method1(System.String)" />
/// and <see cref="M:TheNamespace.Class2.Method2(System.String,System.Object)" />
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 <see cref="T:TheNamespace.GenericClass2`1" />
/// and <see cref="P:TheNamespace.GenericClass2`1.Property" />
/// and <see cref="M:TheNamespace.GenericClass2`1.NonGenericMethod(`0)" />
/// and <see cref="M:TheNamespace.GenericClass2`1.GenericMethod``1(`0,``0)" />
let referringFunction2 () = "result"

(*

## Go to Source links

'fsdocs' normally automatically adds GitHub links to each functions, values and class members for further reference.
Expand Down
14 changes: 14 additions & 0 deletions src/FSharp.Formatting.ApiDocs/GenerateHtml.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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]

]
]
]
Expand Down
113 changes: 74 additions & 39 deletions src/FSharp.Formatting.ApiDocs/GenerateModel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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) =
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -623,8 +631,8 @@ type internal CrossReferenceResolver (root, collectionName, qualify) =
|> Seq.distinctBy fst
|> Seq.toList
let usedNames = Dictionary<_, _>()
let registeredEntitiesToUrlBaseName = Dictionary<FSharpEntity, string>()
let xmlDocNameToEntity = Dictionary<string, FSharpEntity>()
let registeredSymbolsToUrlBaseName = Dictionary<FSharpSymbol, string>()
let xmlDocNameToSymbol = Dictionary<string, FSharpSymbol>()
let niceNameEntityLookup = Dictionary<_, _>()

let nameGen (name:string) =
Expand All @@ -638,23 +646,33 @@ 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)

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)

Expand Down Expand Up @@ -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
Expand All @@ -718,23 +739,25 @@ 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
| entity :: _rest ->
let urlBaseName = getUrlBaseNameForRegisteredEntity entity
{ IsInternal = true
ReferenceLink = internalCrossReference urlBaseName
NiceName = entity.LogicalName
NiceName = entity.DisplayName
HasModuleSuffix=entity.HasFSharpModuleSuffix }
| _ -> failwith "unreachable"
| _ ->
Expand All @@ -745,14 +768,35 @@ 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)
let noParen = removeParen memberName
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 }
Expand All @@ -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
Expand Down Expand Up @@ -1403,15 +1438,15 @@ 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
None

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
Expand All @@ -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
Expand All @@ -1432,21 +1467,21 @@ 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 ->
cmds.["categoryindex"] <- a.Value
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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 """<a href="/reference/creflib2-class2.html#Other">Class2.Other</a>"""
files.["creflib2-class3.html"] |> shouldContainText """and <a href="/reference/creflib2-class2.html#Method0">Class2.Method0</a>"""
files.["creflib2-class3.html"] |> shouldContainText """and <a href="/reference/creflib2-class2.html#Method1">Class2.Method1</a>"""
files.["creflib2-class3.html"] |> shouldContainText """and <a href="/reference/creflib2-class2.html#Method2">Class2.Method2</a>"""

files.["creflib2-class3.html"] |> shouldContainText """and <a href="/reference/creflib2-genericclass2-1.html">GenericClass2</a>"""
files.["creflib2-class3.html"] |> shouldContainText """and <a href="/reference/creflib2-genericclass2-1.html#Property">GenericClass2.Property</a>"""
files.["creflib2-class3.html"] |> shouldContainText """and <a href="/reference/creflib2-genericclass2-1.html#NonGenericMethod">GenericClass2.NonGenericMethod</a>"""
files.["creflib2-class3.html"] |> shouldContainText """and <a href="/reference/creflib2-genericclass2-1.html#GenericMethod">GenericClass2.GenericMethod</a>"""

/// references to non-existent members where the type resolves give an approximation
files.["creflib2-class3.html"] |> shouldContainText """and <a href="/reference/creflib2-class2.html">Class2.NotExistsProperty</a>"""
files.["creflib2-class3.html"] |> shouldContainText """and <a href="/reference/creflib2-class2.html">Class2.NotExistsMethod</a>"""

/// 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"
Expand Down
Loading