diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.0.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.0.md index 379567effb9..139fd7198a4 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.0.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.0.md @@ -24,6 +24,7 @@ * Debugger: provide breakpoint ranges for short lambdas ([#19067](https://github.com/dotnet/fsharp/pull/19067)) * FSharpDiagnostic: add default severity ([#19152](https://github.com/dotnet/fsharp/pull/19152)) * Add support for triple quoted ASCII byte string ([#19182](https://github.com/dotnet/fsharp/pull/19182)) +* Add `` XML documentation support, resolved at tooling time via FCS Symbols API. ([PR #19188](https://github.com/dotnet/fsharp/pull/19188)) ### Changed diff --git a/src/Compiler/Driver/XmlDocFileWriter.fs b/src/Compiler/Driver/XmlDocFileWriter.fs index 004293087bf..be3c2d24d9a 100644 --- a/src/Compiler/Driver/XmlDocFileWriter.fs +++ b/src/Compiler/Driver/XmlDocFileWriter.fs @@ -78,11 +78,14 @@ module XmlDocWriter = doModuleSig None generatedCcu.Contents let WriteXmlDocFile (g, assemblyName, generatedCcu: CcuThunk, xmlFile) = + if not (FileSystemUtils.checkSuffix xmlFile "xml") then error (Error(FSComp.SR.docfileNoXmlSuffix (), Range.rangeStartup)) let mutable members = [] + // elements are written to the XML file as-is. + // Resolution happens at tooling time (IDE tooltips, FCS Symbols API). let addMember id xmlDoc = if hasDoc xmlDoc then let doc = xmlDoc.GetXmlText() diff --git a/src/Compiler/Driver/XmlDocFileWriter.fsi b/src/Compiler/Driver/XmlDocFileWriter.fsi index c8d77bd8476..59d994b7b8b 100644 --- a/src/Compiler/Driver/XmlDocFileWriter.fsi +++ b/src/Compiler/Driver/XmlDocFileWriter.fsi @@ -15,4 +15,5 @@ module XmlDocWriter = /// Writes the XmlDocSig property of each element (field, union case, etc) /// of the specified compilation unit to an XML document in a new text file. + /// elements are written to the XML file as-is; resolution happens at tooling time. val WriteXmlDocFile: g: TcGlobals * assemblyName: string * generatedCcu: CcuThunk * xmlFile: string -> unit diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index f0e69c58ef5..f7d4fadf2d7 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1670,6 +1670,7 @@ forFormatInvalidForInterpolated4,"Interpolated strings used as type IFormattable 3390,xmlDocDuplicateParameter,"This XML comment is invalid: multiple documentation entries for parameter '%s'" 3390,xmlDocUnresolvedCrossReference,"This XML comment is invalid: unresolved cross-reference '%s'" 3390,xmlDocMissingParameter,"This XML comment is incomplete: no documentation for parameter '%s'" +3390,xmlDocInheritDocError,"This XML comment is invalid: inheritdoc error: %s" 3391,tcImplicitConversionUsedForNonMethodArg,"This expression uses the implicit conversion '%s' to convert type '%s' to type '%s'. See https://aka.ms/fsharp-implicit-convs. This warning may be disabled using '#nowarn \"3391\"." 3392,containerDeprecated,"The 'AssemblyKeyNameAttribute' has been deprecated. Use 'AssemblyKeyFileAttribute' instead." 3393,containerSigningUnsupportedOnThisPlatform,"Key container signing is not supported on this platform." diff --git a/src/Compiler/FSharp.Compiler.Service.fsproj b/src/Compiler/FSharp.Compiler.Service.fsproj index a249c5d2bb1..a3656f18485 100644 --- a/src/Compiler/FSharp.Compiler.Service.fsproj +++ b/src/Compiler/FSharp.Compiler.Service.fsproj @@ -472,6 +472,10 @@ + + + + diff --git a/src/Compiler/Symbols/SymbolHelpers.fs b/src/Compiler/Symbols/SymbolHelpers.fs index fed644eeb61..b873a15e31d 100644 --- a/src/Compiler/Symbols/SymbolHelpers.fs +++ b/src/Compiler/Symbols/SymbolHelpers.fs @@ -343,6 +343,8 @@ module internal SymbolHelpers = let GetXmlCommentForItemAux (xmlDoc: XmlDoc option) (infoReader: InfoReader) m d = match xmlDoc with | Some xmlDoc when not xmlDoc.IsEmpty -> + // elements are left intact here. Full resolution happens through the + // Symbols.fs path (FSharpEntity.XmlDoc / FSharpMemberOrFunctionOrValue.XmlDoc). FSharpXmlDoc.FromXmlText xmlDoc | _ -> GetXmlDocHelpSigOfItemForLookup infoReader m d diff --git a/src/Compiler/Symbols/Symbols.fs b/src/Compiler/Symbols/Symbols.fs index 37f0d206fd3..79c3c09ac04 100644 --- a/src/Compiler/Symbols/Symbols.fs +++ b/src/Compiler/Symbols/Symbols.fs @@ -21,6 +21,7 @@ open FSharp.Compiler.SyntaxTreeOps open FSharp.Compiler.Text open FSharp.Compiler.Text.Range open FSharp.Compiler.Xml +open FSharp.Compiler.XmlDocInheritance open FSharp.Compiler.TcGlobals open FSharp.Compiler.TypedTree open FSharp.Compiler.TypedTreeBasics @@ -87,9 +88,270 @@ module Impl = let makeXmlDoc (doc: XmlDoc) = FSharpXmlDoc.FromXmlText doc + /// Returns the XmlText of a doc if non-empty, or None. + let private tryGetXmlDocText (doc: XmlDoc) = + if doc.IsEmpty then None else Some(doc.GetXmlText()) + + /// For nested type crefs (with +), returns an alternative F#-style path + let private parseNestedTypeAlternativePath (cref: string) : string list option = + if cref.Length > 2 && cref.[1] = ':' && cref.[0] = 'T' && cref.Contains("+") then + let typePath = cref.Substring(2) + let lastPlus = typePath.LastIndexOf('+') + if lastPlus > 0 then + let beforePlus = typePath.Substring(0, lastPlus) + let nestedTypeName = typePath.Substring(lastPlus + 1) + let lastDotBeforePlus = beforePlus.LastIndexOf('.') + if lastDotBeforePlus > 0 then + let modulePath = beforePlus.Substring(0, lastDotBeforePlus) + Some((modulePath.Split('.') |> Array.toList) @ [ nestedTypeName ]) + else + Some([ nestedTypeName ]) + else None + else None + + /// Parses a cref string using the shared XmlDocSigParser, returning + /// (typePath, memberName option) for entity/member lookup. + /// Falls back to manual parsing for T: crefs with '+' (nested types) that the regex can't handle. + let private parseCref (cref: string) = + match XmlDocSigParser.parseDocCommentId cref with + | ParsedDocCommentId.Type path -> Some(path, None) + | ParsedDocCommentId.Member(typePath, memberName, _, _) -> Some(typePath, Some memberName) + | ParsedDocCommentId.Field(typePath, fieldName) -> Some(typePath, Some fieldName) + | ParsedDocCommentId.None -> + // The regex doesn't handle '+' in nested type crefs like T:Test.Outer+Inner. + // Replace '+' with '.' to produce a navigable path ["Test"; "Outer"; "Inner"]. + if cref.Length > 2 && cref.[0] = 'T' && cref.[1] = ':' && cref.Contains("+") then + let typePath = cref.Substring(2).Replace('+', '.') + Some(typePath.Split('.') |> Array.toList, None) + else + None + + /// Tries to find a member's or field's XmlDoc on an entity by name + let private tryFindMemberXmlDoc (entity: Entity) (memberName: string) : string option = + entity.MembersOfFSharpTyconSorted + |> List.tryPick (fun vref -> + if vref.DisplayName = memberName || vref.LogicalName = memberName then + tryGetXmlDocText vref.XmlDoc + else None) + |> Option.orElseWith (fun () -> + entity.AllFieldsArray + |> Array.tryPick (fun field -> + if field.DisplayName = memberName || field.LogicalName = memberName then + tryGetXmlDocText field.XmlDoc + else None)) + + /// Tries to find an entity in a module/namespace by path + let rec private tryFindEntityByPath (mtyp: ModuleOrNamespaceType) (path: string list) : Entity option = + match path with + | [] -> None + | [ name ] -> mtyp.AllEntitiesByCompiledAndLogicalMangledNames.TryFind name + | name :: rest -> + match mtyp.AllEntitiesByCompiledAndLogicalMangledNames.TryFind name with + | Some entity -> tryFindEntityByPath entity.ModuleOrNamespaceType rest + | None -> None + + /// Tries to find an entity in the CCU by type path + let private tryFindEntityInCcu (ccu: CcuThunk) (path: string list) : Entity option = + let rootMtyp = ccu.Contents.ModuleOrNamespaceType + match tryFindEntityByPath rootMtyp path with + | Some entity -> Some entity + | None -> + match path with + | ccuName :: rest when not rest.IsEmpty && (ccuName = ccu.AssemblyName || ccuName = ccu.Contents.LogicalName) -> + tryFindEntityByPath rootMtyp rest + | _ -> + rootMtyp.ModuleAndNamespaceDefinitions + |> List.tryPick (fun m -> + match path with + | moduleName :: rest when m.LogicalName = moduleName || m.CompiledName = moduleName -> + match rest with + | [] -> Some m + | _ -> tryFindEntityByPath m.ModuleOrNamespaceType rest + | _ -> None) + |> Option.orElseWith (fun () -> + let rec searchNested (mtyp: ModuleOrNamespaceType) = + match tryFindEntityByPath mtyp path with + | Some e -> Some e + | None -> + mtyp.ModuleAndNamespaceDefinitions + |> List.tryPick (fun m -> searchNested m.ModuleOrNamespaceType) + searchNested rootMtyp) + + /// Dispatches a parsed cref to entity or member doc lookup, with nested-type fallback for T: crefs. + let private tryGetDocByCref + (findEntity: string list -> Entity option) + (cref: string) + : string option = + match parseCref cref with + | Some(path, None) -> + findEntity path + |> Option.bind (fun entity -> tryGetXmlDocText entity.XmlDoc) + |> Option.orElseWith (fun () -> + parseNestedTypeAlternativePath cref + |> Option.bind (fun altPath -> + findEntity altPath + |> Option.bind (fun entity -> tryGetXmlDocText entity.XmlDoc))) + | Some(typePath, Some memberName) -> + findEntity typePath + |> Option.bind (fun entity -> tryFindMemberXmlDoc entity memberName) + | None -> None + + /// Attempts to retrieve XML documentation from a CCU by cref + let private tryGetXmlDocFromCcu (ccu: CcuThunk) (cref: string) : string option = + tryGetDocByCref (tryFindEntityInCcu ccu) cref + + /// Attempts to retrieve XML documentation from a ModuleOrNamespaceType by cref. + /// Used for same-compilation resolution where thisCcuTy provides the current compilation's typed content. + let private tryGetXmlDocFromModuleType (ccuName: string) (mtyp: ModuleOrNamespaceType) (cref: string) : string option = + let findEntityWithFallbacks (path: string list) = + tryFindEntityByPath mtyp path + |> Option.orElseWith (fun () -> + match path with + | firstPart :: rest when firstPart = ccuName && not rest.IsEmpty -> + tryFindEntityByPath mtyp rest + | moduleName :: rest -> + mtyp.ModuleAndNamespaceDefinitions + |> List.tryPick (fun m -> + if m.LogicalName = moduleName || m.CompiledName = moduleName then + match rest with + | [] -> Some m + | _ -> tryFindEntityByPath m.ModuleOrNamespaceType rest + else None) + | _ -> None) + + tryGetDocByCref findEntityWithFallbacks cref + + /// Builds a cref resolver function from the SymbolEnv. + /// The resolver searches same-compilation CCU, all loaded CCUs, and external XML documentation files. + let private buildCrefResolver (cenv: SymbolEnv) : string -> string option = + let allCcus = cenv.tcImports.GetCcusInDeclOrder() + + let tryFindXmlDocBySignature (assemblyName: string) (xmlDocSig: string) : XmlDoc option = + cenv.amap.assemblyLoader.TryFindXmlDocumentationInfo(assemblyName) + |> Option.bind (fun xmlDocInfo -> xmlDocInfo.TryGetXmlDocBySig(xmlDocSig)) + + fun cref -> + // 1. Try same-compilation module type first (most precise for current compilation) + let fromModuleType = + match cenv.thisCcuTy with + | Some mtyp -> tryGetXmlDocFromModuleType cenv.thisCcu.AssemblyName mtyp cref + | None -> None + + match fromModuleType with + | Some doc -> Some doc + | None -> + // 2. Try same-compilation CCU + match tryGetXmlDocFromCcu cenv.thisCcu cref with + | Some doc -> Some doc + | None -> + // 3. Try all loaded CCUs (other F# assemblies) + match allCcus |> List.tryPick (fun ccu -> tryGetXmlDocFromCcu ccu cref) with + | Some doc -> Some doc + | None -> + // 4. Fall back to external XML documentation files (for IL types like System.Exception) + allCcus + |> List.tryPick (fun ccu -> + match tryFindXmlDocBySignature ccu.AssemblyName cref with + | Some xmlDoc when not xmlDoc.IsEmpty -> Some(xmlDoc.GetXmlText()) + | _ -> None) + + /// Returns the XML text if it contains an element, or None. + /// Avoids a second GetXmlText() allocation by returning the text for reuse. + let tryGetInheritDocXmlText (doc: XmlDoc) = + if doc.IsEmpty then None + else + let xmlText = doc.GetXmlText() + if xmlText.IndexOf("= 0 then Some xmlText else None + + /// Creates an FSharpXmlDoc with elements expanded. + /// Takes the pre-computed xmlText to avoid a redundant GetXmlText() call. + let makeExpandedXmlDoc (cenv: SymbolEnv) (implicitTargetCrefOpt: string option) (doc: XmlDoc) (xmlText: string) = + let resolveCref = buildCrefResolver cenv + let expandedText = expandInheritDocFromXmlText resolveCref implicitTargetCrefOpt doc.Range Set.empty xmlText + + if System.String.Equals(xmlText, expandedText, System.StringComparison.Ordinal) then + FSharpXmlDoc.FromXmlText doc + else + FSharpXmlDoc.FromXmlText(XmlDoc([| expandedText |], doc.Range)) + let makeElaboratedXmlDoc (doc: XmlDoc) = makeReadOnlyCollection (doc.GetElaboratedXmlLines()) + /// Computes the implicit target cref for an entity (base class or first implemented interface) + let getImplicitTargetCrefForEntity (cenv: SymbolEnv) (entity: EntityRef) : string option = + try + let ty = generalizedTyconRef cenv.g entity + // First try base class + match GetSuperTypeOfType cenv.g cenv.amap range0 ty with + | Some baseTy when not (isObjTyAnyNullness cenv.g baseTy) -> + // Get the XmlDocSig of the base type + match tryTcrefOfAppTy cenv.g baseTy with + | ValueSome tcref -> Some ("T:" + tcref.CompiledRepresentationForNamedType.FullName) + | ValueNone -> None + | _ -> + // Fall back to first implemented interface + let interfaces = GetImmediateInterfacesOfType SkipUnrefInterfaces.Yes cenv.g cenv.amap range0 ty + match interfaces with + | intfTy :: _ -> + match tryTcrefOfAppTy cenv.g intfTy with + | ValueSome tcref -> Some ("T:" + tcref.CompiledRepresentationForNamedType.FullName) + | ValueNone -> None + | [] -> None + with _ -> None + + /// Computes the implicit target cref for a member (from implemented interface or overridden base method) + let getImplicitTargetCrefForMember (cenv: SymbolEnv) (d: FSharpMemberOrValData) (slotSigs: SlotSig list) : string option = + let crefPrefix = + match d with + | P _ -> "P:" + | E _ -> "E:" + | _ -> "M:" + + match slotSigs with + | slot :: _ -> + try + let declaringTy = slot.DeclaringType + let methodName = slot.Name + match tryTcrefOfAppTy cenv.g declaringTy with + | ValueSome tcref -> + let typeName = tcref.CompiledRepresentationForNamedType.FullName + Some (crefPrefix + typeName + "." + methodName) + | ValueNone -> None + with _ -> None + | [] -> + // For overrides of base class abstract members, slot sigs may be empty. + // Fall back to finding the base type and building a member cref from it. + try + let name = + match d with + | V v -> v.DisplayName + | M m | C m -> m.DisplayName + | P p -> p.PropertyName + | E e -> e.EventName + + let declaringTyOpt = + match d with + | V v -> + match v.TryDeclaringEntity with + | Parent entityRef -> Some(generalizedTyconRef cenv.g entityRef) + | ParentNone -> None + | M m | C m -> Some m.ApparentEnclosingType + | P p -> Some p.ApparentEnclosingType + | E e -> Some e.ApparentEnclosingType + + match declaringTyOpt with + | Some declaringTy -> + match GetSuperTypeOfType cenv.g cenv.amap range0 declaringTy with + | Some baseTy when not (isObjTyAnyNullness cenv.g baseTy) -> + match tryTcrefOfAppTy cenv.g baseTy with + | ValueSome baseTcref -> + let baseName = baseTcref.CompiledRepresentationForNamedType.FullName + Some (crefPrefix + baseName + "." + name) + | ValueNone -> None + | _ -> None + | None -> None + with _ -> None + let rescopeEntity optViewedCcu (entity: Entity) = match optViewedCcu with | None -> mkLocalEntityRef entity @@ -708,7 +970,12 @@ type FSharpEntity(cenv: SymbolEnv, entity: EntityRef, tyargs: TType list) = member _.XmlDoc = if isUnresolved() then XmlDoc.Empty |> makeXmlDoc else - entity.XmlDoc |> makeXmlDoc + let doc = entity.XmlDoc + match tryGetInheritDocXmlText doc with + | None -> makeXmlDoc doc + | Some xmlText -> + let implicitTarget = getImplicitTargetCrefForEntity cenv entity + makeExpandedXmlDoc cenv implicitTarget doc xmlText member _.ElaboratedXmlDoc = if isUnresolved() then XmlDoc.Empty |> makeElaboratedXmlDoc else @@ -2108,11 +2375,24 @@ type FSharpMemberOrFunctionOrValue(cenv, d:FSharpMemberOrValData, item) = member _.XmlDoc = if isUnresolved() then XmlDoc.Empty |> makeXmlDoc else - match d with - | E e -> e.XmlDoc |> makeXmlDoc - | P p -> p.XmlDoc |> makeXmlDoc - | M m | C m -> m.XmlDoc |> makeXmlDoc - | V v -> v.XmlDoc |> makeXmlDoc + let doc = + match d with + | E e -> e.XmlDoc + | P p -> p.XmlDoc + | M m | C m -> m.XmlDoc + | V v -> v.XmlDoc + match tryGetInheritDocXmlText doc with + | None -> makeXmlDoc doc + | Some xmlText -> + // Only compute implicit target and build resolver when doc contains + let slotSigs = + match d with + | E e -> e.AddMethod.ImplementedSlotSignatures + | P p -> p.ImplementedSlotSignatures + | M m | C m -> m.ImplementedSlotSignatures + | V v -> v.ImplementedSlotSignatures + let implicitTarget = getImplicitTargetCrefForMember cenv d slotSigs + makeExpandedXmlDoc cenv implicitTarget doc xmlText member _.ElaboratedXmlDoc = if isUnresolved() then XmlDoc.Empty |> makeElaboratedXmlDoc else diff --git a/src/Compiler/Symbols/XmlDocInheritance.fs b/src/Compiler/Symbols/XmlDocInheritance.fs new file mode 100644 index 00000000000..16268bc4860 --- /dev/null +++ b/src/Compiler/Symbols/XmlDocInheritance.fs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module internal FSharp.Compiler.XmlDocInheritance + +open System.Xml.Linq +open System.Xml.XPath +open FSharp.Compiler.DiagnosticsLogger +open FSharp.Compiler.Text + +/// Represents an inheritdoc directive found in XML documentation +type InheritDocDirective = + { + /// Optional cref attribute specifying explicit target + Cref: string option + /// Optional path attribute for XPath filtering + Path: string option + /// The original XElement for replacement + Element: XElement + } + +/// Checks if an XML document contains elements +let private hasInheritDoc (xmlText: string) = xmlText.IndexOf("= 0 + +/// Extracts inheritdoc directives from parsed XML +let private extractInheritDocDirectives (doc: XDocument) = + let inheritDocName = XName.op_Implicit "inheritdoc" |> Operators.nonNull + + let crefName = XName.op_Implicit "cref" |> Operators.nonNull + let pathName = XName.op_Implicit "path" |> Operators.nonNull + + doc.Descendants(inheritDocName) + |> Seq.map (fun elem -> + let crefAttr = elem.Attribute(crefName) + let pathAttr = elem.Attribute(pathName) + + { + Cref = + match crefAttr with + | null -> None + | attr -> Some attr.Value + Path = + match pathAttr with + | null -> None + | attr -> Some attr.Value + Element = elem + }) + |> List.ofSeq + +/// Applies an XPath filter to XML content +let private applyXPathFilter (m: range) (xpath: string) (sourceXml: string) : string option = + try + let doc = + XDocument.Parse("" + sourceXml + "", LoadOptions.PreserveWhitespace) + + // If the xpath starts with /, it's an absolute path that won't work with our wrapper + // Adjust to search within the doc + let adjustedXpath = + if xpath.StartsWith("/") && not (xpath.StartsWith("//")) then + "/doc" + xpath + else + xpath + + let selectedElements = doc.XPathSelectElements(adjustedXpath) + + if Seq.isEmpty selectedElements then + None + else + let result = + selectedElements + |> Seq.map (fun elem -> elem.ToString(SaveOptions.DisableFormatting)) + |> String.concat "\n" + + Some result + with ex -> + warning (Error(FSComp.SR.xmlDocInheritDocError ($"invalid XPath '{xpath}': {ex.Message}"), m)) + None + +/// Recursively expands inheritdoc in the retrieved documentation +let rec private expandInheritedDoc + (resolveCref: string -> string option) + (implicitTargetCrefOpt: string option) + (m: range) + (visited: Set) + (cref: string) + (xmlText: string) + : string = + if visited.Contains(cref) then + xmlText + else + let newVisited = visited.Add(cref) + expandInheritDocFromXmlText resolveCref implicitTargetCrefOpt m newVisited xmlText + +/// Expands `` elements in XML documentation text. +/// The caller provides a `resolveCref` function that maps a cref string to its resolved XML doc text. +/// Takes an optional implicit target cref for resolving without cref attribute. +/// Tracks visited signatures to prevent infinite recursion. +/// Takes a pre-computed xmlText string, avoiding an extra GetXmlText() call. +and expandInheritDocFromXmlText + (resolveCref: string -> string option) + (implicitTargetCrefOpt: string option) + (m: range) + (visited: Set) + (xmlText: string) + : string = + if not (hasInheritDoc xmlText) then + xmlText + else + try + let wrappedXml = "\n" + xmlText + "\n" + let xdoc = XDocument.Parse(wrappedXml, LoadOptions.PreserveWhitespace) + + let directives = extractInheritDocDirectives xdoc + + if directives.IsEmpty then + xmlText + else + let resolveAndReplace (directive: InheritDocDirective) (cref: string) (implicitCrefForRecursion: string option) = + if visited.Contains(cref) then + warning (Error(FSComp.SR.xmlDocInheritDocError ($"Circular reference detected for '{cref}'"), m)) + else + match resolveCref cref with + | Some inheritedXml -> + let expandedInheritedXml = + expandInheritedDoc resolveCref implicitCrefForRecursion m visited cref inheritedXml + + let contentToInherit = + match directive.Path with + | Some xpath -> + applyXPathFilter m xpath expandedInheritedXml + |> Option.defaultValue expandedInheritedXml + | None -> expandedInheritedXml + + try + let newContent = XElement.Parse("" + contentToInherit + "") + directive.Element.ReplaceWith(newContent.Nodes()) + with ex -> + warning (Error(FSComp.SR.xmlDocInheritDocError ($"Failed to process inheritdoc: {ex.Message}"), m)) + | None -> warning (Error(FSComp.SR.xmlDocInheritDocError ($"Cannot resolve cref '{cref}'"), m)) + + for directive in directives do + match directive.Cref with + | Some cref -> resolveAndReplace directive cref implicitTargetCrefOpt + | None -> + match implicitTargetCrefOpt with + | Some implicitCref -> resolveAndReplace directive implicitCref None + | None -> + warning ( + Error( + FSComp.SR.xmlDocInheritDocError ("Implicit inheritdoc (without cref) requires a base type or interface"), + m + ) + ) + + match xdoc.Root with + | null -> xmlText + | root -> + root.Nodes() + |> Seq.map (fun node -> node.ToString(SaveOptions.DisableFormatting)) + |> String.concat "\n" + with :? System.Xml.XmlException -> + xmlText diff --git a/src/Compiler/Symbols/XmlDocInheritance.fsi b/src/Compiler/Symbols/XmlDocInheritance.fsi new file mode 100644 index 00000000000..6312c62c1dc --- /dev/null +++ b/src/Compiler/Symbols/XmlDocInheritance.fsi @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module internal FSharp.Compiler.XmlDocInheritance + +open FSharp.Compiler.Text + +/// Expands `` elements in XML documentation text. +/// The caller provides a `resolveCref` function to look up documentation by cref string. +/// Takes an optional implicit target cref for resolving without cref attribute. +/// Takes a set of visited signatures to prevent cycles. +/// Takes a pre-computed xmlText string, avoiding an extra GetXmlText() call. +val expandInheritDocFromXmlText: + resolveCref: (string -> string option) -> + implicitTargetCrefOpt: string option -> + m: range -> + visited: Set -> + xmlText: string -> + string diff --git a/src/Compiler/Symbols/XmlDocSigParser.fs b/src/Compiler/Symbols/XmlDocSigParser.fs new file mode 100644 index 00000000000..f23b95771cd --- /dev/null +++ b/src/Compiler/Symbols/XmlDocSigParser.fs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace FSharp.Compiler.Symbols + +open System.Text.RegularExpressions + +/// Represents the kind of element in a documentation comment ID +[] +type DocCommentIdKind = + | Type + | Method + | Property + | Field + | Event + | Namespace + | Unknown + +/// Represents a parsed documentation comment ID (cref format) +[] +type ParsedDocCommentId = + /// Type reference (T:Namespace.Type) + | Type of path: string list + /// Member reference (M:, P:, E:) with type path, member name, generic arity, and kind + | Member of typePath: string list * memberName: string * genericArity: int * kind: DocCommentIdKind + /// Field reference (F:Namespace.Type.field) + | Field of typePath: string list * fieldName: string + /// Invalid or unparseable ID + | None + +module XmlDocSigParser = + // Hoisted to module level to avoid re-creating compiled Regex on every call + let private docCommentIdRx = + Regex(@"^(?\w):(?[\w\d#`.]+)(?\(.+\))?(?:~([\w\d.]+))?$", RegexOptions.Compiled) + + let private fnGenericArgsRx = + Regex(@"^(?.+)``(?\d+)$", RegexOptions.Compiled) + + /// Parse a documentation comment ID string (e.g., "M:Namespace.Type.Method(System.String)") + let parseDocCommentId (docCommentId: string) = + + let m = docCommentIdRx.Match(docCommentId) + let kindStr = m.Groups["kind"].Value + + match m.Success, kindStr with + | true, ("M" | "P" | "E") -> + let parts = m.Groups["entity"].Value.Split('.') + + if parts.Length < 2 then + ParsedDocCommentId.None + else + let entityPath = parts[.. (parts.Length - 2)] |> List.ofArray + let memberOrVal = parts[parts.Length - 1] + + // Try and parse generic params count from the name + let genericM = fnGenericArgsRx.Match(memberOrVal) + + let (memberOrVal, genericParametersCount) = + if genericM.Success then + (genericM.Groups["entity"].Value, int genericM.Groups["typars"].Value) + else + memberOrVal, 0 + + let kind = + match kindStr with + | "M" -> DocCommentIdKind.Method + | "P" -> DocCommentIdKind.Property + | "E" -> DocCommentIdKind.Event + | _ -> DocCommentIdKind.Unknown + + // Handle constructor name conversion (#ctor in doc comments, .ctor in F#) + let finalMemberName = if memberOrVal = "#ctor" then ".ctor" else memberOrVal + + ParsedDocCommentId.Member(entityPath, finalMemberName, genericParametersCount, kind) + + | true, "T" -> + let entityPath = m.Groups["entity"].Value.Split('.') |> List.ofArray + ParsedDocCommentId.Type entityPath + + | true, "F" -> + let parts = m.Groups["entity"].Value.Split('.') + + if parts.Length < 2 then + ParsedDocCommentId.None + else + let entityPath = parts[.. (parts.Length - 2)] |> List.ofArray + let memberOrVal = parts[parts.Length - 1] + ParsedDocCommentId.Field(entityPath, memberOrVal) + + | _ -> ParsedDocCommentId.None diff --git a/src/Compiler/Symbols/XmlDocSigParser.fsi b/src/Compiler/Symbols/XmlDocSigParser.fsi new file mode 100644 index 00000000000..787b0718471 --- /dev/null +++ b/src/Compiler/Symbols/XmlDocSigParser.fsi @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace FSharp.Compiler.Symbols + +/// Represents the kind of element in a documentation comment ID +[] +type DocCommentIdKind = + | Type + | Method + | Property + | Field + | Event + | Namespace + | Unknown + +/// Represents a parsed documentation comment ID (cref format) +[] +type ParsedDocCommentId = + /// Type reference (T:Namespace.Type) + | Type of path: string list + /// Member reference (M:, P:, E:) with type path, member name, generic arity, and kind + | Member of typePath: string list * memberName: string * genericArity: int * kind: DocCommentIdKind + /// Field reference (F:Namespace.Type.field) + | Field of typePath: string list * fieldName: string + /// Invalid or unparseable ID + | None + +module XmlDocSigParser = + /// Parse a documentation comment ID string (e.g., "M:Namespace.Type.Method(System.String)") + val parseDocCommentId: docCommentId: string -> ParsedDocCommentId diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index ace369ab6d0..df456b0c6b5 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -1992,6 +1992,11 @@ Tento komentář XML není platný: několik položek dokumentace pro parametr {0} + + This XML comment is invalid: inheritdoc error: {0} + This XML comment is invalid: inheritdoc error: {0} + + This XML comment is invalid: unknown parameter '{0}' Tento komentář XML není platný: neznámý parametr {0} diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index b085382559c..41dd86de4b1 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -1992,6 +1992,11 @@ Dieser XML-Kommentar ist ungültig: mehrere Dokumentationseinträge für Parameter "{0}". + + This XML comment is invalid: inheritdoc error: {0} + This XML comment is invalid: inheritdoc error: {0} + + This XML comment is invalid: unknown parameter '{0}' Dieser XML-Kommentar ist ungültig: unbekannter Parameter "{0}". diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index ce366b2aa25..01545d8871f 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -1992,6 +1992,11 @@ El comentario XML no es válido: hay varias entradas de documentación para el parámetro "{0}" + + This XML comment is invalid: inheritdoc error: {0} + This XML comment is invalid: inheritdoc error: {0} + + This XML comment is invalid: unknown parameter '{0}' El comentario XML no es válido: parámetro "{0}" desconocido diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index 434fe52667b..171d8422355 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -1992,6 +1992,11 @@ Ce commentaire XML est non valide : il existe plusieurs entrées de documentation pour le paramètre '{0}' + + This XML comment is invalid: inheritdoc error: {0} + This XML comment is invalid: inheritdoc error: {0} + + This XML comment is invalid: unknown parameter '{0}' Ce commentaire XML est non valide : paramètre inconnu '{0}' diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index b56cbd0aac5..ed65a8f5164 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -1992,6 +1992,11 @@ Questo commento XML non è valido: sono presenti più voci della documentazione per il parametro '{0}' + + This XML comment is invalid: inheritdoc error: {0} + This XML comment is invalid: inheritdoc error: {0} + + This XML comment is invalid: unknown parameter '{0}' Questo commento XML non è valido: il parametro '{0}' è sconosciuto diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index 4c30fd2c58c..6f0b90c32c2 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -1992,6 +1992,11 @@ この XML コメントは無効です: パラメーター '{0}' に複数のドキュメント エントリがあります + + This XML comment is invalid: inheritdoc error: {0} + This XML comment is invalid: inheritdoc error: {0} + + This XML comment is invalid: unknown parameter '{0}' この XML コメントは無効です: パラメーター '{0}' が不明です diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index cfad9f364e9..e21761aca2c 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -1992,6 +1992,11 @@ 이 XML 주석이 잘못됨: 매개 변수 '{0}'에 대한 여러 설명서 항목이 있음 + + This XML comment is invalid: inheritdoc error: {0} + This XML comment is invalid: inheritdoc error: {0} + + This XML comment is invalid: unknown parameter '{0}' 이 XML 주석이 잘못됨: 알 수 없는 매개 변수 '{0}' diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index 70d31047619..7a2f88fb5c2 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -1992,6 +1992,11 @@ Ten komentarz XML jest nieprawidłowy: wiele wpisów dokumentacji dla parametru „{0}” + + This XML comment is invalid: inheritdoc error: {0} + This XML comment is invalid: inheritdoc error: {0} + + This XML comment is invalid: unknown parameter '{0}' Ten komentarz XML jest nieprawidłowy: nieznany parametr „{0}” diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index bfe7eec5278..048258d4cf2 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -1992,6 +1992,11 @@ Este comentário XML é inválido: várias entradas de documentação para o parâmetro '{0}' + + This XML comment is invalid: inheritdoc error: {0} + This XML comment is invalid: inheritdoc error: {0} + + This XML comment is invalid: unknown parameter '{0}' Este comentário XML é inválido: parâmetro desconhecido '{0}' diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index 426e70c647b..07f2c091498 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -1992,6 +1992,11 @@ Недопустимый XML-комментарий: несколько записей документации для параметра "{0}" + + This XML comment is invalid: inheritdoc error: {0} + This XML comment is invalid: inheritdoc error: {0} + + This XML comment is invalid: unknown parameter '{0}' Недопустимый XML-комментарий: неизвестный параметр "{0}" diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index 1908a5b1ee0..30003496894 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -1992,6 +1992,11 @@ Bu XML açıklaması geçersiz: '{0}' parametresi için birden çok belge girişi var + + This XML comment is invalid: inheritdoc error: {0} + This XML comment is invalid: inheritdoc error: {0} + + This XML comment is invalid: unknown parameter '{0}' Bu XML açıklaması geçersiz: '{0}' parametresi bilinmiyor diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index 686eb4f48b6..e213fe1fe65 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -1992,6 +1992,11 @@ 此 XML 注释无效: 参数“{0}”有多个文档条目 + + This XML comment is invalid: inheritdoc error: {0} + This XML comment is invalid: inheritdoc error: {0} + + This XML comment is invalid: unknown parameter '{0}' 此 XML 注释无效: 未知参数“{0}” diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index bf14e4068ee..dade33f93dd 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -1992,6 +1992,11 @@ 此 XML 註解無效: '{0}' 參數有多項文件輸入 + + This XML comment is invalid: inheritdoc error: {0} + This XML comment is invalid: inheritdoc error: {0} + + This XML comment is invalid: unknown parameter '{0}' 此 XML 註解無效: 未知的參數 '{0}' diff --git a/tests/FSharp.Compiler.ComponentTests/Miscellaneous/XmlDoc.fs b/tests/FSharp.Compiler.ComponentTests/Miscellaneous/XmlDoc.fs index 806c2ac8354..df425efe5a7 100644 --- a/tests/FSharp.Compiler.ComponentTests/Miscellaneous/XmlDoc.fs +++ b/tests/FSharp.Compiler.ComponentTests/Miscellaneous/XmlDoc.fs @@ -5,6 +5,7 @@ module Miscellaneous.XmlDoc open System.IO open Xunit open FSharp.Compiler.Xml +open FSharp.Compiler.Symbols open TestFramework @@ -45,3 +46,163 @@ let ``Can extract XML docs from a file for a signature`` signature = finally File.Delete xmlFileName + + +// ============================================================================ +// XmlDocSigParser Tests +// ============================================================================ + +module XmlDocSigParserTests = + + // Type reference parsing - parameterized + [] + [] + [] + [] + let ``Parse type reference`` (input: string, expectedPathStr: string) = + let expectedPath = expectedPathStr.Split(';') |> Array.toList + + match XmlDocSigParser.parseDocCommentId input with + | ParsedDocCommentId.Type path -> Assert.Equal(expectedPath, path) + | other -> failwith $"Expected Type, got {other}" + + // Member reference parsing - parameterized via MemberData + let private assertMember input expectedTypePath expectedName expectedArity expectedKind = + match XmlDocSigParser.parseDocCommentId input with + | ParsedDocCommentId.Member(typePath, memberName, genericArity, kind) -> + Assert.Equal(expectedTypePath, typePath) + Assert.Equal(expectedName, memberName) + Assert.Equal(expectedArity, genericArity) + Assert.Equal(expectedKind, kind) + | other -> failwith $"Expected Member, got {other}" + + let memberReferenceData: obj array array = + [| [| "M:System.String.IndexOf"; [ "System"; "String" ]; "IndexOf"; 0; DocCommentIdKind.Method |] + [| "M:System.String.IndexOf(System.String)"; [ "System"; "String" ]; "IndexOf"; 0; DocCommentIdKind.Method |] + [| "M:System.Linq.Enumerable.Select``1"; [ "System"; "Linq"; "Enumerable" ]; "Select"; 1; DocCommentIdKind.Method |] + [| "P:System.String.Length"; [ "System"; "String" ]; "Length"; 0; DocCommentIdKind.Property |] + [| "E:System.Windows.Forms.Control.Click"; [ "System"; "Windows"; "Forms"; "Control" ]; "Click"; 0; DocCommentIdKind.Event |] + [| "M:System.String.#ctor"; [ "System"; "String" ]; ".ctor"; 0; DocCommentIdKind.Method |] |] + + [] + [] + let ``Parse member reference`` (input: string, expectedTypePath: string list, expectedName: string, expectedArity: int, expectedKind: DocCommentIdKind) = + assertMember input expectedTypePath expectedName expectedArity expectedKind + + [] + let ``Parse field reference`` () = + match XmlDocSigParser.parseDocCommentId "F:MyNamespace.MyClass.myField" with + | ParsedDocCommentId.Field(typePath, fieldName) -> + Assert.Equal([ "MyNamespace"; "MyClass" ], typePath) + Assert.Equal("myField", fieldName) + | other -> failwith $"Expected Field, got {other}" + + // Invalid input parsing - parameterized + [] + [] + [] + let ``Parse invalid doc comment ID returns None`` (input: string) = + match XmlDocSigParser.parseDocCommentId input with + | ParsedDocCommentId.None -> () + | other -> failwith $"Expected None, got {other}" + + +// ============================================================================ +// XmlDocInheritance Tests +// ============================================================================ + +module XmlDocInheritanceTests = + open FSharp.Compiler.XmlDocInheritance + open FSharp.Compiler.Text + + let private noResolver (_cref: string) : string option = None + + let private expandWithNoResolver visited xmlText = + expandInheritDocFromXmlText noResolver None Range.range0 visited xmlText + + [] + let ``Empty XmlDoc returns empty`` () = + let result = expandWithNoResolver Set.empty "" + Assert.Equal("", result) + + [] + let ``XmlDoc without inheritdoc returns unchanged`` () = + let xmlText = "Test summary" + let result = expandWithNoResolver Set.empty xmlText + Assert.Equal(xmlText, result) + + // These all pass different inheritdoc variants without resolver - result should be non-null + [] + [")>] + [")>] + [")>] + let ``XmlDoc with inheritdoc but no resolver returns non-null`` (xmlLine: string) = + let result = expandWithNoResolver Set.empty xmlLine + Assert.NotNull(result) + + [] + let ``Malformed XML is handled gracefully`` () = + let xmlText = "" + let result = expandWithNoResolver Set.empty xmlText + Assert.Equal(xmlText, result) + + [] + let ``Cycle detection prevents infinite recursion`` () = + let xmlText = "" + let visited = Set.ofList [ "T:System.String" ] + let result = expandWithNoResolver visited xmlText + Assert.NotNull(result) + + [] + let ``Resolver-based expansion replaces inheritdoc with resolved content`` () = + let resolver (cref: string) = + if cref = "T:Test.BaseType" then + Some "Base type summary" + else + None + + let xmlText = "" + let result = expandInheritDocFromXmlText resolver None Range.range0 Set.empty xmlText + Assert.Contains("Base type summary", result) + Assert.DoesNotContain("] + let ``Recursive chained resolution expands through multiple levels`` () = + let resolver (cref: string) = + match cref with + | "T:GrandBase" -> Some "GrandBase documentation" + | "T:Base" -> Some "" + | _ -> None + + let xmlText = "" + let result = expandInheritDocFromXmlText resolver None Range.range0 Set.empty xmlText + Assert.Contains("GrandBase documentation", result) + + [] + let ``Implicit target resolves when no cref is specified`` () = + let resolver (cref: string) = + if cref = "T:Test.IService" then + Some "Service contract docs" + else + None + + let xmlText = "" + + let result = + expandInheritDocFromXmlText resolver (Some "T:Test.IService") Range.range0 Set.empty xmlText + + Assert.Contains("Service contract docs", result) + + [] + let ``XPath path filter selects only matching elements`` () = + let resolver (cref: string) = + if cref = "T:Test.Base" then + Some "Base summaryBase remarks" + else + None + + let xmlText = "" + let result = expandInheritDocFromXmlText resolver None Range.range0 Set.empty xmlText + Assert.Contains("Base remarks", result) + Assert.DoesNotContain("Base summary", result) + diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index 1954ef2367b..2f7dd2aed1e 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -5080,6 +5080,54 @@ FSharp.Compiler.Interactive.Shell: FSharp.Compiler.Interactive.Shell+FsiEvaluati FSharp.Compiler.Interactive.Shell: FSharp.Compiler.Interactive.Shell+FsiEvaluationSessionHostConfig FSharp.Compiler.Interactive.Shell: FSharp.Compiler.Interactive.Shell+FsiValue FSharp.Compiler.Interactive.Shell: FSharp.Compiler.Interactive.Shell+Settings +FSharp.Compiler.Symbols.DocCommentIdKind+Tags: Int32 Event +FSharp.Compiler.Symbols.DocCommentIdKind+Tags: Int32 Field +FSharp.Compiler.Symbols.DocCommentIdKind+Tags: Int32 Method +FSharp.Compiler.Symbols.DocCommentIdKind+Tags: Int32 Namespace +FSharp.Compiler.Symbols.DocCommentIdKind+Tags: Int32 Property +FSharp.Compiler.Symbols.DocCommentIdKind+Tags: Int32 Type +FSharp.Compiler.Symbols.DocCommentIdKind+Tags: Int32 Unknown +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean Equals(FSharp.Compiler.Symbols.DocCommentIdKind) +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean Equals(FSharp.Compiler.Symbols.DocCommentIdKind, System.Collections.IEqualityComparer) +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean Equals(System.Object) +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean Equals(System.Object, System.Collections.IEqualityComparer) +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean IsEvent +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean IsField +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean IsMethod +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean IsNamespace +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean IsProperty +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean IsType +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean IsUnknown +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean get_IsEvent() +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean get_IsField() +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean get_IsMethod() +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean get_IsNamespace() +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean get_IsProperty() +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean get_IsType() +FSharp.Compiler.Symbols.DocCommentIdKind: Boolean get_IsUnknown() +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind Event +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind Field +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind Method +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind Namespace +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind Property +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind Type +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind Unknown +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind get_Event() +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind get_Field() +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind get_Method() +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind get_Namespace() +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind get_Property() +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind get_Type() +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind get_Unknown() +FSharp.Compiler.Symbols.DocCommentIdKind: FSharp.Compiler.Symbols.DocCommentIdKind+Tags +FSharp.Compiler.Symbols.DocCommentIdKind: Int32 CompareTo(FSharp.Compiler.Symbols.DocCommentIdKind) +FSharp.Compiler.Symbols.DocCommentIdKind: Int32 CompareTo(System.Object) +FSharp.Compiler.Symbols.DocCommentIdKind: Int32 CompareTo(System.Object, System.Collections.IComparer) +FSharp.Compiler.Symbols.DocCommentIdKind: Int32 GetHashCode() +FSharp.Compiler.Symbols.DocCommentIdKind: Int32 GetHashCode(System.Collections.IEqualityComparer) +FSharp.Compiler.Symbols.DocCommentIdKind: Int32 Tag +FSharp.Compiler.Symbols.DocCommentIdKind: Int32 get_Tag() +FSharp.Compiler.Symbols.DocCommentIdKind: System.String ToString() FSharp.Compiler.Symbols.FSharpAbstractParameter: Boolean IsInArg FSharp.Compiler.Symbols.FSharpAbstractParameter: Boolean IsOptionalArg FSharp.Compiler.Symbols.FSharpAbstractParameter: Boolean IsOutArg @@ -5942,6 +5990,54 @@ FSharp.Compiler.Symbols.FSharpXmlDoc: Int32 GetHashCode(System.Collections.IEqua FSharp.Compiler.Symbols.FSharpXmlDoc: Int32 Tag FSharp.Compiler.Symbols.FSharpXmlDoc: Int32 get_Tag() FSharp.Compiler.Symbols.FSharpXmlDoc: System.String ToString() +FSharp.Compiler.Symbols.ParsedDocCommentId+Field: Microsoft.FSharp.Collections.FSharpList`1[System.String] get_typePath() +FSharp.Compiler.Symbols.ParsedDocCommentId+Field: Microsoft.FSharp.Collections.FSharpList`1[System.String] typePath +FSharp.Compiler.Symbols.ParsedDocCommentId+Field: System.String fieldName +FSharp.Compiler.Symbols.ParsedDocCommentId+Field: System.String get_fieldName() +FSharp.Compiler.Symbols.ParsedDocCommentId+Member: FSharp.Compiler.Symbols.DocCommentIdKind get_kind() +FSharp.Compiler.Symbols.ParsedDocCommentId+Member: FSharp.Compiler.Symbols.DocCommentIdKind kind +FSharp.Compiler.Symbols.ParsedDocCommentId+Member: Int32 genericArity +FSharp.Compiler.Symbols.ParsedDocCommentId+Member: Int32 get_genericArity() +FSharp.Compiler.Symbols.ParsedDocCommentId+Member: Microsoft.FSharp.Collections.FSharpList`1[System.String] get_typePath() +FSharp.Compiler.Symbols.ParsedDocCommentId+Member: Microsoft.FSharp.Collections.FSharpList`1[System.String] typePath +FSharp.Compiler.Symbols.ParsedDocCommentId+Member: System.String get_memberName() +FSharp.Compiler.Symbols.ParsedDocCommentId+Member: System.String memberName +FSharp.Compiler.Symbols.ParsedDocCommentId+Tags: Int32 Field +FSharp.Compiler.Symbols.ParsedDocCommentId+Tags: Int32 Member +FSharp.Compiler.Symbols.ParsedDocCommentId+Tags: Int32 None +FSharp.Compiler.Symbols.ParsedDocCommentId+Tags: Int32 Type +FSharp.Compiler.Symbols.ParsedDocCommentId+Type: Microsoft.FSharp.Collections.FSharpList`1[System.String] get_path() +FSharp.Compiler.Symbols.ParsedDocCommentId+Type: Microsoft.FSharp.Collections.FSharpList`1[System.String] path +FSharp.Compiler.Symbols.ParsedDocCommentId: Boolean Equals(FSharp.Compiler.Symbols.ParsedDocCommentId) +FSharp.Compiler.Symbols.ParsedDocCommentId: Boolean Equals(FSharp.Compiler.Symbols.ParsedDocCommentId, System.Collections.IEqualityComparer) +FSharp.Compiler.Symbols.ParsedDocCommentId: Boolean Equals(System.Object) +FSharp.Compiler.Symbols.ParsedDocCommentId: Boolean Equals(System.Object, System.Collections.IEqualityComparer) +FSharp.Compiler.Symbols.ParsedDocCommentId: Boolean IsField +FSharp.Compiler.Symbols.ParsedDocCommentId: Boolean IsMember +FSharp.Compiler.Symbols.ParsedDocCommentId: Boolean IsNone +FSharp.Compiler.Symbols.ParsedDocCommentId: Boolean IsType +FSharp.Compiler.Symbols.ParsedDocCommentId: Boolean get_IsField() +FSharp.Compiler.Symbols.ParsedDocCommentId: Boolean get_IsMember() +FSharp.Compiler.Symbols.ParsedDocCommentId: Boolean get_IsNone() +FSharp.Compiler.Symbols.ParsedDocCommentId: Boolean get_IsType() +FSharp.Compiler.Symbols.ParsedDocCommentId: FSharp.Compiler.Symbols.ParsedDocCommentId NewField(Microsoft.FSharp.Collections.FSharpList`1[System.String], System.String) +FSharp.Compiler.Symbols.ParsedDocCommentId: FSharp.Compiler.Symbols.ParsedDocCommentId NewMember(Microsoft.FSharp.Collections.FSharpList`1[System.String], System.String, Int32, FSharp.Compiler.Symbols.DocCommentIdKind) +FSharp.Compiler.Symbols.ParsedDocCommentId: FSharp.Compiler.Symbols.ParsedDocCommentId NewType(Microsoft.FSharp.Collections.FSharpList`1[System.String]) +FSharp.Compiler.Symbols.ParsedDocCommentId: FSharp.Compiler.Symbols.ParsedDocCommentId None +FSharp.Compiler.Symbols.ParsedDocCommentId: FSharp.Compiler.Symbols.ParsedDocCommentId get_None() +FSharp.Compiler.Symbols.ParsedDocCommentId: FSharp.Compiler.Symbols.ParsedDocCommentId+Field +FSharp.Compiler.Symbols.ParsedDocCommentId: FSharp.Compiler.Symbols.ParsedDocCommentId+Member +FSharp.Compiler.Symbols.ParsedDocCommentId: FSharp.Compiler.Symbols.ParsedDocCommentId+Tags +FSharp.Compiler.Symbols.ParsedDocCommentId: FSharp.Compiler.Symbols.ParsedDocCommentId+Type +FSharp.Compiler.Symbols.ParsedDocCommentId: Int32 CompareTo(FSharp.Compiler.Symbols.ParsedDocCommentId) +FSharp.Compiler.Symbols.ParsedDocCommentId: Int32 CompareTo(System.Object) +FSharp.Compiler.Symbols.ParsedDocCommentId: Int32 CompareTo(System.Object, System.Collections.IComparer) +FSharp.Compiler.Symbols.ParsedDocCommentId: Int32 GetHashCode() +FSharp.Compiler.Symbols.ParsedDocCommentId: Int32 GetHashCode(System.Collections.IEqualityComparer) +FSharp.Compiler.Symbols.ParsedDocCommentId: Int32 Tag +FSharp.Compiler.Symbols.ParsedDocCommentId: Int32 get_Tag() +FSharp.Compiler.Symbols.ParsedDocCommentId: System.String ToString() +FSharp.Compiler.Symbols.XmlDocSigParser: FSharp.Compiler.Symbols.ParsedDocCommentId parseDocCommentId(System.String) FSharp.Compiler.Syntax.DebugPointAtBinding+Tags: Int32 NoneAtDo FSharp.Compiler.Syntax.DebugPointAtBinding+Tags: Int32 NoneAtInvisible FSharp.Compiler.Syntax.DebugPointAtBinding+Tags: Int32 NoneAtLet diff --git a/tests/FSharp.Compiler.Service.Tests/XmlDocTests.fs b/tests/FSharp.Compiler.Service.Tests/XmlDocTests.fs index 8460938b7c1..32271c31243 100644 --- a/tests/FSharp.Compiler.Service.Tests/XmlDocTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/XmlDocTests.fs @@ -1630,6 +1630,636 @@ type Class2() = checkResults |> checkXmlSymbols [ Parameter "MyRather.MyDeep.MyNamespace.Class1.X", [|"x"|] ] checkResults |> checkXmlSymbols [ Parameter "MyRather.MyDeep.MyNamespace.Class1", [|"class1"|] ] +// Tests for in tooltips/quickinfo (design-time) +module InheritDocTooltipTests = + + /// Compiles code, finds an FSharpEntity by name, and returns its resolved XmlDoc text. + let private getEntityXmlText (code: string) (symbolName: string) = + let _, checkResults = getParseAndCheckResults code + let symbol = findSymbolByName symbolName checkResults + let xmlDoc = (symbol :?> FSharpEntity).XmlDoc + + match xmlDoc with + | FSharpXmlDoc.FromXmlText t -> t.UnprocessedLines |> String.concat "\n" + | other -> failwith $"Expected FromXmlText for {symbolName}, got {other}" + + /// Compiles code, finds a member by name on an entity, and returns its resolved XmlDoc text. + let private getMemberXmlText (code: string) (entityName: string) (memberName: string) = + let _, checkResults = getParseAndCheckResults code + let entity = findSymbolByName entityName checkResults :?> FSharpEntity + + let memberSymbol = + entity.MembersFunctionsAndValues + |> Seq.tryFind (fun m -> m.DisplayName = memberName) + |> Option.defaultWith (fun () -> failwith $"Member '{memberName}' not found on entity '{entityName}'") + + match memberSymbol.XmlDoc with + | FSharpXmlDoc.FromXmlText t -> t.UnprocessedLines |> String.concat "\n" + | other -> failwith $"Expected FromXmlText for {entityName}.{memberName}, got {other}" + + [] + [] + [] + let ``inheritdoc should expand for same compilation type`` (symbolName: string, expectedText: string) = + let code = """ +module Test + +/// Base type documentation +/// Important remarks +type BaseType() = class end + +/// +type DerivedType() = class end +""" + let xmlText = getEntityXmlText code symbolName + Assert.Contains(expectedText, xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + + [] + let ``inheritdoc with path should filter for same compilation types``() = + let code = """ +module Test + +/// Base documentation +/// Base remarks +type BaseType() = class end + +/// Derived specific +/// +type DerivedType() = class end +""" + let xmlText = getEntityXmlText code "DerivedType" + Assert.Contains("Derived specific", xmlText) + Assert.Contains("Base remarks", xmlText) + Assert.DoesNotContain("Base documentation", xmlText) + + [] + let ``inheritdoc should expand for method in tooltip``() = + let code = """ +module Test + +type BaseClass() = + /// Base method documentation + /// First parameter + /// Second parameter + /// The sum + abstract member Add: x:int -> y:int -> int + default _.Add(x, y) = x + y + +type DerivedClass() = + inherit BaseClass() + /// + override _.Add(x, y) = x + y + 1 +""" + let xmlText = getMemberXmlText code "DerivedClass" "Add" + Assert.Contains("Base method documentation", xmlText) + Assert.DoesNotContain("] + let ``inheritdoc should resolve nested inheritance for same compilation``() = + let code = """ +module Test + +/// GrandBase documentation +type GrandBase() = class end + +/// +type Base() = class end + +/// +type Derived() = class end +""" + let xmlText = getEntityXmlText code "Derived" + Assert.Contains("GrandBase documentation", xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + + [] + let ``inheritdoc circular reference should not crash tooltip``() = + let code = """ +module Test + +/// +type TypeA() = class end + +/// +type TypeB() = class end +""" + let _, checkResults = getParseAndCheckResults code + + let typeASymbol = findSymbolByName "TypeA" checkResults + let xmlDocA = (typeASymbol :?> FSharpEntity).XmlDoc + + let typeBSymbol = findSymbolByName "TypeB" checkResults + let xmlDocB = (typeBSymbol :?> FSharpEntity).XmlDoc + + // Circular references should not crash; the result should still be FromXmlText + match xmlDocA, xmlDocB with + | FSharpXmlDoc.FromXmlText tA, FSharpXmlDoc.FromXmlText tB -> + let textA = tA.UnprocessedLines |> String.concat "\n" + let textB = tB.UnprocessedLines |> String.concat "\n" + Assert.NotEmpty(textA) + Assert.NotEmpty(textB) + // Cycle detection should prevent the from expanding infinitely. + // One type may resolve (getting the other's unexpanded doc), but at least one + // will still contain the unresolved element. + Assert.True( + textA.Contains(" due to cycle detection" + ) + | _ -> failwith "Expected both types to have FromXmlText XmlDoc" + + [] + [] + [] + let ``inheritdoc should work for interface implementation tooltip`` (symbolName: string, expectedText: string) = + let code = """ +module Test + +/// Service interface +/// Core contract +type IService = + /// Execute operation + /// The input + abstract Execute: input:string -> string + +/// +type ServiceImpl() = + interface IService with + member _.Execute(input) = input +""" + let xmlText = getEntityXmlText code symbolName + Assert.Contains(expectedText, xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + + [] + let ``inheritdoc from same module nested type``() = + let code = """ +module Test + +/// Outer container documentation +type OuterType() = + /// Inner nested type docs + type InnerType() = class end + +/// +type DerivedFromOuter() = class end +""" + let xmlText = getEntityXmlText code "DerivedFromOuter" + Assert.Contains("Outer container documentation", xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + + [] + [] + [] + let ``inheritdoc from previous module in same compilation`` (symbolName: string, expectedText: string) = + let code = """ +module FirstModule + +/// Type in first module +/// Important base type +type BaseInFirst() = class end + +module SecondModule + +/// +type DerivedInSecond() = class end +""" + let xmlText = getEntityXmlText code symbolName + Assert.Contains(expectedText, xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + + [] + let ``inheritdoc from System type via IL``() = + let code = """ +module Test + +/// +type MyException() = + inherit System.Exception() +""" + let _, checkResults = getParseAndCheckResults code + let exSymbol = findSymbolByName "MyException" checkResults + let xmlDoc = (exSymbol :?> FSharpEntity).XmlDoc + + match xmlDoc with + | FSharpXmlDoc.FromXmlText t -> + let xmlText = t.UnprocessedLines |> String.concat "\n" + Assert.DoesNotContain(" () + | _ -> failwith "Expected FromXmlText or FromXmlFile" + + [] + let ``inheritdoc from FSharp.Core type``() = + let code = """ +module Test + +/// +type MyDisposable() = + interface System.IDisposable with + member _.Dispose() = () +""" + let _, checkResults = getParseAndCheckResults code + let symbol = findSymbolByName "MyDisposable" checkResults + let xmlDoc = (symbol :?> FSharpEntity).XmlDoc + + match xmlDoc with + | FSharpXmlDoc.FromXmlText t -> + let xmlText = t.UnprocessedLines |> String.concat "\n" + Assert.DoesNotContain(" () + | _ -> failwith "Expected FromXmlText or FromXmlFile" + + [] + let ``inheritdoc with method cref from same module``() = + let code = """ +module Test + +type BaseClass() = + /// Base method docs + /// The x parameter + /// The result + member _.Calculate(x: int) = x * 2 + +type DerivedClass() = + inherit BaseClass() + /// + member _.Calculate2(x: int) = x * 3 +""" + let xmlText = getMemberXmlText code "DerivedClass" "Calculate2" + Assert.Contains("Base method docs", xmlText) + Assert.DoesNotContain("] + [] + [] + let ``inheritdoc for record type from same module`` (symbolName: string, expectedText: string) = + let code = """ +module Test + +/// Base record documentation +/// This is a data record +type BaseRecord = { Name: string; Value: int } + +/// +type DerivedRecord = { Id: int; Data: string } +""" + let xmlText = getEntityXmlText code symbolName + Assert.Contains(expectedText, xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + + [] + [] + [] + let ``inheritdoc for discriminated union from same module`` (symbolName: string, expectedText: string) = + let code = """ +module Test + +/// Base union type +/// Represents choices +type BaseUnion = + | CaseA + | CaseB of int + +/// +type DerivedUnion = + | OptionX + | OptionY of string +""" + let xmlText = getEntityXmlText code symbolName + Assert.Contains(expectedText, xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + + [] + let ``inheritdoc implicit without cref on interface impl should resolve``() = + let code = """ +module Test + +type IService = + /// Service method + abstract DoWork: unit -> unit + +type ServiceImpl() = + interface IService with + /// + member _.DoWork() = () +""" + let xmlText = getMemberXmlText code "ServiceImpl" "DoWork" + Assert.Contains("Service method", xmlText) + Assert.DoesNotContain("] + [] + [] + let ``implicit inheritdoc should resolve from base class for type`` (symbolName: string, expectedText: string) = + let code = """ +module Test + +/// Base class documentation +/// Base remarks +type BaseClass() = class end + +/// +type DerivedClass() = + inherit BaseClass() +""" + let xmlText = getEntityXmlText code symbolName + Assert.Contains(expectedText, xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + + [] + [] + [] + let ``implicit inheritdoc should resolve from interface for type`` (symbolName: string, expectedText: string) = + let code = """ +module Test + +/// Interface documentation +/// Interface remarks +type IMyInterface = + abstract DoWork: unit -> unit + +/// +type MyImpl() = + interface IMyInterface with + member _.DoWork() = () +""" + let xmlText = getEntityXmlText code symbolName + Assert.Contains(expectedText, xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + + // =========================================== + // IMPLICIT INHERITDOC ON METHODS AND PROPERTIES + // =========================================== + + [] + let ``implicit inheritdoc on method implementing interface should inherit docs``() = + let code = """ +module Test + +type ICalculator = + /// Adds two numbers together + /// First number + /// Second number + /// The sum + abstract Add: a:int * b:int -> int + +type Calculator() = + interface ICalculator with + /// + member _.Add(a, b) = a + b +""" + let xmlText = getMemberXmlText code "Calculator" "Add" + Assert.Contains("Adds two numbers together", xmlText) + Assert.Contains("First number", xmlText) + Assert.Contains("The sum", xmlText) + Assert.DoesNotContain("] + let ``implicit inheritdoc on override method should inherit from base``() = + let code = """ +module Test + +type BaseProcessor() = + /// Processes the input data + /// The data to process + /// Processed result + abstract member Process: data:string -> string + default _.Process(data) = data + +type DerivedProcessor() = + inherit BaseProcessor() + /// + override _.Process(data) = data.ToUpper() +""" + let xmlText = getMemberXmlText code "DerivedProcessor" "Process" + Assert.Contains("Processes the input data", xmlText) + Assert.Contains("The data to process", xmlText) + Assert.DoesNotContain("] + let ``implicit inheritdoc on property implementing interface should inherit docs``() = + let code = """ +module Test + +type INameable = + /// Gets or sets the name + abstract Name: string with get, set + +type Person() = + let mutable name = "" + interface INameable with + /// + member _.Name with get() = name and set v = name <- v +""" + let xmlText = getMemberXmlText code "Person" "Name" + Assert.Contains("Gets or sets the name", xmlText) + Assert.DoesNotContain("] + let ``implicit inheritdoc on override property should inherit from base``() = + let code = """ +module Test + +[] +type BaseConfig() = + /// Gets the connection timeout + abstract Timeout: int + +type AppConfig() = + inherit BaseConfig() + /// + override _.Timeout = 30 +""" + let xmlText = getMemberXmlText code "AppConfig" "Timeout" + Assert.Contains("Gets the connection timeout", xmlText) + Assert.DoesNotContain("] + let ``explicit method cref should resolve and inherit docs``() = + let code = """ +module Test + +type Helper = + /// Helper method docs + /// Input value + static member DoSomething(x: int) = x * 2 + +type Worker = + /// + static member Work(x: int) = x * 3 +""" + let xmlText = getMemberXmlText code "Worker" "Work" + Assert.Contains("Helper method docs", xmlText) + Assert.DoesNotContain("] + let ``explicit property cref should resolve and inherit docs``() = + let code = """ +module Test + +type Config = + /// The application name + static member AppName = "MyApp" + +type Settings = + /// + static member Name = "OtherApp" +""" + let xmlText = getMemberXmlText code "Settings" "Name" + Assert.Contains("The application name", xmlText) + Assert.DoesNotContain("] + let ``generic type cref should resolve``() = + let code = """ +module Test + +/// A generic container +type Container<'T> = { Value: 'T } + +/// +type Box<'T> = { Item: 'T } +""" + let xmlText = getEntityXmlText code "Box`1" + Assert.Contains("A generic container", xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + + [] + let ``nested type cref should resolve``() = + let code = """ +module Test + +type Outer = + /// Inner type docs + type Inner = { X: int } + +/// +type Other = { Y: int } +""" + let xmlText = getEntityXmlText code "Other" + Assert.Contains("Inner type docs", xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + + [] + let ``tooling-time resolution removes all inheritdoc elements``() = + let code = """ +module Test + +/// Base type documentation +/// Base remarks content +type BaseType() = class end + +/// +type DerivedType() = class end +""" + let xmlText = getEntityXmlText code "DerivedType" + Assert.Contains("Base type documentation", xmlText) + Assert.Contains("Base remarks content", xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + + [] + let ``unresolvable cref should not crash``() = + let code = """ +module Test + +/// +type MyType() = class end +""" + let _, checkResults = getParseAndCheckResults code + let symbol = findSymbolByName "MyType" checkResults + let xmlDoc = (symbol :?> FSharpEntity).XmlDoc + // Should not crash; the unresolvable is left in place + match xmlDoc with + | FSharpXmlDoc.FromXmlText t -> + let xmlText = t.UnprocessedLines |> String.concat "\n" + // The inheritdoc element remains because the cref target doesn't exist + Assert.Contains("inheritdoc", xmlText) + | _ -> failwith "Expected FromXmlText" + + [] + let ``inheritdoc preserves surrounding doc elements``() = + let code = """ +module Test + +/// Base summary +type BaseType() = class end + +/// My own summary +/// +/// My own remarks +type DerivedType() = class end +""" + let xmlText = getEntityXmlText code "DerivedType" + Assert.Contains("My own summary", xmlText) + Assert.Contains("My own remarks", xmlText) + Assert.Contains("Base summary", xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + + [] + let ``inheritdoc with malformed XML should not crash``() = + let code = """ +module Test + +/// Malformed unclosed tag +/// +type MyType() = class end + +/// Base docs +type BaseType() = class end +""" + let _, checkResults = getParseAndCheckResults code + let symbol = findSymbolByName "MyType" checkResults + let xmlDoc = (symbol :?> FSharpEntity).XmlDoc + // Should not crash; malformed XML means original doc is returned unchanged + match xmlDoc with + | FSharpXmlDoc.FromXmlText t -> + let xmlText = t.UnprocessedLines |> String.concat "\n" + // Original doc preserved because XML parsing failed + Assert.Contains("Malformed", xmlText) + | _ -> failwith "Expected FromXmlText" + + [] + let ``inheritdoc with invalid XPath should not crash``() = + let code = """ +module Test + +/// Base type docs +type BaseType() = class end + +/// +type DerivedType() = class end +""" + let _, checkResults = getParseAndCheckResults code + let symbol = findSymbolByName "DerivedType" checkResults + let xmlDoc = (symbol :?> FSharpEntity).XmlDoc + // Should not crash; invalid XPath falls back to using the full inherited doc + match xmlDoc with + | FSharpXmlDoc.FromXmlText t -> + let xmlText = t.UnprocessedLines |> String.concat "\n" + Assert.Contains("Base type docs", xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + | _ -> failwith "Expected FromXmlText" + + [] + let ``inheritdoc with field cref should resolve``() = + let code = """ +module Test + +type Config = + /// The database connection string + static val mutable ConnectionString: string + +/// +type Settings() = class end +""" + let xmlText = getEntityXmlText code "Settings" + Assert.Contains("The database connection string", xmlText) + Assert.DoesNotContain("inheritdoc", xmlText) + [] let ``Discriminated Union - triple slash after case definition should warn``(): unit = checkSignatureAndImplementationWithWarnOn3879 """ diff --git a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl index fef4a9e4842..6f5900556a5 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl @@ -40,9 +40,14 @@ [IL]: Error [StackUnexpected]: : .$ServiceLexing+clo::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000082][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$ServiceLexing+clo::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000008B][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$ServiceLexing+clo::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000094][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : FSharp.Compiler.Symbols.Impl::parseNestedTypeAlternativePath(string)][offset 0x00000094][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : FSharp.Compiler.Symbols.Impl::parseCref(string)][offset 0x000000F5][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$Symbols+fullName::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000015][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.CreateILModule+MainModuleBuilder::ConvertProductVersionToILVersionInfo(string)][offset 0x00000011][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.StaticLinking+TypeForwarding::followTypeForwardForILTypeRef([FSharp.Compiler.Service]FSharp.Compiler.AbstractIL.IL+ILTypeRef)][offset 0x00000010][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : FSharp.Compiler.Symbols.XmlDocSigParser::parseDocCommentId(string)][offset 0x00000319][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : FSharp.Compiler.Symbols.XmlDocSigParser::parseDocCommentId(string)][offset 0x000002DD][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : FSharp.Compiler.Symbols.XmlDocSigParser::parseDocCommentId(string)][offset 0x000000D9][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.CompilerOptions::getCompilerOption([FSharp.Compiler.Service]FSharp.Compiler.CompilerOptions+CompilerOption, [FSharp.Core]Microsoft.FSharp.Core.FSharpOption`1)][offset 0x000000E6][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.CompilerOptions::AddPathMapping([FSharp.Compiler.Service]FSharp.Compiler.CompilerConfig+TcConfigBuilder, string)][offset 0x0000000B][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.CompilerOptions::subSystemVersionSwitch([FSharp.Compiler.Service]FSharp.Compiler.CompilerConfig+TcConfigBuilder, string)][offset 0x00000030][found Char] Unexpected type on the stack. diff --git a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl index eb7eb083796..8e2bb4b7205 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl @@ -39,11 +39,16 @@ [IL]: Error [StackUnexpected]: : .$ServiceLexing+clo::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000064][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$ServiceLexing+clo::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000006D][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$ServiceLexing+clo::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000076][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : FSharp.Compiler.Symbols.Impl::parseNestedTypeAlternativePath(string)][offset 0x00000089][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : FSharp.Compiler.Symbols.Impl::parseCref(string)][offset 0x000000DD][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$Symbols+fullName::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000030][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.Driver+ProcessCommandLineFlags::Invoke(string)][offset 0x0000000B][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.Driver+ProcessCommandLineFlags::Invoke(string)][offset 0x00000014][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.CreateILModule+MainModuleBuilder::ConvertProductVersionToILVersionInfo(string)][offset 0x00000010][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.StaticLinking+TypeForwarding::followTypeForwardForILTypeRef([FSharp.Compiler.Service]FSharp.Compiler.AbstractIL.IL+ILTypeRef)][offset 0x00000010][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : FSharp.Compiler.Symbols.XmlDocSigParser::parseDocCommentId(string)][offset 0x00000120][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : FSharp.Compiler.Symbols.XmlDocSigParser::parseDocCommentId(string)][offset 0x000000EC][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : FSharp.Compiler.Symbols.XmlDocSigParser::parseDocCommentId(string)][offset 0x000000A7][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.CompilerOptions::getCompilerOption([FSharp.Compiler.Service]FSharp.Compiler.CompilerOptions+CompilerOption, [FSharp.Core]Microsoft.FSharp.Core.FSharpOption`1)][offset 0x000000A7][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.CompilerOptions::AddPathMapping([FSharp.Compiler.Service]FSharp.Compiler.CompilerConfig+TcConfigBuilder, string)][offset 0x0000000B][found Char] Unexpected type on the stack. [IL]: Error [StackUnderflow]: : FSharp.Compiler.CompilerOptions::DoWithColor([System.Console]System.ConsoleColor, [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2)][offset 0x0000005E] Stack underflow. diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 99b3ff241ad..74df6fb7f75 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -960,99 +960,46 @@ type FSharpCrossLanguageSymbolNavigationService() = else entitiesByXmlSig + /// Convert a documentation comment ID to a navigation path. + /// Uses the shared XmlDocSigParser from FSharp.Compiler.Symbols. static member internal DocCommentIdToPath(docId: string) = - // The groups are following: - // 1 - type (see below). - // 2 - Path - a dotted path to a symbol. - // 3 - parameters, optional, only for methods and properties. - // 4 - return type, optional, only for methods. - let docCommentIdRx = - Regex(@"^(?\w):(?[\w\d#`.]+)(?\(.+\))?(?:~([\w\d.]+))?$", RegexOptions.Compiled) - - // Parse generic args out of the function name - let fnGenericArgsRx = - Regex(@"^(?.+)``(?\d+)$", RegexOptions.Compiled) - // docCommentId is in the following format: - // - // "T:" prefix for types - // "T:N.X.Nested" - type - // "T:N.X.D" - delegate - // - // "M:" prefix is for methods - // "M:N.X.#ctor" - constructor - // "M:N.X.#ctor(System.Int32)" - constructor with one parameter - // "M:N.X.f" - method with unit parameter - // "M:N.X.bb(System.String,System.Int32@)" - method with two parameters - // "M:N.X.gg(System.Int16[],System.Int32[0:,0:])" - method with two parameters, 1d and 2d array - // "M:N.X.op_Addition(N.X,N.X)" - operator - // "M:N.X.op_Explicit(N.X)~System.Int32" - operator with return type - // "M:N.GenericMethod.WithNestedType``1(N.GenericType{``0}.NestedType)" - generic type with one parameter - // "M:N.GenericMethod.WithIntOfNestedType``1(N.GenericType{System.Int32}.NestedType)" - generic type with one parameter - // "M:N.X.N#IX{N#KVP{System#String,System#Int32}}#IXA(N.KVP{System.String,System.Int32})" - explicit interface implementation - // - // "E:" prefix for events - // - // "E:N.X.d". - // - // "F:" prefix for fields - // "F:N.X.q" - field - // - // "P:" prefix for properties - // "P:N.X.prop" - property with getter and setter - - let m = docCommentIdRx.Match(docId) - let t = m.Groups["kind"].Value - - match m.Success, t with - | true, ("M" | "P" | "E") -> - // TODO: Probably, there's less janky way of dealing with those. - let parts = m.Groups["entity"].Value.Split('.') - let entityPath = parts[.. (parts.Length - 2)] |> List.ofArray - let memberOrVal = parts[parts.Length - 1] - - // Try and parse generic params count from the name (e.g. NameOfTheFunction``1, where ``1 is amount of type parameters) - let genericM = fnGenericArgsRx.Match(memberOrVal) - - let (memberOrVal, genericParametersCount) = - if genericM.Success then - (genericM.Groups["entity"].Value, int genericM.Groups["typars"].Value) - else - memberOrVal, 0 - - // A hack/fixup for the constructor name (#ctor in doccommentid and ``.ctor`` in F#) - if memberOrVal = "#ctor" then - DocCommentId.Member( - { - EntityPath = entityPath - MemberOrValName = "``.ctor``" - GenericParameters = 0 - }, - SymbolMemberType.Constructor - ) - else - DocCommentId.Member( - { - EntityPath = entityPath - MemberOrValName = memberOrVal - GenericParameters = genericParametersCount - }, - (SymbolMemberType.FromString t) - ) - | true, "T" -> - let entityPath = m.Groups["entity"].Value.Split('.') |> List.ofArray - DocCommentId.Type entityPath - | true, "F" -> - let parts = m.Groups["entity"].Value.Split('.') - let entityPath = parts[.. (parts.Length - 2)] |> List.ofArray - let memberOrVal = parts[parts.Length - 1] + // Use the shared parser from FSharp.Compiler.Symbols + match XmlDocSigParser.parseDocCommentId docId with + | ParsedDocCommentId.Type path -> DocCommentId.Type path + + | ParsedDocCommentId.Member(typePath, memberName, genericArity, kind) -> + // Convert constructor name format (.ctor in parser, ``.ctor`` needed for F# lookup) + let memberOrValName = if memberName = ".ctor" then "``.ctor``" else memberName + + let symbolMemberType = + match kind with + | DocCommentIdKind.Method -> + if memberName = ".ctor" then + SymbolMemberType.Constructor + else + SymbolMemberType.Method + | DocCommentIdKind.Property -> SymbolMemberType.Property + | DocCommentIdKind.Event -> SymbolMemberType.Event + | _ -> SymbolMemberType.Other + + DocCommentId.Member( + { + EntityPath = typePath + MemberOrValName = memberOrValName + GenericParameters = genericArity + }, + symbolMemberType + ) + | ParsedDocCommentId.Field(typePath, fieldName) -> DocCommentId.Field { - EntityPath = entityPath - MemberOrValName = memberOrVal + EntityPath = typePath + MemberOrValName = fieldName GenericParameters = 0 } - | _ -> DocCommentId.None + + | ParsedDocCommentId.None -> DocCommentId.None interface IFSharpCrossLanguageSymbolNavigationService with member _.TryGetNavigableLocationAsync