Skip to content

Add actual nullable analysis support for with(...) elements.#81978

Merged
CyrusNajmabadi merged 108 commits into
dotnet:features/collection-expression-argumentsfrom
CyrusNajmabadi:collectionExprNullable
Jan 15, 2026
Merged

Add actual nullable analysis support for with(...) elements.#81978
CyrusNajmabadi merged 108 commits into
dotnet:features/collection-expression-argumentsfrom
CyrusNajmabadi:collectionExprNullable

Conversation

@CyrusNajmabadi

@CyrusNajmabadi CyrusNajmabadi commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #72142

Relates to test plan #80613

""";

// We're missing a diagnostic for the `where T : notnull` constraint on the Create method
// Tracked by https://github.com/dotnet/roslyn/issues/68786

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yaay. one bug down.

""";

// We're missing a diagnostic for the `where T : notnull` constraint on the Create method
// Tracked by https://github.com/dotnet/roslyn/issues/68786

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yaay.

expectedOutput: IncludeExpectedOutput("""
42
0, 1
""")).VerifyDiagnostics().VerifyIL("Program.<Main>d__0.System.Runtime.CompilerServices.IAsyncStateMachine.MoveNext()", """

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can remove the IL if desired. but i thought it usefful to show that everything is happening correctly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's assert runtime async code instead. Much shorter and easier to verify.

public void ConstructorNonNullParameterPassedNull_Inference1()
{
// PROTOTYPE: Currently, inference on Goo is producing `Goo<string>` even though it should be `Goo<string!>`.
// This is problematic as it then means T is oblivious and we don't properly report a warning on 'null'.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yaay.

@333fred 333fred left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done review pass

Comment thread src/Compilers/CSharp/Portable/BoundTree/BoundCollectionExpression.cs Outdated
Comment thread src/Compilers/CSharp/Portable/BoundTree/BoundCollectionExpression.cs Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think we can add MemberNotNull tests for collection builders; this would be for another static member on the builder type. Should also be able to test DoesNotReturn and DoesNotReturnIf.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DoesNotReturnIf: 3aba20f

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DoesNotReturn: 10beb60

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MemberNotNull: 8f04bcb

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: membernotnull doesn't work. i added a prototype comment. However, this is a limtation that existed prior to my change. It's also very esoteric. So i don't think this shuold be blocking, and we can track this with an issue. LMK if that's sufficient for you @333fred .

expectedOutput: IncludeExpectedOutput("""
42
0, 1
""")).VerifyDiagnostics().VerifyIL("Program.<Main>d__0.System.Runtime.CompilerServices.IAsyncStateMachine.MoveNext()", """

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's assert runtime async code instead. Much shorter and easier to verify.

// take the final collection type and use its type arguments to re-construct the creation method.

var allTypeArguments = ((NamedTypeSymbol)collectionFinalType).GetAllTypeArgumentsNoUseSiteDiagnostics();
Debug.Assert(allTypeArguments.Length == call.Method.Arity, "Guaranteed by GetCollectionBuilderMethods");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there tests of the form:

// Collection builder method
public static MyCollection<TList> Create<TList, TOther>(TOther arg, ReadOnlySpan<TList> elements) { ... }

MyCollection<int> m = [with(1), 2];

To verify that such cases don't go through here in error recovery modes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 186a431

@CyrusNajmabadi

Copy link
Copy Markdown
Contributor Author

Runtime async done in: c2c780c. Much nicer.

@CyrusNajmabadi

Copy link
Copy Markdown
Contributor Author

@333fred for another pass.

@CyrusNajmabadi

Copy link
Copy Markdown
Contributor Author

@RikkiGibson ptal :)

// Walk into the arguments of the object creation, passing in 'delayCompletionForTargetMember: true'
// so that we reprocess the nullability of the arguments when we have the target-type for the
// collection expression.
var (_, argumentResults, _, completion) = this.VisitArguments(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jcouv I think your change to introduce a named return type for some of these may cause conflicts and/or build failures when merging the branches. I think they will be simple to resolve, just, thought I would give a heads up.

@RikkiGibson RikkiGibson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, comments are not blocking.

var allTypeArguments = ((NamedTypeSymbol)collectionFinalType).GetAllTypeArgumentsNoUseSiteDiagnostics();
Debug.Assert(allTypeArguments.Length == call.Method.Arity, "Guaranteed by GetCollectionBuilderMethods");

var constructed = call.Method.Arity == 0 ? call.Method : call.Method.ConstructedFrom.Construct(allTypeArguments);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if there is an impact to using 'ConstructedFrom' versus 'OriginalDefinition' here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference is in whether the containing type is generic. I'd expect OriginalDefinition to return the original method in the original type, while ConstructedFrom returns the original method in the constructed type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct.

Because the containing type must be a normal-non-generic type, this ends up not mattering (since .OriginalDef and .ConstructedFrom will be the same.

But, conceptually .ConstructedFrom makes the most sense here as we would not be reinferring the type args on the containing type, just reconstructing the method again, off of whatever type it came from.

So going to leave this as is.

// of the arguments passed into them. The other kinds are:
//
// 1. Arrays/Spans. These can't have CollectionCreation expressions at all.
// 2. Array interfaces (e.g. IList<T>). These only have two supported constructors that are called

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be good to translate this list into assertions, so that when conditions change in the future (new collection kinds, etc.), we could find out sooner that a change is also needed here.

{
var count = 0;
for (var current = symbol; current is not null; current = current.ContainingType)
count += current.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics.Length;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Arity? This isn't blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i could switch it to that... but this feels more clearly correct, as it is preallocating the size for exactly what we know we will be adding.

using System;
using System.Collections.Generic;

var v = true ? [with(""), null] : new MyCollection<string>("");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a case like M(true ? [with(null), ""] : [with(""), ""]), with void M<T>(MyCollection<T> arg)? e.g. where nullability is "pushed down" to the other branch and we show there are no warnings (both paths are inferred as nullable). I am assuming we do.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will add teest in followup. In this case, because the elements are all non-null, i would expect this to be inferred as string and the with(null) wouold warn.

It was part of the design that the with(...) call itself doesn't contribute to inference. but we can def have a few variants on this to show what's going on. will incclude in followup PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added in bf9745b

}
""";

// PROTOTYPE: MemberNotNull analysis does not flow across calls to collection builders yet. This is probably

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be good to ensure that a MemberNotNull on a create method, doesn't update a member of the collection type.

It seems also reasonable to test the analogous cases with constructors, if the attribute can be applied to those.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems also reasonable to test the analogous cases with constructors, if the attribute can be applied to those.

It can't be applied to constructors. Only methods and properties.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. intentional that this was only for builders, as it's not legal on constructors (unlike many of the other attributes).

{
string sourceA = """
var source = """
#nullable enable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have nullability tests verifying the symbols post nullability analysis?
I'd expect GetSymbolInfo on with to reflect the re-inferred symbols.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add in followup PR (i have two coming) :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added in f96fb48

Comment thread src/Compilers/CSharp/Portable/Symbols/SymbolExtensions.cs
@CyrusNajmabadi CyrusNajmabadi merged commit 48270ee into dotnet:features/collection-expression-arguments Jan 15, 2026
24 checks passed
@CyrusNajmabadi CyrusNajmabadi deleted the collectionExprNullable branch January 15, 2026 23:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Compilers Feature - Collection Arguments Collection expression with() arguments Needs API Review Needs to be reviewed by the API review council VSCode

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants