Skip to content

Instantly share code, notes, and snippets.

@T-Gro
Last active June 12, 2026 08:47
Show Gist options
  • Select an option

  • Save T-Gro/5e2df1271a16db22b714aaad403c31f0 to your computer and use it in GitHub Desktop.

Select an option

Save T-Gro/5e2df1271a16db22b714aaad403c31f0 to your computer and use it in GitHub Desktop.
Spread IDE service tests - RED on dotnet/fsharp#18927 (apply with git apply)
diff --git a/tests/FSharp.Compiler.Service.Tests/CompletionTests.fs b/tests/FSharp.Compiler.Service.Tests/CompletionTests.fs
index cdcbf7a0b8..048759d779 100644
--- a/tests/FSharp.Compiler.Service.Tests/CompletionTests.fs
+++ b/tests/FSharp.Compiler.Service.Tests/CompletionTests.fs
@@ -860,4 +860,31 @@ let normalize' x = x + 1
normaliz{caret}
"""
- assertHasItemWithNames [ "normalize'" ] info
\ No newline at end of file
+ assertHasItemWithNames [ "normalize'" ] info
+module RecordSpreads =
+ let private getPreviewCompletionInfo markedSource =
+ Checker.getCompletionInfoWithCompilerAndCompletionOptions
+ [| "--langversion:preview" |]
+ FSharpCodeCompletionOptions.Default
+ markedSource
+
+ [<Fact>]
+ let ``spread - completion fires inside nominal record spread`` () =
+ let info = getPreviewCompletionInfo """
+type R = { A: int; B: int }
+let r1 = { A = 1; B = 2 }
+let r2 = { ...{caret} }
+"""
+ let names = info.Items |> Array.map _.NameInCode
+ if not (Array.contains "r1" names) then
+ failwith $"Expected completion at '{{ ...|caret| }}' to offer in-scope record value 'r1', but got %d{names.Length} item(s)."
+
+ [<Fact>]
+ let ``spread - completion fires inside anonymous record spread`` () =
+ let info = getPreviewCompletionInfo """
+let r1 = {| A = 1; B = 2 |}
+let r2 = {| ...{caret} ; X = 1 |}
+"""
+ let names = info.Items |> Array.map _.NameInCode
+ if not (Array.contains "r1" names) then
+ failwith $"Expected completion at '{{| ...|caret| ; X = 1 |}}' to offer in-scope record value 'r1', but got %d{names.Length} item(s)."
\ No newline at end of file
diff --git a/tests/FSharp.Compiler.Service.Tests/Symbols.fs b/tests/FSharp.Compiler.Service.Tests/Symbols.fs
index 25cc0cd4b6..91dc13dfae 100644
--- a/tests/FSharp.Compiler.Service.Tests/Symbols.fs
+++ b/tests/FSharp.Compiler.Service.Tests/Symbols.fs
@@ -1545,3 +1545,55 @@ let result = 1 -.- 2
let rangeLength = range.EndColumn - range.StartColumn
Assert.Equal(3, rangeLength)
+
+module RecordSpreads =
+ open FSharp.Compiler.EditorServices
+
+ [<Fact>]
+ let ``spread - spread operator is not classified as a record field`` () =
+ let _, checkResults =
+ getParseAndCheckResultsPreview """
+type R1 = { A : int; B : int }
+type R2 = { ...R1; C : int }
+"""
+ let items = checkResults.GetSemanticClassification(None, RelatedSymbolUseKind.All)
+ let badItems =
+ items
+ |> Array.filter (fun i ->
+ i.Type = SemanticClassificationType.RecordField
+ && i.Range.StartLine = 3
+ && i.Range.StartColumn < 15
+ && i.Range.EndColumn > 12)
+ if badItems.Length > 0 then
+ failwith $"Expected the '...' spread operator to NOT be classified as RecordField, but found: %A{badItems |> Array.map (fun i -> getRangeCoords i.Range)}"
+
+ [<Fact>]
+ let ``spread - GetSymbolUseAtLocation range excludes leading spread operator`` () =
+ let _, checkResults =
+ getParseAndCheckResultsPreview """
+type R1 = { A : int; B : int }
+let r1 = { A = 1; B = 2 }
+let r2 = { ...r1; C = 3 }
+"""
+ let line4 = "let r2 = { ...r1; C = 3 }"
+ match checkResults.GetSymbolUseAtLocation(4, 16, line4, [ "r1" ]) with
+ | None -> failwith "Expected to resolve symbol 'r1' inside the spread '...r1'."
+ | Some su ->
+ let spreadUse =
+ checkResults.GetUsesOfSymbolInFile(su.Symbol)
+ |> Array.find (fun u -> not u.IsFromDefinition)
+ if getRangeCoords su.Range <> getRangeCoords spreadUse.Range then
+ failwith $"GetSymbolUseAtLocation range %A{getRangeCoords su.Range} should match GetUsesOfSymbolInFile range %A{getRangeCoords spreadUse.Range} (no leading '...')."
+
+ [<Fact>]
+ let ``spread - synthesized fields have distinct DeclarationLocations`` () =
+ let _, checkResults =
+ getParseAndCheckResultsPreview """
+type R1 = { A : int; B : int }
+type R2 = { ...R1; C : int }
+"""
+ let r2 = findSymbolByName "R2" checkResults :?> FSharpEntity
+ let r2A = r2.FSharpFields |> Seq.find (fun f -> f.Name = "A")
+ let r2B = r2.FSharpFields |> Seq.find (fun f -> f.Name = "B")
+ if getRangeCoords r2A.DeclarationLocation = getRangeCoords r2B.DeclarationLocation then
+ failwith $"Expected spread-synthesized fields A and B to have distinct DeclarationLocations, but both are %A{getRangeCoords r2A.DeclarationLocation}."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment