dotnet / dotnet/roslyn

Confusing nullability warning is reported for a deconstruction

Open
#73,374 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Area-Compilers Feature - Nullable Reference Types
Dominant language
C#
Stars
20.7k
Forks
4.3k
PR merge metrics
PR metrics pending

Description

        [Fact]
        [WorkItem("https://github.com/dotnet/roslyn/issues/72750")]
        [WorkItem("https://github.com/dotnet/roslyn/issues/33011")]
        public void SingleCandidate_ResultIsDynamic_Property_Assignment_Deconstruction_Method_01()
        {
            string source = @"
#nullable enable

public class C
{
    public static void Main()
    {
        dynamic d = 1;
        var c = new C();
        var a = (c[d], _) = new C2();
        System.Console.Write(a);        
        Print(a.Item1);
        System.Console.Write("" "");        
        System.Console.Write(c._test1);        
    }

    int _test1 = 0;    
    int this[int x]
    {
        get => _test1;
        set => _test1 = value;
    }

    static void Print(dynamic b) 
    {
    }
}

class C2
{
    public void Deconstruct(out int x, out int y)
    {
        (x, y) = (2, 123);
    }
}
";

            var comp = CreateCompilation(source, options: TestOptions.DebugExe, targetFramework: TargetFramework.StandardAndCSharp);

            var tree = comp.SyntaxTrees.Single();
            var node = tree.GetRoot().DescendantNodes().OfType<IdentifierNameSyntax>().Where(id => id.Identifier.ValueText == "a").First();
            var model = comp.GetSemanticModel(tree);
            var symbolInfo = model.GetSymbolInfo(node);
            Assert.Equal("(dynamic, System.Int32) a", symbolInfo.Symbol.ToTestDisplayString());

            node = tree.GetRoot().DescendantNodes().OfType<IdentifierNameSyntax>().Where(id => id.Identifier.ValueText == "Item1").Single();
            symbolInfo = model.GetSymbolInfo(node);
            Assert.Equal("dynamic (dynamic, System.Int32).Item1", symbolInfo.Symbol.ToTestDisplayString());

            var typeInfo = model.GetTypeInfo(node);
            AssertEx.Equal("dynamic", typeInfo.Type.ToTestDisplayString());
            Assert.Equal(CodeAnalysis.NullableFlowState.NotNull, typeInfo.Nullability.FlowState);

            var elementAccess = tree.GetRoot().DescendantNodes().OfType<ElementAccessExpressionSyntax>().Single();
            symbolInfo = model.GetSymbolInfo(elementAccess);
            AssertEx.Equal("System.Int32 C.this[System.Int32 x] { get; set; }", symbolInfo.Symbol.ToTestDisplayString());
            typeInfo = model.GetTypeInfo(elementAccess);
            AssertEx.Equal("System.Int32", typeInfo.Type.ToTestDisplayString());
            AssertEx.Equal("System.Int32", typeInfo.ConvertedType.ToTestDisplayString());
            AssertEx.Equal("dynamic", typeInfo.Type.ToTestDisplayString());
            AssertEx.Equal("dynamic", typeInfo.ConvertedType.ToTestDisplayString());

            var propertyRef = (IPropertyReferenceOperation)model.GetOperation(elementAccess);
            AssertEx.Equal(symbolInfo.Symbol.ToTestDisplayString(), propertyRef.Property.ToTestDisplayString());
            var propertyRef = (IDynamicIndexerAccessOperation)model.GetOperation(elementAccess);
            Assert.Equal(typeInfo.Type, propertyRef.Type);

            var left = (TupleExpressionSyntax)elementAccess.Parent.Parent;
            var tupleTypeInfo = model.GetTypeInfo(left);
            AssertEx.Equal("(System.Int32, System.Int32)", tupleTypeInfo.Type.ToTestDisplayString());
            AssertEx.Equal("(System.Int32, System.Int32)", tupleTypeInfo.ConvertedType.ToTestDisplayString());
            AssertEx.Equal("(dynamic, System.Int32)", tupleTypeInfo.Type.ToTestDisplayString());
            AssertEx.Equal("(dynamic, System.Int32)", tupleTypeInfo.ConvertedType.ToTestDisplayString());

            var assignment = (AssignmentExpressionSyntax)left.Parent;
            typeInfo = model.GetTypeInfo(assignment);
            AssertEx.Equal("(dynamic, System.Int32)", typeInfo.Type.ToTestDisplayString());
            AssertEx.Equal("(dynamic, System.Int32)", typeInfo.ConvertedType.ToTestDisplayString());

            Assert.True(model.GetDeconstructionInfo(assignment) is { Method: not null, Conversion: null, Nested: [{ Method: null, Conversion: { IsIdentity: true }, Nested: [] }, _] });
            Assert.True(model.GetDeconstructionInfo(assignment) is { Method: not null, Conversion: null, Nested: [{ Method: null, Conversion: { IsBoxing: true }, Nested: [] }, _] });

            var operation = (IDeconstructionAssignmentOperation)model.GetOperation(assignment);
            Assert.Equal(tupleTypeInfo.Type, operation.Target.Type);
            Assert.Equal("C2", operation.Value.Type.ToTestDisplayString());
            Assert.Equal(typeInfo.Type, operation.Type);

            var right = assignment.Right;
            typeInfo = model.GetTypeInfo(right);
            AssertEx.Equal("C2", typeInfo.Type.ToTestDisplayString());
            AssertEx.Equal("C2", typeInfo.ConvertedType.ToTestDisplayString());

            CompileAndVerify(comp, expectedOutput: "(2, 123) 2").VerifyDiagnostics();
            CompileAndVerify(comp, expectedOutput: "(2, 123) 2").VerifyDiagnostics(
                // (10,18): warning CS8624: Argument of type 'dynamic' cannot be used as an output of type 'int' for parameter 'x' in 'void C2.Deconstruct(out int x, out int y)' due to differences in the nullability of reference types.
                //         var a = (c[d], _) = new C2();
                Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgumentForOutput, "c[d]").WithArguments("dynamic", "int", "x", "void C2.Deconstruct(out int x, out int y)").WithLocation(10, 18)
                );

            string source3 = @"
