diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 43601b6e34..60b7d53e1d 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -67,6 +67,7 @@ * Reference assembly MVIDs are now deterministic across compiler invocations. Previously, `--refout` / `true` produced a different MVID every build because the implied signature hash used .NET's randomized `String.GetHashCode()`. ([Issue #19751](https://github.com/dotnet/fsharp/issues/19751), [PR #19801](https://github.com/dotnet/fsharp/pull/19801)) * Parser: recover on unfinished if and binary expressions ([PR #19724](https://github.com/dotnet/fsharp/pull/19724)) +* Fix QuickInfo for overloaded CE custom operators to show all overloads instead of always the last one. ([Issue #11612](https://github.com/dotnet/fsharp/issues/11612), [PR #19865](https://github.com/dotnet/fsharp/pull/19865)) ### Added diff --git a/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs b/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs index 3ab446a668..e1b0f1741f 100644 --- a/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs @@ -178,6 +178,26 @@ let transferVarSpaceReferences (expr: Expr) = for v in vals do v.SetHasBeenReferenced() +let TryGetCustomOperationName g m (methInfo: MethInfo) : string option = + TryBindMethInfoAttribute + g + m + g.attrib_CustomOperationAttribute + methInfo + IgnoreAttribute // We do not respect this attribute for IL methods + (fun attr -> + // NOTE: right now, we support of custom operations with spaces in them ([]) + // In the parameterless CustomOperationAttribute - we use the method name, and also allow it to be ````-quoted (member _.``foo bar`` _ = ...) + match attr with + // Empty string and parameterless constructor - we use the method name + | Attrib(unnamedArgs = [ AttribStringArg "" ]) // Empty string as parameter + | Attrib(unnamedArgs = []) -> // No parameters, same as empty string for compat reasons. + Some methInfo.LogicalName + // Use the specified name + | Attrib(unnamedArgs = [ AttribStringArg msg ]) -> Some msg + | _ -> None) + IgnoreAttribute // We do not respect this attribute for provided methods + let hasMethInfo nm cenv env mBuilderVal ad builderTy = match TryFindIntrinsicOrExtensionMethInfo ResultCollectionSettings.AtMostOneResult cenv env mBuilderVal ad nm builderTy with | [] -> false @@ -198,25 +218,7 @@ let getCustomOperationMethods (cenv: TcFileState) (env: TcEnv) ad mBuilderVal bu [ for methInfo in allMethInfos do if IsMethInfoAccessible cenv.amap mBuilderVal ad methInfo then - let nameSearch = - TryBindMethInfoAttribute - cenv.g - mBuilderVal - cenv.g.attrib_CustomOperationAttribute - methInfo - IgnoreAttribute // We do not respect this attribute for IL methods - (fun attr -> - // NOTE: right now, we support of custom operations with spaces in them ([]) - // In the parameterless CustomOperationAttribute - we use the method name, and also allow it to be ````-quoted (member _.``foo bar`` _ = ...) - match attr with - // Empty string and parameterless constructor - we use the method name - | Attrib(unnamedArgs = [ AttribStringArg "" ]) // Empty string as parameter - | Attrib(unnamedArgs = []) -> // No parameters, same as empty string for compat reasons. - Some methInfo.LogicalName - // Use the specified name - | Attrib(unnamedArgs = [ AttribStringArg msg ]) -> Some msg - | _ -> None) - IgnoreAttribute // We do not respect this attribute for provided methods + let nameSearch = TryGetCustomOperationName cenv.g mBuilderVal methInfo match nameSearch with | None -> () diff --git a/src/Compiler/Checking/Expressions/CheckComputationExpressions.fsi b/src/Compiler/Checking/Expressions/CheckComputationExpressions.fsi index ac9554252f..9486c1e13d 100644 --- a/src/Compiler/Checking/Expressions/CheckComputationExpressions.fsi +++ b/src/Compiler/Checking/Expressions/CheckComputationExpressions.fsi @@ -6,8 +6,14 @@ open FSharp.Compiler.CheckBasics open FSharp.Compiler.ConstraintSolver open FSharp.Compiler.Syntax open FSharp.Compiler.Text +open FSharp.Compiler.Infos +open FSharp.Compiler.TcGlobals open FSharp.Compiler.TypedTree +/// If the given method carries a [] attribute, return the operator name it declares +/// (the attribute argument, or the method's logical name when the attribute is parameterless/empty). +val TryGetCustomOperationName: g: TcGlobals -> m: range -> methInfo: MethInfo -> string option + val TcComputationExpression: cenv: TcFileState -> env: TcEnv -> diff --git a/src/Compiler/Service/ServiceDeclarationLists.fs b/src/Compiler/Service/ServiceDeclarationLists.fs index 98312a6930..15ca8eb226 100644 --- a/src/Compiler/Service/ServiceDeclarationLists.fs +++ b/src/Compiler/Service/ServiceDeclarationLists.fs @@ -13,6 +13,8 @@ open Internal.Utilities.Library.Extras open FSharp.Compiler open FSharp.Compiler.AbstractIL.Diagnostics open FSharp.Compiler.AccessibilityLogic +open FSharp.Compiler.AttributeChecking +open FSharp.Compiler.CheckComputationExpressions open FSharp.Compiler.Diagnostics open FSharp.Compiler.EditorServices open FSharp.Compiler.DiagnosticsLogger @@ -31,6 +33,7 @@ open FSharp.Compiler.Text.TaggedText open FSharp.Compiler.TypedTree open FSharp.Compiler.TypedTreeBasics open FSharp.Compiler.TypedTreeOps +open FSharp.Compiler.TypeHierarchy /// A single data tip display element [] @@ -333,25 +336,55 @@ module DeclarationListHelpers = // Custom operations in queries | Item.CustomOperation (customOpName, usageText, Some minfo) -> + // Gather all sibling [] overloads declared on the same builder type, + // so that QuickInfo shows every overload (with the resolved one first) rather than only the one + // that name resolution happened to pick. See https://github.com/dotnet/fsharp/issues/11612. + let siblingMethInfos = + let enclTy = minfo.ApparentEnclosingType + // NOTE: extension-method-defined custom operators are not surfaced here. The type checker + // discovers them via AllMethInfosOfTypeInScope (which honours nenv); at tooltip time we + // only have the resolved minfo and intrinsic methods. This matches the common case + // (CustomOperation methods are nearly always intrinsic members of the builder type). + let allMeths = GetIntrinsicMethInfosOfType infoReader None ad AllowMultiIntfInstantiations.Yes IgnoreOverrides m enclTy + let matched = allMeths |> List.filter (fun mi -> TryGetCustomOperationName g m mi = Some customOpName) + let isResolved (mi: MethInfo) = MethInfosEquivByNameAndSig EraseAll true g amap m mi minfo + match List.tryFind isResolved matched with + | Some resolved -> resolved :: (matched |> List.filter (fun mi -> not (isResolved mi))) + | None -> if List.isEmpty matched then [minfo] else matched + // Build 'custom operation: where (bool) // - // Calls QueryBuilder.Where' - let layout = + // Calls QueryBuilder.Where' for each overload + let showInputTy = List.length siblingMethInfos > 1 + let layoutOne (mi: MethInfo) = + let inputTyL = + if not showInputTy then emptyL else + match mi.GetParamTypes(amap, m, mi.FormalMethodInst) with + | (firstTy :: _) :: _ -> + let firstTy, _ = PrettyTypes.PrettifyType g firstTy + SepL.colon ^^ layoutType denv firstTy + | _ -> emptyL wordL (tagText (FSComp.SR.typeInfoCustomOperation())) ^^ RightL.colon ^^ ( match usageText() with | Some t -> wordL (tagText t) | None -> - let argTys = ParamNameAndTypesOfUnaryCustomOperation g minfo |> List.map (fun (ParamNameAndType(_, ty)) -> ty) + let argTys = ParamNameAndTypesOfUnaryCustomOperation g mi |> List.map (fun (ParamNameAndType(_, ty)) -> ty) let argTys, _ = PrettyTypes.PrettifyTypes g argTys wordL (tagMethod customOpName) ^^ sepListL SepL.space (List.map (fun ty -> LeftL.leftParen ^^ layoutType denv ty ^^ SepL.rightParen) argTys) ) ^^ SepL.lineBreak ^^ SepL.lineBreak ^^ wordL (tagText (FSComp.SR.typeInfoCallsWord())) ^^ - layoutTyconRef denv minfo.ApparentEnclosingTyconRef ^^ + layoutTyconRef denv mi.ApparentEnclosingTyconRef ^^ SepL.dot ^^ - wordL (tagMethod minfo.DisplayName) + wordL (tagMethod mi.DisplayName) ^^ + inputTyL + + let layout = + siblingMethInfos + |> List.map layoutOne + |> List.reduce (fun a b -> a ^^ SepL.lineBreak ^^ SepL.lineBreak ^^ b) let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout diff --git a/tests/FSharp.Compiler.Service.Tests/TooltipTests.fs b/tests/FSharp.Compiler.Service.Tests/TooltipTests.fs index 2947e09f56..7220147402 100644 --- a/tests/FSharp.Compiler.Service.Tests/TooltipTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/TooltipTests.fs @@ -852,3 +852,99 @@ let inline fo{caret}o< ^T> (x: ^T) = x |> fun text -> // Type param appears in tooltip Assert.Contains("'T", text) + +// https://github.com/dotnet/fsharp/issues/11612 +// Tooltip for overloaded CE [] should reflect the resolved overload, +// not just the last one registered with the builder. +let private renderAllGroups (ToolTipText elements) = + let sb = System.Text.StringBuilder() + for el in elements do + match el with + | ToolTipElement.Group items -> + for item in items do + for line in item.MainDescription do + sb.Append(line.Text) |> ignore + sb.Append('\n') |> ignore + | ToolTipElement.CompositionError msg -> sb.AppendLine(msg) |> ignore + | ToolTipElement.None -> () + sb.ToString() + +[] +let ``CE custom operator QuickInfo shows resolved overload`` () = + Checker.getTooltip """ +type Builder() = + member _.Yield(x) = [x] + member _.For(xs, body) = xs |> List.collect body + [] + member _.WhereInt(xs: int list, [] f: int -> bool) = List.filter f xs + [] + member _.WhereStr(xs: string list, [] f: string -> bool) = List.filter f xs + +let b = Builder() +let result = b { for x in [1;2;3] do whereO{caret}p (x > 0) } +""" + |> renderAllGroups + |> fun text -> Assert.Contains("int", text) + +[] +let ``CE custom operator with three overloads shows resolved float overload`` () = + Checker.getTooltip """ +type Builder() = + member _.Yield(x) = [x] + member _.For(xs, body) = xs |> List.collect body + [] + member _.F1(xs: int list, [] f: int -> bool) = List.filter f xs + [] + member _.F2(xs: float list, [] f: float -> bool) = List.filter f xs + [] + member _.F3(xs: string list, [] f: string -> bool) = List.filter f xs + +let b = Builder() +let result = b { for x in [1.0;2.0] do filterO{caret}p (x > 0.0) } +""" + |> renderAllGroups + |> fun text -> Assert.Contains("float", text) + +[] +let ``CE single custom operator QuickInfo still works`` () = + Checker.getTooltip """ +type Builder() = + member _.Yield(x) = [x] + member _.For(xs, body) = xs |> List.collect body + [] + member _.W(xs: int list, [] f: int -> bool) = List.filter f xs + +let b = Builder() +let result = b { for x in [1;2;3] do whereSing{caret}le (x > 0) } +""" + |> renderAllGroups + |> fun text -> Assert.Contains("whereSingle", text) + +[] +let ``Regular method overload QuickInfo unaffected`` () = + Checker.getTooltip """ +type T() = + member _.M(x: int) = x + member _.M(x: string) = x.Length +let t = T() +let r = t.M{caret}(42) +""" + |> renderAllGroups + |> fun text -> Assert.Contains("int", text) + +[] +let ``GetSymbolUse resolves correct CE operator overload`` () = + Checker.getTooltip """ +type Builder() = + member _.Yield(x) = [x] + member _.For(xs, body) = xs |> List.collect body + [] + member _.PickInt(xs: int list, [] f: int -> bool) = List.filter f xs + [] + member _.PickStr(xs: string list, [] f: string -> bool) = List.filter f xs + +let b = Builder() +let result = b { for x in [1;2;3] do pickO{caret}ne (x > 0) } +""" + |> renderAllGroups + |> fun text -> Assert.Contains("int", text) \ No newline at end of file