Skip to content

Allow foreach on typed null enumerables#80783

Merged
CyrusNajmabadi merged 20 commits intomainfrom
copilot/fix-null-enumerable-error
Oct 21, 2025
Merged

Allow foreach on typed null enumerables#80783
CyrusNajmabadi merged 20 commits intomainfrom
copilot/fix-null-enumerable-error

Conversation

Copy link
Contributor

Copilot AI commented Oct 16, 2025

Problem

When using a foreach loop on a constant null value that has been cast to an enumerable type (like IEnumerable<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:

foreach (var i in (IEnumerable<int>)null) { }
foreach (int x in (int[])null) { }
foreach (int x in default(int[])) { }

Was incorrectly reporting:

error CS0186: Use of null is not valid in this context

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:

  1. Pattern-based enumeration (IEnumerable with GetEnumerator)
  2. Interface-based enumeration (types implementing IEnumerable)
  3. Special types (arrays, dynamic, strings)

The only case where we still report a compile-time error is when the collection expression has no type at all (e.g., bare null literal as a BoundLiteral).

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 error
  • foreach (var x in default(int[])) → Compiles, runtime error
  • foreach (var x in (int[])null) → Compiles, runtime error

This 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:

  • Tests for default(int[]), default(string), default(IEnumerable<T>)
  • Tests for (int[])null, (string)null, (IEnumerable<T>)null
  • Tests for (dynamic)null and (int[,])null (multidimensional arrays)
  • Tests for type parameter (T)null where T : class, IEnumerable<int>
  • Tests for untyped expressions: default, new(), []
  • Test for plain null (still reports error as expected)
  • Nullable-enabled versions of all tests verifying appropriate warnings
  • All tests use VerifyEmitDiagnostics where appropriate with complete type definitions

Fixes #45616

Original prompt

This section details on the original issue you should resolve

<issue_title>Undeserved error when foreach loop used on null enumerable</issue_title>
<issue_description>This program

using System;
using System.Collections.Generic;
public class C
{
    static void Main()
    {
        foreach (var i in (IEnumerable<int>)null)
        {
            Console.Write(i);
        }
    }
}

causes the compiler to produce

error CS0186: Use of null is not valid in this context

but that error is not justified by the language specification.
</issue_description>

<agent_instructions>@copilot This does seem inappropriate. Can you determine why this is happening?</agent_instructions>

Comments on the Issue (you are @copilot in this section)

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.

…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 AI changed the title [WIP] Fix undeserved error for null enumerable in foreach loop Fix incorrect error reporting for foreach on null IEnumerable Oct 16, 2025
Copilot AI requested a review from CyrusNajmabadi October 16, 2025 22:23
@CyrusNajmabadi
Copy link
Contributor

@copilot i think you misunderstood. This code should be legal. It is legal to cast null to an IEnumerable<T> and it is legal to enumerate such an IEnumerable<T>. So the code shuld be legal (though if it has a Nullable Reference Type warning, that's appropriate).

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]>
Copy link
Contributor Author

Copilot AI commented Oct 16, 2025

@copilot i think you misunderstood. This code should be legal. It is legal to cast null to an IEnumerable<T> and it is legal to enumerate such an IEnumerable<T>. So the code shuld be legal (tho...

You're absolutely right - I misunderstood the issue. I've fixed it now in commit 0b49420.

The code now allows foreach on null-typed enumerables ((IEnumerable<T>)null, (int[])null, default(int[])) to compile. These will fail at runtime with NullReferenceException, but they're type-safe from a compilation perspective, so they should be allowed.

The only case that still reports a compile error is when there's no type information at all (e.g., bare null literal).

Copilot AI changed the title Fix incorrect error reporting for foreach on null IEnumerable Allow foreach on typed null enumerables Oct 16, 2025
}

[Fact]
public void TestNullCastToIEnumerable()
Copy link
Contributor

Choose a reason for hiding this comment

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

@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()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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]
Copy link
Contributor

Choose a reason for hiding this comment

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

@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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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()
Copy link
Contributor

Choose a reason for hiding this comment

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

@copilot add a test where null is casted to (dynamic). And a case with null casted to a multidimensional array (like int[,]).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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()
Copy link
Contributor

Choose a reason for hiding this comment

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

@copilot add a test where null is being casted to a type parameter T where T is constrained to some IEnumerable<> type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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(
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 19, 2025

Choose a reason for hiding this comment

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

VerifyEmitDiagnostics

@copilot Please update baseline for this test #Closed

Copy link
Contributor

Choose a reason for hiding this comment

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

@copilot Please run the enclosing test and add a missing argument based on the output.

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 20, 2025

@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

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 20)

// await foreach (var i in (IAsyncEnumerable<int>)null)
Diagnostic(ErrorCode.ERR_NullNotValid, "(IAsyncEnumerable<int>)null").WithLocation(9, 33)
);
.VerifyEmitDiagnostics();
Copy link
Member

Choose a reason for hiding this comment

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

It would be even better to verify execution (throws NRE). Comment also applies to other tests that no longer have errors

Copy link
Contributor

Choose a reason for hiding this comment

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

@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.

Copy link
Contributor

Choose a reason for hiding this comment

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

will consider in followup.


public enum AttributeTargets
{
Assembly = 0x0001,
Copy link
Member

Choose a reason for hiding this comment

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

nit: do we need all these values for this test?

@jcouv jcouv self-assigned this Oct 21, 2025
Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

LGTM Thanks (commit 20) with two suggestions to consider

@CyrusNajmabadi CyrusNajmabadi merged commit d46a551 into main Oct 21, 2025
25 checks passed
@CyrusNajmabadi CyrusNajmabadi deleted the copilot/fix-null-enumerable-error branch October 21, 2025 18:00
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Oct 21, 2025
333fred added a commit to 333fred/roslyn that referenced this pull request Oct 22, 2025
* 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
  ...
@davidwengier davidwengier modified the milestones: Next, 18.3 Jan 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Undeserved error when foreach loop used on null enumerable

5 participants