#nullable enable

public class C
{
    public static void Main()
    {
        dynamic d = 1;
        var c = new C();
        var a = (c[d], _) = new C2();
        Print(a.Item1);
    }

    int? _test1 = 0;    
    int? this[int x]
    {
        get => _test1;
        set => _test1 = value;
    }

    static void Print(dynamic b) 
    {
    }
}

class C2
{
    public void Deconstruct(out int? x, out int y)
    {
        (x, y) = (2, 123);
    }
}
";
            var comp3 = CreateCompilation(source3, targetFramework: TargetFramework.StandardAndCSharp);

            // Nullability is not tracked across deconstruction, this is a pre-existing condition - https://github.com/dotnet/roslyn/issues/33011 
            comp3.VerifyDiagnostics();
            comp3.VerifyDiagnostics(
                // (10,18): warning CS8624: Argument of type 'dynamic' cannot be used as an output of type 'int?' for parameter 'x' in 'void C2.Deconstruct(out int? x, out int y)' due to differences in the nullability of reference types.
                //         var a = (c[d], _) = new C2();
                Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgumentForOutput, "c[d]").WithArguments("dynamic", "int?", "x", "void C2.Deconstruct(out int? x, out int y)").WithLocation(10, 18)
                );

            tree = comp3.SyntaxTrees.Single();
            node = tree.GetRoot().DescendantNodes().OfType<IdentifierNameSyntax>().Where(id => id.Identifier.ValueText == "Item1").Single();
            model = comp3.GetSemanticModel(tree);

            typeInfo = model.GetTypeInfo(node);
            AssertEx.Equal("dynamic", typeInfo.Type.ToTestDisplayString());
            Assert.Equal(CodeAnalysis.NullableFlowState.NotNull, typeInfo.Nullability.FlowState);
        }

Observed confusing warning:

                // (10,18): warning CS8624: Argument of type 'dynamic' cannot be used as an output of type 'int' for parameter 'x' in 'void C2.Deconstruct(out int x, out int y)' due to differences in the nullability of reference types.
                //         var a = (c[d], _) = new C2();
                Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgumentForOutput, "c[d]").WithArguments("dynamic", "int", "x", "void C2.Deconstruct(out int x, out int y)").WithLocation(10, 18)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the SingleCandidate_ResultIsDynamic_Property_Assignment_Deconstruction_Method_01 test and its two VerifyDiagnostics calls. Read the deconstruction and nullable-flow behavior exercised by GetDeconstructionInfo and GetTypeInfo, then trace the diagnostic for c[d]. Done means the reported warning is no longer confusing and the test assertions accurately capture the intended diagnostic behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
compilers, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.