Skip to content

Conversation

@mohamedsamehsalah
Copy link
Contributor

@mohamedsamehsalah mohamedsamehsalah commented Jul 23, 2025

JUnit's parameterized tests, annotated with @MethodSource, do not have generic type information at runtime. This means that we can pass a Set of Integers as an argument to a test method which accepts a Set of Strings, and JUnit would disregard the accepted type.

This bug checker flags such cases.

Potential Bug ⚠️

  private static ImmutableSet<Arguments> demoTestCases() {
    return ImmutableSet.of(arguments(ImmutableSet.of(1)));
  }

  @ParameterizedTest
  @MethodSource("demoTestCases")
  void demo(ImmutableSet<String> setOfStrings) {
    ImmutableSet<Integer> setOfIntegers = ImmutableSet.of(1);

    assertThat(setOfStrings).isEqualTo(setOfIntegers); // true
  }

Suggested commit message

Introduce `JUnitMethodSourceGenericParams` bug checker

@github-actions
Copy link

  • Surviving mutants in this change: 31
  • Killed mutants in this change: 78
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitMethodSourceGenericParams 17 78
🧟tech.picnic.errorprone.utils.MoreASTHelpers 12 0
🧟tech.picnic.errorprone.utils.MoreJUnitMatchers 1 0
🧟tech.picnic.errorprone.bugpatterns.JUnitMethodSourceGenericParams$1 1 0

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

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

Nice check idea! Mostly testing that should be improved. I didn't go through the code yet properly, will do that after my holiday :)

}

Optional<AnnotationTree> methodSourceAnnotation = findMethodSourceAnnotation(tree, state);
if (methodSourceAnnotation.isEmpty()) {
Copy link
Member

Choose a reason for hiding this comment

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

Make sure to go over the mutation tests by adding some more coverage :)

@mohamedsamehsalah mohamedsamehsalah force-pushed the mohamedsamehsalah/junit-method-source-generic-params branch from 032b7ac to 704b79e Compare August 16, 2025 11:07
@github-actions
Copy link

  • Surviving mutants in this change: 21
  • Killed mutants in this change: 85
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitMethodSourceGenericParams 13 79
🧟tech.picnic.errorprone.utils.MoreASTHelpers 6 6
🧟tech.picnic.errorprone.utils.MoreJUnitMatchers 1 0
🧟tech.picnic.errorprone.bugpatterns.JUnitMethodSourceGenericParams$1 1 0

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

@github-actions
Copy link

  • Surviving mutants in this change: 20
  • Killed mutants in this change: 86
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitMethodSourceGenericParams 13 79
🧟tech.picnic.errorprone.utils.MoreASTHelpers 6 6
🧟tech.picnic.errorprone.bugpatterns.JUnitMethodSourceGenericParams$1 1 0
🎉tech.picnic.errorprone.utils.MoreJUnitMatchers 0 1

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

@mohamedsamehsalah mohamedsamehsalah force-pushed the mohamedsamehsalah/junit-method-source-generic-params branch from d273d2a to 81947fd Compare August 16, 2025 12:20
@github-actions
Copy link

  • Surviving mutants in this change: 22
  • Killed mutants in this change: 86
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitMethodSourceGenericParams 15 79
🧟tech.picnic.errorprone.utils.MoreASTHelpers 6 6
🧟tech.picnic.errorprone.bugpatterns.JUnitMethodSourceGenericParams$1 1 0
🎉tech.picnic.errorprone.utils.MoreJUnitMatchers 0 1

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

@mohamedsamehsalah mohamedsamehsalah force-pushed the mohamedsamehsalah/junit-method-source-generic-params branch from 81947fd to 0823854 Compare August 16, 2025 12:27
@github-actions
Copy link

  • Surviving mutants in this change: 20
  • Killed mutants in this change: 86
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitMethodSourceGenericParams 13 79
🧟tech.picnic.errorprone.utils.MoreASTHelpers 6 6
🧟tech.picnic.errorprone.bugpatterns.JUnitMethodSourceGenericParams$1 1 0
🎉tech.picnic.errorprone.utils.MoreJUnitMatchers 0 1

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Contributor Author

@mohamedsamehsalah mohamedsamehsalah left a comment

Choose a reason for hiding this comment

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

Thanks for review @rickie 🙌

I tried to address the mutation test reports, but I will need help to cover all of them. I tried to put the reasoning in the comments.

"class A {",
// Used to explicitly highlight that we ignore non-parameterized types during this test,
// as they are not subject to the check.
" static class IrrelevantNonParameterizedType {}",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(1) I want to ignore non-parameterised types explicitly. The reasoning: These type checks are either performed by Java during compile time or checked by JUnit at runtime.

The checker, as the documentation mentions, should flag cases that are not covered by either.

