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 JUnit factory method BugChecker #319

Open
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

eric-picnic
Copy link
Contributor

The BugChecker flags JUnit factory methods which do not have a comment with the parameter names of the test method, or which do not follow the naming convention of suffixing the test method name with TestCases.

@eric-picnic eric-picnic force-pushed the eric-picnic/canonical-junit-factory-methods branch 2 times, most recently from 77f83b4 to aeb0beb Compare October 31, 2022 07:54
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.

It's a very interesting check with some questions we (either deferred) or haven't had to ask ourselves yet. Additionally, this is quite a Picnic-opinionated check 😄.

To make it easier to review this PR + getting it merged, I think we can do two things:

  1. Change the base from this PR to Introduce JUnitClassModifiers check #214 there we have some first changes that we should do either way and make this PR a bit easier.
  2. Secondly, extract the things from JUnit into a different PR.

Added a commit with some suggestions. Didn't really review functionality wise yet but wanted to start the discussion already.

return Optional.empty();
}

private static boolean isMethodInEnclosingClass(String methodName, 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.

This is useful in a much bigger context. I'd suggest moving this to MoreASTHelpers.

import java.util.Optional;
import javax.lang.model.type.TypeKind;

/** A set of JUnit-specific helpers for working with the AST. */
Copy link
Member

Choose a reason for hiding this comment

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

I'm not yet sure what we should do with this... Ideally this would live in google/error-prone in the JUnitMatchers class.

For the reason that class exists, I'd suggest we either name this class MoreJUnitMatchers or more it to MoreMatchers till we see a reason to split it...? WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm fine with either option, but I think it makes sense to parallel JUnitMatchers here. Let's start with renaming it MoreJUnitMatchers, and get back to it when we have a clearer idea of in which order the currently open JUnit-related BugChecker PRs should be merged and what code we want to share between the BugCheckers.

* A {@link BugChecker} that flags non-canonical JUnit factory method declarations.
*
* <p>A canonical JUnit factory method is one which - has the same name as the test method it
* provides test cases for, but with a `TestCases` suffix, and - has a comment which connects the
Copy link
Member

Choose a reason for hiding this comment

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

It makes more sense to list the requirements in an (un)ordered list. As this is quite an opinionated check we could decide to write something like "we consider a canonical". WDYT?

@eric-picnic
Copy link
Contributor Author

I have broken out some of the changes to a separate PR (#335), and rebased this branch on top of them.

@eric-picnic eric-picnic force-pushed the eric-picnic/canonical-junit-factory-methods branch from cdd48ef to 732eb79 Compare November 9, 2022 12:08
@eric-picnic eric-picnic changed the base branch from master to eric-picnic/more-matcher-classes November 9, 2022 12:08
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.

Added a commit.

Simplified some parts of the code. Flushing what I already have to show some progress :).
Will need more time for this nice check 😄.

anyOf(
annotations(AT_LEAST_ONE, isType("java.lang.Override")),
allOf(
not(hasModifier(FINAL)),
Copy link
Member

Choose a reason for hiding this comment

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

Although we could prefer to always statically import these, until now, we always didn't in hasModifier calls, so I'd suggest to keep it consistent.

ImmutableList<MethodTree> factoryMethods =
Optional.ofNullable(state.findEnclosing(ClassTree.class))
.map(
enclosingClass ->
Copy link
Member

Choose a reason for hiding this comment

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

We are not using the enclosingclass, so we can simplify here.

ASTHelpers.getAnnotationWithSimpleName(
tree.getModifiers().getAnnotations(), "MethodSource");

if (methodSourceAnnotation == null) {
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 assume by now that this is true. The first if statement verifies that it is present.

}

Optional<String> factoryMethodName =
MoreJUnitMatchers.extractSingleFactoryMethodName(methodSourceAnnotation);
Copy link
Member

Choose a reason for hiding this comment

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

Using the Optional#map we can combine this call and the following, and therefore omit one of the if statements :).

return Description.NO_MATCH;
}

private ImmutableList<Description> getSuggestedFixes(
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 inlining this method is as readable and makes the code significantly shorter. LMKWYT :)

}

private ImmutableList<Description> getFactoryMethodNameFixes(
MethodTree tree,
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 also provide "just" the Name of the MethodTree.

"import org.junit.jupiter.params.provider.Arguments;",
"import org.junit.jupiter.params.provider.MethodSource;",
"",
"class A {",
Copy link
Member

Choose a reason for hiding this comment

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

In the identification test we already nicely show what exact cases are flagged. What we usually do in the replacement test is verify the actual rewrite for one or two cases. In this example I think we can get away with using only one example in the replacement test that shows all the rewrites. This means we can simplify the test code a lot.

@rickie rickie force-pushed the eric-picnic/more-matcher-classes branch 2 times, most recently from 5e2ab41 to c841481 Compare November 10, 2022 21:15
@Stephan202 Stephan202 force-pushed the eric-picnic/more-matcher-classes branch from 541df42 to 6bcc3bd Compare November 13, 2022 14:08
@rickie rickie force-pushed the eric-picnic/more-matcher-classes branch from 3494a88 to 36c710d Compare November 18, 2022 13:38
@Stephan202 Stephan202 force-pushed the eric-picnic/more-matcher-classes branch from 7f1dd5e to 0e7276b Compare November 22, 2022 07:23
Base automatically changed from eric-picnic/more-matcher-classes to master November 22, 2022 12:35
@rickie rickie force-pushed the eric-picnic/canonical-junit-factory-methods branch from fe8a4c2 to fff620a Compare December 8, 2022 20:34
@rickie
Copy link
Member

rickie commented Dec 8, 2022

So went through a big rebase. Not sure what made it harder, but I think because there were a lot of overlapping changes.

Let's see what Pitest has to say and let's focus on this one again.

@rickie rickie added this to the 0.7.0 milestone Dec 8, 2022
@github-actions
Copy link

github-actions bot commented Dec 8, 2022

  • Surviving mutants in this change: 6
  • Killed mutants in this change: 63
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitFactoryMethodDeclaration 4 43
🧟tech.picnic.errorprone.bugpatterns.util.ConflictDetection 1 16
🧟tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers 1 2
🎉tech.picnic.errorprone.bugpatterns.JUnitFactoryMethodDeclaration$IndexedStatement 0 2

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.

Just flushing some comments I have.

Introducing a new utility (ConflictDetection) comes with the requirement to add a test for that as well 😉.

/**
* A set of helper methods for finding conflicts which would be caused by applying certain fixes.
*/
public final class ConflictDetection {
Copy link
Member

Choose a reason for hiding this comment

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

Isn't it about MethodNameConflictDetection? I think, especially for the current state, ConflictDetection is a bit too generic 🤔.

* @return A human-readable argument against assigning the proposed name to an existing method, or
* {@link Optional#empty()} if no blocker was found.
*/
public static Optional<String> findMethodRenameBlocker(String methodName, 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.

This can probably be a CharSequence, we also use that in other Utils.

if (methodExistsInEnclosingClass(methodName, state)) {
return Optional.of(
String.format("a method named `%s` already exists in this class", methodName));
}
Copy link
Member

Choose a reason for hiding this comment

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

Maybe a matter of taste, but using else if would make it shorter 👀.

@eric-picnic
Copy link
Contributor Author

@rickie, I have pushed a commit for refining some of the tests to get better mutation testing coverage.

@github-actions
Copy link

  • Surviving mutants in this change: 3
  • Killed mutants in this change: 66
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitFactoryMethodDeclaration 1 46
🧟tech.picnic.errorprone.bugpatterns.util.ConflictDetection 1 16
🧟tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers 1 2
🎉tech.picnic.errorprone.bugpatterns.JUnitFactoryMethodDeclaration$IndexedStatement 0 2

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

@eric-picnic eric-picnic force-pushed the eric-picnic/canonical-junit-factory-methods branch 2 times, most recently from cf726c2 to b338a54 Compare December 19, 2022 14:34
@rickie rickie force-pushed the eric-picnic/canonical-junit-factory-methods branch from b338a54 to d7adfbc Compare January 12, 2023 07:37
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.

Added a commit and left some comments. Had to first go over the code with style things before diving in deeper. Cool stuff!

import tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers;

/**
* A {@link BugChecker} that flags non-canonical JUnit factory method declarations.
Copy link
Member

Choose a reason for hiding this comment

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

It actually does more than that right 👀. It also kind of flags things about it's usages.

For that exact reason we might want to drop the Declaration part of this class name.
Which kind of makes me doubt, does the check do too many things? Should we split it 🤔.

}

@BugPattern(summary = "Flags blockers for renaming methods", severity = ERROR)
public static final class RenameBlockerFlagger extends BugChecker implements MethodTreeMatcher {
Copy link
Member

Choose a reason for hiding this comment

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

Should have a Javadoc :).

* @return The name of the factory method referred to in the annotation. Alternatively, {@link
* Optional#empty()} if there is more than one.
*/
public static Optional<String> extractSingleFactoryMethodName(
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 add a test case for this in the MoreJUnitMatchersTest, are you up for that?

@github-actions
Copy link

  • Surviving mutants in this change: 3
  • Killed mutants in this change: 69
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitFactoryMethodDeclaration 1 46
🧟tech.picnic.errorprone.bugpatterns.util.ConflictDetection 1 19
🧟tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers 1 2
🎉tech.picnic.errorprone.bugpatterns.JUnitFactoryMethodDeclaration$IndexedStatement 0 2

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.

Some more comments and a commit. PTAL :).

List<? extends StatementTree> statements = factoryMethod.getBody().getStatements();

Stream<? extends StatementTree> returnStatementsNeedingComment =
Streams.mapWithIndex(statements.stream(), IndexedStatement::new)
Copy link
Member

Choose a reason for hiding this comment

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

Can you elaborate a bit on this part? Why do we need an IndexedStatement exactly? What are we trying to achieve here?

.map(Object::toString)
.collect(toImmutableList());

String expectedComment = parameterNames.stream().collect(joining(", ", "/* { ", " } */"));
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 use the collect in the statement above directly.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe extracting to a method would make the intent a bit more clear.


List<? extends StatementTree> statements = factoryMethod.getBody().getStatements();

Stream<? extends StatementTree> returnStatementsNeedingComment =
Copy link
Member

Choose a reason for hiding this comment

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

Can there be more than one statement that needs a comment? Don't think so right? If so, it would require an extra test for sure (or I am missing something). Otherwise, we can improve the code a bit I think.

@github-actions
Copy link

  • Surviving mutants in this change: 3
  • Killed mutants in this change: 70
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitFactoryMethodDeclaration 1 47
🧟tech.picnic.errorprone.bugpatterns.util.ConflictDetection 1 19
🧟tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers 1 2
🎉tech.picnic.errorprone.bugpatterns.JUnitFactoryMethodDeclaration$IndexedStatement 0 2

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

@rickie rickie modified the milestones: 0.8.0, 0.9.0 Jan 27, 2023
@rickie rickie modified the milestones: 0.9.0, 0.10.0 Feb 20, 2023
@rickie rickie modified the milestones: 0.10.0, 0.11.0 Apr 26, 2023
@Stephan202 Stephan202 modified the milestones: 0.11.0, 0.12.0 May 14, 2023
@Stephan202 Stephan202 modified the milestones: 0.12.0, 0.13.0 Jun 21, 2023
@rickie rickie modified the milestones: 0.13.0, 0.14.0 Aug 25, 2023
@rickie rickie modified the milestones: 0.14.0, 0.15.0 Oct 4, 2023
@rickie rickie modified the milestones: 0.15.0, 0.16.0 Jan 28, 2024
@rickie rickie modified the milestones: 0.16.0, 0.17.0 Mar 8, 2024
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.

None yet

4 participants