Language proposal: https://github.com/dotnet/csharplang/issues/3194
General compiler test plan: https://github.com/dotnet/roslyn/blob/master/docs/contributing/Compiler%20Test%20Plan.md
private extension methods when inside a static class (TestGetEnumeratorPatternViaAccessiblePrivateExtension, TestGetAsyncEnumeratorPatternViaAccessiblePrivateExtension)out/ref disallowed.out (, )ref (TestGetEnumeratorPatternViaRefExtensionOnNonAssignableVariable, TestGetAsyncEnumeratorPatternViaExtensionWithParams)in allowed in the same places as Deconstruct. (TestGetEnumeratorPatternViaInExtensionOnNonAssignableVariable, TestGetAsyncEnumeratorPatternViaInExtensionOnAssignableVariable)params arrays (TestGetEnumeratorPatternViaExtensionWithParams, TestGetAsyncEnumeratorPatternViaExtensionWithParams)__arglist parameter (TestGetEnumeratorPatternViaExtensionWithArgList, TestGetAsyncEnumeratorPatternViaExtensionWithArgList)dynamic prefers existing attempt to call instance GetEnumerator, not an extension method, or does not work on async (TestPreferIEnumeratorInterfaceOnDynamicThanViaExtension, TestCannotUseExtensionGetAsyncEnumeratorOnDynamic)this types:class (a lot)interface (TestGetEnumeratorPatternViaExtensionsOnInterface, TestGetAsyncEnumeratorPatternViaExtensionsOnInterface)delegate (TestGetEnumeratorPatternViaExtensionsOnDelegate, TestGetAsyncEnumeratorPatternViaExtensionsOnDelegate)struct (, TestGetAsyncEnumeratorPatternOnRange)enum (TestGetEnumeratorPatternViaExtensionsOnEnum, TestGetAsyncEnumeratorPatternViaExtensionsOnEnum)nullable (TestGetEnumeratorPatternViaExtensionsOnNullable, TestGetAsyncEnumeratorPatternViaExtensionsOnNullable)GetEnumerator is defined on the class, extensions are not searched, even if return is incorrect. (TestPreferEnumeratorPatternFromInstanceThanViaExtensionEvenWhenInvalid, TestPreferAsyncEnumeratorPatternFromInstanceThanViaExtensionEvenWhenInvalid)ref returns (TestGetEnumeratorPatternViaExtensionWithRefReturn, TestGetAsyncEnumeratorPatternViaExtensionWithRefReturn)Obsolete extension methods (TestGetEnumeratorPatternViaObsoleteExtension, TestWithObsoletePatternMethodsViaExtension)GetEnumerator() returning IDisposable types (TestForEachViaExtensionExplicitlyDisposableStruct, TestAwaitForEachViaExtensionExplicitlyDisposableStruct)Dispose method on async, not on sync (TestForEachViaExtensionDisposeStruct, TestAwaitForEachViaExtensionAsyncDisposeStruct)GetEnumeratorGetEnumerator into scopeforeach to Linqforeach@YairHalberstadt I've updated the test plan. I haven't yet gone through the existing tests to check the boxes, and we'll need to do a compiler review of the plan to see what's blocking. I'll try to go through the existing tests either this afternoon or tomorrow so we have an idea of what's outstanding.
@YairHalberstadt I've filled in the checkboxes. First check is for synchronous, second is for async.
Thanks. Going through them 1 by 1. Will comment here as I have issues:
It looks like __arglist isn't allowed currently for an instance GetAsyncEnumerator either.
Nullable (value type) conversions
Extension methods on a nullable value type don't work on a non-nullable valuetype and vice versa.
Therefore I would expect extension foreach to fail in both cases.
Unboxing (should show helpful error, like for regular invocation)
Do you mean an extension method on int, when you the expression is object?
It looks like __arglist isn't allowed currently for an instance GetAsyncEnumerator either.
That's correct. Many of these are error scenarios, we just need to make sure we don't crash.
Extension methods on a nullable value type don't work on a non-nullable valuetype and vice versa.
Therefore I would expect extension foreach to fail in both cases.
Yep. We do have a good error message for the regular extension method case, I would expect a similar message here.
Do you mean an extension method on int, when you the expression is object?
From int? to int, for example.
I removed the Nullable (value type) conversion bullet, I meant to do so after I broke it up into boxing and unboxing and forgot.
Actually, for an instance GetEnumerator, we unwrap nullable value types. SHould we do the same for extension GetEnumerator?
Interpolated string conversions (IFormattableString vs object)
This goes straight to foreach on a string (which is what we want)
Do you want to add delegate conversion to the test plan?
As a result of the nullable unwrapping this doesn't work:
using System;
public class C
{
public static void Main()
{
foreach (var i in (int?)null)
{
Console.Write(i);
}
}
public sealed class Enumerator
{
public int Current { get; private set; }
public bool MoveNext() => Current++ != 3;
}
}
public static class Extensions
{
public static C.Enumerator GetEnumerator(this int? self) => new C.Enumerator();
}
Is this what we want?
Do you want to add delegate conversion to the test plan?
Do you mean method group/lambda to delegate type?
As a result of the nullable unwrapping this doesn't work:
using System; public class C { public static void Main() { foreach (var i in (int?)null) { Console.Write(i); } } public sealed class Enumerator { public int Current { get; private set; } public bool MoveNext() => Current++ != 3; } } public static class Extensions { public static C.Enumerator GetEnumerator(this int? self) => new C.Enumerator(); }Is this what we want?
No. I would expect that example to work, given that you can do ((int?)null).GetEnumerator().
But we should still unwrap nullable types? I'm guessing this will require two passes of overload resolution, one with the type and one with the unwrapped type.
Although even then I'm not sure that's correct. Which of them would win if the extension method on the nullable type is in a further namespace than the extension method on the unwrapped type?
But we should still unwrap nullable types? I'm guessing this will require two passes of overload resolution, one with the type and one with the unwrapped type.
You mean should we unbox the nullable type? Absolutely not. You can't call an extension method defined on int on a value of type int? today, and I wouldn't expect that to change. We _should_ allow implicit boxing conversions, but unboxing is never implicit.
@333fred
The following currently works:
public struct Program {
public static void Main() {
foreach (var a in (Program?)null)
{
}
}
public IEnumerator<int> GetEnumerator() => throw null;
}
So it makes sense for instance members and extension members to be consistent in this regard.
// This behavior is not spec'd, but it's what Dev10 does.
// This behavior is not spec'd, but it's what Dev10 does.
Given that text, I'd rather not take what is effectively a "the native compiler did this weird thing so let's replicate" into a new feature. I'll double check with the LDM on this, but I'd expect the rules for calling an extension method to be followed.
// This behavior is not spec'd, but it's what Dev10 does.
Given that text, I'd rather not take what is effectively a "the native compiler did this weird thing so let's replicate" into a new feature. I'll double check with the LDM on this, but I'd expect the rules for calling an extension method to be followed.
We're not going to replicate the behavior. This should behave the same as regular extension methods.
Another case I've discovered:
Any object known to be null is invalid in a foreach (e.g. default(IEnumerable)), unless the type is a nullable value type (e.g. default(ImmutableArray<int>?) is allowed).
I'm going to make an exception to this when we use an extension GetEnumerator, since they may be able to handle nulls.
Implementation of this testplan ongoing at https://github.com/dotnet/roslyn/pull/45160
GetSymbolInfo/LookupSymbols/GetMethodGroup work correctly
Can you show some examples of where we have these tests in the codebase
AddParameter to a GetEnumerator
foreach to Linq
Linq to foreach
What am I testing for in each of these cases?
GetSymbolInfo/LookupSymbols/GetMethodGroup work correctly
Can you show some examples of where we have these tests in the codebase
Look for tests that use the GetForEachStatementInfo API.
AddParameter to a GetEnumerator
foreach to Linq
Linq to foreachWhat am I testing for in each of these cases?
Generally speaking, that they work. For AddParameter, we should just make sure it doesn't crash, really. For the other two, I would expect them to work.
AddUsing to bring extension GetEnumerator into scope
PR available at https://github.com/dotnet/roslyn/pull/45348
Seperate PR since it's IDE and a separate feature.
For the other two, I would expect them to work.
I'm not sure that makes much sense. Extension GetEnumerator only applies if somethings not IEnumerable, and hence it will not be convertible to linq. Similiarly linq to foreach will never require use of extension GetEnumerator since it will implement IEnumerable.
I've added a test for find all references for extension GetEnumerator. I discovered that find all references doesn't work at all for GetAsyncEnumerator. I've opened an issue for that (https://github.com/dotnet/roslyn/issues/45352) and will hopefully deal with it at some point, but I presume it is not critical for now.
All I've got left to do are the following:
GetSymbolInfo/LookupSymbols/GetMethodGroup work correctly
AddParameter to a GetEnumerator
foreach to Linq
Linq to foreach
Though as I said above, I'm not sure what there is to test in foreach to linq/linq to foreach
I think it's probably worth starting to review my PR now.
@YairHalberstadt I've updated the test plan with new cases from test plan review, and I'll go through the latest tests in the branch next to make sure the status is fully up to date.
@YairHalberstadt I've updated the test plan. It looks like we're in very good shape, just another couple of tests to add:
out as a extension method receiver. Not sure this one is actually important, given that you can't have out as a this parameter anyway.TestForEachViaExtensionImplicitlyDisposableStruct doesn't actually test what it says it does, and I believe the scenario is covered by TestForEachViaExtensionDisposeStruct anyway.I didn't see any explicit tests for GetForEachStatementInfo.
The pattern for all existing ForEach tests is to call GetBoundForEachStatement which internally calls and tests GetForEachStatementInfo. I added some tests which do this.
The pattern for all existing ForEach tests is to call GetBoundForEachStatement which internally calls and tests GetForEachStatementInfo. I added some tests which do this.
@YairHalberstadt I see this for the sync version, is there an async version I'm missing?
Test plan is complete and the feature is merged.