-
Notifications
You must be signed in to change notification settings - Fork 300
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
base: main
Are you sure you want to change the base?
Conversation
@PublicAPI(usage = ACCESS) | ||
public boolean isOverridden() { | ||
Method method = methodSupplier.get(); | ||
return Arrays.asList(method.getDeclaringClass().getSuperclass().getDeclaredMethods()).contains(method); |
There was a problem hiding this comment.
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
.
I couldn't figure out a direct way to get the inherited methods of the super class I tried to use |
There was a problem hiding this 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); |
There was a problem hiding this comment.
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 JavaMethod
s 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()); |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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()
returnsfalse
.
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. 😉
…instead of reflection
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.
There was a problem hiding this 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
withf
for all commits after the first one and do git commit --amend -s
to create the sign-off, followed bygit push -f
import java.util.Set; | ||
import java.util.function.Function; | ||
import java.util.function.Supplier; | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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. 😅
There was a problem hiding this 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()) |
There was a problem hiding this comment.
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 | ||
} | ||
} |
There was a problem hiding this comment.
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 .
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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.
So far I think I know what we can do to fix the issue. Also ignore |
@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. |
Fixed the generic issue, however code is too ugly and slow, we should try to enhance it. |
I'm thinking of adding a |
Fixes #982
Currently added a
JavaMethod#isOverridden
method for now.