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 9160a6db309..4bf5f4b80f1 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.100.md @@ -35,6 +35,7 @@ * Checker: fix declaring type for abbreviated types extensions ([PR #18909](https://github.com/dotnet/fsharp/pull/18909)) * Caches: type subsumption cache key perf regression ([Issue #18925](https://github.com/dotnet/fsharp/issues/18925) [PR #18926](https://github.com/dotnet/fsharp/pull/18926)) * Ensure that line directives are applied to source identifiers (issue [#18908](https://github.com/dotnet/fsharp/issues/18908), PR [#18918](https://github.com/dotnet/fsharp/pull/18918)) +* Fix expected and actual types in ErrorFromAddingTypeEquation message and extended diagnostic data. ([PR #18915](https://github.com/dotnet/fsharp/pull/18915)) ### 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/Driver/CompilerDiagnostics.fs b/src/Compiler/Driver/CompilerDiagnostics.fs index d2a64521648..c5a04657a72 100644 --- a/src/Compiler/Driver/CompilerDiagnostics.fs +++ b/src/Compiler/Driver/CompilerDiagnostics.fs @@ -733,21 +733,15 @@ type Exception with if m.StartLine <> m2.StartLine then os.AppendString(SeeAlsoE().Format(stringOfRange m)) - | ConstraintSolverTypesNotInEqualityRelation(denv, (TType_measure _ as ty1), (TType_measure _ as ty2), m, m2, _) -> - // REVIEW: consider if we need to show _cxs (the type parameter constraints) - let ty1, ty2, _cxs = NicePrint.minimalStringsOfTwoTypes denv ty1 ty2 - - os.AppendString(ConstraintSolverTypesNotInEqualityRelation1E().Format ty1 ty2) - - if m.StartLine <> m2.StartLine then - os.AppendString(SeeAlsoE().Format(stringOfRange m)) - | ConstraintSolverTypesNotInEqualityRelation(denv, ty1, ty2, m, m2, contextInfo) -> // REVIEW: consider if we need to show _cxs (the type parameter constraints) - let ty1, ty2, _cxs = NicePrint.minimalStringsOfTwoTypes denv ty1 ty2 + let ty1str, ty2str, _cxs = NicePrint.minimalStringsOfTwoTypes denv ty1 ty2 - OutputTypesNotInEqualityRelationContextInfo contextInfo ty1 ty2 m os (fun _ -> - os.AppendString(ConstraintSolverTypesNotInEqualityRelation2E().Format ty1 ty2)) + match ty1, ty2 with + | TType_measure _, TType_measure _ -> os.AppendString(ConstraintSolverTypesNotInEqualityRelation1E().Format ty1str ty2str) + | _ -> + OutputTypesNotInEqualityRelationContextInfo contextInfo ty1str ty2str m os (fun _ -> + os.AppendString(ConstraintSolverTypesNotInEqualityRelation2E().Format ty1str ty2str)) if m.StartLine <> m2.StartLine then os.AppendString(SeeAlsoE().Format(stringOfRange m)) @@ -816,11 +810,26 @@ type Exception with os.AppendString(SeeAlsoE().Format(stringOfRange m1)) | ErrorFromAddingTypeEquation(g, denv, ty1, ty2, e, _) -> - if not (typeEquiv g ty1 ty2) then - let ty1, ty2, tpcs = NicePrint.minimalStringsOfTwoTypes denv ty1 ty2 + let e = + if not (typeEquiv g ty1 ty2) then + let ty1, ty2, tpcs = NicePrint.minimalStringsOfTwoTypes denv ty1 ty2 - if ty1 <> ty2 + tpcs then - os.AppendString(ErrorFromAddingTypeEquation2E().Format ty1 ty2 tpcs) + if ty1 <> ty2 + tpcs then + os.AppendString(ErrorFromAddingTypeEquation2E().Format ty1 ty2 tpcs) + + e + + else + // Fix for https://github.com/dotnet/fsharp/issues/18905 + // If ty1 = ty2 after the type solving, then ty2 holds an actual type. + // The order of expected and actual types in ConstraintSolverTypesNotInEqualityRelation can be arbitrary + // due to type solving logic. + // If ty1 = ty2 = ty2b, it means ty2b is also an actual type, and it needs to be swapped with ty1b + // to be correctly used in the type mismatch error message based on ConstraintSolverTypesNotInEqualityRelation + match e with + | ConstraintSolverTypesNotInEqualityRelation(env, ty1b, ty2b, m, m2, contextInfo) when typeEquiv g ty2 ty2b -> + ConstraintSolverTypesNotInEqualityRelation(env, ty2b, ty1b, m, m2, contextInfo) + | _ -> e e.Output(os, suggestNames) diff --git a/src/Compiler/Symbols/FSharpDiagnostic.fs b/src/Compiler/Symbols/FSharpDiagnostic.fs index 8854e9eea7e..cafaf1c788b 100644 --- a/src/Compiler/Symbols/FSharpDiagnostic.fs +++ b/src/Compiler/Symbols/FSharpDiagnostic.fs @@ -176,6 +176,7 @@ type FSharpDiagnostic(m: range, severity: FSharpDiagnosticSeverity, message: str | Some symbolEnv -> match diagnostic.Exception with + | ErrorFromAddingConstraint(displayEnv, ConstraintSolverTypesNotInEqualityRelation(_, actualType, expectedType, _, _, contextInfo), _) | ErrorFromAddingTypeEquation(_, displayEnv, expectedType, actualType, ConstraintSolverTupleDiffLengths(contextInfo = contextInfo), _) | ErrorsFromAddingSubsumptionConstraint(_, displayEnv, expectedType, actualType, _, contextInfo, _) -> let context = DiagnosticContextInfo.From(contextInfo) @@ -187,6 +188,8 @@ type FSharpDiagnostic(m: range, severity: FSharpDiagnosticSeverity, message: str ty1, ty2 elif not (typeEquiv g ty1 ty2) then ty1, ty2 + elif typeEquiv g ty2 ty2b then + ty1b, ty2b else ty2b, ty1b let context = DiagnosticContextInfo.From(contextInfo) diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/Types/RecordTypes/RecordTypes.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/Types/RecordTypes/RecordTypes.fs index bebf3652605..da18bda2f33 100644 --- a/tests/FSharp.Compiler.ComponentTests/Conformance/Types/RecordTypes/RecordTypes.fs +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/Types/RecordTypes/RecordTypes.fs @@ -116,7 +116,7 @@ module RecordTypes = (Warning 464, Line 15, Col 22, Line 15, Col 28, "This code is less generic than indicated by its annotations. A unit-of-measure specified using '_' has been determined to be '1', i.e. dimensionless. Consider making the code generic, or removing the use of '_'.") (Warning 464, Line 15, Col 35, Line 15, Col 42, "This code is less generic than indicated by its annotations. A unit-of-measure specified using '_' has been determined to be '1', i.e. dimensionless. Consider making the code generic, or removing the use of '_'.") (Error 5, Line 17, Col 1, Line 17, Col 5, "This field is not mutable") - (Error 1, Line 17, Col 16, Line 17, Col 22, "The type 'decimal' does not match the type 'float'") + (Error 1, Line 17, Col 16, Line 17, Col 22, "The type 'float' does not match the type 'decimal'") (Error 5, Line 18, Col 1, Line 18, Col 5, "This field is not mutable") (Error 1, Line 18, Col 16, Line 18, Col 21, "This expression was expected to have type\n 'float' \nbut here has type\n 'decimal' ") ] diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/Types/TypeConstraints/IWSAMsAndSRTPs/IWSAMsAndSRTPsTests.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/Types/TypeConstraints/IWSAMsAndSRTPs/IWSAMsAndSRTPsTests.fs index bfa44185a1c..32c16b46e04 100644 --- a/tests/FSharp.Compiler.ComponentTests/Conformance/Types/TypeConstraints/IWSAMsAndSRTPs/IWSAMsAndSRTPsTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/Types/TypeConstraints/IWSAMsAndSRTPs/IWSAMsAndSRTPsTests.fs @@ -1145,7 +1145,7 @@ module StaticAbstractBug = |> compile |> shouldFail |> withDiagnostics [ - (Error 1, Line 14, Col 41, Line 14, Col 42, "The type 'bool' does not match the type 'int'") + (Error 1, Line 14, Col 41, Line 14, Col 42, "The type 'int' does not match the type 'bool'") (Error 1, Line 16, Col 32, Line 16, Col 33, "This expression was expected to have type 'bool' but here has type diff --git a/tests/FSharp.Compiler.ComponentTests/ErrorMessages/ExtendedDiagnosticDataTests.fs b/tests/FSharp.Compiler.ComponentTests/ErrorMessages/ExtendedDiagnosticDataTests.fs index f3448e09440..5d86b6883c7 100755 --- a/tests/FSharp.Compiler.ComponentTests/ErrorMessages/ExtendedDiagnosticDataTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/ErrorMessages/ExtendedDiagnosticDataTests.fs @@ -4,15 +4,21 @@ open FSharp.Compiler.Text open FSharp.Compiler.Diagnostics open FSharp.Compiler.Diagnostics.ExtendedData +open FSharp.Test.Assert open FSharp.Test.Compiler open Xunit +let checkTypes (diagnosticData: TypeMismatchDiagnosticExtendedData) expected actual = + {| ExpectedType = expected; ActualType = actual |} + |> shouldBe {| ExpectedType = diagnosticData.ExpectedType.Format(diagnosticData.DisplayContext) + ActualType = diagnosticData.ActualType.Format(diagnosticData.DisplayContext) |} + let inline checkDiagnosticData (diagnosticNumber, message) (check: 'a -> unit) (checkResults: 'b when 'b: (member Diagnostics: FSharpDiagnostic[])) = match checkResults.Diagnostics |> Array.tryFind (fun d -> d.ErrorNumber = diagnosticNumber) with - | None -> failwith "Expected diagnostic not found" + | None -> failwith $"Expected diagnostic (number {diagnosticNumber}) not found" | Some diagnostic -> Assert.Equal(message, diagnostic.Message) @@ -20,6 +26,29 @@ let inline checkDiagnosticData | Some(:? 'a as data) -> check data | _ -> failwith "Expected diagnostic extended data not found" + checkResults + +let inline checkExpectedActualTypesInContext + diagnosticNumber + expectedType actualType + message + context + checkResults = + + checkResults + |> checkDiagnosticData (diagnosticNumber, message) + (fun (data: TypeMismatchDiagnosticExtendedData) -> + checkTypes data expectedType actualType + Assert.Equal(context, data.ContextInfo)) + +let inline checkExpectedActualTypes + diagnosticNumber + expectedType actualType + message + checkResults = + + checkResults + |> checkExpectedActualTypesInContext diagnosticNumber expectedType actualType message DiagnosticContextInfo.NoContext [] let ``TypeMismatchDiagnosticExtendedData 01`` () = @@ -27,13 +56,10 @@ let ``TypeMismatchDiagnosticExtendedData 01`` () = let x, y, z = 1, 2 """ |> typecheckResults - |> checkDiagnosticData - (1, "Type mismatch. Expecting a tuple of length 3 of type\n 'a * 'b * 'c \nbut given a tuple of length 2 of type\n int * int \n") - (fun (typeMismatch: TypeMismatchDiagnosticExtendedData) -> - Assert.Equal(DiagnosticContextInfo.NoContext, typeMismatch.ContextInfo) - let displayContext = typeMismatch.DisplayContext - Assert.Equal("obj * obj * obj", typeMismatch.ExpectedType.Format(displayContext)) - Assert.Equal("int * int", typeMismatch.ActualType.Format(displayContext))) + |> checkExpectedActualTypes 1 + "obj * obj * obj" + "int * int" + "Type mismatch. Expecting a tuple of length 3 of type\n 'a * 'b * 'c \nbut given a tuple of length 2 of type\n int * int \n" [] let ``TypeMismatchDiagnosticExtendedData 02`` () = @@ -41,13 +67,10 @@ let ``TypeMismatchDiagnosticExtendedData 02`` () = let x, y = 1 """ |> typecheckResults - |> checkDiagnosticData - (1, "This expression was expected to have type\n ''a * 'b' \nbut here has type\n 'int' ") - (fun (typeMismatch: TypeMismatchDiagnosticExtendedData) -> - let displayContext = typeMismatch.DisplayContext - Assert.Equal(DiagnosticContextInfo.NoContext, typeMismatch.ContextInfo) - Assert.Equal("obj * obj", typeMismatch.ExpectedType.Format(displayContext)) - Assert.Equal("int", typeMismatch.ActualType.Format(displayContext))) + |> checkExpectedActualTypes 1 + "obj * obj" + "int" + "This expression was expected to have type\n ''a * 'b' \nbut here has type\n 'int' " [] let ``TypeMismatchDiagnosticExtendedData 03`` () = @@ -55,13 +78,11 @@ let ``TypeMismatchDiagnosticExtendedData 03`` () = if true then 5 """ |> typecheckResults - |> checkDiagnosticData - (1, "This 'if' expression is missing an 'else' branch. Because 'if' is an expression, and not a statement, add an 'else' branch which also returns a value of type 'int'.") - (fun (typeMismatch: TypeMismatchDiagnosticExtendedData) -> - let displayContext = typeMismatch.DisplayContext - Assert.Equal(DiagnosticContextInfo.OmittedElseBranch, typeMismatch.ContextInfo) - Assert.Equal("unit", typeMismatch.ExpectedType.Format(displayContext)) - Assert.Equal("int", typeMismatch.ActualType.Format(displayContext))) + |> checkExpectedActualTypesInContext 1 + "unit" + "int" + "This 'if' expression is missing an 'else' branch. Because 'if' is an expression, and not a statement, add an 'else' branch which also returns a value of type 'int'." + DiagnosticContextInfo.OmittedElseBranch [] let ``TypeMismatchDiagnosticExtendedData 04`` () = @@ -69,13 +90,10 @@ let ``TypeMismatchDiagnosticExtendedData 04`` () = 1 :> string """ |> typecheckResults - |> checkDiagnosticData - (193, "Type constraint mismatch. The type \n 'int' \nis not compatible with type\n 'string' \n") - (fun (typeMismatch: TypeMismatchDiagnosticExtendedData) -> - let displayContext = typeMismatch.DisplayContext - Assert.Equal(DiagnosticContextInfo.NoContext, typeMismatch.ContextInfo) - Assert.Equal("string", typeMismatch.ExpectedType.Format(displayContext)) - Assert.Equal("int", typeMismatch.ActualType.Format(displayContext))) + |> checkExpectedActualTypes 193 + "string" + "int" + "Type constraint mismatch. The type \n 'int' \nis not compatible with type\n 'string' \n" [] let ``TypeMismatchDiagnosticExtendedData 05`` () = @@ -85,13 +103,11 @@ match 0 with | 1 -> "a" """ |> typecheckResults - |> checkDiagnosticData - (1, "All branches of a pattern match expression must return values implicitly convertible to the type of the first branch, which here is 'int'. This branch returns a value of type 'string'.") - (fun (typeMismatch: TypeMismatchDiagnosticExtendedData) -> - let displayContext = typeMismatch.DisplayContext - Assert.Equal(DiagnosticContextInfo.FollowingPatternMatchClause, typeMismatch.ContextInfo) - Assert.Equal("int", typeMismatch.ExpectedType.Format(displayContext)) - Assert.Equal("string", typeMismatch.ActualType.Format(displayContext))) + |> checkExpectedActualTypesInContext 1 + "int" + "string" + "All branches of a pattern match expression must return values implicitly convertible to the type of the first branch, which here is 'int'. This branch returns a value of type 'string'." + DiagnosticContextInfo.FollowingPatternMatchClause //TODO: FollowingPatternMatchClause should be provided for type equation diagnostics come from a return type only [] @@ -106,13 +122,11 @@ match 0 with 1 """ |> typecheckResults - |> checkDiagnosticData - (1, "This expression was expected to have type\n 'int' \nbut here has type\n 'string' ") - (fun (typeMismatch: TypeMismatchDiagnosticExtendedData) -> - let displayContext = typeMismatch.DisplayContext - Assert.Equal(DiagnosticContextInfo.FollowingPatternMatchClause, typeMismatch.ContextInfo) //Should be NoContext - Assert.Equal("int", typeMismatch.ExpectedType.Format(displayContext)) - Assert.Equal("string", typeMismatch.ActualType.Format(displayContext))) + |> checkExpectedActualTypesInContext 1 + "int" + "string" + "This expression was expected to have type\n 'int' \nbut here has type\n 'string' " + DiagnosticContextInfo.FollowingPatternMatchClause //Should be NoContext [] let ``TypeMismatchDiagnosticExtendedData 06`` () = @@ -121,13 +135,11 @@ let _: bool = if true then "a" else "b" """ |> typecheckResults - |> checkDiagnosticData - (1, "The 'if' expression needs to have type 'bool' to satisfy context type requirements. It currently has type 'string'.") - (fun (typeMismatch: TypeMismatchDiagnosticExtendedData) -> - let displayContext = typeMismatch.DisplayContext - Assert.Equal(DiagnosticContextInfo.IfExpression, typeMismatch.ContextInfo) - Assert.Equal("bool", typeMismatch.ExpectedType.Format(displayContext)) - Assert.Equal("string", typeMismatch.ActualType.Format(displayContext))) + |> checkExpectedActualTypesInContext 1 + "bool" + "string" + "The 'if' expression needs to have type 'bool' to satisfy context type requirements. It currently has type 'string'." + DiagnosticContextInfo.IfExpression [] let ``TypeMismatchDiagnosticExtendedData 07`` () = @@ -135,13 +147,11 @@ let ``TypeMismatchDiagnosticExtendedData 07`` () = if true then 1 else "a" """ |> typecheckResults - |> checkDiagnosticData - (1, "All branches of an 'if' expression must return values implicitly convertible to the type of the first branch, which here is 'int'. This branch returns a value of type 'string'.") - (fun (typeMismatch: TypeMismatchDiagnosticExtendedData) -> - let displayContext = typeMismatch.DisplayContext - Assert.Equal(DiagnosticContextInfo.ElseBranchResult, typeMismatch.ContextInfo) - Assert.Equal("int", typeMismatch.ExpectedType.Format(displayContext)) - Assert.Equal("string", typeMismatch.ActualType.Format(displayContext))) + |> checkExpectedActualTypesInContext 1 + "int" + "string" + "All branches of an 'if' expression must return values implicitly convertible to the type of the first branch, which here is 'int'. This branch returns a value of type 'string'." + DiagnosticContextInfo.ElseBranchResult [] let ``TypeMismatchDiagnosticExtendedData 08`` () = @@ -150,13 +160,10 @@ type R = { Field1: int } let f (x: R) = "" + x.Field1 """ |> typecheckResults - |> checkDiagnosticData - (1, "The type 'int' does not match the type 'string'") - (fun (typeMismatch: TypeMismatchDiagnosticExtendedData) -> - let displayContext = typeMismatch.DisplayContext - Assert.Equal(DiagnosticContextInfo.NoContext, typeMismatch.ContextInfo) - Assert.Equal("string", typeMismatch.ExpectedType.Format(displayContext)) - Assert.Equal("int", typeMismatch.ActualType.Format(displayContext))) + |> checkExpectedActualTypes 1 + "string" + "int" + "The type 'int' does not match the type 'string'" [] let ``TypeMismatchDiagnosticExtendedData 09`` () = @@ -164,13 +171,10 @@ let ``TypeMismatchDiagnosticExtendedData 09`` () = let x: string = 1 """ |> typecheckResults - |> checkDiagnosticData - (1, "This expression was expected to have type\n 'string' \nbut here has type\n 'int' ") - (fun (typeMismatch: TypeMismatchDiagnosticExtendedData) -> - let displayContext = typeMismatch.DisplayContext - Assert.Equal(DiagnosticContextInfo.NoContext, typeMismatch.ContextInfo) - Assert.Equal("string", typeMismatch.ExpectedType.Format(displayContext)) - Assert.Equal("int", typeMismatch.ActualType.Format(displayContext))) + |> checkExpectedActualTypes 1 + "string" + "int" + "This expression was expected to have type\n 'string' \nbut here has type\n 'int' " [] let ``TypeMismatchDiagnosticExtendedData 10`` () = @@ -179,15 +183,57 @@ let f1 (x: outref<'T>) = 1 let f2 (x: inref<'T>) = f1 &x """ |> typecheckResults - |> checkDiagnosticData - (1, "Type mismatch. Expecting a\n 'outref<'T>' \nbut given a\n 'inref<'T>' \nThe type 'ByRefKinds.Out' does not match the type 'ByRefKinds.In'") - (fun (typeMismatch: TypeMismatchDiagnosticExtendedData) -> - let displayContext = typeMismatch.DisplayContext - Assert.Equal(DiagnosticContextInfo.NoContext, typeMismatch.ContextInfo) - Assert.Equal("outref<'T>", typeMismatch.ExpectedType.Format(displayContext)) - Assert.Equal("inref<'T>", typeMismatch.ActualType.Format(displayContext))) + |> checkExpectedActualTypes 1 + "outref<'T>" + "inref<'T>" + // TODO: wrong additional generic parameters mismatch message, + // should be "The type 'ByRefKinds.In' does not match the type 'ByRefKinds.Out'" + "Type mismatch. Expecting a\n 'outref<'T>' \nbut given a\n 'inref<'T>' \nThe type 'ByRefKinds.Out' does not match the type 'ByRefKinds.In'" -[] +[] +let ``TypeMismatchDiagnosticExtendedData 11`` () = + FSharp """ +type T() = + static member P1 = T.P2 + 1 + static member P2 = "" +""" + |> typecheckResults + // static member P1 = T.P2 ->+<- 1 + |> checkExpectedActualTypes 43 + "string" + "int" + "The type 'int' does not match the type 'string'" + + // static member P2 = ->""<- + |> checkExpectedActualTypes 1 + "int" + "string" + "The type 'string' does not match the type 'int'" + +[] +let ``TypeMismatchDiagnosticExtendedData 12`` () = + FSharp """ +let x: string = 1 + 1 +""" + |> typecheckResults + |> checkExpectedActualTypes 1 + "string" + "int" + "The type 'int' does not match the type 'string'" + +[] +let ``TypeMismatchDiagnosticExtendedData 13`` () = + FSharp """ +let x: string -> string = id +let y: unit -> string = x +""" + |> typecheckResults + |> checkExpectedActualTypes 1 + "unit -> string" + "string -> string" + "Type mismatch. Expecting a\n 'unit -> string' \nbut given a\n 'string -> string' \nThe type 'string' does not match the type 'unit'" + +[] [] [] let ``ArgumentsInSigAndImplMismatchExtendedData 01`` useTransparentCompiler =