Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test plan for "Extension GetEnumerator" #43184

Closed
56 of 60 tasks
jcouv opened this issue Apr 8, 2020 · 33 comments
Closed
56 of 60 tasks

Test plan for "Extension GetEnumerator" #43184

jcouv opened this issue Apr 8, 2020 · 33 comments
Assignees
Labels
Area-Compilers Feature - Extension Foreach Supporting extension GetEnumerator in foreach Test Test failures in roslyn-CI
Milestone

Comments

@jcouv
Copy link
Member

jcouv commented Apr 8, 2020

Language proposal: dotnet/csharplang#3194
General compiler test plan: https://github.com/dotnet/roslyn/blob/master/docs/contributing/Compiler%20Test%20Plan.md

Spec:

  • Complete proposal spec is checked into csharplang

Compiler (Both sync and async for each of these):

  • [x] LangVer (TestGetEnumeratorPatternViaExtensionsCSharp8, )
  • [x] Feature spec exists and reviewed with LDM
  • [x] Update compiler test plan
  • [x] IOperation/CFG
    • [x] CFG shows the extension method getting called
  • [ ] GetForEachStatementInfo
  • [x] Differing access modifiers
    • [x] Extension GetEnumerator should work for internal, unlike regular GetEnumerator (TestGetEnumeratorPatternViaInternalExtensions, TestGetAsyncEnumeratorPatternViaInternalExtensions)
    • [x] Test with internal instance method and extension method on the same type (TestGetEnumeratorPatternViaExtensionWithInternalInstanceGetEnumerator, TestGetAsyncEnumeratorPatternViaExtensionWithInternalInstanceGetAsyncEnumerator)
    • [x] private extension methods when inside a static class (TestGetEnumeratorPatternViaAccessiblePrivateExtension, TestGetAsyncEnumeratorPatternViaAccessiblePrivateExtension)
  • [x] out/ref disallowed.
    • [x] out (, )
    • [x] ref (TestGetEnumeratorPatternViaRefExtensionOnNonAssignableVariable, TestGetAsyncEnumeratorPatternViaExtensionWithParams)
  • [x] in allowed in the same places as Deconstruct. (TestGetEnumeratorPatternViaInExtensionOnNonAssignableVariable, TestGetAsyncEnumeratorPatternViaInExtensionOnAssignableVariable)
  • [x] Default parameters (TestGetEnumeratorPatternViaExtensionWithOptionalParameter, TestGetAsyncEnumeratorPatternViaExtensionWithOptionalParameter)
  • [x] params arrays (TestGetEnumeratorPatternViaExtensionWithParams, TestGetAsyncEnumeratorPatternViaExtensionWithParams)
  • [x] __arglist parameter (TestGetEnumeratorPatternViaExtensionWithArgList, TestGetAsyncEnumeratorPatternViaExtensionWithArgList)
  • [x] dynamic prefers existing attempt to call instance GetEnumerator, not an extension method, or does not work on async (TestPreferIEnumeratorInterfaceOnDynamicThanViaExtension, TestCannotUseExtensionGetAsyncEnumeratorOnDynamic)
  • [x] Conversions
    • [x] Reference conversions (TestGetEnumeratorPatternViaExtensionsWithUpcast, TestGetAsyncEnumeratorPatternViaExtensionsWithUpcast)
    • [x] Boxing conversion (TestGetEnumeratorPatternViaExtensionsWithNullableValueTypeConversion, TestGetAsyncEnumeratorPatternViaExtensionsWithNullableValueTypeConversion)
    • [x] Unboxing (should show helpful error, like for regular invocation) (TestGetEnumeratorPatternViaExtensionsWithUnboxingConversion, TestGetAsyncEnumeratorPatternViaExtensionsWithUnboxingConversion)
    • [x] Literal 0 to enum (same as Unboxing) (TestGetEnumeratorPatternViaExtensionsWithZeroToEnumConversion, TestGetAsyncEnumeratorPatternViaExtensionsWithZeroToEnumConversion)
    • [x] Unconstrained generics (TestGetEnumeratorPatternViaExtensionsWithUnconstrainedGenericConversion, TestGetAsyncEnumeratorPatternViaExtensionsWithUnconstrainedGenericConversion)
    • [x] Constrained generics (TestGetEnumeratorPatternViaExtensionsWithConstrainedGenericConversion, TestGetAsyncEnumeratorPatternViaExtensionsWithConstrainedGenericConversion)
    • [x] Interpolated string conversions (IFormattableString vs object) (TestGetEnumeratorPatternViaExtensionsWithFormattableStringConversion, TestGetAsyncEnumeratorPatternViaExtensionsWithFormattableStringConversion1)
    • [x] User-defined conversions (should be invalid) (TestGetEnumeratorPatternViaExtensionsWithUserDefinedImplicitConversion, TestGetAsyncEnumeratorPatternViaExtensionsWithUserDefinedImplicitConversion)
    • [x] Tuple type conversions (TestGetEnumeratorPatternViaExtensionsOnTupleWithNestedConversions, TestGetAsyncEnumeratorPatternViaExtensionsOnTupleWithNestedConversions)
  • [x] this types:
    • [x] class (a lot)
    • [x] interface (TestGetEnumeratorPatternViaExtensionsOnInterface, TestGetAsyncEnumeratorPatternViaExtensionsOnInterface)
    • [x] delegate (TestGetEnumeratorPatternViaExtensionsOnDelegate, TestGetAsyncEnumeratorPatternViaExtensionsOnDelegate)
    • [x] struct (, TestGetAsyncEnumeratorPatternOnRange)
    • [x] enum (TestGetEnumeratorPatternViaExtensionsOnEnum, TestGetAsyncEnumeratorPatternViaExtensionsOnEnum)
    • [x] nullable (TestGetEnumeratorPatternViaExtensionsOnNullable, TestGetAsyncEnumeratorPatternViaExtensionsOnNullable)
    • [x] type parameters (TestGetEnumeratorPatternViaExtensionsOnTypeParameter, TestGetAsyncEnumeratorPatternViaExtensionsOnTypeParameter)
  • [x] Ambiguous overloads (TestGetEnumeratorPatternViaAmbiguousExtensions, TestGetAsyncEnumeratorPatternViaAmbiguousExtensions)
  • [x] Correct name signature, but return doesn't fulfill the pattern (, TestMoveNextAsyncPatternViaExtensions1)
  • [x] Extension is preferred last (TestPreferEnumeratorPatternFromInstanceThanViaExtension, TestPreferAsyncEnumeratorPatternFromInstanceThanViaExtension)
  • [x] If GetEnumerator is defined on the class, extensions are not searched, even if return is incorrect. (TestPreferEnumeratorPatternFromInstanceThanViaExtensionEvenWhenInvalid, TestPreferAsyncEnumeratorPatternFromInstanceThanViaExtensionEvenWhenInvalid)
  • [x] Multiple extensions in different namespace levels
    • [x] Valid overload in closest namespace (with invalid in outer namespace) (TestGetEnumeratorPatternViaValidExtensionInClosestNamespaceInvalidInFurtherNamespace1-2, TestGetAsyncEnumeratorPatternViaValidExtensionInClosestNamespaceInvalidInFurtherNamespace1-2)
    • [x] Invalid overload in closest namespace (with valid in outer namespace) (TestGetEnumeratorPatternViaInvalidExtensionInClosestNamespaceValidInFurtherNamespace1-2, TestGetAsyncEnumeratorPatternViaInvalidExtensionInClosestNamespaceValidInFurtherNamespace1-2)
  • [x] ref returns (TestGetEnumeratorPatternViaExtensionWithRefReturn, TestGetAsyncEnumeratorPatternViaExtensionWithRefReturn)
  • [x] Nullable warnings
    • [x] No warning for extension method accepting null (ForEach_ExtensionGetEnumerator6, ForEach_ExtensionGetAsyncEnumerator6)
    • [x] Warning for extension method not accepting null (ForEach_ExtensionGetEnumerator1, ForEach_ExtensionGetAsyncEnumerator5)
    • [x] Constraint mismatch warnings (ForEach_ExtensionGetEnumerator3, ForEach_ExtensionGetAsyncEnumerator3)
    • [x] Warning for nullable return (ForEach_ExtensionGetEnumerator15, ForEach_ExtensionGetAsyncEnumerator15)
    • [x] Output attributes are honored after call (ForEach_ExtensionGetEnumerator7-8, ForEach_ExtensionGetAsyncEnumerator7)
  • [x] Obsolete extension methods (TestGetEnumeratorPatternViaObsoleteExtension, TestWithObsoletePatternMethodsViaExtension)
  • Disposal
    • [x] GetEnumerator() returning IDisposable types (TestForEachViaExtensionExplicitlyDisposableStruct, TestAwaitForEachViaExtensionExplicitlyDisposableStruct)
    • [x] Uses pattern-based Dispose method on async, not on sync (TestForEachViaExtensionDisposeStruct, TestAwaitForEachViaExtensionAsyncDisposeStruct)
    • [x] Uses pattern-based for both for ref structs (, TestAwaitForEachViaExtensionImplicitlyDisposableStruct)

