Skip to content

Recognized private members in base class for Source attributes #4033

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

Merged
merged 4 commits into from
Feb 13, 2022

Conversation

manfred-brands
Copy link
Member

Fixes #4032

Copy link
Member

@mikkelbu mikkelbu left a comment

Choose a reason for hiding this comment

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

This of course changes the behaviour a little, but I don't think that this change will break anyone, as I think very few will have a method/field/property in the base class with the same name as the source.
@nunit/framework-team do you agree?

@stevenaw
Copy link
Member

stevenaw commented Feb 5, 2022

This of course changes the behaviour a little, but I don't think that this change will break anyone, as I think very few will have a method/field/property in the base class with the same name as the source. @nunit/framework-team do you agree?

I'm a little hesitant about this myself. While it seems like a simple fix to look for private members, I'm concerned it may also introduce a lot of unexpected edge cases. For example, what if a base class and a child class each define identically named methods with identical arguments? Non-private members would have this caught by the C# compiler as an error but the guard rails are off if we try to go around that.

I'm a little rusty on how/if private members can be used in other inheritance situations (like a virtual test method which gets overridden by a child class) but I see there's a bit of a discussion in the originating issue that this works with protected or public members and that perhaps an analyzer could help guide around this. I'm wondering if a in-framework fix could instead provide a more helpful error message.

Co-authored-by: Mikkel Nylander Bundgaard <mikkelbu@users.noreply.github.com>
@manfred-brands
Copy link
Member Author

manfred-brands commented Feb 6, 2022

For example, what if a base class and a child class each define identically named methods with identical arguments?

In the previous case, the GetMember would have passed the child class parameters to both the child and the base class and never the base class parameter to the base class test. Most likely causing the base class test to fail.
So we have no real 'hiding' of naming in base classes. It is more like the child class can 'override' a static member.

This of course changes the behaviour a little, but I don't think that this change will break anyone, as I think very few will have a method/field/property in the base class with the same name as the source.

This is actually used in various NUnit Framework test where the base class (ConstraintTestBase) depends on a SuccessData source which it doesn't define itself. The child class (EmptyConstraintTest) defines this, but the compiler complains it isn't used.
Looks like a candidate for abstract static members

