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

NUnit 2045: Assert.Multiple fixer might be slightly too greedy due to the lack of lookahead #777

Open
danielmarbach opened this issue Aug 21, 2024 · 3 comments

Comments

@danielmarbach
Copy link

For example

            var configuration = new Configuration();
            Assert.That(configuration, Is.Not.Null);
            Assert.That(configuration.Value1, Is.EqualTo(0));
            Assert.That(configuration.Value2, Is.EqualTo(0.0));
            Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
            Assert.That(configuration.Values, Is.Not.Empty);
            Assert.That(configuration.Values.ElementAt(1), Is.EqualTo(string.Empty));

will be rewritten to

            var configuration = new Configuration();
            Assert.That(configuration, Is.Not.Null);
            Assert.Multiple(() =>
            {
                Assert.That(configuration.Value1, Is.EqualTo(0));
                Assert.That(configuration.Value2, Is.EqualTo(0.0));
                Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
                Assert.That(configuration.Values, Is.Not.Empty);
            });
            Assert.That(configuration.Values.ElementAt(1), Is.EqualTo(string.Empty));

while my intuition tells me it should do

            var configuration = new Configuration();
            Assert.That(configuration, Is.Not.Null);
            Assert.Multiple(() =>
            {
                Assert.That(configuration.Value1, Is.EqualTo(0));
                Assert.That(configuration.Value2, Is.EqualTo(0.0));
                Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
            });
            Assert.That(configuration.Values, Is.Not.Empty);
            Assert.That(configuration.Values.ElementAt(1), Is.EqualTo(string.Empty));

or if Values would contain more complex objects

            var configuration = new Configuration();
            Assert.That(configuration, Is.Not.Null);
            Assert.That(configuration.Value1, Is.EqualTo(0));
            Assert.That(configuration.Value2, Is.EqualTo(0.0));
            Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
            Assert.That(configuration.Values, Is.Not.Empty);
            Assert.That(configuration.Values[0].Value1, Is.EqualTo(string.Empty));
            Assert.That(configuration.Values[0].Value2, Is.EqualTo(string.Empty));
            Assert.That(configuration.Values[0].Value11, Is.EqualTo(string.Empty));

will be rewritten to

            var configuration = new Configuration();
            Assert.That(configuration, Is.Not.Null);
            Assert.Multiple(() =>
            {
                Assert.That(configuration.Value1, Is.EqualTo(0));
                Assert.That(configuration.Value2, Is.EqualTo(0.0));
                Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
                Assert.That(configuration.Values, Is.Not.Empty);
            });
            Assert.Multiple(() =>
            {
                Assert.That(configuration.Values[0].Value1, Is.EqualTo(string.Empty));
                Assert.That(configuration.Values[0].Value2, Is.EqualTo(string.Empty));
                Assert.That(configuration.Values[0].Value11, Is.EqualTo(string.Empty));
            });

while my intuition would tell me it should do

            var configuration = new Configuration();
            Assert.That(configuration, Is.Not.Null);
            Assert.Multiple(() =>
            {
                Assert.That(configuration.Value1, Is.EqualTo(0));
                Assert.That(configuration.Value2, Is.EqualTo(0.0));
                Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
            });
             Assert.That(configuration.Values, Is.Not.Empty);
            Assert.Multiple(() =>
            {
                Assert.That(configuration.Values[0].Value1, Is.EqualTo(string.Empty));
                Assert.That(configuration.Values[0].Value2, Is.EqualTo(string.Empty));
                Assert.That(configuration.Values[0].Value11, Is.EqualTo(string.Empty));
            });

Or with nested objects

            var configuration = new Configuration();
            Assert.That(configuration, Is.Not.Null);
            Assert.That(configuration.Value1, Is.EqualTo(0));
            Assert.That(configuration.Value2, Is.EqualTo(0.0));
            Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
            Assert.That(configuration.Inner, Is.Not.Null);
            Assert.That(configuration.Inner.Value1, Is.EqualTo(0));
            Assert.That(configuration.Inner.Value2, Is.EqualTo(0.0));
            Assert.That(configuration.Inner.Value11, Is.EqualTo(string.Empty));

