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

More easily detect overridden methods & calls to super #1040

Open
wants to merge 20 commits into
base: main
Choose a base branch
from

Conversation

java-coding-prodigy
Copy link

Fixes #982

Currently added a JavaMethod#isOverridden method for now.

@PublicAPI(usage = ACCESS)
public boolean isOverridden() {
Method method = methodSupplier.get();
return Arrays.asList(method.getDeclaringClass().getSuperclass().getDeclaredMethods()).contains(method);
Copy link
Member

@hankem hankem Jan 6, 2023

Choose a reason for hiding this comment

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

It would be nice to avoid the (expensive) reflection API, but more importantly: Does this even work for you? Please provide a unit test that proves the correctnes of your code.

I'd start with something like this:

public class JavaMethodTest {
    @Test
    public void isOverridden() {
        class Base {
            void method1() {}
            void method1(int x) {}
        }
        class Child extends Base {
            void method1() {}
            void method2() {}
        }

        JavaClass childClass = new ClassFileImporter().importClass(Child.class);
        assertThat(childClass.getMethod("method1").isOverridden()).isTrue();
        assertThat(childClass.getMethod("method1", int.class).isOverridden()).isFalse();
        assertThat(childClass.getMethod("method2").isOverridden()).isFalse();
}

Your implementation however gives me childClass.getMethod("method1").isOverridden() == false.

@java-coding-prodigy
Copy link
Author

I couldn't figure out a direct way to get the inherited methods of the super class I tried to use .getMethods() but that only returns the public methods.
If we have something like a class extending a class which also extends another class, and the lowest class has a method that the highest class has, but the middle class does not, then .isOverridden() returns false.
I don't know whether our API or the reflection API has an easy way to fix this or not

Copy link
Member

@hankem hankem left a comment

Choose a reason for hiding this comment

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

You may have seen that the PR's DCO check fails. Can you please squash your changes and sign it off according to the DCO?

It's also preferable to rebase to the current main instead of merging it in order to create a simple linear history.

JavaClass baseClass = new ClassFileImporter().importClass(Base.class);
JavaMethod childMethod1 = childClass.getMethod("method1");
JavaMethod baseMethod1 = baseClass.getMethod("method1");
assertNotEquals(childMethod1, baseMethod1);
Copy link
Member

Choose a reason for hiding this comment

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

This assertion doesn't test what you probably want it to, as the following would also pass:

JavaClass childClass1 = new ClassFileImporter().importClass(Child.class);
JavaClass childClass2 = new ClassFileImporter().importClass(Child.class);
JavaMethod childMethod1 = childClass1.getMethod("method1");
JavaMethod childMethod2 = childClass2.getMethod("method1");
assertNotEquals(childMethod1, childMethod2);

even though both JavaMethods refer to the same method Child#method1().

JavaMethod childMethod1 = childClass.getMethod("method1");
JavaMethod baseMethod1 = baseClass.getMethod("method1");
assertNotEquals(childMethod1, baseMethod1);
assertEquals(childMethod1.getName(), baseMethod1.getName());
Copy link
Member

@hankem hankem Jan 7, 2023

Choose a reason for hiding this comment

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

I don't think that such an assertion is needed in the isOverriddenTest.

Copy link
Author

Choose a reason for hiding this comment

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

Oops, I used that assertion while I was debugging why my check failed and forgot to remove it, will do so in the next commit.

assertNotEquals(childMethod1, baseMethod1);
assertEquals(childMethod1.getName(), baseMethod1.getName());
assertThat(childClass.getMethod("method1").isOverridden()).isTrue();
assertThat(childClass.getMethod("method1", int.class).isOverridden()).isFalse();
Copy link
Member

Choose a reason for hiding this comment

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

Does this work for you? Sorry, I myself had suggested this line in a previous comment (off the top of my head 🙄), but it turns out that we cannot even get the method Base#method1(int) from childClass. When actually executing your test, childClass.getMethod("method1", int.class) gives me:

java.lang.IllegalArgumentException: No code unit with name 'method1' and parameters [int] in codeUnits [JavaMethod{com.tngtech.archunit.core.domain.JavaMethodTest$1Child.method2()}, JavaMethod{com.tngtech.archunit.core.domain.JavaMethodTest$1Child.method1()}] of class com.tngtech.archunit.core.domain.JavaMethodTest$1Child

@PublicAPI(usage = ACCESS)
public boolean isOverridden() {
Class<?>[] reflectedParameters = reflect(getRawParameterTypes());
return Arrays.stream(reflect().getDeclaringClass().getSuperclass().getDeclaredMethods()).anyMatch(superMethod -> superMethod.getName().equals(getName()) && Arrays.equals(superMethod.getParameterTypes(), reflectedParameters));
Copy link
Member

Choose a reason for hiding this comment

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

Do you agree that isOverridden() should support the following test cases?

@Test
public void isOverridden_considers_class_hierarchy() {
    class GrandParent {
        void method() {}
    }
    class Parent extends GrandParent {
    }
    class Child extends Parent {
        void method() {}
    }

    JavaClass childClass = new ClassFileImporter().importClass(Child.class);
    assertThat(childClass.getMethod("method").isOverridden()).isTrue();
}

interface Grandma {
}
interface Grandpa {
    void method();
}
interface Parent extends Grandma, Grandpa {
}

@Test
public void isOverridden_considers_interface_hierarchy() {
    class Child implements Parent {
        public void method() {}
    }

    JavaClass childClass = new ClassFileImporter().importClass(Child.class);
    assertThat(childClass.getMethod("method").isOverridden()).isTrue();
}

As you mentioned, your current implementation doesn't.

I couldn't figure out a direct way to get the inherited methods of the super class I tried to use .getMethods() but that only returns the public methods.
If we have something like a class extending a class which also extends another class, and the lowest class has a method that the highest class has, but the middle class does not, then .isOverridden() returns false.
I don't know whether our API or the reflection API has an easy way to fix this or not

Have you seen the OverridesSuperClassMethod predicate in #982? The implementation within ArchUnit's domain model (i.e. without using reflection) is actually almost there already. 😉

I was checking if the parameters are equal by using `getParameters().equals()` which did not work as `JavaParameter#equals` checks for equality of the owner(in this case, super method) as well. `JavaType#equals` only checks for equality of the actual parameter type regardless of source, and hence works.
Copy link
Member

@hankem hankem left a comment

Choose a reason for hiding this comment

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

Nice; I think you're getting close! 👍

About the DCO: I would do

  • git rebase -i origin/main
  • replace pick with f for all commits after the first one and do
  • git commit --amend -s to create the sign-off, followed by
  • git push -f

import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;

Copy link
Member

Choose a reason for hiding this comment

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

Can we please adhere to the import order specified in CONTRIBUTING.md, i.e. keep the java.* imports at the top?

Copy link
Author

Choose a reason for hiding this comment

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

Cool, I'll do so in JavaMethodTest as well

@@ -125,6 +125,14 @@ public String getDescription() {
return "Method <" + getFullName() + ">";
}

@PublicAPI(usage = ACCESS)
public boolean isOverridden() {
return getOwner().getAllRawSuperclasses().stream()
Copy link
Member

Choose a reason for hiding this comment

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

If we want to conisder overridden methods from interfaces as well, we can replace this stream with

Streams.concat(getOwner().getAllRawSuperclasses().stream(), getOwner().getAllRawInterfaces().stream())

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2022 TNG Technology Consulting GmbH
* Copyright 2014-2023 TNG Technology Consulting GmbH
Copy link
Member

Choose a reason for hiding this comment

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

I guess we should isolate this "Happy new year, code base!" change into a separate commit (that also fixes all java files, which happens automatically when building the project).

Copy link
Author

Choose a reason for hiding this comment

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

Yeah for some reason my IntelliJ or some other plugin automatically made this change for all the classes with this documentation, I forgot to undo the changes on this file itself but did them for all other classes. 😅

Copy link
Member

@hankem hankem left a comment

Choose a reason for hiding this comment

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

Could you please sign-off your commits (most easily done after rebasing & squashing all into one), as suggested before in #1040 (review)?
Without the DCO check passing, GitHub won't let us merge the PR.

@@ -125,6 +126,14 @@ public String getDescription() {
return "Method <" + getFullName() + ">";
}

@PublicAPI(usage = ACCESS)
public boolean isOverridden() {
return Stream.concat(getOwner().getAllRawSuperclasses().stream(), getOwner().getAllRawInterfaces().stream())
Copy link
Member

Choose a reason for hiding this comment

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

Can we pleae add a small test to cover the feature that overriden methods from interfaces are detected?

assertThat(grandChildClass.getMethod("method3").isOverridden()).isFalse();
//TODO add testing for methods with generic parameters
}
}
Copy link
Member

Choose a reason for hiding this comment

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

We should also have a test for the use case of #1047 .

Copy link
Author

Choose a reason for hiding this comment

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

I believe that is already in place, currently we check if the parameters and name of the method are equal, and that implies compatibility of return type.
If that is not the case, then the user code will not compile in the first place

Copy link
Member

Choose a reason for hiding this comment

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

I thought that we should document that this works (which it curently does, but we also want to avoid any future regression):

    @Test
    public void more_specific_return_type_is_supported() {
        class Parent {
            CharSequence method() { return "base"; }
        }
        class Child extends Parent {
            @Override
            String method() { return "overridden"; }
        }
        JavaClass childClass = new ClassFileImporter().importClass(Child.class);
        JavaMethod method = childClass.getMethod("method");
        assertThat(method.isOverridden()).isTrue();
    }

assertThat(grandChildClass.getMethod("method1", int.class).isOverridden()).isTrue();
assertThat(grandChildClass.getMethod("method2").isOverridden()).isTrue();
assertThat(grandChildClass.getMethod("method3").isOverridden()).isFalse();
//TODO add testing for methods with generic parameters
Copy link
Member

Choose a reason for hiding this comment

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

It seems that we indeed don't recognize all overriden generic methods:

    @Test
    public void overridden_generic_methods_are_supported() {
        class Parent<T extends Number> {
            void method(T t) { }
        }
        class Child extends Parent<Integer> {
            @Override
            void method(Integer t) { }
        }
        JavaClass childClass = new ClassFileImporter().importClass(Child.class);
        JavaMethod method = childClass.getMethod("method", Integer.class);
        assertThat(method.isOverridden()).isTrue();  // Expecting value to be true but was false 
    }

Choose a reason for hiding this comment

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

Hmmm, this might complicate the code now.
I'll see what I can do.

@java-coding-prodigy
Copy link
Author

So far I think I know what we can do to fix the issue.
We need to get the supperclasses and interfaces of the method's class but they cannot be the raw ones as they don't have type arguments.
So looks like we can keep on calling a .supperclass() again and again unti we reach java.lang.Object as well as use .interfaces
However this solves the current problem at hand, it may not be bug-free.

Also ignore JavaMethodTest.java that is just me playing around with the objects while debugging.

@codecholeric
Copy link
Collaborator

@java-coding-prodigy are you still working on this? Do you need any support?

@java-coding-prodigy
Copy link
Author

@java-coding-prodigy are you still working on this? Do you need any support?

I was working on it some time ago and I could continue now. However I had run into some issues earlier.
I'll see what I can do.

@java-coding-prodigy
Copy link
Author

Fixed the generic issue, however code is too ugly and slow, we should try to enhance it.
I have only done this for classes, but I presume it would be similar for interfaces too.

@java-coding-prodigy
Copy link
Author

I'm thinking of adding a JavaMethod#overridenFrom(JavaType and make isOverriden call that method. What do you think @hankem ?

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.

More easily detect overridden methods & calls to super
3 participants