From e8f9625f2b254bed7c1d9dd29226e94f8afac7fa Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 29 May 2026 18:22:28 +0200 Subject: [PATCH 1/4] Add failing FCS tooltip tests for overloaded CE custom operators (#11612) Captures issue #11612: when a CE builder defines multiple [] overloads, FCS GetToolTip always returns the generic 'custom operation: NAME (bool)' description from the last registered overload rather than the resolved overload's parameter types. Current failures (RED): - CE custom operator QuickInfo shows resolved overload: tooltip is 'custom operation: whereOp (bool)\nCalls Builder.Wh...' - missing 'int' from resolved WhereInt overload - CE custom operator with three overloads shows resolved float overload: tooltip is 'custom operation: filterOp (bool)\nCalls Builder.F...' - missing 'float' - GetSymbolUse resolves correct CE operator overload: tooltip is 'custom operation: pickOne (bool)\nCalls Builder.Pi...' - missing 'int' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TooltipTests.fs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/tests/FSharp.Compiler.Service.Tests/TooltipTests.fs b/tests/FSharp.Compiler.Service.Tests/TooltipTests.fs index 2947e09f565..7220147402d 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 From 5fd6c99f811ecc0cd1e6b9142e94c6b724099f6b Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 29 May 2026 19:33:41 +0200 Subject: [PATCH 2/4] Show all overloads in QuickInfo for overloaded CE custom operators (#11612) Aggregate sibling [] methods on the builder type when rendering Item.CustomOperation tooltips, placing the resolved overload first. When there is more than one overload, also append the first parameter's type so that distinct overloads are visibly distinguished. Fixes #11612. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/9.0.300.md | 1 + .../CheckComputationExpressions.fs | 40 +++++++++-------- .../CheckComputationExpressions.fsi | 6 +++ .../Service/ServiceDeclarationLists.fs | 43 ++++++++++++++++--- 4 files changed, 66 insertions(+), 24 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md index 7f5ab7289d2..126a9858442 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md @@ -1,4 +1,5 @@ ### Fixed +* 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)) * Fix missing TailCall warning in TOp.IntegerForLoop ([PR #18399](https://github.com/dotnet/fsharp/pull/18399)) * Fix classification of `nameof` in `nameof<'T>`, `match … with nameof ident -> …`. ([Issue #10026](https://github.com/dotnet/fsharp/issues/10026), [PR #18300](https://github.com/dotnet/fsharp/pull/18300)) * Fix Realsig+ generates nested closures with incorrect Generic ([Issue #17797](https://github.com/dotnet/fsharp/issues/17797), [PR #17877](https://github.com/dotnet/fsharp/pull/17877)) diff --git a/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs b/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs index 3ab446a6687..e1b0f1741fc 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 ac9554252f3..9486c1e13de 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 98312a69306..15ca8eb2260 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 From 6c5c9518c27c40efa84ba0ea37fadab4af40a387 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 29 May 2026 20:53:34 +0200 Subject: [PATCH 3/4] Add PR link to release notes entry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/9.0.300.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md index 126a9858442..7287d250453 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md @@ -1,5 +1,5 @@ ### Fixed -* 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)) +* 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)) * Fix missing TailCall warning in TOp.IntegerForLoop ([PR #18399](https://github.com/dotnet/fsharp/pull/18399)) * Fix classification of `nameof` in `nameof<'T>`, `match … with nameof ident -> …`. ([Issue #10026](https://github.com/dotnet/fsharp/issues/10026), [PR #18300](https://github.com/dotnet/fsharp/pull/18300)) * Fix Realsig+ generates nested closures with incorrect Generic ([Issue #17797](https://github.com/dotnet/fsharp/issues/17797), [PR #17877](https://github.com/dotnet/fsharp/pull/17877)) From de10d70fc0cd69beb9d92b516d3de980dac29ae1 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 29 May 2026 22:16:36 +0200 Subject: [PATCH 4/4] Move release notes entry for #11612 to 11.0.100.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + docs/release-notes/.FSharp.Compiler.Service/9.0.300.md | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) 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 43601b6e34c..60b7d53e1d0 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/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md index 7287d250453..7f5ab7289d2 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md @@ -1,5 +1,4 @@ ### Fixed -* 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)) * Fix missing TailCall warning in TOp.IntegerForLoop ([PR #18399](https://github.com/dotnet/fsharp/pull/18399)) * Fix classification of `nameof` in `nameof<'T>`, `match … with nameof ident -> …`. ([Issue #10026](https://github.com/dotnet/fsharp/issues/10026), [PR #18300](https://github.com/dotnet/fsharp/pull/18300)) * Fix Realsig+ generates nested closures with incorrect Generic ([Issue #17797](https://github.com/dotnet/fsharp/issues/17797), [PR #17877](https://github.com/dotnet/fsharp/pull/17877))