it currently does

var configuration = new Configuration();
Assert.That(configuration, Is.Not.Null);
Assert.Multiple(() =>
{
    Assert.That(configuration.Value1, Is.EqualTo(0));
    Assert.That(configuration.Value2, Is.EqualTo(0.0));
    Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
    Assert.That(configuration.Inner, Is.Not.Null);
});
Assert.Multiple(() =>
{
    Assert.That(configuration.Inner.Value1, Is.EqualTo(0));
    Assert.That(configuration.Inner.Value2, Is.EqualTo(0.0));
    Assert.That(configuration.Inner.Value11, Is.EqualTo(string.Empty));
});

while my intuition tells me to

var configuration = new Configuration();
Assert.That(configuration, Is.Not.Null);
Assert.Multiple(() =>
{
    Assert.That(configuration.Value1, Is.EqualTo(0));
    Assert.That(configuration.Value2, Is.EqualTo(0.0));
    Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
});
Assert.That(configuration.Inner, Is.Not.Null);
Assert.Multiple(() =>
{
    Assert.That(configuration.Inner.Value1, Is.EqualTo(0));
    Assert.That(configuration.Inner.Value2, Is.EqualTo(0.0));
    Assert.That(configuration.Inner.Value11, Is.EqualTo(string.Empty));
});

I do realize this can be highly depending on "taste". I think my intuition comes from the explanation of Assert.Multiple that an argument is not suffixed later and that is an indication that individual subtrees should be groupe together

@manfred-brands
Copy link
Member

@danielmarbach Thanks for your report.

The current CodeFix uses common prefix to determine if something is independent.
So in your first example: configuration.Values is independent of the earlier and hence is grouped inside the Assert.Multiple.
The configuration.Values.ElementAt(1) has the same prefix as that statement and hence is not independent.

Logically you might say that these two statements belong together, but the CodeFix looks at maximum grouping ability and is technically correct.
Maybe we can add a configuration option to not put a prefix inside the last group and always leave it on its own.
As you said, it is a matter of "taste".

@danielmarbach
Copy link
Author

danielmarbach commented Aug 22, 2024

@manfred-brands Agreed it is technically correct. I was first even hesitant to raise it as an issue. I think what caused me to raise it was that the prefix can be seen as "start of a hierarchy level" and every new prefix is a different "hierarchy". I went down on this path of thinking because when I saw

var configuration = new Configuration();
Assert.That(configuration, Is.Not.Null);
Assert.Multiple(() =>
{
    Assert.That(configuration.Value1, Is.EqualTo(0));
    Assert.That(configuration.Value2, Is.EqualTo(0.0));
    Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
});

I immediately thought "kind of smart because essentially every . or descending into subtrees is a good candidate for grouping". But when I look at the application of the fix through our more than 20 plus repositories, we got quite surprising groupings. My hunch that the originally intent of the feature was exactly what my intuition told me is the following rule

https://github.com/nunit/nunit.analyzers/blob/master/src/nunit.analyzers/UseAssertMultiple/UseAssertMultipleAnalyzer.cs#L49-L50
which not only looks at the prefix but whether a new hierarchy is started (or at least that was my interpretation of that check and it might be biased 🤣 ), especially because it almost achieves that except in the samples I provided it tends to be slightly too greedy.

Here are a few live example Particular/ServiceControl@60c8d48

@manfred-brands
Copy link
Member

@danielmarbach The lines you referred to say that instance.Property.Nested is not independent from instance.Property because it starts with the same prefx.
It does say that instance.Values is independent from instance.Property and hence can be grouped.

What you want is looking at 3 items. Only if instance.Values is not followed by instance.Values[0] it is independent.

That would require maintaining more previous arguments in the loop and check something like assert[-1].IsIndependent(assert[0]) && assert[-2].IsIndependent(assert[-1])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants