-
Notifications
You must be signed in to change notification settings - Fork 39
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
Conversation
There was a problem hiding this 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(""); |
There was a problem hiding this comment.
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?
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)); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
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()); | ||
} |
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
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 😉.
There was a problem hiding this comment.
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
This is exactly what this PR needed, so thanks for that. Not sure when I'll go over the suggestions yet. |
Thanks for the changes btw, they look good 🚀 ! |
There was a problem hiding this 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)) |
There was a problem hiding this comment.
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.
.filter(member -> !ASTHelpers.isGeneratedConstructor(member)) | |
.filter(not(ASTHelpers::isGeneratedConstructor)) |
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()); |
There was a problem hiding this comment.
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());
There was a problem hiding this comment.
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 😄!
6209354
to
44d4179
Compare
dfcf1ce
to
9176a57
Compare
There was a problem hiding this 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. */) |
There was a problem hiding this comment.
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.
@SuppressWarnings("DeclarationOrder" /* The private constructor should come first. */) |
* <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>. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- For this kind of thing we should always link to the source of truth, which is the JLS.
- This is what the Javadoc
@see
param is for :)
*/ | ||
private static final ImmutableSet<String> JAVA_KEYWORDS = | ||
ImmutableSet.of( | ||
"assert", |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
private void reportIncorrectMethodName( | ||
String methodName, MethodTree tree, String message, VisitorState state) { | ||
state.reportMatch( | ||
buildDescription(tree).setMessage(String.format(message, methodName)).build()); | ||
} |
There was a problem hiding this comment.
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
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; | ||
} |
There was a problem hiding this comment.
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.", |
There was a problem hiding this comment.
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.", |
There was a problem hiding this comment.
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.
private void reportIncorrectMethodName( | ||
String methodName, MethodTree tree, String message, VisitorState state) { | ||
state.reportMatch( | ||
buildDescription(tree).setMessage(String.format(message, methodName)).build()); | ||
} |
There was a problem hiding this comment.
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 ;)
There was a problem hiding this comment.
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.
" @Test void testBar() {}", | ||
" private void bar() {}", |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
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(); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private static boolean isMethodNameInClass(String methodName, VisitorState state) { | |
private static boolean isMethodInEnclosingClass(String methodName, VisitorState state) { |
5a4a644
to
c27c0ff
Compare
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 😄? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does!
No description provided.