diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.DebugVerifier.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.DebugVerifier.cs index 74265fff9c9e..804fc9d97fb1 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.DebugVerifier.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.DebugVerifier.cs @@ -115,6 +115,7 @@ private void VerifyExpression(BoundExpression expression, bool overrideSkippedEx { Visit(node.IterationVariableType); Visit(node.AwaitOpt); + Visit(node.EnumeratorInfoOpt?.DisposeAwaitableInfo); Visit(node.Expression); // https://github.com/dotnet/roslyn/issues/35010: handle the deconstruction //this.Visit(node.DeconstructionOpt); diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs index c70edd0c6005..8a42a4b08a85 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs @@ -2739,7 +2739,6 @@ private void VisitLocalFunctionUse(LocalFunctionSymbol symbol) public override BoundNode? VisitForEachStatement(BoundForEachStatement node) { DeclareLocals(node.IterationVariables); - Visit(node.AwaitOpt); return base.VisitForEachStatement(node); } @@ -5328,6 +5327,17 @@ private void VisitArgumentConversionAndInboundAssignmentsAndPreConditions( case RefKind.In: { // Note: for lambda arguments, they will be converted in the context/state we saved for that argument + if (conversion is { Kind: ConversionKind.ImplicitUserDefined }) + { + var argumentResultType = resultType.Type; + conversion = GenerateConversion(_conversions, argumentNoConversion, argumentResultType, parameterType.Type, fromExplicitCast: false, extensionMethodThisArgument: false); + if (!conversion.Exists && !argumentNoConversion.IsSuppressed) + { + Debug.Assert(argumentResultType is not null); + ReportNullabilityMismatchInArgument(argumentNoConversion.Syntax, argumentResultType, parameter, parameterType.Type, forOutput: false); + } + } + var stateAfterConversion = VisitConversion( conversionOpt: conversionOpt, conversionOperand: argumentNoConversion, @@ -6392,17 +6402,6 @@ private void TrackNullableStateOfNullableValue(int containingSlot, TypeSymbol co } } - private void TrackNullableStateOfNullableValue(BoundExpression node, BoundExpression operand, TypeSymbol convertedType, TypeWithAnnotations underlyingType) - { - int valueSlot = MakeSlot(operand); - if (valueSlot > 0) - { - int containingSlot = GetOrCreatePlaceholderSlot(node); - Debug.Assert(containingSlot > 0); - TrackNullableStateOfNullableValue(containingSlot, convertedType, operand, underlyingType.ToTypeWithState(), valueSlot); - } - } - private void TrackNullableStateOfTupleConversion( BoundConversion? conversionOpt, BoundExpression convertedNode, @@ -8571,6 +8570,41 @@ protected override void VisitForEachExpression(BoundForEachStatement node) currentPropertyGetterTypeWithState = ApplyUnconditionalAnnotations( currentPropertyGetter.ReturnTypeWithAnnotations.ToTypeWithState(), currentPropertyGetter.ReturnTypeFlowAnalysisAnnotations); + + // Analyze `await MoveNextAsync()` + if (node.AwaitOpt is { AwaitableInstancePlaceholder: BoundAwaitableValuePlaceholder moveNextPlaceholder } awaitMoveNextInfo) + { + var moveNextAsyncMethod = (MethodSymbol)AsMemberOfType(reinferredGetEnumeratorMethod.ReturnType, node.EnumeratorInfoOpt.MoveNextMethod); + + EnsureAwaitablePlaceholdersInitialized(); + var result = new VisitResult(GetReturnTypeWithState(moveNextAsyncMethod), moveNextAsyncMethod.ReturnTypeWithAnnotations); + _awaitablePlaceholdersOpt.Add(moveNextPlaceholder, (moveNextPlaceholder, result)); + Visit(awaitMoveNextInfo); + _awaitablePlaceholdersOpt.Remove(moveNextPlaceholder); + } + + // Analyze `await DisposeAsync()` + if (node.EnumeratorInfoOpt is { NeedsDisposal: true, DisposeAwaitableInfo: BoundAwaitableInfo awaitDisposalInfo }) + { + var disposalPlaceholder = awaitDisposalInfo.AwaitableInstancePlaceholder; + bool addedPlaceholder = false; + if (node.EnumeratorInfoOpt.DisposeMethod is not null) // no statically known Dispose method if doing a runtime check + { + Debug.Assert(disposalPlaceholder is not null); + var disposeAsyncMethod = (MethodSymbol)AsMemberOfType(reinferredGetEnumeratorMethod.ReturnType, node.EnumeratorInfoOpt.DisposeMethod); + EnsureAwaitablePlaceholdersInitialized(); + var result = new VisitResult(GetReturnTypeWithState(disposeAsyncMethod), disposeAsyncMethod.ReturnTypeWithAnnotations); + _awaitablePlaceholdersOpt.Add(disposalPlaceholder, (disposalPlaceholder, result)); + addedPlaceholder = true; + } + + Visit(awaitDisposalInfo); + + if (addedPlaceholder) + { + _awaitablePlaceholdersOpt!.Remove(disposalPlaceholder!); + } + } } SetResultType(expression: null, currentPropertyGetterTypeWithState); @@ -8921,7 +8955,7 @@ private TypeWithState InferResultNullabilityOfBinaryLogicalOperator(BoundExpress var placeholder = awaitableInfo.AwaitableInstancePlaceholder; Debug.Assert(placeholder is object); - _awaitablePlaceholdersOpt ??= PooledDictionary.GetInstance(); + EnsureAwaitablePlaceholdersInitialized(); _awaitablePlaceholdersOpt.Add(placeholder, (node.Expression, _visitResult)); Visit(awaitableInfo); _awaitablePlaceholdersOpt.Remove(placeholder); @@ -9512,6 +9546,12 @@ protected override void VisitCatchBlock(BoundCatchBlock node, ref LocalState fin return null; } + [MemberNotNull(nameof(_awaitablePlaceholdersOpt))] + private void EnsureAwaitablePlaceholdersInitialized() + { + _awaitablePlaceholdersOpt ??= PooledDictionary.GetInstance(); + } + public override BoundNode? VisitAwaitableValuePlaceholder(BoundAwaitableValuePlaceholder node) { if (_awaitablePlaceholdersOpt != null && _awaitablePlaceholdersOpt.TryGetValue(node, out var value)) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs index 8aad88173bd0..90e4975fd261 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs @@ -60761,7 +60761,7 @@ static void G(A a2, B b2) Diagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, "b2").WithArguments("B", "IOut").WithLocation(19, 13)); } - [Fact] + [Fact, WorkItem(29898, "https://github.com/dotnet/roslyn/issues/29898")] public void ImplicitConversions_07() { var source = @@ -60781,13 +60781,107 @@ static void Main(object? x) var y = F(x); G(y); if (x == null) return; - var z = F(x); + var z = F(x); G(z); // warning + + var z2 = F(x); + G(z2!); } }"; var comp = CreateCompilation(new[] { source }, options: WithNonNullTypesTrue()); - // https://github.com/dotnet/roslyn/issues/29898: Report warning for `G(z)`? - comp.VerifyDiagnostics(); + comp.VerifyDiagnostics( + // (18,11): warning CS8620: Argument of type 'B' cannot be used for parameter 'a' of type 'A' in 'void C.G(A a)' due to differences in the nullability of reference types. + // G(z); // warning + Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, "z").WithArguments("B", "A", "a", "void C.G(A a)").WithLocation(18, 11) + ); + } + + [Fact] + public void ImplicitConversion_Params() + { + var source = +@"class A +{ +} +class B +{ + public static implicit operator A(B b) => throw null!; +} +class C +{ + static B F(T t) => throw null!; + static void G(params A[] a) => throw null!; + + static void Main(object? x) + { + var y = F(x); + G(y); // 1 + + if (x == null) return; + var z = F(x); + G(z); + } +}"; + var comp = CreateCompilation(source, options: WithNonNullTypesTrue()); + comp.VerifyDiagnostics( + // (16,11): warning CS8620: Argument of type 'B' cannot be used for parameter 'a' of type 'A' in 'void C.G(params A[] a)' due to differences in the nullability of reference types. + // G(y); // 1 + Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, "y").WithArguments("B", "A", "a", "void C.G(params A[] a)").WithLocation(16, 11) + ); + } + + [Fact] + public void ImplicitConversion_Typeless() + { + var source = @" +public struct Optional +{ + public static implicit operator Optional(T value) => throw null!; +} + +class C +{ + static void G1(Optional a) => throw null!; + static void G2(Optional a) => throw null!; + + static void M() + { + G1(null); // 1 + G2(null); + } +}"; + var comp = CreateCompilation(source, options: WithNonNullTypesTrue()); + comp.VerifyDiagnostics( + // (14,12): warning CS8625: Cannot convert null literal to non-nullable reference type. + // G1(null); // 1 + Diagnostic(ErrorCode.WRN_NullAsNonNullable, "null").WithLocation(14, 12) + ); + } + + [Fact] + public void ImplicitConversion_Typeless_WithConstraint() + { + var source = @" +public struct Optional where T : class +{ + public static implicit operator Optional(T value) => throw null!; +} + +class C +{ + static void G(Optional a) => throw null!; + + static void M() + { + G(null); + } +}"; + var comp = CreateCompilation(source, options: WithNonNullTypesTrue()); + comp.VerifyDiagnostics( + // (13,11): warning CS8625: Cannot convert null literal to non-nullable reference type. + // G(null); + Diagnostic(ErrorCode.WRN_NullAsNonNullable, "null").WithLocation(13, 11) + ); } [Fact, WorkItem(41763, "https://github.com/dotnet/roslyn/issues/41763")] @@ -82657,10 +82751,11 @@ static void F3((B?, B) t) }"; var comp = CreateCompilation(source, options: WithNonNullTypesTrue()); // https://github.com/dotnet/roslyn/issues/32599: Handle tuple element conversions. + // Diagnostics on user-defined conversions need improvement: https://github.com/dotnet/roslyn/issues/31798 comp.VerifyDiagnostics( - // (16,12): warning CS8620: Argument of type '(A?, A)?' cannot be used for parameter 't' of type '(A, A?)?' in 'void Program.F2((A, A?)? t)' due to differences in the nullability of reference types. + // (16,12): warning CS8620: Argument of type 'S' cannot be used for parameter 't' of type '(A, A?)?' in 'void Program.F2((A, A?)? t)' due to differences in the nullability of reference types. // F2(s); // 1 - Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, "s").WithArguments("(A?, A)?", "(A, A?)?", "t", "void Program.F2((A, A?)? t)").WithLocation(16, 12)); + Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, "s").WithArguments("S", "(A, A?)?", "t", "void Program.F2((A, A?)? t)").WithLocation(16, 12)); } [Fact] @@ -117092,9 +117187,9 @@ static void Main() // (12,15): warning CS8619: Nullability of reference types in value of type 'A' doesn't match target type 'A'. // B b = a; // 1 Diagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, "a").WithArguments("A", "A").WithLocation(12, 15), - // (13,11): warning CS8620: Nullability of reference types in argument of type 'A' doesn't match target type 'A' for parameter 'b' in 'void Program.F(B b)'. + // (13,11): warning CS8620: Argument of type 'A' cannot be used for parameter 'b' of type 'B' in 'void Program.F(B b)' due to differences in the nullability of reference types. // F(a); // 2 - Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, "a").WithArguments("A", "A", "b", "void Program.F(B b)").WithLocation(13, 11)); + Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, "a").WithArguments("A", "B", "b", "void Program.F(B b)").WithLocation(13, 11)); } [Fact] @@ -117122,9 +117217,9 @@ static void Main() // (12,24): warning CS8619: Nullability of reference types in value of type 'A' doesn't match target type 'A'. // A a = b; // 1 Diagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, "b").WithArguments("A", "A").WithLocation(12, 24), - // (13,11): warning CS8620: Nullability of reference types in argument of type 'A' doesn't match target type 'A' for parameter 'a' in 'void Program.F(A a)'. + // (13,11): warning CS8620: Argument of type 'B' cannot be used for parameter 'a' of type 'A' in 'void Program.F(A a)' due to differences in the nullability of reference types. // F(b); // 2 - Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, "b").WithArguments("A", "A", "a", "void Program.F(A a)").WithLocation(13, 11)); + Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, "b").WithArguments("B", "A", "a", "void Program.F(A a)").WithLocation(13, 11)); } [WorkItem(31864, "https://github.com/dotnet/roslyn/issues/31864")] @@ -132292,17 +132387,74 @@ static async Task Main() await foreach (var o in Create(x)) // 2 { } - await foreach (var o in Create(y)) + await foreach (var o in Create(y)) // 3 { } } }"; var comp = CreateCompilationWithTasksExtensions(new[] { s_IAsyncEnumerable, source }); - // Should report warning for GetAwaiter(). comp.VerifyDiagnostics( // (24,20): warning CS8600: Converting null literal or possible null value to non-nullable type. // object y = null; // 1 - Diagnostic(ErrorCode.WRN_ConvertingNullableToNonNullable, "null").WithLocation(24, 20)); + Diagnostic(ErrorCode.WRN_ConvertingNullableToNonNullable, "null").WithLocation(24, 20), + // (25,33): warning CS8620: Argument of type 'StructAwaitable1' cannot be used for parameter 's' of type 'StructAwaitable1' in 'TaskAwaiter Program.GetAwaiter(StructAwaitable1 s)' due to differences in the nullability of reference types. + // await foreach (var o in Create(x)) // 2 + Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, "Create(x)").WithArguments("StructAwaitable1", "StructAwaitable1", "s", "TaskAwaiter Program.GetAwaiter(StructAwaitable1 s)").WithLocation(25, 33), + // (28,33): warning CS8620: Argument of type 'StructAwaitable2' cannot be used for parameter 's' of type 'StructAwaitable2' in 'TaskAwaiter Program.GetAwaiter(StructAwaitable2 s)' due to differences in the nullability of reference types. + // await foreach (var o in Create(y)) // 3 + Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, "Create(y)").WithArguments("StructAwaitable2", "StructAwaitable2", "s", "TaskAwaiter Program.GetAwaiter(StructAwaitable2 s)").WithLocation(28, 33) + ); + } + + [Fact] + [WorkItem(30956, "https://github.com/dotnet/roslyn/issues/30956")] + public void GetAwaiterExtensionMethod_AwaitForEach_InverseAnnotations() + { + var source = +@"#nullable enable +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +struct StructAwaitable1 { } +struct StructAwaitable2 { } +class Enumerable +{ + public Enumerator GetAsyncEnumerator() => new Enumerator(); +} +class Enumerator +{ + public object Current => null!; + public StructAwaitable1 MoveNextAsync() => new StructAwaitable1(); + public StructAwaitable2 DisposeAsync() => new StructAwaitable2(); +} +static class Program +{ + static TaskAwaiter GetAwaiter(this StructAwaitable1 s) => default; + static TaskAwaiter GetAwaiter(this StructAwaitable2 s) => default; + static Enumerable Create(T t) => new Enumerable(); + static async Task Main() + { + object? x = new object(); + object y = null; // 1 + await foreach (var o in Create(x)) // 2 + { + } + await foreach (var o in Create(y)) // 3 + { + } + } +}"; + var comp = CreateCompilationWithTasksExtensions(new[] { s_IAsyncEnumerable, source }); + comp.VerifyDiagnostics( + // (24,20): warning CS8600: Converting null literal or possible null value to non-nullable type. + // object y = null; // 1 + Diagnostic(ErrorCode.WRN_ConvertingNullableToNonNullable, "null").WithLocation(24, 20), + // (25,33): warning CS8620: Argument of type 'StructAwaitable2' cannot be used for parameter 's' of type 'StructAwaitable2' in 'TaskAwaiter Program.GetAwaiter(StructAwaitable2 s)' due to differences in the nullability of reference types. + // await foreach (var o in Create(x)) // 2 + Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, "Create(x)").WithArguments("StructAwaitable2", "StructAwaitable2", "s", "TaskAwaiter Program.GetAwaiter(StructAwaitable2 s)").WithLocation(25, 33), + // (28,33): warning CS8620: Argument of type 'StructAwaitable1' cannot be used for parameter 's' of type 'StructAwaitable1' in 'TaskAwaiter Program.GetAwaiter(StructAwaitable1 s)' due to differences in the nullability of reference types. + // await foreach (var o in Create(y)) // 3 + Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, "Create(y)").WithArguments("StructAwaitable1", "StructAwaitable1", "s", "TaskAwaiter Program.GetAwaiter(StructAwaitable1 s)").WithLocation(28, 33) + ); } [Fact]