diff --git a/.gitignore b/.gitignore index 6ca107a5779..a5b80d11cc0 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,7 @@ positive.exe *.bsl.actual /src/FSharp.DependencyManager.Nuget/StandardError.txt /src/FSharp.DependencyManager.Nuget/StandardOutput.txt + +# Standard output/error files in root directory +StandardOutput.txt +StandardError.txt diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.100.md index c7f68777166..8276598fab9 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.100.md @@ -13,6 +13,7 @@ * Shorthand lambda: fix completion for chained calls and analysis for unfinished expression ([PR #18560](https://github.com/dotnet/fsharp/pull/18560)) * Completion: fix previous namespace considered opened [PR #18609](https://github.com/dotnet/fsharp/pull/18609) * Fix active pattern typechecking regression. ([Issue #18638](https://github.com/dotnet/fsharp/issues/18638), [PR #18642](https://github.com/dotnet/fsharp/pull/18642)) +* Fix nullness warnings when casting non-nullable values to `IEquatable` to match C# behavior. ([Issue #18759](https://github.com/dotnet/fsharp/issues/18759)) ### Changed * Use `errorR` instead of `error` in `CheckDeclarations.fs` when possible. ([PR #18645](https://github.com/dotnet/fsharp/pull/18645)) diff --git a/src/Compiler/Checking/ConstraintSolver.fs b/src/Compiler/Checking/ConstraintSolver.fs index 02829f564ad..1132eefa448 100644 --- a/src/Compiler/Checking/ConstraintSolver.fs +++ b/src/Compiler/Checking/ConstraintSolver.fs @@ -1407,11 +1407,17 @@ and SolveTypeEqualsTypeEqns csenv ndeep m2 trace cxsln origl1 origl2 = ErrorD(ConstraintSolverTupleDiffLengths(csenv.DisplayEnv, csenv.eContextInfo, origl1, origl2, csenv.m, m2)) loop origl1 origl2 -and SolveTypeEqualsTypeWithContravarianceEqns (csenv:ConstraintSolverEnv) ndeep m2 trace cxsln origl1 origl2 typars = +and SolveTypeEqualsTypeWithContravarianceEqns (csenv:ConstraintSolverEnv) ndeep m2 trace cxsln origl1 origl2 typars tyconRef = let isContravariant (t:Typar) = t.typar_opt_data |> Option.map (fun d -> d.typar_is_contravariant) |> Option.defaultValue(false) + + // Special case for IEquatable: treat its type parameter as contravariant for nullness purposes + // This matches C# behavior where IEquatable is treated as contravariant for nullness, even though + // it's not formally marked as contravariant in IL. + // See: https://github.com/dotnet/fsharp/issues/18759 and https://github.com/dotnet/roslyn/issues/37187 + let isIEquatableContravariantForNullness = tyconRefEq csenv.g tyconRef csenv.g.system_GenericIEquatable_tcref match origl1, origl2, typars with | [], [], [] -> CompleteD @@ -1425,7 +1431,8 @@ and SolveTypeEqualsTypeWithContravarianceEqns (csenv:ConstraintSolverEnv) ndeep let h1 = // For contravariant typars (` in C#'), if the required type is WithNull, the actual type can have any nullness it wants // Without this added logic, their nullness would be forced to be equal. - if isContravariant hTp && (nullnessOfTy csenv.g h2).TryEvaluate() = ValueSome NullnessInfo.WithNull then + // Special case: IEquatable is treated as contravariant for nullness purposes to match C# behavior + if (isContravariant hTp || isIEquatableContravariantForNullness) && (nullnessOfTy csenv.g h2).TryEvaluate() = ValueSome NullnessInfo.WithNull then replaceNullnessOfTy csenv.g.knownWithNull h1 else h1 @@ -1534,11 +1541,11 @@ and SolveTypeSubsumesType (csenv: ConstraintSolverEnv) ndeep m2 (trace: Optional (tyconRefEq g tagc1 g.byrefkind_In_tcr || tyconRefEq g tagc1 g.byrefkind_Out_tcr) ) -> () | _ -> return! SolveTypeEqualsType csenv ndeep m2 trace cxsln tag1 tag2 } - | _ -> SolveTypeEqualsTypeWithContravarianceEqns csenv ndeep m2 trace cxsln l1 l2 tc1.TyparsNoRange + | _ -> SolveTypeEqualsTypeWithContravarianceEqns csenv ndeep m2 trace cxsln l1 l2 tc1.TyparsNoRange tc1 | TType_app (tc1, l1, _) , TType_app (tc2, l2, _) when tyconRefEq g tc1 tc2 -> trackErrors { - do! SolveTypeEqualsTypeWithContravarianceEqns csenv ndeep m2 trace cxsln l1 l2 tc1.TyparsNoRange + do! SolveTypeEqualsTypeWithContravarianceEqns csenv ndeep m2 trace cxsln l1 l2 tc1.TyparsNoRange tc1 do! SolveNullnessSubsumesNullness csenv m2 trace ty1 ty2 (nullnessOfTy g sty1) (nullnessOfTy g sty2) } diff --git a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs index ced7e02fdca..4d0710ba899 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs @@ -1540,4 +1540,17 @@ let main _ = |> compile //|> verifyIL ["abc"] |> run - |> 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"|] \ No newline at end of file + |> 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"|] + +[] +let ``No nullness warning when casting non-nullable to IEquatable`` () = + FSharp """module Test + +open System + +let x = "" +let y = x :> IEquatable // Should not warn about nullness + """ + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldSucceed