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) - -