diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 6212188b65f..8d35f19996f 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -1,6 +1,7 @@ ### Added * F# Interactive gains a JSON-RPC server mode, `--fsi-server-jsonrpc:`, in which a host submits interactions over a named pipe and receives structured results — diagnostics with positions, escaping exceptions, the values each interaction bound, and the session's own process id — instead of recovering them by looking for a `SERVER-PROMPT>` marker in the output text. Program output continues to flow through the redirected console streams. The pipe admits only the user running the session; `--fsi-server-client-pid:` names the host process whose exit ends the session. `FsiEvaluationSession` exposes both options as `JsonRpcServerPipeName` and `JsonRpcClientProcessId`. The mode is part of the .NET fsi only. ([PR #20396](https://github.com/dotnet/fsharp/pull/20396)) +* `XmlDoc` keeps the range of every `///` line (`LineRanges`) and lists the `name` and `cref` attribute values it contains with their source ranges (`GetRefs`). The checker reports each ``, ``, `` and `` name that matches a parameter or type parameter of the documented declaration as a `RelatedSymbolUseKind.XmlDocParameter` use, which `GetUsesOfSymbolInFile` returns only when asked for and Find All References and semantic classification never see. ([Issue #20630](https://github.com/dotnet/fsharp/issues/20630), [Issue #15134](https://github.com/dotnet/fsharp/issues/15134), [PR #20637](https://github.com/dotnet/fsharp/pull/20637)) ### Fixed diff --git a/src/Compiler/Checking/CheckDeclarations.fs b/src/Compiler/Checking/CheckDeclarations.fs index 05ce83c5f6b..21a7e97c3b2 100644 --- a/src/Compiler/Checking/CheckDeclarations.fs +++ b/src/Compiler/Checking/CheckDeclarations.fs @@ -597,6 +597,16 @@ module TcRecdUnionAndEnumDeclarations = let checkXmlDocs = cenv.diagnosticOptions.CheckXmlDocs let xmlDoc = xmldoc.ToXmlDoc(checkXmlDocs, Some names) + + match parent with + | Parent tcref -> + let fields = + [ for i, f in List.indexed rfields do + if not f.rfield_name_generated then + f.LogicalName, Item.UnionCaseField (UnionCaseInfo (thisTyInst, UnionCaseRef (tcref, id.idText)), i) ] + ReportXmlDocRefUses cenv.tcSink xmlDoc fields [] + | ParentNone -> () + let attrs, getFinalAttrs, _ = TcAttributesCanFail cenv env AttributeTargets.UnionCaseDecl synAttrs let unionCase = Construct.NewUnionCase id rfields recordTy attrs xmlDoc vis @@ -2935,6 +2945,7 @@ module EstablishTypeDefinitionCores = let checkXmlDocs = cenv.diagnosticOptions.CheckXmlDocs let xmlDoc = xmlDoc.ToXmlDoc(checkXmlDocs, Some paramNames ) + ReportXmlDocRefUses cenv.tcSink xmlDoc [] [ for tp in checkedTypars -> tp.Name, Item.TypeVar(tp.Name, tp) ] Construct.NewTycon (cpath, id.idText, id.idRange, vis, visOfRepr, TyparKind.Type, LazyWithContext.NotLazy checkedTypars, xmlDoc, preferPostfix, preEstablishedHasDefaultCtor, hasSelfReferentialCtor, lmodTy) diff --git a/src/Compiler/Checking/CheckIncrementalClasses.fs b/src/Compiler/Checking/CheckIncrementalClasses.fs index 8e1c4636c6f..9f8d3900c88 100644 --- a/src/Compiler/Checking/CheckIncrementalClasses.fs +++ b/src/Compiler/Checking/CheckIncrementalClasses.fs @@ -204,6 +204,11 @@ let TcImplicitCtorInfo_Phase2A(cenv: cenv, env, tpenv, tcref: TyconRef, vis, att let checkXmlDocs = cenv.diagnosticOptions.CheckXmlDocs let xmlDoc = xmlDoc.ToXmlDoc(checkXmlDocs, Some paramNames) let ctorVal = MakeAndPublishVal cenv env (Parent tcref, false, ModuleOrMemberBinding, ValInRecScope isComplete, ctorValScheme, attribs, xmlDoc, None, false) + + // The `` tags of a primary constructor live on the type's doc + let ctorParameters = [ for v in ctorArgs -> v.LogicalName, Item.Value(mkLocalValRef v) ] + ReportXmlDocRefUses cenv.tcSink xmlDoc ctorParameters [] + ReportXmlDocRefUses cenv.tcSink tcref.Deref.XmlDoc ctorParameters [] ctorValScheme, ctorVal let thisVal = diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index aaca860a630..2fa469f9ca7 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -11759,6 +11759,18 @@ and TcNormalizedBinding declKind (cenv: cenv) env tpenv overallTy safeThisValOpt if isFixed then TcAndBuildFixedExpr cenv env (overallPatTy, rhsExprChecked, overallExprTy, mBinding) else rhsExprChecked + // The parameters of a function binding are the binders of its outer lambda chain + let rec parameterVals expr = + match stripDebugPoints expr with + | Expr.Lambda (_, _, _, vs, body, _, _) -> + [ for v in vs do + if not (v.IsMemberThisVal || v.IsCtorThisVal || v.IsCompilerGenerated) then + v.LogicalName, Item.Value(mkLocalValRef v) ] + @ parameterVals body + | _ -> [] + + ReportXmlDocRefUses cenv.tcSink xmlDoc (parameterVals rhsExprChecked) [ for tp in declaredTypars -> tp.Name, Item.TypeVar(tp.Name, tp) ] + match apinfoOpt with | Some (apinfo, apOverallTy, m) -> let activePatResTys = NewInferenceTypes g apinfo.ActiveTags @@ -13665,9 +13677,10 @@ let private PublishArguments (cenv: cenv) (env: TcEnv) vspec (synValSig: SynValS |> Seq.collect (fun x -> x ||> Seq.zip) |> Seq.choose (fun (synArgInfo, argInfo) -> synArgInfo.Ident |> Option.map (pair argInfo)) - for (argTy, argReprInfo), ident in argData do + [ for (argTy, argReprInfo), ident in argData do let item = Item.OtherName (Some ident, argTy, Some argReprInfo, None, ident.idRange) CallNameResolutionSink cenv.tcSink (ident.idRange, env.NameEnv, item, emptyTyparInst, ItemOccurrence.Binding, env.AccessRights) + ident.idText, item ] let TcAndPublishValSpec (cenv: cenv, env, containerInfo: ContainerInfo, declKind : DeclKind, memFlagsOpt, tpenv, synValSig) = @@ -13756,7 +13769,8 @@ let TcAndPublishValSpec (cenv: cenv, env, containerInfo: ContainerInfo, declKind let vspec = MakeAndPublishVal cenv env (altActualParent, true, declKind, ValNotInRecScope, valscheme, attrs, xmlDoc, literalValue, isGeneratedEventVal) - PublishArguments cenv env vspec synValSig allDeclaredTypars.Length + let parameters = PublishArguments cenv env vspec synValSig allDeclaredTypars.Length + ReportXmlDocRefUses cenv.tcSink xmlDoc parameters [ for tp in allDeclaredTypars -> tp.Name, Item.TypeVar(tp.Name, tp) ] assert(vspec.InlineInfo = inlineFlag) diff --git a/src/Compiler/Checking/NameResolution.fs b/src/Compiler/Checking/NameResolution.fs index a80bbe04c65..a917d1fc370 100644 --- a/src/Compiler/Checking/NameResolution.fs +++ b/src/Compiler/Checking/NameResolution.fs @@ -4,6 +4,7 @@ /// Name environment and name resolution module internal FSharp.Compiler.NameResolution +open System open System.Collections.Generic open Internal.Utilities.Collections @@ -33,6 +34,7 @@ open FSharp.Compiler.TypedTree open FSharp.Compiler.TypedTreeBasics open FSharp.Compiler.TypedTreeOps open FSharp.Compiler.TypeHierarchy +open FSharp.Compiler.Xml #if !NO_TYPEPROVIDERS open FSharp.Compiler.TypeProviders @@ -2653,6 +2655,25 @@ let CallRelatedSymbolSink (sink: TcResultsSink) (m: range, item: Item, kind: Rel | None -> () | Some currentSink -> currentSink.NotifyRelatedSymbolUse(m, item, kind) +/// Report each ``/``/``/`` of a declaration's XML doc +/// as a related use of the parameter or type parameter it names, at the range of the attribute value. +let ReportXmlDocRefUses (sink: TcResultsSink) (doc: XmlDoc) (parameters: (string * Item) list) (typars: (string * Item) list) = + match sink.CurrentSink with + | Some currentSink when doc.NonEmpty && (not parameters.IsEmpty || not typars.IsEmpty) -> + for docRef in doc.GetRefs() do + let candidates = + match docRef.Kind with + | XmlDocRefKind.Param + | XmlDocRefKind.ParamRef -> parameters + | XmlDocRefKind.TypeParam + | XmlDocRefKind.TypeParamRef -> typars + | XmlDocRefKind.Cref -> [] + + for name, item in candidates do + if String.Equals(name, docRef.Text, StringComparison.Ordinal) then + currentSink.NotifyRelatedSymbolUse(docRef.Range, item, RelatedSymbolUseKind.XmlDocParameter) + | _ -> () + /// Report a specific expression typing at a source range let CallExprHasTypeSink (sink: TcResultsSink) (m: range, nenv, ty, ad) = match sink.CurrentSink with diff --git a/src/Compiler/Checking/NameResolution.fsi b/src/Compiler/Checking/NameResolution.fsi index fac10862948..6e3716f3b72 100755 --- a/src/Compiler/Checking/NameResolution.fsi +++ b/src/Compiler/Checking/NameResolution.fsi @@ -14,6 +14,7 @@ open FSharp.Compiler.Text open FSharp.Compiler.TypedTree open FSharp.Compiler.TypedTreeOps open FSharp.Compiler.TcGlobals +open FSharp.Compiler.Xml exception NoConstructorsAvailableForType of TType * DisplayEnv * range @@ -654,6 +655,11 @@ val internal RegisterUnionCaseTesterForProperty: TcResultsSink -> identRange: ra /// Report a related symbol use at a source range (does not affect colorization or symbol info) val internal CallRelatedSymbolSink: TcResultsSink -> range * Item * RelatedSymbolUseKind -> unit +/// Report each ``/``/``/`` of a declaration's XML doc +/// as a related use of the parameter or type parameter it names, at the range of the attribute value. +val internal ReportXmlDocRefUses: + TcResultsSink -> doc: XmlDoc -> parameters: (string * Item) list -> typars: (string * Item) list -> unit + /// Report a specific name resolution at a source range val internal CallExprHasTypeSink: TcResultsSink -> range * NameResolutionEnv * TType * AccessorDomain -> unit diff --git a/src/Compiler/Checking/RelatedSymbolUse.fs b/src/Compiler/Checking/RelatedSymbolUse.fs index 2c120294dba..c12e8c12580 100644 --- a/src/Compiler/Checking/RelatedSymbolUse.fs +++ b/src/Compiler/Checking/RelatedSymbolUse.fs @@ -13,5 +13,8 @@ type RelatedSymbolUseKind = | UnionCaseTester = 1 /// Record type via copy-and-update expression (e.g., { r with ... } → RecordType) | CopyAndUpdateRecord = 2 + /// Parameter or type parameter via the `name` of a `param`, `paramref`, `typeparam` or `typeparamref` + /// tag in the declaration's XML doc + | XmlDocParameter = 4 /// All related symbol kinds | All = 0x7FFFFFFF diff --git a/src/Compiler/Service/IncrementalBuild.fs b/src/Compiler/Service/IncrementalBuild.fs index f68f3495547..a878bb42408 100644 --- a/src/Compiler/Service/IncrementalBuild.fs +++ b/src/Compiler/Service/IncrementalBuild.fs @@ -343,9 +343,10 @@ type BoundModel private ( if not r.IsSynthetic && preventDuplicates.Add struct(r.Start, r.End) then builder.Write(cnr.Range, cnr.Item)) + // A name inside a `///` comment is for rename and highlighting, not for symbol search sResolutions.CapturedRelatedSymbolUses - |> Seq.iter (fun (m, item, _kind) -> - if not m.IsSynthetic then + |> Seq.iter (fun (m, item, kind) -> + if not m.IsSynthetic && kind <> RelatedSymbolUseKind.XmlDocParameter then builder.Write(m, item)) let semanticClassification = sResolutions.GetSemanticClassification(tcGlobals, tcImports.GetImportMap(), sink.GetFormatSpecifierLocations(), None, RelatedSymbolUseKind.All) diff --git a/src/Compiler/Service/SemanticClassification.fs b/src/Compiler/Service/SemanticClassification.fs index 1bf5a752e18..9d91adc4972 100644 --- a/src/Compiler/Service/SemanticClassification.fs +++ b/src/Compiler/Service/SemanticClassification.fs @@ -455,7 +455,8 @@ module TcResolutionsExtensions = match relatedSymbolKinds with | Some kinds -> for (m, item, kind) in sResolutions.CapturedRelatedSymbolUses do - if kinds.HasFlag kind then + // A name inside a `///` comment is never classified, whatever the caller asked for + if kinds.HasFlag kind && kind <> RelatedSymbolUseKind.XmlDocParameter then match range, item with | Some r, _ when not (rangeContainsPos r m.Start || rangeContainsPos r m.End) -> () | _, Item.UnionCase _ -> results.Add(SemanticClassificationItem((m, SemanticClassificationType.UnionCase))) diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs index eef56443bf6..ba8aba2cb6e 100644 --- a/src/Compiler/Service/TransparentCompiler.fs +++ b/src/Compiler/Service/TransparentCompiler.fs @@ -2094,9 +2094,10 @@ type internal TransparentCompiler if not r.IsSynthetic && preventDuplicates.Add struct (r.Start, r.End) then builder.Write(cnr.Range, cnr.Item)) + // A name inside a `///` comment is for rename and highlighting, not for symbol search sResolutions.CapturedRelatedSymbolUses - |> Seq.iter (fun (m, item, _kind) -> - if not m.IsSynthetic then + |> Seq.iter (fun (m, item, kind) -> + if not m.IsSynthetic && kind <> RelatedSymbolUseKind.XmlDocParameter then builder.Write(m, item)) builder.TryBuildAndReset()) diff --git a/src/Compiler/SyntaxTree/XmlDoc.fs b/src/Compiler/SyntaxTree/XmlDoc.fs index 3a987257a17..8d7c2d2a5d2 100644 --- a/src/Compiler/SyntaxTree/XmlDoc.fs +++ b/src/Compiler/SyntaxTree/XmlDoc.fs @@ -12,10 +12,150 @@ open Internal.Utilities.Collections open FSharp.Compiler.DiagnosticsLogger open FSharp.Compiler.IO open FSharp.Compiler.Text +open FSharp.Compiler.Text.Position open FSharp.Compiler.Text.Range +/// The tag an XML doc attribute value belongs to +[] +type XmlDocRefKind = + | Param + | ParamRef + | TypeParam + | TypeParamRef + | Cref + +/// An attribute value in an XML doc that names something else: a `name` or a `cref` +[] +type XmlDocRef = + { + Kind: XmlDocRefKind + Text: string + Range: range + } + +module private XmlDocRefScanner = + + let private tags = + [| + "typeparamref", XmlDocRefKind.TypeParamRef, "name" + "typeparam", XmlDocRefKind.TypeParam, "name" + "paramref", XmlDocRefKind.ParamRef, "name" + "param", XmlDocRefKind.Param, "name" + "permission", XmlDocRefKind.Cref, "cref" + "exception", XmlDocRefKind.Cref, "cref" + "seealso", XmlDocRefKind.Cref, "cref" + "see", XmlDocRefKind.Cref, "cref" + |] + + /// The three slashes of `///` are not part of the stored line + let private lineTextOffset = 3 + + let private isNameEnd (text: string) i = + i >= text.Length || Char.IsWhiteSpace text[i] || text[i] = '/' || text[i] = '>' + + let private tagAt (text: string) i = + tags + |> Array.tryFind (fun (tag, _, _) -> + String.CompareOrdinal(text, i, tag, 0, tag.Length) = 0 + && isNameEnd text (i + tag.Length)) + + let private skipWhiteSpace (text: string) i = + let mutable i = i + + while i < text.Length && Char.IsWhiteSpace text[i] do + i <- i + 1 + + i + + let private attributeNameEnd (text: string) i = + let mutable i = i + + while not (isNameEnd text i) && text[i] <> '=' do + i <- i + 1 + + i + + /// The quoted value of `attribute` in the tag whose attributes start at `start`, as an offset and a length in `text`. + /// Walks the attributes one at a time, so an attribute whose name ends in `attribute` and a `>` inside a quoted value + /// are not mistaken for it. A value that runs over a line break has no single-line range and is skipped. + let private attributeValue (text: string) (attribute: string) (start: int) = + let rec next i = + let nameStart = skipWhiteSpace text i + + if nameStart >= text.Length || text[nameStart] = '>' || text[nameStart] = '/' then + ValueNone + else + let nameEnd = attributeNameEnd text nameStart + let equals = skipWhiteSpace text nameEnd + + if equals >= text.Length || text[equals] <> '=' then + ValueNone + else + let quote = skipWhiteSpace text (equals + 1) + + if quote >= text.Length || (text[quote] <> '"' && text[quote] <> '\'') then + ValueNone + else + match text.IndexOf(text[quote], quote + 1) with + | -1 -> ValueNone + | quoteEnd -> + let valueStart = quote + 1 + let valueLength = quoteEnd - valueStart + + if + nameEnd - nameStart = attribute.Length + && String.CompareOrdinal(text, nameStart, attribute, 0, attribute.Length) = 0 + then + if text.IndexOf('\n', valueStart, valueLength) >= 0 then + ValueNone + else + ValueSome struct (valueStart, valueLength) + else + next (quoteEnd + 1) + + next start + + /// Scans the lines as one text, since a tag and its attributes may run over several `///` lines + let scan (lines: string[]) (lineRanges: range[]) = + let text = String.Join("\n", lines) + let lineStarts = Array.zeroCreate lines.Length + + for i in 1 .. lines.Length - 1 do + lineStarts[i] <- lineStarts[i - 1] + lines[i - 1].Length + 1 + + [| + let mutable lt = text.IndexOf '<' + + while lt >= 0 do + match tagAt text (lt + 1) with + | Some(tag, kind, attribute) -> + match attributeValue text attribute (lt + 1 + tag.Length) with + | ValueSome struct (offset, length) -> + let line = + match Array.BinarySearch(lineStarts, offset) with + | i when i >= 0 -> i + | i -> ~~~i - 1 + + let m = lineRanges[line] + let column = m.StartColumn + lineTextOffset + offset - lineStarts[line] + + { + Kind = kind + Text = text.Substring(offset, length) + Range = mkFileIndexRange m.FileIndex (mkPos m.StartLine column) (mkPos m.StartLine (column + length)) + } + | ValueNone -> () + | None -> () + + lt <- text.IndexOf('<', lt + 1) + |] + /// Represents collected XmlDoc lines -type XmlDoc(unprocessedLines: string[], range: range) = +type XmlDoc(unprocessedLines: string[], lineRanges: range[], range: range) = + do + if lineRanges.Length <> 0 && lineRanges.Length <> unprocessedLines.Length then + invalidArg (nameof lineRanges) "one range per line, or none" + let rec processLines (lines: string list) = match lines with | [] -> [] @@ -31,6 +171,8 @@ type XmlDoc(unprocessedLines: string[], range: range) = @ (lines |> List.map Internal.Utilities.XmlAdapters.escape) @ [ "" ] + new(unprocessedLines: string[], range: range) = XmlDoc(unprocessedLines, [||], range) + /// Get the lines before insertion of implicit summary tags and encoding member _.UnprocessedLines = unprocessedLines @@ -44,6 +186,16 @@ type XmlDoc(unprocessedLines: string[], range: range) = member _.Range = range + /// The source range of each unprocessed line; empty when the doc did not come from source + member _.LineRanges = lineRanges + + /// The `name` and `cref` attribute values in the doc, with their source ranges; empty without line ranges + member _.GetRefs() = + if lineRanges.Length = 0 then + [||] + else + XmlDocRefScanner.scan unprocessedLines lineRanges + static member Empty = XmlDocStatics.Empty member _.IsEmpty = unprocessedLines |> Array.forall String.IsNullOrWhiteSpace @@ -56,7 +208,16 @@ type XmlDoc(unprocessedLines: string[], range: range) = elif doc2.IsEmpty then doc1.Range else unionRanges doc1.Range doc2.Range - XmlDoc(Array.append doc1.UnprocessedLines doc2.UnprocessedLines, range) + let lineRanges = + if + doc1.LineRanges.Length = doc1.UnprocessedLines.Length + && doc2.LineRanges.Length = doc2.UnprocessedLines.Length + then + Array.append doc1.LineRanges doc2.LineRanges + else + [||] + + XmlDoc(Array.append doc1.UnprocessedLines doc2.UnprocessedLines, lineRanges, range) member doc.GetXmlText() = if doc.IsEmpty then @@ -241,8 +402,9 @@ type PreXmlDoc = XmlDoc.Empty else let lines = Array.map fst preLines - let m = Array.reduce unionRanges (Array.map snd preLines) - let doc = XmlDoc(lines, m) + let lineRanges = Array.map snd preLines + let m = Array.reduce unionRanges lineRanges + let doc = XmlDoc(lines, lineRanges, m) if check then doc.Check(paramNamesOpt) diff --git a/src/Compiler/SyntaxTree/XmlDoc.fsi b/src/Compiler/SyntaxTree/XmlDoc.fsi index 619d6be53cd..1942bd11819 100644 --- a/src/Compiler/SyntaxTree/XmlDoc.fsi +++ b/src/Compiler/SyntaxTree/XmlDoc.fsi @@ -4,12 +4,35 @@ namespace FSharp.Compiler.Xml open FSharp.Compiler.Text +/// The tag an XML doc attribute value belongs to +[] +type XmlDocRefKind = + | Param + | ParamRef + | TypeParam + | TypeParamRef + | Cref + +/// An attribute value in an XML doc that names something else: a `name` or a `cref` +[] +type XmlDocRef = + { + Kind: XmlDocRefKind + /// The attribute value as written + Text: string + /// The source range of the value, without its quotes + Range: range + } + /// Represents collected XmlDoc lines [] type public XmlDoc = new: unprocessedLines: string[] * range: range -> XmlDoc + /// Lines with their source ranges; one range per line, or none when the doc did not come from source + new: unprocessedLines: string[] * lineRanges: range[] * range: range -> XmlDoc + /// Merge two XML documentation static member Merge: doc1: XmlDoc -> doc2: XmlDoc -> XmlDoc @@ -37,6 +60,13 @@ type public XmlDoc = /// Indicates the overall original source range of the XmlDoc member Range: range + /// The source range of each unprocessed line; empty when the doc did not come from source + member LineRanges: range[] + + /// The `name` and `cref` attribute values of `param`, `paramref`, `typeparam`, `typeparamref`, + /// `see`, `seealso`, `exception` and `permission` tags, with their source ranges. Empty without line ranges. + member GetRefs: unit -> XmlDocRef[] + /// Get the lines before insertion of implicit summary tags and encoding member UnprocessedLines: string[] diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/FindReferences.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/FindReferences.fs index d4a070d51f3..441e2c91afa 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/FindReferences.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/FindReferences.fs @@ -1066,6 +1066,156 @@ let count = list.Count $"Assembly should be a system assembly, got: {assembly.SimpleName}")) } +/// https://github.com/dotnet/fsharp/issues/20630 https://github.com/dotnet/fsharp/issues/15134 +module XmlDocParameters = + + let private rangesOf (uses: FSharpSymbolUse seq) = + uses + |> Seq.map (fun su -> su.Range.StartLine, su.Range.StartColumn, su.Range.EndColumn) + |> Seq.sort + |> Seq.toList + + /// The doc `name` ranges show up only when asked for as XmlDocParameter uses, never by default + let private expectDocUses source (symbolAt: FSharpSymbolUse -> bool) codeRanges docRanges = + checkAllSymbols source (fun result allUses -> + let symbol = (allUses |> Seq.find symbolAt).Symbol + + result.GetUsesOfSymbolInFile symbol |> rangesOf |> fun actual -> Assert.Equal<(int * int * int) list>(codeRanges, actual) + + result.GetUsesOfSymbolInFile(symbol, relatedSymbolKinds = RelatedSymbolUseKind.XmlDocParameter) + |> rangesOf + |> fun actual -> Assert.Equal<(int * int * int) list>(List.sort (codeRanges @ docRanges), actual) + + // A caller that asks for everything else still gets nothing from the docs + let otherKinds = RelatedSymbolUseKind.UnionCaseTester ||| RelatedSymbolUseKind.CopyAndUpdateRecord + + result.GetUsesOfSymbolInFile(symbol, relatedSymbolKinds = otherKinds) + |> rangesOf + |> fun actual -> Assert.Equal<(int * int * int) list>(codeRanges, actual)) + + // SyntheticProject puts its own `module` header on line 1; a source starting with a newline begins on line 3 + let private definitionNamed name (su: FSharpSymbolUse) = + su.IsFromDefinition && su.Symbol.DisplayName = name + + let private definitionAt line column (su: FSharpSymbolUse) = + su.IsFromDefinition && su.Range.StartLine = line && su.Range.StartColumn = column + + // A type parameter's declaration is reported as a use in a type, not as a definition + let private typarNamed name line (su: FSharpSymbolUse) = + match su.Symbol with + | :? FSharp.Compiler.Symbols.FSharpGenericParameter as p -> p.Name = name && su.Range.StartLine = line + | _ -> false + + [] + let ``param and paramref of a let-bound function`` () = + let source = """ +/// Adds. +/// The first number. +/// Added to . +let add x y = x + y +""" + expectDocUses source (definitionNamed "x") [ (6, 8, 9); (6, 14, 15) ] [ (4, 17, 18); (5, 45, 46) ] + + [] + let ``typeparam and typeparamref of an explicitly generic function`` () = + let source = """ +/// The element type, see . +/// The value. +let id<'T> (x: 'T) = x +""" + expectDocUses source (typarNamed "T" 5) [ (5, 7, 9); (5, 15, 17) ] [ (3, 21, 22); (3, 66, 67) ] + + [] + let ``param of a member, not confused with this`` () = + let source = """ +type C() = + /// What to keep. + member this.Keep(value: int) = value +""" + expectDocUses source (definitionNamed "value") [ (5, 21, 26); (5, 35, 40) ] [ (4, 21, 26) ] + + [] + let ``param of a primary constructor documented on the type`` () = + let source = """ +/// A holder. +/// The initial value. +type Holder(seed: int) = + member _.Seed = seed +""" + expectDocUses source (definitionNamed "seed") [ (5, 12, 16); (6, 20, 24) ] [ (4, 17, 21) ] + + [] + let ``typeparam of a generic type`` () = + let source = """ +/// The payload. +type Box<'T>(value: 'T) = + member _.Value = value +""" + expectDocUses source (typarNamed "T" 4) [ (4, 9, 11); (4, 20, 22) ] [ (3, 21, 22) ] + + [] + let ``param of a union case field`` () = + let source = """ +type Shape = + /// Distance from the centre. + | Circle of radius: float +""" + expectDocUses source (definitionNamed "radius") [ (5, 16, 22) ] [ (4, 21, 27) ] + + [] + let ``param of a signature file val is found in code but its doc stays out of find all references`` () = + let signature = """ +/// The first number. +val add: x: int -> y: int -> int +""" + let implementation = "let add x y = x + y" + SyntheticProject.Create( + { sourceFile "Source" [] with Source = implementation; SignatureFile = Custom signature }) + .Workflow { + placeCursor "Source" "x" + findAllReferences (expectToFind [ + "FileSource.fsi", 4, 9, 10 + "FileSource.fs", 2, 8, 9 + "FileSource.fs", 2, 14, 15 + ]) + } + + [] + let ``an unknown name in the doc reports nothing and does not break the others`` () = + let source = """ +/// Not a parameter. +/// The parameter. +let f x = x +""" + expectDocUses source (definitionNamed "x") [ (5, 6, 7); (5, 10, 11) ] [ (4, 17, 18) ] + + [] + let ``a duplicated param name reports every occurrence`` () = + let source = """ +/// Once. +/// Twice. +let f x = x +""" + expectDocUses source (definitionNamed "x") [ (5, 6, 7); (5, 10, 11) ] [ (3, 17, 18); (4, 17, 18) ] + + [] + let ``a backticked parameter is matched by its bare name`` () = + let source = """ +/// Spaced. +let f ``a b`` = ``a b`` +""" + expectDocUses source (definitionAt 4 6) [ (4, 6, 13); (4, 16, 23) ] [ (3, 17, 20) ] + + [] + let ``inheritdoc and include tags do not disturb the scan`` () = + let source = """ +/// +/// +/// Still found. +let f x = x +""" + expectDocUses source (definitionNamed "x") [ (6, 6, 7); (6, 10, 11) ] [ (5, 17, 18) ] + /// https://github.com/dotnet/fsharp/issues/16993 module CSharpExtensionMethods = diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SemanticClassificationRegressions.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SemanticClassificationRegressions.fs index d77b221db87..b25370565ec 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SemanticClassificationRegressions.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SemanticClassificationRegressions.fs @@ -74,6 +74,30 @@ let updated = { x with ValidationErrors = [] } ) } +/// (#20630) A parameter named in a `///` doc is reported as a related symbol use so that rename can +/// rewrite it, but nothing inside a comment may be coloured, even when the caller asks for every related kind. +[] +let ``XML doc param names are not classified`` () : Task = + task { + let source = + """ +module Test + +/// The first number. +/// Added to . +let add x y = x + y +""" + + let! items = getClassifications source + + let insideDocs = + items + |> Array.filter (fun item -> item.Range.StartLine = 4 || item.Range.StartLine = 5) + |> Array.map (fun i -> i.Range, i.Type) + + Assert.True(insideDocs.Length = 0, $"Nothing inside the doc comment should be classified, but found: %A{insideDocs}") + } + /// (#16621) Helper: assert UnionCase classifications on expected lines. /// Each entry is (line, expectedCount, maxRangeWidth). /// maxRangeWidth guards against dot-coloring regressions (range including "x." prefix). 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 bce0bc86e9b..b22a11c77f4 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 @@ -2623,6 +2623,7 @@ FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind: FSharp.Compiler.CodeAnalysis. FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind: FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind CopyAndUpdateRecord FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind: FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind None FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind: FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind UnionCaseTester +FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind: FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind XmlDocParameter FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind: Int32 value__ FSharp.Compiler.CodeAnalysis.TransparentCompiler.CacheSizes: Boolean Equals(FSharp.Compiler.CodeAnalysis.TransparentCompiler.CacheSizes) FSharp.Compiler.CodeAnalysis.TransparentCompiler.CacheSizes: Boolean Equals(FSharp.Compiler.CodeAnalysis.TransparentCompiler.CacheSizes, System.Collections.IEqualityComparer) @@ -12731,14 +12732,70 @@ FSharp.Compiler.Xml.XmlDoc: Boolean get_IsEmpty() FSharp.Compiler.Xml.XmlDoc: Boolean get_NonEmpty() FSharp.Compiler.Xml.XmlDoc: FSharp.Compiler.Text.Range Range FSharp.Compiler.Xml.XmlDoc: FSharp.Compiler.Text.Range get_Range() +FSharp.Compiler.Xml.XmlDoc: FSharp.Compiler.Text.Range[] LineRanges +FSharp.Compiler.Xml.XmlDoc: FSharp.Compiler.Text.Range[] get_LineRanges() FSharp.Compiler.Xml.XmlDoc: FSharp.Compiler.Xml.XmlDoc Empty FSharp.Compiler.Xml.XmlDoc: FSharp.Compiler.Xml.XmlDoc Merge(FSharp.Compiler.Xml.XmlDoc, FSharp.Compiler.Xml.XmlDoc) FSharp.Compiler.Xml.XmlDoc: FSharp.Compiler.Xml.XmlDoc get_Empty() +FSharp.Compiler.Xml.XmlDoc: FSharp.Compiler.Xml.XmlDocRef[] GetRefs() FSharp.Compiler.Xml.XmlDoc: System.String GetXmlText() FSharp.Compiler.Xml.XmlDoc: System.String[] GetElaboratedXmlLines() FSharp.Compiler.Xml.XmlDoc: System.String[] UnprocessedLines FSharp.Compiler.Xml.XmlDoc: System.String[] get_UnprocessedLines() FSharp.Compiler.Xml.XmlDoc: Void .ctor(System.String[], FSharp.Compiler.Text.Range) +FSharp.Compiler.Xml.XmlDoc: Void .ctor(System.String[], FSharp.Compiler.Text.Range[], FSharp.Compiler.Text.Range) +FSharp.Compiler.Xml.XmlDocRef: Boolean Equals(FSharp.Compiler.Xml.XmlDocRef) +FSharp.Compiler.Xml.XmlDocRef: Boolean Equals(FSharp.Compiler.Xml.XmlDocRef, System.Collections.IEqualityComparer) +FSharp.Compiler.Xml.XmlDocRef: Boolean Equals(System.Object) +FSharp.Compiler.Xml.XmlDocRef: Boolean Equals(System.Object, System.Collections.IEqualityComparer) +FSharp.Compiler.Xml.XmlDocRef: FSharp.Compiler.Text.Range Range +FSharp.Compiler.Xml.XmlDocRef: FSharp.Compiler.Text.Range get_Range() +FSharp.Compiler.Xml.XmlDocRef: FSharp.Compiler.Xml.XmlDocRefKind Kind +FSharp.Compiler.Xml.XmlDocRef: FSharp.Compiler.Xml.XmlDocRefKind get_Kind() +FSharp.Compiler.Xml.XmlDocRef: Int32 GetHashCode() +FSharp.Compiler.Xml.XmlDocRef: Int32 GetHashCode(System.Collections.IEqualityComparer) +FSharp.Compiler.Xml.XmlDocRef: System.String Text +FSharp.Compiler.Xml.XmlDocRef: System.String ToString() +FSharp.Compiler.Xml.XmlDocRef: System.String get_Text() +FSharp.Compiler.Xml.XmlDocRef: Void .ctor(FSharp.Compiler.Xml.XmlDocRefKind, System.String, FSharp.Compiler.Text.Range) +FSharp.Compiler.Xml.XmlDocRefKind+Tags: Int32 Cref +FSharp.Compiler.Xml.XmlDocRefKind+Tags: Int32 Param +FSharp.Compiler.Xml.XmlDocRefKind+Tags: Int32 ParamRef +FSharp.Compiler.Xml.XmlDocRefKind+Tags: Int32 TypeParam +FSharp.Compiler.Xml.XmlDocRefKind+Tags: Int32 TypeParamRef +FSharp.Compiler.Xml.XmlDocRefKind: Boolean Equals(FSharp.Compiler.Xml.XmlDocRefKind) +FSharp.Compiler.Xml.XmlDocRefKind: Boolean Equals(FSharp.Compiler.Xml.XmlDocRefKind, System.Collections.IEqualityComparer) +FSharp.Compiler.Xml.XmlDocRefKind: Boolean Equals(System.Object) +FSharp.Compiler.Xml.XmlDocRefKind: Boolean Equals(System.Object, System.Collections.IEqualityComparer) +FSharp.Compiler.Xml.XmlDocRefKind: Boolean IsCref +FSharp.Compiler.Xml.XmlDocRefKind: Boolean IsParam +FSharp.Compiler.Xml.XmlDocRefKind: Boolean IsParamRef +FSharp.Compiler.Xml.XmlDocRefKind: Boolean IsTypeParam +FSharp.Compiler.Xml.XmlDocRefKind: Boolean IsTypeParamRef +FSharp.Compiler.Xml.XmlDocRefKind: Boolean get_IsCref() +FSharp.Compiler.Xml.XmlDocRefKind: Boolean get_IsParam() +FSharp.Compiler.Xml.XmlDocRefKind: Boolean get_IsParamRef() +FSharp.Compiler.Xml.XmlDocRefKind: Boolean get_IsTypeParam() +FSharp.Compiler.Xml.XmlDocRefKind: Boolean get_IsTypeParamRef() +FSharp.Compiler.Xml.XmlDocRefKind: FSharp.Compiler.Xml.XmlDocRefKind Cref +FSharp.Compiler.Xml.XmlDocRefKind: FSharp.Compiler.Xml.XmlDocRefKind Param +FSharp.Compiler.Xml.XmlDocRefKind: FSharp.Compiler.Xml.XmlDocRefKind ParamRef +FSharp.Compiler.Xml.XmlDocRefKind: FSharp.Compiler.Xml.XmlDocRefKind TypeParam +FSharp.Compiler.Xml.XmlDocRefKind: FSharp.Compiler.Xml.XmlDocRefKind TypeParamRef +FSharp.Compiler.Xml.XmlDocRefKind: FSharp.Compiler.Xml.XmlDocRefKind get_Cref() +FSharp.Compiler.Xml.XmlDocRefKind: FSharp.Compiler.Xml.XmlDocRefKind get_Param() +FSharp.Compiler.Xml.XmlDocRefKind: FSharp.Compiler.Xml.XmlDocRefKind get_ParamRef() +FSharp.Compiler.Xml.XmlDocRefKind: FSharp.Compiler.Xml.XmlDocRefKind get_TypeParam() +FSharp.Compiler.Xml.XmlDocRefKind: FSharp.Compiler.Xml.XmlDocRefKind get_TypeParamRef() +FSharp.Compiler.Xml.XmlDocRefKind: FSharp.Compiler.Xml.XmlDocRefKind+Tags +FSharp.Compiler.Xml.XmlDocRefKind: Int32 CompareTo(FSharp.Compiler.Xml.XmlDocRefKind) +FSharp.Compiler.Xml.XmlDocRefKind: Int32 CompareTo(System.Object) +FSharp.Compiler.Xml.XmlDocRefKind: Int32 CompareTo(System.Object, System.Collections.IComparer) +FSharp.Compiler.Xml.XmlDocRefKind: Int32 GetHashCode() +FSharp.Compiler.Xml.XmlDocRefKind: Int32 GetHashCode(System.Collections.IEqualityComparer) +FSharp.Compiler.Xml.XmlDocRefKind: Int32 Tag +FSharp.Compiler.Xml.XmlDocRefKind: Int32 get_Tag() +FSharp.Compiler.Xml.XmlDocRefKind: System.String ToString() Internal.Utilities.Library.DelayInitArrayMap`3[T,TDictKey,TDictValue]: System.Collections.Generic.IDictionary`2[TDictKey,TDictValue] CreateDictionary(T[]) Internal.Utilities.Library.DelayInitArrayMap`3[T,TDictKey,TDictValue]: System.Collections.Generic.IDictionary`2[TDictKey,TDictValue] GetDictionary() Internal.Utilities.Library.DelayInitArrayMap`3[T,TDictKey,TDictValue]: T[] GetArray() diff --git a/tests/FSharp.Compiler.Service.Tests/XmlDocTests.fs b/tests/FSharp.Compiler.Service.Tests/XmlDocTests.fs index 82992cc3c96..5955beceddc 100644 --- a/tests/FSharp.Compiler.Service.Tests/XmlDocTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/XmlDocTests.fs @@ -205,6 +205,105 @@ let checkParsingErrors expected (parseResults: FSharpParseFileResults) = error, Line range.StartLine, Col range.StartColumn, Line range.EndLine, Col range.EndColumn, x.Message) |> shouldEqual expected +module XmlDocRefs = + + open FSharp.Compiler.Text + open FSharp.Compiler.Xml + + /// A doc whose `///` lines start at column 4 of consecutive lines from 10, as the lexer records them + let private docOf (lines: string[]) = + let lineRanges = + lines + |> Array.mapi (fun i line -> + let lineNumber = 10 + i + Range.mkRange "test.fs" (Position.mkPos lineNumber 4) (Position.mkPos lineNumber (4 + 3 + line.Length))) + + XmlDoc(lines, lineRanges, Array.reduce Range.unionRanges lineRanges) + + let private refsOf lines = + (docOf lines).GetRefs() + |> Array.map (fun r -> r.Kind, r.Text, (r.Range.StartLine, r.Range.StartColumn, r.Range.EndColumn)) + |> Array.toList + + [] + let ``every tag kind, with the value column offset by the three slashes`` () = + // `///` at column 4, so the stored text starts at column 7; ` first""" + """ """ + """ t""" + """ """ + """ """ + """ """ + """ when""" + """ """ |] + |> shouldEqual + [ XmlDocRefKind.Param, "x", (10, 21, 22) + XmlDocRefKind.ParamRef, "x", (11, 24, 25) + XmlDocRefKind.TypeParam, "T", (12, 25, 26) + XmlDocRefKind.TypeParamRef, "T", (13, 28, 29) + XmlDocRefKind.Cref, "MyType", (14, 19, 25) + XmlDocRefKind.Cref, "M:A.B.C", (15, 23, 30) + XmlDocRefKind.Cref, "System.Exception", (16, 25, 41) + XmlDocRefKind.Cref, "P", (17, 26, 27) ] + + [] + let ``attribute syntax variations`` () = + refsOf [| """spaces and single quotes""" + """other attributes first""" + """two tags on one line""" |] + |> shouldEqual + [ XmlDocRefKind.Param, "x", (10, 24, 25) + XmlDocRefKind.Param, "y", (11, 28, 29) + XmlDocRefKind.ParamRef, "z", (12, 23, 24) + XmlDocRefKind.ParamRef, "w", (12, 44, 45) ] + + [] + let ``only a whole attribute name matches, and a quoted > does not end the tag`` () = + refsOf [| """suffix of another attribute""" + """angle bracket in a value""" |] + |> shouldEqual + [ XmlDocRefKind.Param, "x", (10, 36, 37) + XmlDocRefKind.Param, "y", (11, 30, 31) ] + + [] + let ``a tag whose attributes run over several lines`` () = + refsOf [| """on the next line""" + """after another attribute""" |] + |> shouldEqual + [ XmlDocRefKind.Param, "x", (11, 16, 17) + XmlDocRefKind.TypeParam, "T", (13, 24, 25) ] + + [] + let ``a value that runs over a line break yields nothing`` () = + refsOf [| """split value""" |] + |> shouldEqual [] + + [] + let ``blank lines, malformed xml and unrelated tags yield nothing`` () = + refsOf [| "" + "not a param" + "not one of the tags" + "unquoted" + "no name" + "text with a < that is not a tag" |] + |> shouldEqual [] + + [] + let ``a doc without line ranges has no refs`` () = + XmlDoc([| """""" |], Range.range0).GetRefs() |> shouldEqual [||] + + [] + let ``a merged doc keeps its refs when both halves have line ranges`` () = + let merged = XmlDoc.Merge (docOf [| """""" |]) (docOf [| """""" |]) + merged.GetRefs() |> Array.map (fun r -> r.Text) |> shouldEqual [| "x"; "y" |] + + let withoutRanges = XmlDoc.Merge (docOf [| """""" |]) (XmlDoc([| """""" |], Range.range0)) + withoutRanges.GetRefs() |> shouldEqual [||] + [] let ``xml-doc eof``(): unit = checkSignatureAndImplementation """