Allow foreach on typed null enumerables#80783
Conversation
…r for IEnumerable types - Added check for constant null before reporting missing GetEnumerator error - Applied fix to both sync (IEnumerable) and async (IAsyncEnumerable) cases - Added tests to verify correct error reporting for null IEnumerable<T> and IEnumerable Co-authored-by: CyrusNajmabadi <[email protected]>
|
@copilot i think you misunderstood. This code should be legal. It is legal to cast null to an |
Remove compile-time null checks for typed foreach collections. Casting null to IEnumerable<T>, arrays, or strings is now legal and will fail at runtime instead of compile-time. This matches C# semantics where type-safe casts are valid even if the value is null. - Removed null checks for pattern-based enumeration - Removed null checks for interface-based enumeration - Removed null checks for arrays, dynamic, and strings - Updated tests to verify the new behavior Co-authored-by: CyrusNajmabadi <[email protected]>
You're absolutely right - I misunderstood the issue. I've fixed it now in commit 0b49420. The code now allows The only case that still reports a compile error is when there's no type information at all (e.g., bare |
| } | ||
|
|
||
| [Fact] | ||
| public void TestNullCastToIEnumerable() |
There was a problem hiding this comment.
@copilot add WorkItem attributes to all tests. Use raw string literals for all tests. Add tests that validate what happens in a #nullable enabled file. Add an explicit test for default(int[]) default(string) default(IEnumerable<T>). And add an explicit test for just plain null. Remove the explanatory comments in tests. It is redundant with the calls to .VerifyDiagnostics()
There was a problem hiding this comment.
Done in commit 570e4da. All tests now use raw string literals, have WorkItem attributes, include nullable enabled scenarios, and cover all the default/cast/null cases you requested.
- Added WorkItem attributes referencing issue #45616 - Converted all test code to raw string literals - Added explicit tests for default(int[]), default(string), default(IEnumerable<T>) - Added explicit test for plain null - Added tests for all scenarios with #nullable enable - Removed explanatory comments from tests - Fixed test names to follow consistent naming pattern Co-authored-by: CyrusNajmabadi <[email protected]>
| } | ||
|
|
||
| [WorkItem(45616, "https://github.com/dotnet/roslyn/issues/45616")] | ||
| [Fact] |
There was a problem hiding this comment.
@copilot merge all WorkItem and Fact attributes you added like so: [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/45616")]. Note that you don't need to repeat the issue number at the start of the WorkItem, it is already in the URL in the second arg.
There was a problem hiding this comment.
Done in commit 987f561. All attributes now use the merged format [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/45616")].
Changed all test attributes from separate lines: [WorkItem(45616, "#45616")] [Fact] To merged format: [Fact, WorkItem("#45616")] Removed redundant issue number from WorkItem since it's already in the URL. Co-authored-by: CyrusNajmabadi <[email protected]>
| } | ||
|
|
||
| [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/45616")] | ||
| public void ForeachOnNullCastToStringWithNullableEnabled() |
There was a problem hiding this comment.
@copilot add a test where null is casted to (dynamic). And a case with null casted to a multidimensional array (like int[,]).
There was a problem hiding this comment.
Done in commit 2f4f633. Added tests for (dynamic)null and (int[,])null, both with and without #nullable enable.
Added 4 new tests as requested: - ForeachOnNullCastToDynamic: Test (dynamic)null without nullable - ForeachOnNullCastToMultidimensionalArray: Test (int[,])null without nullable - ForeachOnNullCastToDynamicWithNullableEnabled: Test (dynamic)null with #nullable enable - ForeachOnNullCastToMultidimensionalArrayWithNullableEnabled: Test (int[,])null with #nullable enable All tests verify that these typed null casts compile successfully, with appropriate warnings in nullable-enabled contexts. Co-authored-by: CyrusNajmabadi <[email protected]>
| } | ||
|
|
||
| [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/45616")] | ||
| public void ForeachOnNullCastToMultidimensionalArrayWithNullableEnabled() |
There was a problem hiding this comment.
@copilot add a test where null is being casted to a type parameter T where T is constrained to some IEnumerable<> type.
There was a problem hiding this comment.
Done in commit 7f1c97c. Added tests for (T)null where T : class, IEnumerable<int>, both with and without #nullable enable. The class constraint is necessary to allow the null cast.
Added abstract class System.Enum that derives from ValueType to the systemSource in ForEach_StringNotIEnumerable test. This is required for emitting the AttributeTargets enum properly when using VerifyEmitDiagnostics. Co-authored-by: AlekseyTs <[email protected]>
|
|
||
| var comp = CreateEmptyCompilation(new[] { source, systemSource }, options: WithNullableEnable()); | ||
| comp.VerifyDiagnostics( | ||
| comp.VerifyEmitDiagnostics( |
|
@CyrusNajmabadi It looks like this PR needs some attention. Copilot is unable to fixup baseline for a failing test, and an attempt to do that through a GitHub suggestion comment failed (presumably due to a size of the affected file). #Closed |
| // await foreach (var i in (IAsyncEnumerable<int>)null) | ||
| Diagnostic(ErrorCode.ERR_NullNotValid, "(IAsyncEnumerable<int>)null").WithLocation(9, 33) | ||
| ); | ||
| .VerifyEmitDiagnostics(); |
There was a problem hiding this comment.
It would be even better to verify execution (throws NRE). Comment also applies to other tests that no longer have errors
There was a problem hiding this comment.
@copilot can you figure out how tests validate that they throw at runtime, and update all the passing tests to then run and demonstrate that they do crash with a NullReferenceException as expected.
There was a problem hiding this comment.
will consider in followup.
|
|
||
| public enum AttributeTargets | ||
| { | ||
| Assembly = 0x0001, |
There was a problem hiding this comment.
nit: do we need all these values for this test?
jcouv
left a comment
There was a problem hiding this comment.
LGTM Thanks (commit 20) with two suggestions to consider
* upstream/main: (123 commits) Fix SafeContext of Span-valued collection expressions to match specification (dotnet#80684) Improve detection of invalid references for implicitly typed expression variables declared within implicit object creation expressions. (dotnet#80546) Add test demonstrating behavior of ToMinimalDisplayString. (dotnet#80757) Only set DOTNET_HOST_PATH if something was installed (dotnet#80842) Clean up a Razor external access service (dotnet#80830) Remove unused statement (dotnet#80823) Allow foreach on typed null enumerables (dotnet#80783) Update documentation for DeclaringSyntaxReferences and Locations to clarify partial members behavior (dotnet#80704) Fix issue converting an auto prop to a full prop when 'field' and 'initializers' are involved Rename childIsSimple to innerExpressionHasPrimaryPrecedence and add more test cases Remove placeholder WorkItem attributes from new tests Fix RemoveUnnecessaryParentheses to detect simple expressions in bitwise operations [main] Update dependencies from dotnet/arcade (dotnet#80828) Fix ITypeSymbol.BaseType documentation for type parameters (dotnet#80770) soft-select select camelcase matched item if user might be typing an undefined type parameter (dotnet#80809) Allow semantic tokens in Razor to be better behaved (dotnet#80815) Rebase Remove using Update test Add fix all test ...
Problem
When using a
foreachloop on a constant null value that has been cast to an enumerable type (likeIEnumerable<T>, arrays, or strings), the compiler was incorrectly reporting a compile-time error instead of allowing it to compile and fail at runtime.For example, this code:
Was incorrectly reporting:
This is semantically incorrect - if you explicitly cast null to a type, that cast is valid from a type-safety perspective, and the foreach should be allowed to compile (even though it will throw a NullReferenceException at runtime).
Solution
Removed the compile-time constant null checks for typed foreach collections in
ForEachLoopBinder.cs:The only case where we still report a compile-time error is when the collection expression has no type at all (e.g., bare
nullliteral as aBoundLiteral).Behavior Change
Before:
foreach (var x in null)→ Error (still an error)foreach (var x in (IEnumerable<int>)null)→ Error (now allowed)foreach (var x in default(int[]))→ Error (now allowed)foreach (var x in (int[])null)→ Error (now allowed)After:
foreach (var x in null)→ Error (no type information)foreach (var x in (IEnumerable<int>)null)→ Compiles, runtime errorforeach (var x in default(int[]))→ Compiles, runtime errorforeach (var x in (int[])null)→ Compiles, runtime errorThis matches the general C# principle that type casts are validated at compile time based on types, not values.
With
#nullable enable, these scenarios correctly produce nullable reference type warnings (CS8602: Dereference of a possibly null reference) without blocking compilation.Testing
Comprehensive test coverage including:
default(int[]),default(string),default(IEnumerable<T>)(int[])null,(string)null,(IEnumerable<T>)null(dynamic)nulland(int[,])null(multidimensional arrays)(T)nullwhereT : class, IEnumerable<int>default,new(),[]null(still reports error as expected)Fixes #45616
Original prompt
Fixes #45616
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.