In the changes for this PR it would test nothing :-( as GetMemberIncludingFromBase returns 2 members and that is translated to null and silently ignored! Personally I would prefer an error if the requested data source cannot be (uniquely) found.

To return previous behaviour, I will update the GetMemberIncludingFromBase method to bail if it finds a member. So that if the member is defined in the child class it will use that regardless of what is defined in the base class.

@stevenaw
Copy link
Member

stevenaw commented Feb 7, 2022

This is actually used in various NUnit Framework test where the base class (ConstraintTestBase) depends on a SuccessData source which it doesn't define itself.

Hm, you are quite right, I hadn't thought about it this way. I suppose this is mostly to facilitate using nameof(), which those tests predate.

It still "feels" more right to me to expect a member which is declared in a base class and potentially defined or overridden in a child class to require "protected" or "public" visibility much the way the C# compiler enforces member visibility, but I can also see it being a bit awkward when the resolution (via nameof()) and invocation (via the test runner) don't both occur at compile-time.

I still think we may want to improve error messages to guide a user to use protected visibility over private when using nameof() in an abstract class, but perhaps that can just be done via an analyzer (I think @mikkelbu has already made the corresponding GH issue). Automagically resolving private members here definitely seems like a nice usability feature.

Thanks @manfred-brands (and thanks for the latest changes to help ensure this is non-breaking). Things are a bit busy for me, but I'll try and do a proper review over the next few days

@manfred-brands
Copy link
Member Author

manfred-brands commented Feb 8, 2022

It still "feels" more right to me to expect a member which is declared in a base class and potentially defined or overridden in a child class to require "protected" or "public" visibility.

There are two ways of 'overriding' a static member.

  1. If the member is private, the derived class can have a member with the same name without getting any warnings.
  2. If the member is protected, the derived class must use the new keyword to replace it.

The initial ticket has both the attribute and the member applied to the base class only. The fact is hidden from the child class which doesn't need to know. Therefore private accessibility sounds right. But NUnit operates on the child class and will find the member in the derived class first which might not be what is wanted.

Thinking more about this, I now tend to agree with you. If the member would be protected then a developer making a child class would get a warning if it accidentally uses the same name for a member. If deliberate, they can use the new keyword to state that intention. We do need to verify the latter works.

I still think we may want to improve error messages to guide a user to use protected visibility over private when using nameof() in an abstract class.

I agree that we would need error messages, both analyzer and runtime, if the child class has a member with the same name as a private member in a base class if such a member is used in any of the Source attributes.

If nunit itself would use nunit.analyzer (which has been discussed), then they would have a warning in ConstraintTestBase that the "SuccessData" string passed to the TestCaseSource doesn't exist, unless we don't warn on abstract classes and only check the child which would be ideal as we otherwise have no way to enforce the child class defining the member.

What is the underlying reason for the if (members.Length == 1) tests in the ...SourceAttribute classes. We neither get an error if the member is not defined nor if it defined twice. Both in my opinion are errors.

@stevenaw
Copy link
Member

stevenaw commented Feb 8, 2022

Ah, yes, you're quite right. Somehow I had missed the "static" part of this scenario. You're absolutely right. Sorry about that @manfred-brands

@manfred-brands
Copy link
Member Author

@stevenaw I was not shouting, I thought I was numbering, but they became big bold headings. :-(

@stevenaw
Copy link
Member

stevenaw commented Feb 8, 2022

Not to worry, no offense taken at all @manfred-brands . I hadn't realized when making my earlier comments that we had also been considering static overriding, but of course your suggestions make sense in that context, and I am glad the conversation was steered back in that direction 🙂

@@ -44,9 +44,12 @@ public class TestFixtureSourceTests
[TestCase(typeof(SourceReturnsFixtureParameters))]
[TestCase(typeof(ExtraTestFixtureAttributeIsIgnored))]
[TestCase(typeof(TestFixtureMayUseMultipleSourceAttributes))]
[TestCase(typeof(DerivedClassUsingBaseClassDataSource))]
[TestCase(typeof(BaseClassUsingDerivedClassDataSource))]
Copy link
Member

Choose a reason for hiding this comment

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

👍 Thanks for the thorough test cases!

Copy link
Member Author

Choose a reason for hiding this comment

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

Test Driven Development. First create a test case that shows the problem, then fix it.

Copy link
Member

@stevenaw stevenaw left a comment

Choose a reason for hiding this comment

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

Thanks @manfred-brands ! Everything looks great. I left a few suggestions for a few existing classes or extensions which could help what you're trying to do here.


namespace NUnit.Framework
{
internal static class TypeGetMemberExtension
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for making these extensions internal visibility by default.

Suggestion: I think there may be an existing + recently created TypeExtensions class which lives in Internal/Extensions. Would it make sense for this new method to live there too?

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 had looked for extension classes, but missed this one. Method moved and Tests added.

{
members.AddRange(type.GetMember(name, flags));
}
while (members.Count == 0 && (type = type.BaseType) != null);
Copy link
Member

@stevenaw stevenaw Feb 10, 2022

Choose a reason for hiding this comment

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

Suggestion: NUnit.Framework.Internal.Reflect also defines a few extension methods over the Type type, including this "walking the class hierarchy" behaviour. Would it make sense to use TypeAndBaseTypes here instead?

I know this part changed recently, but if I'm reading the current version of the code right, we also only walk the hierarchy until the first time that GetMember returns something. Is the List<> still required? Coupled with the above suggestion, would it be simpler to change this to a simple foreach + early return?

Copy link
Member Author

Choose a reason for hiding this comment

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

That class is an even better location, moved it and the tests again. Then there also is the class Extensions.

You are absolutely right that the List<> is no longer required. Factored out.

@@ -198,8 +198,8 @@ public IEnumerable<ITestFixtureData> GetParametersFor(Type sourceType)
if (SourceName == null)
return Reflect.Construct(sourceType) as IEnumerable;

MemberInfo[] members = sourceType.GetMember(SourceName,
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for catching this. It's a little awkward for our purposes that FlattenHierarchy doesn't handle private members

Factored out the now unnecessary List<>.
@stevenaw
Copy link
Member

LGTM! Thanks for your contribution @manfred-brands 👍

@stevenaw stevenaw merged commit de1fe90 into nunit:v3.13-dev Feb 13, 2022
@stevenaw stevenaw added this to the 3.13.3 milestone Feb 13, 2022
@manfred-brands manfred-brands deleted the issue/4032_TestFixtureSource branch February 16, 2022 23:26
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

Successfully merging this pull request may close these issues.

3 participants