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

JUnitMethodDeclarationCheck: emit warning instead of SuggestedFix if method name clashes #35

Merged
merged 9 commits into from
Apr 12, 2022

Conversation

rickie
Copy link
Member

@rickie rickie commented Jan 24, 2022

No description provided.

@rickie rickie requested review from Stephan202 and werli January 24, 2022 11:55
Copy link
Member

@werli werli left a comment

Choose a reason for hiding this comment

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

Frankly, these suggestions are only on a syntactical level, and no more.

Actual functionality looks to be covered by tests quite well 🙂

// XXX: In theory this rename could clash with an existing method or static import. In that
// case we should emit a warning without a suggested replacement.
tryCanonicalizeMethodName(tree, state).ifPresent(builder::merge);
String newMethodName = tryCanonicalizeMethodName(tree).orElse("");
Copy link
Member

Choose a reason for hiding this comment

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

Seeing how we use the newMethodName, we should not fall back to an empty string only to check whether the string is empty. Why can't we use an empty optional?

Comment on lines 130 to 135
return state.findEnclosing(ClassTree.class).getMembers().stream()
.filter(member -> !ASTHelpers.isGeneratedConstructor((MethodTree) member))
.map(MethodTree.class::cast)
.map(MethodTree::getName)
.map(Name::toString)
.anyMatch(n -> n.equals(methodName));
Copy link
Member

Choose a reason for hiding this comment

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

(1) The casting without check looks off.
(2) anyMatch(methodName::equals)

    return state.findEnclosing(ClassTree.class).getMembers().stream()
        .filter(MethodTree.class::isInstance)
        .map(MethodTree.class::cast)
        .filter(member -> !ASTHelpers.isGeneratedConstructor(member))
        .map(MethodTree::getName)
        .map(Name::toString)
        .anyMatch(methodName::equals);


return compilationUnit.getImports().stream()
.filter(Objects::nonNull)
.map(ImportTree.class::cast)
Copy link
Member

Choose a reason for hiding this comment

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

Looks like we don't need this cast?

Comment on lines 150 to 170
private static CharSequence getStaticImportIdentifier(Tree tree, VisitorState state) {
String source = Util.treeToString(tree, state);
int lastDot = source.lastIndexOf('.');
return lastDot < 0 ? source : source.subSequence(lastDot + 1, source.length());
}
Copy link
Member

Choose a reason for hiding this comment

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

This looks 10/10 hacky, there must be a different way for this 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

I reused logic from RefasterCheck but I'll try to improve. 10/10 is a good score though 😉.

Copy link
Member Author

@rickie rickie Jan 26, 2022

Choose a reason for hiding this comment

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

Just realised we can omit the hacky check. This should be enough right? WDYT? @werli

@rickie
Copy link
Member Author

rickie commented Jan 25, 2022

Frankly, these suggestions are only on a syntactical level, and no more.

This is exactly what this PR needed, so thanks for that. Not sure when I'll go over the suggestions yet.

@rickie
Copy link
Member Author

rickie commented Jan 26, 2022

Thanks for the changes btw, they look good 🚀 !

@rickie rickie requested a review from werli January 26, 2022 12:47
Copy link
Member

@werli werli left a comment

Choose a reason for hiding this comment

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

Applied my solely syntactical suggestions.
Semantically I cannot contribute meaningful, sorry!

return state.findEnclosing(ClassTree.class).getMembers().stream()
.filter(MethodTree.class::isInstance)
.map(MethodTree.class::cast)
.filter(member -> !ASTHelpers.isGeneratedConstructor(member))
Copy link
Member

Choose a reason for hiding this comment

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

Should do the same thing.

Suggested change
.filter(member -> !ASTHelpers.isGeneratedConstructor(member))
.filter(not(ASTHelpers::isGeneratedConstructor))

Comment on lines 87 to 93
Optional<String> newMethodName = tryCanonicalizeMethodName(tree);

if (newMethodName.isEmpty()) {
return describeMatchesIfPresent(tree, builder);
}

boolean reportedNameClash =
reportDescriptionForPossibleNameClash(tree, newMethodName.orElseThrow(), state);
if (!reportedNameClash) {
builder.merge(SuggestedFixes.renameMethod(tree, newMethodName.orElseThrow(), state));
}
}
return describeMatchesIfPresent(tree, builder);
}

private Description describeMatchesIfPresent(MethodTree tree, SuggestedFix.Builder builder) {
return builder.isEmpty() ? Description.NO_MATCH : describeMatch(tree, builder.build());
Copy link
Member

Choose a reason for hiding this comment

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

I feel like we can utilize the Optional API a little more 👀

    if (isTestMethod) {
      tryCanonicalizeMethodName(tree)
          .filter(methodName -> !reportDescriptionForPossibleNameClash(tree, methodName, state))
          .ifPresent(
              methodName -> builder.merge(SuggestedFixes.renameMethod(tree, methodName, state)));
    }
    return builder.isEmpty() ? Description.NO_MATCH : describeMatch(tree, builder.build());

Copy link
Member Author

Choose a reason for hiding this comment

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

Really nice, thanks, this makes it a lot better. Will definitely look into utilizing that API better next time 😄!

@rickie rickie force-pushed the rossendrijver/junit_method_already_exists branch 2 times, most recently from 6209354 to 44d4179 Compare February 16, 2022 10:46
@rickie rickie added this to the 0.1.0 milestone Apr 4, 2022
@Stephan202 Stephan202 force-pushed the rossendrijver/junit_method_already_exists branch from dfcf1ce to 9176a57 Compare April 9, 2022 13:29
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.

Rebased and added a commit. So far only reviewed the JavaKeywords class; remainder TBD.


import com.google.common.collect.ImmutableSet;

@SuppressWarnings("DeclarationOrder" /* The private constructor should come first. */)
Copy link
Member

Choose a reason for hiding this comment

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

Nope, fields go before the constructor.

Suggested change
@SuppressWarnings("DeclarationOrder" /* The private constructor should come first. */)

Comment on lines 12 to 16
* <p>See: the <a
* href="https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html">Oracle
* Documentation</a> on Java Keywords.
*
* <p>In addition, `sealed` is added for Java 17. See: <a href="openjdk.net/jeps/409">JEP-409</a>.
Copy link
Member

Choose a reason for hiding this comment

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

  1. For this kind of thing we should always link to the source of truth, which is the JLS.
  2. This is what the Javadoc @see param is for :)