Productivity:

  • [ ] test FindAllReferences
  • [x] AddParameter to a GetEnumerator
  • [ ] AddUsing to bring extension GetEnumerator into scope
  • [ ] foreach to Linq
  • [ ] Linq to foreach
@333fred
Copy link
Member

333fred commented Jun 15, 2020

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

@333fred
Copy link
Member

333fred commented Jun 16, 2020

@YairHalberstadt I've filled in the checkboxes. First check is for synchronous, second is for async.

@YairHalberstadt
Copy link
Contributor

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.

@YairHalberstadt
Copy link
Contributor

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.

@YairHalberstadt
Copy link
Contributor

Unboxing (should show helpful error, like for regular invocation)

Do you mean an extension method on int, when you the expression is object?

@333fred
Copy link
Member

333fred commented Jun 17, 2020

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.

@333fred
Copy link
Member

333fred commented Jun 17, 2020

I removed the Nullable (value type) conversion bullet, I meant to do so after I broke it up into boxing and unboxing and forgot.

@YairHalberstadt
Copy link
Contributor

Actually, for an instance GetEnumerator, we unwrap nullable value types. SHould we do the same for extension GetEnumerator?

@YairHalberstadt
Copy link
Contributor

Interpolated string conversions (IFormattableString vs object)

This goes straight to foreach on a string (which is what we want)

