Skip to content
New issue

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

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

Already on GitHub? # to your account

Handle infinite cycles through params collection initializers #74899

Merged
merged 9 commits into from
Sep 10, 2024

Conversation

jjonescz
Copy link
Member

Fixes #74734.

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Aug 26, 2024
@jjonescz jjonescz force-pushed the 74734-ParamsCollection-StackOverflow branch from 593527d to 59f91fa Compare August 26, 2024 15:18
@jjonescz jjonescz force-pushed the 74734-ParamsCollection-StackOverflow branch from 59f91fa to d6fc080 Compare August 26, 2024 16:43
@jjonescz jjonescz marked this pull request as ready for review August 27, 2024 07:30
@jjonescz jjonescz requested a review from a team as a code owner August 27, 2024 07:30
if (CompilationExtensions.EnableVerifyIOperation &&
ExecutionConditionUtil.Configuration == ExecutionConfiguration.Debug)
{
// A debug assert fails but an invalid operation is correctly created otherwise.
Copy link
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 work item tracking this?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, I will create one, thanks.

Copy link
Member Author

Choose a reason for hiding this comment

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

On a second thought, maybe it's better to just remove the assert.

using System.Collections;
using System.Collections.Generic;

public class Base : IEnumerable<object>
Copy link
Member

Choose a reason for hiding this comment

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

Base

Minor: All of the tests use a base and derived type. Is that necessary to repro the issue? If not, consider merging the base and derived in at least one test where there is currently only one derived type.

Copy link
Member

@cston cston Aug 27, 2024

Choose a reason for hiding this comment

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

Let's also test a generic collection type. Perhaps this existing test could be modified to combine base and derived, and test a generic type:

public class MyCollection1<T> : IEnumerable<T>
{
    public IEnumerator<T> GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
    public void Add(params MyCollection1<T> c) {}
}

public class C
{
    MyCollection1<object> M() => [1];
}

Copy link
Member

Choose a reason for hiding this comment

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

Consider testing:

public class MyCollection1<T> : IEnumerable<T>
{
    public IEnumerator<T> GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
    public void Add(params MyCollection1<object> c) {}
}

public class C
{
    MyCollection1<int> M() => [1];
}

Copy link
Member

@cston cston Aug 27, 2024

Choose a reason for hiding this comment

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

Should we test overloads, such as the following? (It looks like this should succeed.)

public class MyCollection1<T> : IEnumerable<T>
{
    public IEnumerator<T> GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
    public void Add(T x) {}
    public void Add(params MyCollection1<T> y) {}
}

public class C
{
    MyCollection1<object> M() => [1];
}

while (current?.Flags.Includes(BinderFlags.CollectionInitializerAddMethod) == true)
{
if (current is CollectionInitializerAddMethodBinder binder &&
binder.Syntax.FullSpan.Equals(syntax.FullSpan) &&
Copy link
Member

Choose a reason for hiding this comment

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

binder.Syntax.FullSpan.Equals(syntax.FullSpan) &&

Why check spans rather than binder.Syntax == (object)syntax?

Copy link
Member Author

@jjonescz jjonescz Aug 28, 2024

Choose a reason for hiding this comment

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

I'm not sure if the syntax nodes are guaranteed to be the same reference?

Copy link
Member Author

Choose a reason for hiding this comment

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

But it seems it should, and I see other places in roslyn using that, so I will switch to reference equality as well, thanks.

@Spolutrean
Copy link

@jjonescz Is it a good idea to also test different call-sites? It is leading to VS crash as well.

public class MyCollection1<T> : IEnumerable<T>
{
    public IEnumerator<T> GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
    public void Add(params MyCollection1<object> c) {}
}

public class C
{
    public void M() 
    {
        MyCollection1<int> c = new() { 1 };
        c.Add(2);
    }
}

@jjonescz jjonescz requested review from 333fred and cston August 29, 2024 08:50
@@ -6585,7 +6585,6 @@ public override IOperation VisitInstanceReference(IInstanceReferenceOperation op
}
else
{
Copy link
Member

Choose a reason for hiding this comment

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

I'd still like to understand how we're getting here, as well as having explicit ioperation tests for it.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we are getting here because of the cycle - the _currentImplicitInstance is unset because the receiver is the same collection expression as the one being visited. I will add tests.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not comfortable with getting rid of this assert entirely. Let's assert the specific case we're talking about, so we still catch any other violations.

var collectionInitializerAddMethodBinder = this.WithAdditionalFlags(BinderFlags.CollectionInitializerAddMethod);
if (!elements.IsDefaultOrEmpty && HasCollectionInitializerTypeInProgress(syntax, targetType))
{
diagnostics.Add(ErrorCode.ERR_CollectionInitializerInfiniteChainOfAddCalls, syntax, targetType);
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 6, 2024

Choose a reason for hiding this comment

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

ERR_CollectionInitializerInfiniteChainOfAddCalls

We are handling cycles through constructors (ERR_ParamsCollectionInfiniteChainOfConstructorCalls) elsewhere and with a different approach. I assume that at the time that handling was introduced, there was no way to get into a cycle through an Add method. It feels like we should unify and combine both checks, i.e. do them in the same place and use similar approaches for both. Also, we should confirm that there is no way to get into a cycle when constructors and Add methods are involved at the same time, otherwise we might be missing a guard. #Closed

Copy link
Member Author

@jjonescz jjonescz Sep 9, 2024

Choose a reason for hiding this comment

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

Also, we should confirm that there is no way to get into a cycle when constructors and Add methods are involved at the same time, otherwise we might be missing a guard.

I didn't think of a way that would be possible without hitting the existing guards. I can add some more tests though.

It feels like we should unify and combine both checks, i.e. do them in the same place and use similar approaches for both.

The constructor check is separate and happens before binding the conversion - I considered implementing the Add check at the same place but that would require duplicating the complicated Add binding logic. The other way around (moving the existing constructor check) could be probably done - but it seems orthogonal to this bug fix, perhaps we can just file an issue. Also I think we would need another intermediate binder so it wouldn't share code with the Add check, i.e., maybe there are no advantages to moving the constructor check.

Copy link
Contributor

Choose a reason for hiding this comment

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

perhaps we can just file an issue

I think this should be sufficient for now.

Copy link
Member Author

Choose a reason for hiding this comment

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

To avoid error CS8103: Combined length of user strings
used by the program exceeds allowed limit.
@jjonescz jjonescz merged commit a69841b into dotnet:main Sep 10, 2024
24 checks passed
@jjonescz jjonescz deleted the 74734-ParamsCollection-StackOverflow branch September 10, 2024 09:11
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Sep 10, 2024
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Params collection in collection expression's .Add method causes crash by StackOverflow
6 participants