Skip to content

Conversation

mohamedsamehsalah
Copy link
Contributor

When ExplicitArgumentEnumeration makes a replacement suggestion, it does not take into account that the replacement method is an override of the same type.

Example:

Counter.builder("foo").tags(ImmutableList.of(Tag.of("bar", "baz")));

would have been replaced to

Counter.builder("foo").tags(Tag.of("bar", "baz"));

While the #tags method has no overloaded method that accepts varargs of type Tag (only String...).

Suggested commit message 🎉

Fix `ExplicitArgumentEnumeration` suggestion 

By checking that there exists at least one overload that accepts varargs 
parameter that is of the same type as the original method.

Copy link

Looks good. All 9 mutations in this change were killed.

class surviving killed
🎉tech.picnic.errorprone.bugpatterns.ExplicitArgumentEnumeration 0 9

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

Copy link
Member

@Stephan202 Stephan202 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 jumping on his @mohamedsamehsalah! I added a commit. Suggested commit message:

Avoid invalid `ExplicitArgumentEnumeration` suggestions (#1637)

While still imperfect, this change improves the heuristics that 
determine whether an explicit `Iterable` creator invocation can be 
unwrapped, such that the enumerated values will be passed to a 
compatible varargs overload instead. The new logic validates that the
overload accepts values of the appropriate type.

}

/**
* Return true when at least one of the overloads accepts varargs parameter that is of the same
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Return true when at least one of the overloads accepts varargs parameter that is of the same
* Returns true when at least one of the overloads has a varargs parameter that is of the same

Comment on lines 222 to 227
private static boolean isSubtype(
Type methodParameterType, Type overloadArgumentType, VisitorState state) {
return ASTHelpers.isSubtype(
methodParameterType.getTypeArguments().get(0),
state.getTypes().elemtype(overloadArgumentType),
state);
Copy link
Member

Choose a reason for hiding this comment

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

ASTHelpers.isSubtype performs type erasure; we should avoid that. At the same time, in some test cases this code is now used to check whether ? extends ELEMENT is considered a subtype of ELEMENT, and that's not true without erasure. (With erasure both side just become java.lang.Object.) I played around with some options, and it appears that we can use the containsType operation instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah; I was also not 💯 sure about this one 👍

* parameter type as the original method's.
*/
private static boolean hasVarArgsOverLoadOfSameParameterType(
ImmutableList<MethodSymbol> overloads, MethodSymbol method, VisitorState state) {
Copy link
Member

Choose a reason for hiding this comment

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

Logically I'd expect the method parameter to be listed first.

Comment on lines 197 to 199
boolean hasLikelySuitableVarargsOverload =
overloads.stream().allMatch(m -> m.params().size() == 1)
&& overloads.stream().anyMatch(MethodSymbol::isVarArgs);
&& hasVarArgsOverLoadOfSameParameterType(overloads, method, state);
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 just encapsulate all logic into the new method.

.anyMatch(
overload ->
isSubtype(
method.getParameters().get(0).type,
Copy link
Member

Choose a reason for hiding this comment

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

This expression is loop invariant; it can be pulled out.

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 also use Iterables.getOnlyElement, to explicitly communicate our expectations.

.collect(toImmutableList());

/*
* If all overloads have a single parameter, and at least one of them is a varargs method, then
Copy link
Member

Choose a reason for hiding this comment

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

Now it's not just any varargs method :)

// XXX: There are certainly cases where it _would_ be nice to unwrap the arguments to
// `org.jooq.impl.DSL#row(Collection<?>)`. Look into this.
// XXX: Ideally we do check that one of the overloads accepts the unwrapped arguments.
// XXX: Ideally we validate that eligible overloads have compatible return 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 can now drop one of the XXX comments, IIUC.

private static boolean isSubtype(
Type methodParameterType, Type overloadArgumentType, VisitorState state) {
return ASTHelpers.isSubtype(
methodParameterType.getTypeArguments().get(0),
Copy link
Member

Choose a reason for hiding this comment

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

This code makes the assumption that the unwrapped iterable's type has generic type parameters, and that the first represents the element type. This doesn't hold in general. (Consider e.g. class StringList extends ArrayList<String>() {}). Due to the matchers used this construct may code may in practice, but then we should at least document this gotcha.

Comment on lines 27 to 31
" // BUG: Diagnostic contains:",
" private final int value = unaryMethod(ImmutableList.of(1, 2));",
"",
" private final Builder value2 =",
" Counter.builder(\"foo\").tags(ImmutableList.of(Tag.of(\"bar\", \"baz\")));",
Copy link
Member

Choose a reason for hiding this comment

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

The existing field declaration is there to cover a branch in the isLocalOverload; we can simplify this test case by moving the expression to the method below, and omitting the field declaration.

@Stephan202 Stephan202 added this to the 0.22.0 milestone Apr 13, 2025
@Stephan202 Stephan202 changed the title Fix ExplicitArgumentEnumeration suggestion Avoid invalid ExplicitArgumentEnumeration suggestions Apr 13, 2025
Copy link

Looks good. All 15 mutations in this change were killed.

class surviving killed
🎉tech.picnic.errorprone.bugpatterns.ExplicitArgumentEnumeration 0 15

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

@mohamedsamehsalah
Copy link
Contributor Author

Thanks a lot for the review and suggestions @Stephan202 🙏 LGTM!

mohamedsamehsalah and others added 2 commits April 14, 2025 11:22
By checking that the varargs overload accepts a parameter of the same type as the original method invocation.
@rickie rickie force-pushed the mohamedsamehsalah/fix-explicit-argument-enumeration branch from a8979c0 to 0539457 Compare April 14, 2025 09:22
Copy link

Copy link

Looks good. All 15 mutations in this change were killed.

class surviving killed
🎉tech.picnic.errorprone.bugpatterns.ExplicitArgumentEnumeration 0 15

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

@Stephan202 Stephan202 merged commit ac6ffc7 into master Apr 14, 2025
17 checks passed
@Stephan202 Stephan202 deleted the mohamedsamehsalah/fix-explicit-argument-enumeration branch April 14, 2025 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

3 participants