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

Introduce JUnitSingleArguments bug checker #816

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package tech.picnic.errorprone.bugpatterns;

import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;

import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;

/**
* A {@link BugChecker} that flags uses of {@link
* org.junit.jupiter.params.provider.Arguments#arguments(Object...)} with a single (or no) argument;
* in such cases the use of {@link org.junit.jupiter.params.provider.Arguments} is not required as a
* {@link java.util.stream.Stream} of objects can be directly provided to a {@link
* org.junit.jupiter.params.provider.MethodSource}.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "JUnit arguments wrapping a single object are redundant",
link = BUG_PATTERNS_BASE_URL + "JUnitSingleArguments",
linkType = CUSTOM,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class JUnitSingleArguments extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> ARGUMENTS_ARGUMENTS =
staticMethod().onClass("org.junit.jupiter.params.provider.Arguments").named("arguments");

/** Instantiates a new {@link JUnitSingleArguments} instance. */
public JUnitSingleArguments() {}

@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (ARGUMENTS_ARGUMENTS.matches(tree, state) && tree.getArguments().size() <= 1) {
return describeMatch(tree, SourceCode.unwrapMethodInvocation(tree, state));
Copy link
Member

Choose a reason for hiding this comment

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

This would cause a compilation error w.r.t. the return type, right? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I wasn't sure how we deal with the fix suggestions. AFAIK not all suggestions necessarily lead to directly compiling code? We can ofc. remove it here and just flag instead 🤷🏻‍♂️

Copy link
Member

Choose a reason for hiding this comment

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

We try either produce compilable code or only flag the issue, with a strong preference for the former (as the latter really introduces quite a bit more friction). That said, especially some of the Refaster rules can in special circumstances break the code.

For this case we could:

  1. Simplify the factory method if it has a single return statement, using similar logic we use in JUnitValueSource.
  2. Only flag otherwise.

(But that's considerably more complicated than the current proposal, so if you're like "uuuuuh", I get that. Don't mind having a stab; would just take a bit of time.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah understood. In the most common use case we 'only' need to replace <Arguments> with <T> in the method definition. Perhaps it's relatively easy to add the suggestion for this use case and still resort to flagging any other occurrences (they would be weird but you never know 😛 ).

Copy link
Member

Choose a reason for hiding this comment

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

Indeed, that's the lines along which I'm thinking 👍

(Both inside Picnic and for the upcoming integration test framework against open-source repos we run Error Prone in a loop until it no longer makes any changes, which can only work if each intermediate state compiles successfully.)

Copy link
Member

Choose a reason for hiding this comment

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

I've looked a bit closer at this topic, and the logic in JUnitValueSource shows that there are quite a lot of details to consider:

  1. There are different ways in which the Arguments can be wrapped in a top-level collection/stream/array.
  2. The factory method can have multiple return statements.
  3. The return type could change from MonadType<Arguments> or Arguments[] to MonadType<Object> or Object[], but ideally we're more specific.
  4. Only if all Arguments instances have exactly one value can we do the unwrapping (in case of variability we should likely emit a separate warning).
  5. Ideally we do all this in a way that reuses relevant pieces of logic from JUnitValueSource.

I'm likely missing a few other details. Happy to dive deeper into this, but it'll be a while before I find the time to really sit down for this.

}

return Description.NO_MATCH;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Arguments before(@Repeated T objects) {
}

@AfterTemplate
@SuppressWarnings("JUnitSingleArguments" /* The bugpattern does not understand Repeated. */)
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
Arguments after(@Repeated T objects) {
return arguments(objects);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package tech.picnic.errorprone.bugpatterns;

import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;

final class JUnitSingleArgumentsTest {
@Test
void identification() {
CompilationTestHelper.newInstance(JUnitSingleArguments.class, getClass())
.addSourceLines(
Venorcis marked this conversation as resolved.
Show resolved Hide resolved
"A.java",
"import static java.util.Objects.requireNonNull;",
"import static java.util.function.Function.identity;",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"class A {",
" void m() {",
" // BUG: Diagnostic contains:",
" arguments();",
" // BUG: Diagnostic contains:",
" arguments(1);",
" arguments(1,2);",
"",
" identity();",
" requireNonNull(null);",
" }",
"}")
.doTest();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.params.provider.Arguments.arguments;

import com.google.common.collect.ImmutableList;
import com.google.errorprone.BugPattern;
Expand All @@ -17,7 +16,6 @@
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

final class MethodMatcherFactoryTest {
Expand All @@ -29,13 +27,13 @@ final class MethodMatcherFactoryTest {
"com.example.A#m2(java.lang.String)",
"com.example.sub.B#m3(int,int)"));

private static Stream<Arguments> createWithMalformedSignaturesTestCases() {
private static Stream<ImmutableList<String>> createWithMalformedSignaturesTestCases() {
/* { signatures } */
return Stream.of(
arguments(ImmutableList.of("foo.bar")),
arguments(ImmutableList.of("foo.bar#baz")),
arguments(ImmutableList.of("a", "foo.bar#baz()")),
arguments(ImmutableList.of("foo.bar#baz()", "a")));
ImmutableList.of("foo.bar"),
ImmutableList.of("foo.bar#baz"),
ImmutableList.of("a", "foo.bar#baz()"),
ImmutableList.of("foo.bar#baz()", "a"));
}

@MethodSource("createWithMalformedSignaturesTestCases")
Expand Down
Loading