@YairHalberstadt
Copy link
Contributor

Do you want to add delegate conversion to the test plan?

@YairHalberstadt
Copy link
Contributor

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?

@333fred
Copy link
Member

333fred commented Jun 17, 2020

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

@YairHalberstadt
Copy link
Contributor

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.

@YairHalberstadt
Copy link
Contributor

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?

@333fred
Copy link
Member

333fred commented Jun 17, 2020

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.

@YairHalberstadt
Copy link
Contributor

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

@YairHalberstadt
Copy link
Contributor

// If collectionExprType is a nullable type, then use the underlying type and take the value (i.e. .Value) of collectionExpr.
// This behavior is not spec'd, but it's what Dev10 does.
if ((object)collectionExprType != null && collectionExprType.IsNullableType())

// This behavior is not spec'd, but it's what Dev10 does.

@333fred
Copy link
Member

333fred commented Jun 17, 2020

// If collectionExprType is a nullable type, then use the underlying type and take the value (i.e. .Value) of collectionExpr.
// This behavior is not spec'd, but it's what Dev10 does.
if ((object)collectionExprType != null && collectionExprType.IsNullableType())

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

@333fred
Copy link
Member

333fred commented Jun 18, 2020

// If collectionExprType is a nullable type, then use the underlying type and take the value (i.e. .Value) of collectionExpr.
// This behavior is not spec'd, but it's what Dev10 does.
if ((object)collectionExprType != null && collectionExprType.IsNullableType())

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

@YairHalberstadt
Copy link
Contributor

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.

@YairHalberstadt
Copy link
Contributor

Implementation of this testplan ongoing at #45160

@YairHalberstadt
Copy link
Contributor

GetSymbolInfo/LookupSymbols/GetMethodGroup work correctly

Can you show some examples of where we have these tests in the codebase

@YairHalberstadt
Copy link
Contributor

AddParameter to a GetEnumerator
foreach to Linq
Linq to foreach

What am I testing for in each of these cases?

@333fred
Copy link
Member

333fred commented Jun 18, 2020

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 foreach

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

@YairHalberstadt
Copy link
Contributor

AddUsing to bring extension GetEnumerator into scope

PR available at #45348

Seperate PR since it's IDE and a separate feature.

@YairHalberstadt
Copy link
Contributor

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.

@YairHalberstadt
Copy link
Contributor

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 (#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.

@jaredpar jaredpar modified the milestones: Compiler.Net5, 16.8 Jun 23, 2020
@333fred
Copy link
Member

333fred commented Jul 17, 2020

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

@333fred
Copy link
Member

333fred commented Jul 23, 2020

@YairHalberstadt I've updated the test plan. It looks like we're in very good shape, just another couple of tests to add:

@YairHalberstadt
Copy link
Contributor

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.

@YairHalberstadt
Copy link
Contributor

#46299

@333fred
Copy link
Member

333fred commented Jul 31, 2020

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?

@jaredpar jaredpar added the Test Test failures in roslyn-CI label Aug 4, 2020
@333fred
Copy link
Member

333fred commented Aug 7, 2020

Test plan is complete and the feature is merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers Feature - Extension Foreach Supporting extension GetEnumerator in foreach Test Test failures in roslyn-CI
Projects
None yet
Development

No branches or pull requests

4 participants