Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
* Reference assembly MVIDs are now deterministic across compiler invocations. Previously, `--refout` / `<ProduceReferenceAssembly>true</ProduceReferenceAssembly>` 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

Expand Down
40 changes: 21 additions & 19 deletions src/Compiler/Checking/Expressions/CheckComputationExpressions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ([<CustomOperation("foo bar")>])
// 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
Expand All @@ -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 ([<CustomOperation("foo bar")>])
// 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 -> ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 [<CustomOperation>] 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 ->
Expand Down
43 changes: 38 additions & 5 deletions src/Compiler/Service/ServiceDeclarationLists.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
[<RequireQualifiedAccess>]
Expand Down Expand Up @@ -333,25 +336,55 @@ module DeclarationListHelpers =
// Custom operations in queries
| Item.CustomOperation (customOpName, usageText, Some minfo) ->

// Gather all sibling [<CustomOperation(customOpName)>] 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
Expand Down
96 changes: 96 additions & 0 deletions tests/FSharp.Compiler.Service.Tests/TooltipTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 [<CustomOperation>] 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()

[<Fact>]
let ``CE custom operator QuickInfo shows resolved overload`` () =
Checker.getTooltip """
type Builder() =
member _.Yield(x) = [x]
member _.For(xs, body) = xs |> List.collect body
[<CustomOperation("whereOp")>]
member _.WhereInt(xs: int list, [<ProjectionParameter>] f: int -> bool) = List.filter f xs
[<CustomOperation("whereOp")>]
member _.WhereStr(xs: string list, [<ProjectionParameter>] 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)

[<Fact>]
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
[<CustomOperation("filterOp")>]
member _.F1(xs: int list, [<ProjectionParameter>] f: int -> bool) = List.filter f xs
[<CustomOperation("filterOp")>]
member _.F2(xs: float list, [<ProjectionParameter>] f: float -> bool) = List.filter f xs
[<CustomOperation("filterOp")>]
member _.F3(xs: string list, [<ProjectionParameter>] 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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we dump all the overloads instead?


[<Fact>]
let ``CE single custom operator QuickInfo still works`` () =
Checker.getTooltip """
type Builder() =
member _.Yield(x) = [x]
member _.For(xs, body) = xs |> List.collect body
[<CustomOperation("whereSingle")>]
member _.W(xs: int list, [<ProjectionParameter>] 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)

[<Fact>]
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)

[<Fact>]
let ``GetSymbolUse resolves correct CE operator overload`` () =
Checker.getTooltip """
type Builder() =
member _.Yield(x) = [x]
member _.For(xs, body) = xs |> List.collect body
[<CustomOperation("pickOne")>]
member _.PickInt(xs: int list, [<ProjectionParameter>] f: int -> bool) = List.filter f xs
[<CustomOperation("pickOne")>]
member _.PickStr(xs: string list, [<ProjectionParameter>] 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)
Loading