List<? extends ExpressionTree> arguments = argumentMethodInvocationTree.getArguments();
if (treeToString(argumentMethodInvocationTree.getMethodSelect(), state)
.contentEquals(ARGUMENT_SET)) {
// We skip the first argument, because it's the argument set name.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(2) This and the next mutation test will be hard to cover because the first argument of the argumentSet API is non-parametrised, thus, ignored.

private static boolean allParameterisedArgumentsHaveApplicableTypes(
Type testMethodParameterType, Type methodSourceProviderType, Types types) {
if (!testMethodParameterType.isParameterized() || !methodSourceProviderType.isParameterized()) {
// Skip non-parameterised arguments, as any type incompatibility will be flagged by JUnit at
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(3) This is intended, to limit the scope of the checker.

* @see ASTHelpers#getUpperBound(Type, Types)
*/
public static Type getLowerBound(Type type, Types types) {
if (type.hasTag(TypeTag.WILDCARD)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(This should be covered by the BugChecker test) 🤔

Copy link
Member

Choose a reason for hiding this comment

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

If I drop the second and third if statement, the test + the BugCheck test succeeds 🤔.

-    if (type.hasTag(TypeTag.TYPEVAR) && ((TypeVar) type).isCaptured()) {
-      return types.cvarLowerBound(type);
-    }
-
-    if (type.getLowerBound() != null) {
-      return type.getLowerBound();
-    }
+//    if (type.hasTag(TypeTag.TYPEVAR) && ((TypeVar) type).isCaptured()) {
+//      return types.cvarLowerBound(type);
+//    }
+//
+//    if (type.getLowerBound() != null) {
+//      return type.getLowerBound();
+//    }

Copy link
Member

Choose a reason for hiding this comment

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

I think we should figure out if we need these statements or not. Otherwise we should find how to test them. If we can kill more mutants, we can be more sure about what we are actually testing / trying to catch here.

@rickie rickie force-pushed the mohamedsamehsalah/junit-method-source-generic-params branch from 0823854 to c0a3c4a Compare September 3, 2025 14:12
@github-actions
Copy link

github-actions bot commented Sep 3, 2025

  • Surviving mutants in this change: 20
  • Killed mutants in this change: 86
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitMethodSourceGenericParams 13 79
🧟tech.picnic.errorprone.utils.MoreASTHelpers 6 6
🧟tech.picnic.errorprone.bugpatterns.JUnitMethodSourceGenericParams$1 1 0
🎉tech.picnic.errorprone.utils.MoreJUnitMatchers 0 1

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

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

Only some small comments so far. Takes a while to review, but flashing already.

*
* @param methodTree The method tree on which to find the annotation from.
* @param state The {@link VisitorState} from which to derive the AST location of interest.
* @return Returns the {@code @MethodSource} annotation on the given method, if any.
Copy link
Member

Choose a reason for hiding this comment

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

Double word if we keep "Returns". And we can use link with fully qualified name as we do above in this class :)

.doTest();
}

@Test
Copy link
Member

Choose a reason for hiding this comment

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

Great coverage, looks good.

* @return The lower bound of the type, or the type itself if it has no lower bound.
* @see ASTHelpers#getUpperBound(Type, Types)
*/
public static Type getLowerBound(Type type, Types types) {
Copy link
Member

Choose a reason for hiding this comment

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

We could argue this belongs in a class MoreTypes as it's on the Types class, but I think it's fine to leave it here TBH.

* @see ASTHelpers#getUpperBound(Type, Types)
*/
public static Type getLowerBound(Type type, Types types) {
if (type.hasTag(TypeTag.WILDCARD)) {
Copy link
Member

Choose a reason for hiding this comment

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

If I drop the second and third if statement, the test + the BugCheck test succeeds 🤔.

-    if (type.hasTag(TypeTag.TYPEVAR) && ((TypeVar) type).isCaptured()) {
-      return types.cvarLowerBound(type);
-    }
-
-    if (type.getLowerBound() != null) {
-      return type.getLowerBound();
-    }
+//    if (type.hasTag(TypeTag.TYPEVAR) && ((TypeVar) type).isCaptured()) {
+//      return types.cvarLowerBound(type);
+//    }
+//
+//    if (type.getLowerBound() != null) {
+//      return type.getLowerBound();
+//    }

private static final Matcher<ExpressionTree> SUPPORTED_METHOD_SOURCE_PROVIDERS =
staticMethod()
.onDescendantOfAny(
List.class.getCanonicalName(),
Copy link
Member

Choose a reason for hiding this comment

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

Let's sort those :)

@rickie rickie added this to the 0.25.0 milestone Sep 8, 2025
@sonarqubecloud
Copy link

@github-actions
Copy link

  • Surviving mutants in this change: 20
  • Killed mutants in this change: 86
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitMethodSourceGenericParams 13 79
🧟tech.picnic.errorprone.utils.MoreASTHelpers 6 6
🧟tech.picnic.errorprone.bugpatterns.JUnitMethodSourceGenericParams$1 1 0
🎉tech.picnic.errorprone.utils.MoreJUnitMatchers 0 1

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

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

Lol I confused myself with flushing what I had and not 😬.
Pushing a commit with some changes. Let's discuss some of the mutants to get that more sorted out :).

The check has a nice shape. Let's do these two things:

  • Run it on a repo of ours internally (I recall you might have done this a while back as you asked me about it 🤔).
  • I've to continue reviewing :)

* @see ASTHelpers#getUpperBound(Type, Types)
*/
public static Type getLowerBound(Type type, Types types) {
if (type.hasTag(TypeTag.WILDCARD)) {
Copy link
Member

Choose a reason for hiding this comment

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

I think we should figure out if we need these statements or not. Otherwise we should find how to test them. If we can kill more mutants, we can be more sure about what we are actually testing / trying to catch here.

@rickie rickie modified the milestones: 0.25.0, 0.26.0 Sep 22, 2025
@Stephan202 Stephan202 removed this from the 0.26.0 milestone Oct 26, 2025
@Stephan202 Stephan202 added this to the 0.28.0 milestone Oct 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

4 participants