3

I've found some behaviour with the nullability analysis in a C# project that I don't understand, and I was hoping someone might be able to shed some light on it.

In short, I've got a nullability warning which I can't work out why it's appearing and, more confusingly, it disappears when I comment out some code that doesn't look like it should affect the nullability analysis.

It's probably just easier to show the code than try to describe it in more detail - an MRE is as follows...

ConsoleApp1.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

Program.cs

static object? GetTask()
{
    return new object();
}

static void DoSomething()
{

    var plans = Enumerable.Empty<Plan>();

    var filtered = plans
        .Select(
            plan => new
            {
                Value = plan.Schedule
            }
        );

    var tasks = filtered.Select(
        anon => GetTask()
    );

}

DoSomething();

class Plan
{
    public object? Schedule
    {
        get;
        set;
    }
}

The code as-is gives a nullability warning:

CS8619 Nullability of reference types in value of type '<anonymous type: object Value>' doesn't match target type '<anonymous type: object? Value>'.

enter image description here

but if I comment out the var tasks = ... statement the nullability warning disappears...

enter image description here

It looks like the analyser is getting its knickers in a twist because it decides that the Value property of the anonymous type should be "not null" (i.e. <anonymous type: object Value>) but then proceeds to create an anonymous object with a nullable Value (i.e. <anonymous type: object? Value>.

It also seems to be related to the GetTasks method - if you replace anon => GetTask() with anon => null as object the nullability warning goes away as well:

enter image description here

I'm half-imagining this might be a bug in the nullability analyser, but is there a rational explanation that I just can't see?

For reference, versions are:

Microsoft Visual Studio Professional 2022 (64-bit) - Preview
Version 17.3.0 Preview 1.1

and

C:>dotnet --list-sdks
3.1.419 [C:\Program Files\dotnet\sdk]
5.0.200 [C:\Program Files\dotnet\sdk]
5.0.214 [C:\Program Files\dotnet\sdk]
6.0.203 [C:\Program Files\dotnet\sdk]
6.0.300 [C:\Program Files\dotnet\sdk]
7.0.100-preview.4.22252.9 [C:\Program Files\dotnet\sdk]

Update

Moving the code out of the DoSomething method also seems to remove the warning:

enter image description here

mclayton
  • 8,025
  • 2
  • 21
  • 26
  • A bug, and and an interesting one at that. Somehow the non-trivial use in the second `.Select` causes a different definition of the anonymous type to stick in the analyzer's mind, even though logically there's no relation. For an even simpler difference that removes any possible confusion about nullable references, consider the difference between `_ => GetInt()` (with `GetInt() => 3`) vs. `_ => 3` -- the former triggers the warning, the latter does not, even though the type isn't used at all, in either case – Jeroen Mostert May 25 '22 at 12:57
  • @mclayton, I agree - which is why I initially deleted the comment. It does appear to be anonymous type reuse, though. Not sure why. If you look at the generated IL, it does have two selects with the return type of the anonymous type. If you provide an explicit type for either, the bug seems to disappear. – Mitch May 25 '22 at 13:00
  • Ah, the trouble appears to be specifically that the function being invoked is a local one, triggering closures (or at least the logic for it, since it's `static`). If we make the function a static method of a different class and call it, the warning doesn't appear either. The function being in the `Select` is not the issue. In any case it's a subtle confluence of things. – Jeroen Mostert May 25 '22 at 13:00
  • For those confused by the deleted comment, it went something like "This seems like a bug where the compiler is reusing the same anonymous type since it has the same number and type of fields - despite the differing nullability." – Mitch May 25 '22 at 13:01
  • @JeroenMostert - fwiw, in my real code ```GetTask``` is a private *instance* method (not a local static function) in the same class as the ```DoSomething``` method. ```Plan``` is also a record - ```record Plan(object? Schedule)```, but I still see the same behaviour. The differences were really just to reduce the MRE size... – mclayton May 25 '22 at 13:07
  • Yeah, the exact trigger is a little unclear. I can repro it by removing the `tasks` assignment entirely and then it boils down strictly to whether there's a `var int = GetInt();` call after the `filtered` assignment. – Jeroen Mostert May 25 '22 at 13:09
  • I'll raise an issue on the GitHub Roslyn repo later today - I couldn't find one when I tried looking there earlier but I wasn't really sure what to search for so I might have missed it :-). – mclayton May 25 '22 at 13:12
  • Here's the [shortest repro](https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEUCuA7AHwGI8cAbMgQ2DJgAIY9raBYAKAAEAGOjgRgB0AGQCWeAI4Budu35JeKOgBEcAW1UBPABQBKOgG8AvjM595HRQDFdB9nXt0teGAHc6AYQDaAXQOGdAgDKMLRgGFpgdAC8AHx0zm76dGACAPJ0/tJsDnQA9LkeEOqMGHQQOKUYABb0zgilZGL0GBB0qpQA1vQulFB4YgDmdAAmIgDOlAAOkzC9dg4q6to6WcZssgBMHga8AMxlwABWMGEA/HSpkhnsQA===) I could come up with. The `.Select` delegate invocation appears necessary, as is the use of local methods (for this repro at least). I suspect there's something quite subtle going on due to how finicky it is. – Jeroen Mostert May 25 '22 at 13:20
  • 1
    Raised as https://github.com/dotnet/roslyn/issues/61516 – mclayton May 25 '22 at 20:13

0 Answers0