diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs index 18d4c691a6950..59e99223b809f 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs @@ -5631,17 +5631,32 @@ private BoundExpression BindNullCoalescingAssignmentOperator(AssignmentExpressio ReportSuppressionIfNeeded(leftOperand, diagnostics); BoundExpression rightOperand = BindValue(node.Right, diagnostics, BindValueKind.RValue); - // If either operand is bad, bail out preventing more cascading errors - if (leftOperand.HasAnyErrors || rightOperand.HasAnyErrors) + TypeSymbol leftType = leftOperand.Type; + + var rightOperandTargetType = leftType switch + { + { IsReferenceType: true } or { IsValueType: false, TypeKind: TypeKind.TypeParameter } => leftType, + { IsValueType: true } when leftType.IsNullableType() => leftType.GetNullableUnderlyingType(), + _ => null + }; + + // If left operand is bad or right operator is bad and we cannot determine its type, take the default error recovery path + if (leftOperand.HasAnyErrors || (rightOperandTargetType is null && rightOperand.HasAnyErrors)) { leftOperand = BindToTypeForErrorRecovery(leftOperand); rightOperand = BindToTypeForErrorRecovery(rightOperand); return new BoundNullCoalescingAssignmentOperator(node, leftOperand, rightOperand, CreateErrorType(), hasErrors: true); } + // If right operand is bad, but we know its type, make sure conversion is in place for better error recovery + else if (rightOperand.HasAnyErrors) + { + leftOperand = BindToTypeForErrorRecovery(leftOperand); + var conversion = GenerateConversionForAssignment(rightOperandTargetType, rightOperand, diagnostics, ConversionForAssignmentFlags.CompoundAssignment); + return new BoundNullCoalescingAssignmentOperator(node, leftOperand, conversion, rightOperandTargetType, hasErrors: true); + } // Given a ??= b, the type of a is A, the type of B is b, and if A is a nullable value type, the underlying // non-nullable value type of A is A0. - TypeSymbol leftType = leftOperand.Type; Debug.Assert((object)leftType != null); // If A is a non-nullable value type, a compile-time error occurs diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/NullCoalesceAssignmentTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/NullCoalesceAssignmentTests.cs index 158198d32499a..6a1c7dc0b62e8 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/NullCoalesceAssignmentTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/NullCoalesceAssignmentTests.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.CSharp.UnitTests @@ -186,5 +187,635 @@ void M(int? a) Assert.Equal(SpecialType.System_Int32, semanticModel.GetTypeInfo(defaultLiteral).Type.SpecialType); Assert.Equal(SpecialType.System_Int32, semanticModel.GetTypeInfo(defaultLiteral).ConvertedType.SpecialType); } + + [Fact] + public void ErrorRecovery_ImplicitObjectCreation_ReferenceType() + { + var source = """ + class C + { + void M() + { + C c = default; + c ??= /**/new(a)/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (6,29): error CS0103: The name 'a' does not exist in the current context + // c ??= /**/new(a)/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 29)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.Equal("C", semanticInfo.Type.ToTestDisplayString()); + Assert.Equal("C", semanticInfo.ConvertedType.ToTestDisplayString()); + + Assert.Null(semanticInfo.Symbol); + Assert.Collection(semanticInfo.CandidateSymbols, + static c => Assert.Equal("C..ctor()", c.ToTestDisplayString())); + } + + [Fact] + public void ErrorRecovery_ImplicitObjectCreation_NullableValueType() + { + var source = """ + struct S + { + void M() + { + S? s = default; + s ??= /**/new(a)/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (6,29): error CS0103: The name 'a' does not exist in the current context + // s ??= /**/new(a)/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 29)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.Equal("S", semanticInfo.Type.ToTestDisplayString()); + Assert.Equal("S", semanticInfo.ConvertedType.ToTestDisplayString()); + + Assert.Null(semanticInfo.Symbol); + Assert.Collection(semanticInfo.CandidateSymbols, + static c => Assert.Equal("S..ctor()", c.ToTestDisplayString())); + } + + [Fact] + public void ErrorRecovery_ImplicitObjectCreation_NotNullableValueType() + { + var source = """ + struct S + { + void M() + { + S s = default; + s ??= /**/new(a)/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (6,29): error CS0103: The name 'a' does not exist in the current context + // s ??= /**/new(a)/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 29)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.True(semanticInfo.Type.IsErrorType()); + Assert.True(semanticInfo.ConvertedType.IsErrorType()); + + Assert.Null(semanticInfo.Symbol); + Assert.Empty(semanticInfo.CandidateSymbols); + } + + [Fact] + public void ErrorRecovery_ImplicitObjectCreation_UnconstrainedGenericType() + { + var source = """ + class C + { + void M() where T : new() + { + T t = default; + t ??= /**/new(a)/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (6,29): error CS0103: The name 'a' does not exist in the current context + // t ??= /**/new(a)/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 29)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.Equal("T", semanticInfo.Type.ToTestDisplayString()); + Assert.Equal("T", semanticInfo.ConvertedType.ToTestDisplayString()); + + Assert.Null(semanticInfo.Symbol); + Assert.Collection(semanticInfo.CandidateSymbols, + static c => Assert.Equal("T", c.ToTestDisplayString())); + } + + [Fact] + public void ErrorRecovery_ImplicitObjectCreation_ReferenceGenericType() + { + var source = """ + class C + { + void M() where T : class, new() + { + T t = default; + t ??= /**/new(a)/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (6,29): error CS0103: The name 'a' does not exist in the current context + // t ??= /**/new(a)/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 29)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.Equal("T", semanticInfo.Type.ToTestDisplayString()); + Assert.Equal("T", semanticInfo.ConvertedType.ToTestDisplayString()); + + Assert.Null(semanticInfo.Symbol); + Assert.Collection(semanticInfo.CandidateSymbols, + static c => Assert.Equal("T", c.ToTestDisplayString())); + } + + [Fact] + public void ErrorRecovery_ImplicitObjectCreation_ValueGenericType_Nullable() + { + var source = """ + class C + { + void M() where T : struct + { + T? t = default; + t ??= /**/new(a)/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (6,29): error CS0103: The name 'a' does not exist in the current context + // t ??= /**/new(a)/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 29)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.Equal("T", semanticInfo.Type.ToTestDisplayString()); + Assert.Equal("T", semanticInfo.ConvertedType.ToTestDisplayString()); + + Assert.Null(semanticInfo.Symbol); + Assert.Collection(semanticInfo.CandidateSymbols, + static c => Assert.Equal("T", c.ToTestDisplayString())); + } + + [Fact] + public void ErrorRecovery_ImplicitObjectCreation_ValueGenericType_NotNullable() + { + var source = """ + class C + { + void M() where T : struct + { + T t = default; + t ??= /**/new(a)/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (6,29): error CS0103: The name 'a' does not exist in the current context + // t ??= /**/new(a)/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 29)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.True(semanticInfo.Type.IsErrorType()); + Assert.True(semanticInfo.ConvertedType.IsErrorType()); + + Assert.Null(semanticInfo.Symbol); + Assert.Empty(semanticInfo.CandidateSymbols); + } + + [Fact] + public void ErrorRecovery_CollectionExpression_ReferenceType() + { + var source = """ + class C + { + void M() + { + int[] arr = default; + arr ??= /**/[a]/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (6,28): error CS0103: The name 'a' does not exist in the current context + // arr ??= /**/[a]/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 28)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.Null(semanticInfo.Type); + Assert.Equal("System.Int32[]", semanticInfo.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void ErrorRecovery_CollectionExpression_NullableValueType() + { + var source = """ + using System.Collections.Immutable; + + class C + { + void M() + { + ImmutableArray? arr = default; + arr ??= /**/[a]/**/; + } + } + """; + + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + comp.VerifyDiagnostics( + // (8,28): error CS0103: The name 'a' does not exist in the current context + // arr ??= /**/[a]/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(8, 28)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.Null(semanticInfo.Type); + Assert.Equal("System.Collections.Immutable.ImmutableArray", semanticInfo.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void ErrorRecovery_CollectionExpression_NotNullableValueType() + { + var source = """ + using System.Collections.Immutable; + + class C + { + void M() + { + ImmutableArray arr = default; + arr ??= /**/[a]/**/; + } + } + """; + + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + comp.VerifyDiagnostics( + // (8,28): error CS0103: The name 'a' does not exist in the current context + // arr ??= /**/[a]/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(8, 28)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.Null(semanticInfo.Type); + Assert.Null(semanticInfo.ConvertedType); + } + + [Fact] + public void ErrorRecovery_TargetTypedTuple() + { + var source = """ + class C + { + void M() + { + (C, int)? t = default; + t ??= /**/(new(a), 1)/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (6,30): error CS0103: The name 'a' does not exist in the current context + // t ??= /**/(new(a), 1)/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 30)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.Null(semanticInfo.Type); + Assert.Equal("(C, System.Int32)", semanticInfo.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void ErrorRecovery_TargetTypedConditionalExpression_ReferenceType() + { + var source = """ + class C + { + void M(bool b) + { + C c = default; + c ??= /**/b ? new(a) : default/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (6,33): error CS0103: The name 'a' does not exist in the current context + // c ??= /**/b ? new(a) : default/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 33)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.True(semanticInfo.Type.IsErrorType()); + Assert.Equal("C", semanticInfo.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void ErrorRecovery_TargetTypedConditionalExpression_NullableValueType() + { + var source = """ + struct S + { + void M(bool b) + { + S? s = default; + s ??= /**/b ? new(a) : default/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (6,33): error CS0103: The name 'a' does not exist in the current context + // s ??= /**/b ? new(a) : default/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 33)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.True(semanticInfo.Type.IsErrorType()); + Assert.Equal("S", semanticInfo.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void ErrorRecovery_TargetTypedConditionalExpression_NotNullableValueType() + { + var source = """ + struct S + { + void M(bool b) + { + S s = default; + s ??= /**/b ? new(a) : default/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (6,33): error CS0103: The name 'a' does not exist in the current context + // s ??= /**/b ? new(a) : default/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 33)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.True(semanticInfo.Type.IsErrorType()); + Assert.True(semanticInfo.ConvertedType.IsErrorType()); + } + + [Fact] + public void ErrorRecovery_TargetTypedSwitchExpression_ReferenceType() + { + var source = """ + class C + { + void M(int i) + { + C c = default; + c ??= /**/i switch + { + 1 => new(a), + _ => default, + }/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (8,22): error CS0103: The name 'a' does not exist in the current context + // 1 => new(a), + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(8, 22)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.True(semanticInfo.Type.IsErrorType()); + Assert.Equal("C", semanticInfo.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void ErrorRecovery_TargetTypedSwitchExpression_NullableValueType() + { + var source = """ + struct S + { + void M(int i) + { + S? s = default; + s ??= /**/i switch + { + 1 => new(a), + _ => default, + }/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (8,22): error CS0103: The name 'a' does not exist in the current context + // 1 => new(a), + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(8, 22)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.True(semanticInfo.Type.IsErrorType()); + Assert.Equal("S", semanticInfo.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void ErrorRecovery_TargetTypedSwitchExpression_NotNullableValueType() + { + var source = """ + struct S + { + void M(int i) + { + S s = default; + s ??= /**/i switch + { + 1 => new(a), + _ => default, + }/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (8,22): error CS0103: The name 'a' does not exist in the current context + // 1 => new(a), + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(8, 22)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.True(semanticInfo.Type.IsErrorType()); + Assert.True(semanticInfo.ConvertedType.IsErrorType()); + } + + [Fact] + public void ErrorRecovery_TargetTypedInterpolatedString_ReferenceType() + { + var source = """ + class C + { + void M(int i) + { + CustomInterpolatedStringHandler h = default; + h ??= /**/$"The value is {a}"/**/; + } + } + """; + + var comp = CreateCompilation([source, GetInterpolatedStringCustomHandlerType("CustomInterpolatedStringHandler", "class", useBoolReturns: false)]); + comp.VerifyDiagnostics( + // (6,41): error CS0103: The name 'a' does not exist in the current context + // h ??= /**/$"The value is {a}"/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 41)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.Equal("System.String", semanticInfo.Type.ToTestDisplayString()); + Assert.Equal("CustomInterpolatedStringHandler", semanticInfo.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void ErrorRecovery_TargetTypedInterpolatedString_NullableValueType() + { + var source = """ + class C + { + void M(int i) + { + CustomInterpolatedStringHandler? h = default; + h ??= /**/$"The value is {a}"/**/; + } + } + """; + + var comp = CreateCompilation([source, GetInterpolatedStringCustomHandlerType("CustomInterpolatedStringHandler", "struct", useBoolReturns: false)]); + comp.VerifyDiagnostics( + // (6,41): error CS0103: The name 'a' does not exist in the current context + // h ??= /**/$"The value is {a}"/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 41)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.Equal("System.String", semanticInfo.Type.ToTestDisplayString()); + Assert.Equal("CustomInterpolatedStringHandler", semanticInfo.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void ErrorRecovery_TargetTypedInterpolatedString_NotNullableValueType() + { + var source = """ + class C + { + void M(int i) + { + CustomInterpolatedStringHandler h = default; + h ??= /**/$"The value is {a}"/**/; + } + } + """; + + var comp = CreateCompilation([source, GetInterpolatedStringCustomHandlerType("CustomInterpolatedStringHandler", "struct", useBoolReturns: false)]); + comp.VerifyDiagnostics( + // (6,41): error CS0103: The name 'a' does not exist in the current context + // h ??= /**/$"The value is {a}"/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 41)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.Equal("System.String", semanticInfo.Type.ToTestDisplayString()); + Assert.Equal("System.String", semanticInfo.ConvertedType.ToTestDisplayString()); + } + + [Fact] + public void ErrorRecovery_ReferenceType_ImplicitConversion() + { + var source = """ + class C + { + void M() + { + object o = default; + o ??= /**/new C(a)/**/; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (6,29): error CS1729: 'C' does not contain a constructor that takes 1 arguments + // o ??= /**/new C(a)/**/; + Diagnostic(ErrorCode.ERR_BadCtorArgCount, "C").WithArguments("C", "1").WithLocation(6, 29), + // (6,31): error CS0103: The name 'a' does not exist in the current context + // o ??= /**/new C(a)/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 31)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.Equal("C", semanticInfo.Type.ToTestDisplayString()); + Assert.Equal("System.Object", semanticInfo.ConvertedType.ToTestDisplayString()); + + Assert.Equal(ConversionKind.ImplicitReference, semanticInfo.ImplicitConversion.Kind); + } + + [Fact] + public void ErrorRecovery_NullableValueType_ImplicitConversion() + { + var source = """ + struct S + { + void M() + { + S? s = default; + s ??= /**/new ConvertibleToS(a)/**/; + } + } + + class ConvertibleToS + { + public static implicit operator S(ConvertibleToS c) => default; + } + """; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (6,29): error CS1729: 'ConvertibleToS' does not contain a constructor that takes 1 arguments + // s ??= /**/new ConvertibleToS(a)/**/; + Diagnostic(ErrorCode.ERR_BadCtorArgCount, "ConvertibleToS").WithArguments("ConvertibleToS", "1").WithLocation(6, 29), + // (6,44): error CS0103: The name 'a' does not exist in the current context + // s ??= /**/new ConvertibleToS(a)/**/; + Diagnostic(ErrorCode.ERR_NameNotInContext, "a").WithArguments("a").WithLocation(6, 44)); + + var semanticInfo = GetSemanticInfoForTest(comp); + + Assert.Equal("ConvertibleToS", semanticInfo.Type.ToTestDisplayString()); + Assert.Equal("S", semanticInfo.ConvertedType.ToTestDisplayString()); + + Assert.Equal(ConversionKind.ImplicitUserDefined, semanticInfo.ImplicitConversion.Kind); + Assert.Equal("S ConvertibleToS.op_Implicit(ConvertibleToS c)", semanticInfo.ImplicitConversion.Method.ToTestDisplayString()); + } } } diff --git a/src/Compilers/Test/Utilities/CSharp/SemanticModelTestBase.cs b/src/Compilers/Test/Utilities/CSharp/SemanticModelTestBase.cs index 7faf243c35585..2fc14d5567eab 100644 --- a/src/Compilers/Test/Utilities/CSharp/SemanticModelTestBase.cs +++ b/src/Compilers/Test/Utilities/CSharp/SemanticModelTestBase.cs @@ -162,5 +162,10 @@ protected CompilationUtils.SemanticInfoSummary GetSemanticInfoForTest(string tes { return GetSemanticInfoForTest(testSrc); } + + protected CompilationUtils.SemanticInfoSummary GetSemanticInfoForTest(CSharpCompilation compilation) + { + return GetSemanticInfoForTest(compilation); + } } }