dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
18.85k stars 4.01k forks source link

Analysis of local functions/lambdas is broken for speculation #45825

Open 333fred opened 4 years ago

333fred commented 4 years ago

This test should pass:

        [Fact]
        public void LocalFunctionReturnTypeConversions()
        {
            var comp = CreateCompilation(@"
#nullable enable
class C
{
    public static implicit operator C(string s) => null!;

    C M()
    {
        string s = """";
        local();
        return null!;

        string local()
        {
            s.ToString();
            return s;
        }
    }
}");

            comp.VerifyDiagnostics();

            var tree = comp.SyntaxTrees[0];
            var model = comp.GetSemanticModel(tree);

            var localFunctionBody = tree.GetRoot().DescendantNodes().OfType<LocalFunctionStatementSyntax>().Single();
            var typeInfo = model.GetTypeInfo(localFunctionBody.DescendantNodes().OfType<ReturnStatementSyntax>().Single().Expression!);
            Assert.Equal("System.String!", typeInfo.ConvertedType.ToTestDisplayString(includeNonNullable: true));

            var @return = (ReturnStatementSyntax)SyntaxFactory.ParseStatement("return s;");
            Assert.True(model.TryGetSpeculativeSemanticModel(localFunctionBody.Body!.OpenBraceToken.SpanStart + 1, @return, out var specModel));
            typeInfo = specModel!.GetTypeInfo(@return.Expression!);
            Assert.Equal("System.String!", typeInfo.ConvertedType.ToTestDisplayString(includeNonNullable: true));
        }

The final assert currently fails because we don't enter a new scope in the walker when entering the local function, so when it's restored for speculative analysis we use the wrong containing symbol. Nullable analysis then reinfers the conversion on the return type, which ends up resulting in the speculative model thinking that the converted type of the return is actually C!, as if it was converted to the outer scope.

333fred commented 4 years ago
        [Fact]
        public void LambdaReturnSpeculation()
        {
            var comp = CreateCompilation(@"
class C
{
    public static implicit operator C(string s) => null!;
    C M()
    {
        string s = """";
        System.Func<string> local = () => 
        {
            s.ToString();
            return s;
        };
        local();
        return null!;
    }
}");
            comp.VerifyDiagnostics(
            );
            var tree = comp.SyntaxTrees[0];
            var model = comp.GetSemanticModel(tree);
            var localFunctionBody = tree.GetRoot().DescendantNodes().OfType<LambdaExpressionSyntax>().Single();
            var typeInfo = model.GetTypeInfo(localFunctionBody.DescendantNodes().OfType<ReturnStatementSyntax>().Single().Expression!);
            Assert.Equal("System.String", typeInfo.ConvertedType.ToTestDisplayString(includeNonNullable: false));
            var @return = (ReturnStatementSyntax)SyntaxFactory.ParseStatement("return s;");
            Assert.True(model.TryGetSpeculativeSemanticModel(localFunctionBody.Block!.Statements[0].SpanStart + 1, @return, out var specModel));
            typeInfo = specModel!.GetTypeInfo(@return.Expression!);
            Assert.Equal("C", typeInfo.ConvertedType.ToTestDisplayString(includeNonNullable: false));
        }

This is completely unrelated to nullable analysis: when we speculatively bind we are using the wrong symbol.

333fred commented 4 years ago

Alright, I clarified the historical context here with @mavasani. This is the expected behavior of the API: if you want to speculate in a nested method (lambda or local function), you have to replace it.

That being said, I'm not a particular fan of the behavior. It certainly isn't what I expected, and it's inconsistent with how GetSpeculativeTypeInfo works today. @jasonmalinowski @CyrusNajmabadi, I know the IDE has some code that expects the local function/lambda speculation to work the way it does today, and actually replaces the local function/lambda entirely when doing speculation. I think most of that code would continue working the same as it does today, given that it specifically walks out of the local function scope to compensate for this behavior, but I'm curious if there's any immediate red flags that pop up for you if we were to change it.

jasonmalinowski commented 4 years ago

My main red flag would be what about other callers that aren't us? Would this break analyzers that are out in the wild?

333fred commented 4 years ago

Would this break analyzers that are out in the wild?

So, I think that falls into two categories:

  1. People who are compensating like the IDE does today. They wouldn't be broken because they already don't speculate inside lambda or local functions.
  2. People who have a bug they aren't aware of. I really struggle to see the person who expects and counts on the current behavior to act the way it does.
mavasani commented 4 years ago

Yeah, I believe this is just an oversight/bug in the original implementation, and is likely to not break existing clients, though hard to guarantee that.