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 5e1ce2c8ed7..54f5a122d6d 100644
--- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
+++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
@@ -83,8 +83,8 @@
* Fix signature conformance: overloaded member with unit parameter `M(())` now matches sig `member M: unit -> unit`. ([Issue #19596](https://github.com/dotnet/fsharp/issues/19596), [PR #19615](https://github.com/dotnet/fsharp/pull/19615))
* Fix `--quiet` not suppressing NuGet restore output on stdout in F# Interactive ([Issue #18086](https://github.com/dotnet/fsharp/issues/18086))
* 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))
+* Parser: recover on unfinished if and binary expressions ([PR #19724](https://github.com/dotnet/fsharp/pull/19724))
+* Fix QuickInfo / `GetAllUsesOfAllSymbolsInFile` for overloaded CE `[]` keywords to report the actually-resolved overload instead of the first-registered one. ([Issue #11612](https://github.com/dotnet/fsharp/issues/11612), [Issue #15206](https://github.com/dotnet/fsharp/issues/15206), [PR #19865](https://github.com/dotnet/fsharp/pull/19865))
* Fix `open` declaration insertion in `.fsx`/`.fsscript` scripts being placed before `#r`/`#load` directives, producing invalid script files. ([Issue #16271](https://github.com/dotnet/fsharp/issues/16271), [PR #19879](https://github.com/dotnet/fsharp/pull/19879))
* Warn FS3888 when a compiler-semantic attribute on a value/member or type/module is present in the `.fs` but missing from the `.fsi`. Such attributes were previously ignored at the consumer side. Under the `ErrorOnMissingSignatureAttribute` preview language feature, FS3888 is an error. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880))
* Emit debug points at a stack-empty position ([PR #19877](https://github.com/dotnet/fsharp/pull/19877))
diff --git a/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs b/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs
index 2e258b1b2c9..8c2b84011f3 100644
--- a/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs
+++ b/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs
@@ -7,6 +7,7 @@ module internal FSharp.Compiler.CheckComputationExpressions
open Internal.Utilities.Library
open FSharp.Compiler.AccessibilityLogic
open FSharp.Compiler.AttributeChecking
+open FSharp.Compiler.CheckComputationExpressionsCustomOps
open FSharp.Compiler.CheckExpressionsOps
open FSharp.Compiler.CheckExpressions
open FSharp.Compiler.CheckBasics
@@ -61,6 +62,7 @@ type ComputationExpressionContext<'a> =
origComp: SynExpr
mWhole: range
emptyVarSpace: LazyWithContext * TcEnv, range>
+ deferredCustomOpSinks: ResizeArray
}
let inline noTailCall ceenv = { ceenv with tailCall = false }
@@ -178,6 +180,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 +220,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 -> ()
@@ -1124,15 +1128,16 @@ let rec TryTranslateComputationExpression
| Some opDatas ->
let opName, _, _, _, _, _, _, _, methInfo = opDatas[0]
- // Record the resolution of the custom operation for posterity
- let item =
- Item.CustomOperation(opName, (fun () -> customOpUsageText ceenv nm), Some methInfo)
-
- // FUTURE: consider whether we can do better than emptyTyparInst here, in order to display instantiations
- // of type variables in the quick info provided in the IDE.
- CallNameResolutionSink
- cenv.tcSink
- (nm.idRange, ceenv.env.NameEnv, item, emptyTyparInst, ItemOccurrence.Use, ceenv.env.eAccessRights)
+ enqueueDeferredCustomOpSink
+ ceenv.cenv.tcSink
+ ceenv.env.NameEnv
+ ceenv.env.eAccessRights
+ ceenv.deferredCustomOpSinks
+ nm
+ opName
+ (fun () -> customOpUsageText ceenv nm)
+ (mOpCore.MakeSynthetic())
+ methInfo
let mkJoinExpr keySelector1 keySelector2 innerPat e =
let mSynthetic = mOpCore.MakeSynthetic()
@@ -2430,15 +2435,16 @@ and ConsumeCustomOpClauses
let isLikeGroupJoin = customOperationIsLikeZip ceenv nm
- // Record the resolution of the custom operation for posterity
- let item =
- Item.CustomOperation(opName, (fun () -> customOpUsageText ceenv nm), Some methInfo)
-
- // FUTURE: consider whether we can do better than emptyTyparInst here, in order to display instantiations
- // of type variables in the quick info provided in the IDE.
- CallNameResolutionSink
+ enqueueDeferredCustomOpSink
ceenv.cenv.tcSink
- (nm.idRange, ceenv.env.NameEnv, item, emptyTyparInst, ItemOccurrence.Use, ceenv.env.eAccessRights)
+ ceenv.env.NameEnv
+ ceenv.env.eAccessRights
+ ceenv.deferredCustomOpSinks
+ nm
+ opName
+ (fun () -> customOpUsageText ceenv nm)
+ (mClause.MakeSynthetic())
+ methInfo
if isLikeZip || isLikeJoin || isLikeGroupJoin then
errorR (Error(FSComp.SR.tcBinaryOperatorRequiresBody (nm.idText, Option.get (customOpUsageText ceenv nm)), nm.idRange))
@@ -3024,6 +3030,8 @@ let TcComputationExpression (cenv: TcFileState) env (overallTy: OverallTy) tpenv
let origComp = comp
+ let deferredCustomOpSinks = ResizeArray()
+
let ceenv =
{
cenv = cenv
@@ -3041,6 +3049,7 @@ let TcComputationExpression (cenv: TcFileState) env (overallTy: OverallTy) tpenv
origComp = origComp
mWhole = mWhole
emptyVarSpace = LazyWithContext.NotLazy([], env)
+ deferredCustomOpSinks = deferredCustomOpSinks
}
/// Inside the 'query { ... }' use a modified name environment that contains fake 'CustomOperation' entries
@@ -3114,7 +3123,8 @@ let TcComputationExpression (cenv: TcFileState) env (overallTy: OverallTy) tpenv
| _ -> env
let lambdaExpr, tpenv =
- TcExpr cenv (MustEqual(mkFunTy cenv.g builderTy overallTy)) env tpenv lambdaExpr
+ captureCustomOperationOverloads cenv.tcSink deferredCustomOpSinks (fun () ->
+ TcExpr cenv (MustEqual(mkFunTy cenv.g builderTy overallTy)) env tpenv lambdaExpr)
// For queries, transfer HasBeenReferenced from compiler-generated varSpace Vals to user Vals
if isQuery then
diff --git a/src/Compiler/Checking/Expressions/CheckComputationExpressionsCustomOps.fs b/src/Compiler/Checking/Expressions/CheckComputationExpressionsCustomOps.fs
new file mode 100644
index 00000000000..29cbdfcbb8e
--- /dev/null
+++ b/src/Compiler/Checking/Expressions/CheckComputationExpressionsCustomOps.fs
@@ -0,0 +1,125 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+/// Sinks the resolved overload's `MethInfo` at the keyword range of an overloaded
+/// `[]` usage in a computation expression — fixes #11612 / #15206.
+module internal FSharp.Compiler.CheckComputationExpressionsCustomOps
+
+open System.Collections.Generic
+open FSharp.Compiler.AccessibilityLogic
+open FSharp.Compiler.Infos
+open FSharp.Compiler.NameResolution
+open FSharp.Compiler.Syntax
+open FSharp.Compiler.Text
+open FSharp.Compiler.TypedTree
+open FSharp.Compiler.TypedTreeOps
+
+[]
+type DeferredCustomOpSink =
+ {
+ KeywordRange: range
+ OpName: string
+ UsageText: unit -> string option
+ SyntheticCallRange: range
+ Fallback: MethInfo
+ NameEnv: NameResolutionEnv
+ AccessRights: AccessorDomain
+ }
+
+let private makeCustomOpResolutionCapturingSink
+ (forwardTo: ITypecheckResultsSink)
+ (capturedResolutions: Dictionary)
+ : ITypecheckResultsSink =
+
+ let tryCapture (m: range) (item: Item) (tpinst: TyparInstantiation) =
+ match item with
+ | Item.MethodGroup(name, [ mi ], _) ->
+ match capturedResolutions.TryGetValue m with
+ | true, (expectedName, _, _) when name = expectedName -> capturedResolutions[m] <- (expectedName, mi, tpinst)
+ | _ -> ()
+ | _ -> ()
+
+ { new ITypecheckResultsSink with
+ member _.NotifyEnvWithScope(m, nenv, ad) =
+ forwardTo.NotifyEnvWithScope(m, nenv, ad)
+
+ member _.NotifyExprHasType(ty, nenv, ad, m) =
+ forwardTo.NotifyExprHasType(ty, nenv, ad, m)
+
+ member _.NotifyExprHasTypeSynthetic(ty, nenv, ad, m) =
+ forwardTo.NotifyExprHasTypeSynthetic(ty, nenv, ad, m)
+
+ member _.NotifyNameResolution(endPos, item, tpinst, occurrenceType, nenv, ad, m, replace) =
+ tryCapture m item tpinst
+ forwardTo.NotifyNameResolution(endPos, item, tpinst, occurrenceType, nenv, ad, m, replace)
+
+ member _.NotifyMethodGroupNameResolution(endPos, item, itemMethodGroup, tpinst, occurrenceType, nenv, ad, m, replace) =
+ tryCapture m item tpinst
+ forwardTo.NotifyMethodGroupNameResolution(endPos, item, itemMethodGroup, tpinst, occurrenceType, nenv, ad, m, replace)
+
+ member _.NotifyFormatSpecifierLocation(m, numArgs) =
+ forwardTo.NotifyFormatSpecifierLocation(m, numArgs)
+
+ member _.NotifyRelatedSymbolUse(m, item, kind) =
+ forwardTo.NotifyRelatedSymbolUse(m, item, kind)
+
+ member _.NotifyOpenDeclaration openDeclaration =
+ forwardTo.NotifyOpenDeclaration openDeclaration
+
+ member _.CurrentSourceText = forwardTo.CurrentSourceText
+
+ member _.FormatStringCheckContext = forwardTo.FormatStringCheckContext
+ }
+
+let enqueueDeferredCustomOpSink
+ (sink: TcResultsSink)
+ (nenv: NameResolutionEnv)
+ (ad: AccessorDomain)
+ (queue: ResizeArray)
+ (nm: Ident)
+ opName
+ usageText
+ syntheticCallRange
+ (fallback: MethInfo)
+ =
+ let fallbackItem = Item.CustomOperation(opName, usageText, Some fallback)
+
+ CallNameResolutionSink sink (nm.idRange, nenv, fallbackItem, emptyTyparInst, ItemOccurrence.Use, ad)
+
+ queue.Add
+ {
+ KeywordRange = nm.idRange
+ OpName = opName
+ UsageText = usageText
+ SyntheticCallRange = syntheticCallRange
+ Fallback = fallback
+ NameEnv = nenv
+ AccessRights = ad
+ }
+
+let captureCustomOperationOverloads (sink: TcResultsSink) (queue: ResizeArray) (action: unit -> 'T) : 'T =
+ match sink.CurrentSink with
+ | Some oldSink when queue.Count > 0 ->
+ let capturedResolutions =
+ Dictionary(Range.comparer)
+
+ for entry in queue do
+ capturedResolutions[entry.SyntheticCallRange] <- (entry.Fallback.LogicalName, entry.Fallback, emptyTyparInst)
+
+ let captureSink = makeCustomOpResolutionCapturingSink oldSink capturedResolutions
+
+ let result =
+ use _holder = WithNewTypecheckResultsSink(captureSink, sink)
+ action ()
+
+ for entry in queue do
+ let _, resolved, tpinst = capturedResolutions[entry.SyntheticCallRange]
+
+ if not (MethInfo.MethInfosUseIdenticalDefinitions resolved entry.Fallback) then
+ let item = Item.CustomOperation(entry.OpName, entry.UsageText, Some resolved)
+
+ CallNameResolutionSinkReplacing
+ sink
+ (entry.KeywordRange, entry.NameEnv, item, tpinst, ItemOccurrence.Use, entry.AccessRights)
+
+ result
+ | _ -> action ()
diff --git a/src/Compiler/FSharp.Compiler.Service.fsproj b/src/Compiler/FSharp.Compiler.Service.fsproj
index 360247d7a20..5510af6b3f6 100644
--- a/src/Compiler/FSharp.Compiler.Service.fsproj
+++ b/src/Compiler/FSharp.Compiler.Service.fsproj
@@ -407,6 +407,7 @@
+
diff --git a/tests/FSharp.Compiler.Service.Tests/TooltipTests.fs b/tests/FSharp.Compiler.Service.Tests/TooltipTests.fs
index 8dae1b5f572..5bee4573952 100644
--- a/tests/FSharp.Compiler.Service.Tests/TooltipTests.fs
+++ b/tests/FSharp.Compiler.Service.Tests/TooltipTests.fs
@@ -4,6 +4,7 @@
#nowarn "57"
open FSharp.Compiler.CodeAnalysis
+open FSharp.Compiler.Diagnostics
open FSharp.Compiler.Service.Tests.Common
open FSharp.Compiler.Text
open FSharp.Compiler.EditorServices
@@ -893,6 +894,261 @@ let inline fo{caret}o< ^T> (x: ^T) = x
// Type param appears in tooltip
Assert.Contains("'T", text)
+// https://github.com/dotnet/fsharp/issues/11612 / #15206
+// QuickInfo / SymbolUse for an overloaded CE [] keyword must reflect the
+// overload picked by normal F# overload resolution, not the first-registered one (which
+// corresponded to the last declared overload of the builder).
+//
+// NOTE: F# only allows overloaded custom operations whose underlying CLR members share the
+// same name (overloading is by signature, not by member name). Tests below use a single
+// shared method name `Filter` to mirror what the language actually supports.
+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
+ for line in item.XmlDoc |> (function FSharpXmlDoc.FromXmlText t -> t.UnprocessedLines |> Array.toList | _ -> []) do
+ sb.AppendLine(line) |> ignore
+ | ToolTipElement.CompositionError msg -> sb.AppendLine(msg) |> ignore
+ | ToolTipElement.None -> ()
+ sb.ToString()
+
+[]
+let ``CE custom operator QuickInfo XmlDoc reflects resolved int overload`` () =
+ let text =
+ Checker.getTooltip """
+type Builder() =
+ member _.Yield(x) = [x]
+ member _.For(xs, body) = xs |> List.collect body
+ /// INT_OVERLOAD_MARKER
+ []
+ member _.Filter(xs: int list, [] f: int -> bool) = List.filter f xs
+ /// STRING_OVERLOAD_MARKER
+ []
+ member _.Filter(xs: string list, [] f: string -> bool) = List.filter f xs
+
+let b = Builder()
+let result = b { for x in [1;2;3] do filterO{caret}p (x > 0) }
+"""
+ |> renderAllGroups
+ Assert.Contains("INT_OVERLOAD_MARKER", text)
+ Assert.DoesNotContain("STRING_OVERLOAD_MARKER", text)
+
+[]
+let ``CE custom operator QuickInfo XmlDoc reflects resolved string overload`` () =
+ let text =
+ Checker.getTooltip """
+type Builder() =
+ member _.Yield(x) = [x]
+ member _.For(xs, body) = xs |> List.collect body
+ /// INT_OVERLOAD_MARKER
+ []
+ member _.Filter(xs: int list, [] f: int -> bool) = List.filter f xs
+ /// STRING_OVERLOAD_MARKER
+ []
+ member _.Filter(xs: string list, [] f: string -> bool) = List.filter f xs
+
+let b = Builder()
+let result = b { for x in ["a";"b"] do filterO{caret}p (x.Length > 0) }
+"""
+ |> renderAllGroups
+ Assert.Contains("STRING_OVERLOAD_MARKER", text)
+ Assert.DoesNotContain("INT_OVERLOAD_MARKER", 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)
+
+// https://github.com/dotnet/fsharp/issues/15206
+// GetAllUsesOfAllSymbolsInFile must report the resolved overload's MethInfo for each
+// keyword usage, not the first-registered one.
+[]
+let ``GetAllUsesOfAllSymbolsInFile reports resolved overload for each CE custom operation use`` () =
+ let source = """
+module M
+type FooBuilder() =
+ member _.Yield _ = 1
+ member _.For(xs, body) = xs |> Seq.iter body
+ []
+ member _.Create(_, i1: int, s: string, i2: int) = [i1; i2]
+ []
+ member _.Create(_, i: int, s1: string, s2: string) = [i; s1.Length; s2.Length]
+
+let b = FooBuilder()
+let _ = b { for x in [1] do create 1 "" 2 }
+let _ = b { for x in [1] do create 1 "" "" }
+"""
+ let _, checkResults = getParseAndCheckResults source
+
+ // Find the two usages of the `create` keyword.
+ let createKeywordUses =
+ checkResults.GetAllUsesOfAllSymbolsInFile()
+ |> Seq.filter (fun u ->
+ match u.Symbol with
+ | :? FSharpMemberOrFunctionOrValue as mfv ->
+ mfv.LogicalName = "Create"
+ && (u.Range.StartLine = 12 || u.Range.StartLine = 13)
+ && not u.IsFromDefinition
+ | _ -> false)
+ |> Seq.sortBy (fun u -> u.Range.StartLine)
+ |> List.ofSeq
+
+ Assert.Equal(2, createKeywordUses.Length)
+
+ let lastParamType (mfv: FSharpMemberOrFunctionOrValue) =
+ mfv.CurriedParameterGroups
+ |> Seq.collect id
+ |> Seq.last
+ |> fun p -> p.Type.Format(FSharpDisplayContext.Empty)
+
+ // First usage: 'create 1 "" 2' should resolve to the (int, string, int) overload.
+ let firstMfv = createKeywordUses[0].Symbol :?> FSharpMemberOrFunctionOrValue
+ Assert.Contains("int", lastParamType firstMfv)
+
+ // Second usage: 'create 1 "" ""' should resolve to the (int, string, string) overload.
+ let secondMfv = createKeywordUses[1].Symbol :?> FSharpMemberOrFunctionOrValue
+ Assert.Contains("string", lastParamType secondMfv)
+
+ // Sanity: each usage must resolve to a *different* overload (different MethInfo).
+ Assert.NotEqual(lastParamType firstMfv, lastParamType secondMfv)
+
+// Covers the join/zip/groupJoin code path which enqueues with `mOpCore.MakeSynthetic()`
+// in `mkJoinExpr`/`mkZipExpr` rather than `mClause.MakeSynthetic()` used by the unary
+// `ConsumeCustomOpClauses` path. Validates that the deferred-sink mechanism also picks
+// the resolved overload here.
+[]
+let ``GetAllUsesOfAllSymbolsInFile reports resolved overload for IsLikeZip CE custom operation`` () =
+ let source = """
+module M
+type ZBuilder() =
+ member _.Yield (x: 'a) = [x]
+ member _.For(xs: 'a list, body: 'a -> 'b list) = xs |> List.collect body
+ []
+ member _.Select(xs: 'a list, [] f: 'a -> 'b) = List.map f xs
+ []
+ member _.MyZip(outer: int list, inner: int list, resultSelector: int -> int -> 'r) =
+ List.map2 resultSelector outer inner
+ []
+ member _.MyZip(outer: string list, inner: string list, resultSelector: string -> string -> 'r) =
+ List.map2 resultSelector outer inner
+
+let b = ZBuilder()
+let _ = b { for x in [1;2] do
+ myzip y in [3;4]
+ select (x + y) }
+let _ = b { for x in ["a";"b"] do
+ myzip y in ["c";"d"]
+ select (x + y) }
+"""
+ let _, checkResults = getParseAndCheckResults source
+
+ let myzipKeywordUses =
+ checkResults.GetAllUsesOfAllSymbolsInFile()
+ |> Seq.filter (fun u ->
+ match u.Symbol with
+ | :? FSharpMemberOrFunctionOrValue as mfv ->
+ mfv.LogicalName = "MyZip" && not u.IsFromDefinition
+ | _ -> false)
+ |> Seq.sortBy (fun u -> u.Range.StartLine)
+ |> List.ofSeq
+
+ Assert.Equal(2, myzipKeywordUses.Length)
+
+ let firstParamType (mfv: FSharpMemberOrFunctionOrValue) =
+ mfv.CurriedParameterGroups
+ |> Seq.collect id
+ |> Seq.head
+ |> fun p -> p.Type.Format(FSharpDisplayContext.Empty)
+
+ let firstMfv = myzipKeywordUses[0].Symbol :?> FSharpMemberOrFunctionOrValue
+ Assert.Contains("int", firstParamType firstMfv)
+
+ let secondMfv = myzipKeywordUses[1].Symbol :?> FSharpMemberOrFunctionOrValue
+ Assert.Contains("string", firstParamType secondMfv)
+
+ Assert.NotEqual(firstParamType firstMfv, firstParamType secondMfv)
+
+// When `Item.MethodGroup` arrives at the synthetic call range with *more than one* MethInfo
+// (the unrefined group fired by `ResolveExprDotLongIdentAndComputeRange` BEFORE
+// `AfterResolution.RecordResolution` settles), our wrapper's `[ mi ]` singleton pattern
+// must NOT match — capturing the unrefined list would replay the wrong overload (or all of
+// them). When overload resolution then fails (broken user code) and the refined singleton
+// notification never arrives, the dictionary slot stays at the pre-populated `Fallback`,
+// so the drain's `MethInfosUseIdenticalDefinitions` check returns true → no `Replacing`
+// → the early-sunk `Item.CustomOperation(opName, _, Some Fallback)` record stays at the
+// keyword. This is the right error-recovery behaviour: the user still sees *a* MethInfo
+// in QuickInfo / Find-All-References instead of nothing.
+[]
+let ``Broken overloaded CE custom op call falls back to the eager opDatas[0] sink record`` () =
+ // The for-loop iterates over a list whose element type matches NEITHER overload's outer
+ // type, so F# overload resolution cannot pick a single overload. We expect:
+ // * A type-error diagnostic.
+ // * The keyword still has an Item.CustomOperation symbol-use record (the fallback) —
+ // we just don't know which overload was picked, because none was.
+ let source = """
+module M
+type FooBuilder() =
+ member _.Yield _ = 1
+ member _.For(xs, body) = xs |> Seq.iter body
+ []
+ member _.Pick(_, x: int) = x
+ []
+ member _.Pick(_, x: string) = x
+
+let b = FooBuilder()
+let _ = b { for x in [true] do pick true }
+"""
+ let _, checkResults = getParseAndCheckResults source
+
+ // We expect a type-check error from overload resolution failing.
+ let errors =
+ checkResults.Diagnostics
+ |> Array.filter (fun d -> d.Severity = FSharpDiagnosticSeverity.Error)
+
+ Assert.NotEmpty errors
+
+ // But the symbol-use record at the keyword 'pick' must still exist (the eager fallback
+ // sunk by enqueueDeferredCustomOpSink) — the wrapper just didn't get a chance to upgrade
+ // it. Without this graceful fallback, IDE features at the keyword would go blank on
+ // broken CE code.
+ let pickKeywordUses =
+ checkResults.GetAllUsesOfAllSymbolsInFile()
+ |> Seq.filter (fun u ->
+ match u.Symbol with
+ | :? FSharpMemberOrFunctionOrValue as mfv ->
+ mfv.LogicalName = "Pick" && not u.IsFromDefinition
+ | _ -> false)
+ |> List.ofSeq
+
+ Assert.Equal(1, pickKeywordUses.Length)
+
let private getFullNameRemarks (source: string) =
let _mainDesc, _xml, remarks =
Checker.getTooltip source
@@ -957,5 +1213,3 @@ let _ = List.m{caret}ap id [1]
"""
Assert.Contains("List.map", remarks)
Assert.DoesNotContain("ListModule", remarks)
-
-