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