Skip to content

Commit 53929f2

Browse files
authored
Fix IEquatable<T> nullness contravariance to match C# behavior (#18770)
1 parent 3005b7d commit 53929f2

File tree

4 files changed

+30
-5
lines changed

4 files changed

+30
-5
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,7 @@ positive.exe
137137
*.bsl.actual
138138
/src/FSharp.DependencyManager.Nuget/StandardError.txt
139139
/src/FSharp.DependencyManager.Nuget/StandardOutput.txt
140+
141+
# Standard output/error files in root directory
142+
StandardOutput.txt
143+
StandardError.txt

docs/release-notes/.FSharp.Compiler.Service/10.0.100.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* Shorthand lambda: fix completion for chained calls and analysis for unfinished expression ([PR #18560](https://github.com/dotnet/fsharp/pull/18560))
1414
* Completion: fix previous namespace considered opened [PR #18609](https://github.com/dotnet/fsharp/pull/18609)
1515
* Fix active pattern typechecking regression. ([Issue #18638](https://github.com/dotnet/fsharp/issues/18638), [PR #18642](https://github.com/dotnet/fsharp/pull/18642))
16+
* Fix nullness warnings when casting non-nullable values to `IEquatable<T>` to match C# behavior. ([Issue #18759](https://github.com/dotnet/fsharp/issues/18759))
1617

1718
### Changed
1819
* Use `errorR` instead of `error` in `CheckDeclarations.fs` when possible. ([PR #18645](https://github.com/dotnet/fsharp/pull/18645))

src/Compiler/Checking/ConstraintSolver.fs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,11 +1407,17 @@ and SolveTypeEqualsTypeEqns csenv ndeep m2 trace cxsln origl1 origl2 =
14071407
ErrorD(ConstraintSolverTupleDiffLengths(csenv.DisplayEnv, csenv.eContextInfo, origl1, origl2, csenv.m, m2))
14081408
loop origl1 origl2
14091409

1410-
and SolveTypeEqualsTypeWithContravarianceEqns (csenv:ConstraintSolverEnv) ndeep m2 trace cxsln origl1 origl2 typars =
1410+
and SolveTypeEqualsTypeWithContravarianceEqns (csenv:ConstraintSolverEnv) ndeep m2 trace cxsln origl1 origl2 typars tyconRef =
14111411
let isContravariant (t:Typar) =
14121412
t.typar_opt_data
14131413
|> Option.map (fun d -> d.typar_is_contravariant)
14141414
|> Option.defaultValue(false)
1415+
1416+
// Special case for IEquatable<T>: treat its type parameter as contravariant for nullness purposes
1417+
// This matches C# behavior where IEquatable<T> is treated as contravariant for nullness, even though
1418+
// it's not formally marked as contravariant in IL.
1419+
// See: https://github.com/dotnet/fsharp/issues/18759 and https://github.com/dotnet/roslyn/issues/37187
1420+
let isIEquatableContravariantForNullness = tyconRefEq csenv.g tyconRef csenv.g.system_GenericIEquatable_tcref
14151421

14161422
match origl1, origl2, typars with
14171423
| [], [], [] -> CompleteD
@@ -1425,7 +1431,8 @@ and SolveTypeEqualsTypeWithContravarianceEqns (csenv:ConstraintSolverEnv) ndeep
14251431
let h1 =
14261432
// For contravariant typars (`<in T> in C#'), if the required type is WithNull, the actual type can have any nullness it wants
14271433
// Without this added logic, their nullness would be forced to be equal.
1428-
if isContravariant hTp && (nullnessOfTy csenv.g h2).TryEvaluate() = ValueSome NullnessInfo.WithNull then
1434+
// Special case: IEquatable<T> is treated as contravariant for nullness purposes to match C# behavior
1435+
if (isContravariant hTp || isIEquatableContravariantForNullness) && (nullnessOfTy csenv.g h2).TryEvaluate() = ValueSome NullnessInfo.WithNull then
14291436
replaceNullnessOfTy csenv.g.knownWithNull h1
14301437
else
14311438
h1
@@ -1534,11 +1541,11 @@ and SolveTypeSubsumesType (csenv: ConstraintSolverEnv) ndeep m2 (trace: Optional
15341541
(tyconRefEq g tagc1 g.byrefkind_In_tcr || tyconRefEq g tagc1 g.byrefkind_Out_tcr) ) -> ()
15351542
| _ -> return! SolveTypeEqualsType csenv ndeep m2 trace cxsln tag1 tag2
15361543
}
1537-
| _ -> SolveTypeEqualsTypeWithContravarianceEqns csenv ndeep m2 trace cxsln l1 l2 tc1.TyparsNoRange
1544+
| _ -> SolveTypeEqualsTypeWithContravarianceEqns csenv ndeep m2 trace cxsln l1 l2 tc1.TyparsNoRange tc1
15381545

15391546
| TType_app (tc1, l1, _) , TType_app (tc2, l2, _) when tyconRefEq g tc1 tc2 ->
15401547
trackErrors {
1541-
do! SolveTypeEqualsTypeWithContravarianceEqns csenv ndeep m2 trace cxsln l1 l2 tc1.TyparsNoRange
1548+
do! SolveTypeEqualsTypeWithContravarianceEqns csenv ndeep m2 trace cxsln l1 l2 tc1.TyparsNoRange tc1
15421549
do! SolveNullnessSubsumesNullness csenv m2 trace ty1 ty2 (nullnessOfTy g sty1) (nullnessOfTy g sty2)
15431550
}
15441551

tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1540,4 +1540,17 @@ let main _ =
15401540
|> compile
15411541
//|> verifyIL ["abc"]
15421542
|> run
1543-
|> verifyOutputContains [|"Test true;,1 true,2 true,3 true,4 true,5 true,6 false,7 true,8 false,9 false,10 false,11 false,12 true"|]
1543+
|> verifyOutputContains [|"Test true;,1 true,2 true,3 true,4 true,5 true,6 false,7 true,8 false,9 false,10 false,11 false,12 true"|]
1544+
1545+
[<Fact>]
1546+
let ``No nullness warning when casting non-nullable to IEquatable`` () =
1547+
FSharp """module Test
1548+
1549+
open System
1550+
1551+
let x = ""
1552+
let y = x :> IEquatable<string> // Should not warn about nullness
1553+
"""
1554+
|> asLibrary
1555+
|> typeCheckWithStrictNullness
1556+
|> shouldSucceed

0 commit comments

Comments
 (0)