*/
private static final ImmutableSet<String> JAVA_KEYWORDS =
ImmutableSet.of(
"assert",
Copy link
Member

Choose a reason for hiding this comment

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

We're missing abstract and _ 👀

"protected",
"public",
"return",
"sealed",
Copy link
Member

Choose a reason for hiding this comment

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

sealed is a contextual keyword and can be used as a method name. We can debate whether one should use it as such, but it is allowed.

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.

Added one more commit. There are ~1-2 open points.

Suggested commit message:

`JUnitMethodDeclarationCheck`: better handle possibly-problematic method renames (#35)

By flagging affected test methods, but not suggesting a specific rename.

Comment on lines 120 to 147
private void reportIncorrectMethodName(
String methodName, MethodTree tree, String message, VisitorState state) {
state.reportMatch(
buildDescription(tree).setMessage(String.format(message, methodName)).build());
}
Copy link
Member

Choose a reason for hiding this comment

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

This logic isn't tested

Comment on lines 97 to 140
if (isMethodNameInClass(methodName, state)) {
reportIncorrectMethodName(
methodName, tree, "A method with name %s already exists in the class.", state);
return false;
}

if (isMethodNameStaticallyImported(methodName, state)) {
reportIncorrectMethodName(
methodName, tree, "A method with name %s is already statically imported.", state);
return false;
}
Copy link
Member

Choose a reason for hiding this comment

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

These heuristic aren't perfect:

  • The existing method may have a different signature, such that renaming the test method will merely produce an overload.
  • The code currently doesn't test superclass method declarations, while renaming could cause a method to be overridden.
  • The test method could be scoped such that upon renaming it does not conflict with any usages of the static import.

Not saying we should spend effort on improving this logic, but at the very least we should call it out.

reportIncorrectMethodName(
methodName,
tree,
"Method name `%s` is not possible because it is a Java keyword.",
Copy link
Member

Choose a reason for hiding this comment

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

We generally omit dots from these messages.

reportIncorrectMethodName(
methodName,
tree,
"Method name `%s` is not possible because it is a Java keyword.",
Copy link
Member

Choose a reason for hiding this comment

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

We generally omit dots from these messages.

Comment on lines 120 to 147
private void reportIncorrectMethodName(
String methodName, MethodTree tree, String message, VisitorState state) {
state.reportMatch(
buildDescription(tree).setMessage(String.format(message, methodName)).build());
}
Copy link
Member

Choose a reason for hiding this comment

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

As-is Error Prone cannot prove that message has exactly one placeholder. With a bit of extra verbosity we can convert this into a proper @FormatMethod.

Edit: but with even more refactoring the problem goes away again ;)

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh didn't know this existed 😅, nice one.

Comment on lines 192 to 193
" @Test void testBar() {}",
" private void bar() {}",
Copy link
Member

Choose a reason for hiding this comment

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

It seems that in none of the new cases we verify that, while we don't auto-rename, we do auto-fix the method's modifiers.

return builder.isEmpty() ? Description.NO_MATCH : describeMatch(tree, builder.build());
}

private static Optional<SuggestedFix> tryCanonicalizeMethodName(
MethodTree tree, VisitorState state) {
private boolean isValidMethodName(MethodTree tree, 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 method signature hints at a pure method, while actually it may report an error as a side-effect. We should refactor the code to avoid this pattern.

.anyMatch(methodName::contentEquals);
}

private static CharSequence getStaticImportIdentifier(Tree tree, 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.

The whole thing is also an identifier.

Suggested change
private static CharSequence getStaticImportIdentifier(Tree tree, VisitorState state) {
private static CharSequence getStaticImportSimpleName(Tree tree, VisitorState state) {

}

private static boolean isMethodNameStaticallyImported(String methodName, VisitorState state) {
CompilationUnitTree compilationUnit = state.getPath().getCompilationUnit();
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 inline this one.

buildDescription(tree).setMessage(String.format(message, methodName)).build());
}

private static boolean isMethodNameInClass(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.

Suggested change
private static boolean isMethodNameInClass(String methodName, VisitorState state) {
private static boolean isMethodInEnclosingClass(String methodName, VisitorState state) {

@rickie rickie force-pushed the rossendrijver/junit_method_already_exists branch from 5a4a644 to c27c0ff Compare April 12, 2022 13:03
@rickie
Copy link
Member Author

rickie commented Apr 12, 2022

Looked into retrieving the inherited members but decided to defer it for now. It requires some more work to get it in a nice shape such that it would fit in this PR.

So with that, we addressed all open points for this PR, right 😄?

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.

It does!

@Stephan202 Stephan202 merged commit e859c27 into master Apr 12, 2022
@Stephan202 Stephan202 deleted the rossendrijver/junit_method_already_exists branch April 12, 2022 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants