Introduce More{ASTHelpers,JUnitMatchers,Matchers} utility classes#335
Introduce More{ASTHelpers,JUnitMatchers,Matchers} utility classes#335
More{ASTHelpers,JUnitMatchers,Matchers} utility classes#335Conversation
rickie
left a comment
There was a problem hiding this comment.
Left a few comments and questions. Let me know what you think, we can discuss this as well Monday :).
Should we link to the class they are actually "extending"? In the Javadoc I mean. For example, should MoreJUnitMatchers in a way link to JUnitMatchers?
Sorry for maybe being a bit nitpicky on some of the things, but these classes can potentially be used a lot in the future 👀.
| .collect(toImmutableList()); | ||
| } | ||
|
|
||
| static boolean isMethodInEnclosingClass(String methodName, VisitorState state) { |
There was a problem hiding this comment.
I'd say it makes sense to just make this public right?
There was a problem hiding this comment.
Either way, it would be nice to add a Javadoc here :).
|
|
||
| /** A set of JUnit-specific helpers for working with the AST. */ | ||
| public final class MoreJUnitMatchers { | ||
| /** Matches JUnit test methods. */ |
There was a problem hiding this comment.
Maybe we should be more specific and call it JUnit Jupiter, WDYT? The things in here are quite JUnit 5 specific
| import java.util.Optional; | ||
| import javax.lang.model.type.TypeKind; | ||
|
|
||
| /** A set of JUnit-specific helpers for working with the AST. */ |
There was a problem hiding this comment.
Just for reference, the Javadoc from JUnitMatchers itself:
- Matchers for code patterns which appear to be JUnit-based tests.
I think mentioning AST here isn't as accurate as we can be 😬.
| /** A set of JUnit-specific helpers for working with the AST. */ | |
| /** A set of JUnit Jupiter-specific helper methods and {@link Matcher Matchers}. */ |
| * | ||
| * @param enclosingClass The class to search in. | ||
| * @param methodName The method name to search for. | ||
| * @return The method trees of the methods with the given name in the class. |
There was a problem hiding this comment.
| * @return The method trees of the methods with the given name in the class. | |
| * @return The {@link MethodTree}s of the methods with the given name found in the class. |
Maybe even "in the enclosing class"? Or "given class".
| isType("org.junit.jupiter.api.BeforeEach"))); | ||
|
|
||
| /** | ||
| * Matches methods which have a {@link org.junit.jupiter.params.provider.MethodSource} annotation. |
There was a problem hiding this comment.
| * Matches methods which have a {@link org.junit.jupiter.params.provider.MethodSource} annotation. | |
| * Matches methods that have a {@link org.junit.jupiter.params.provider.MethodSource} annotation. |
Right?
I think that could be helpful. 👍 I have pushed a commit addressing this as well as implementing your other suggestions. |
| * | ||
| * @param methodSourceAnnotation The {@link org.junit.jupiter.params.provider.MethodSource} | ||
| * annotation to extract a method name from. | ||
| * @return The name of the factory methods referred to in the annotation if there is only one, or |
There was a problem hiding this comment.
s/methods/method singular is important here :). Reflowed the sentence a bit, not really happy with the word "Alternatively" that I pushed. Feel free to tweak.
| import javax.lang.model.type.TypeKind; | ||
|
|
||
| /** | ||
| * A set of JUnit Jupiter-specific helper methods and {@link Matcher Matchers}, adding on to the |
There was a problem hiding this comment.
The first sentence is usually the summary in the Javadoc and we try to keep that as concise as possible. I feel the second part "deserves" it's own sentence.
| import com.sun.source.tree.Tree; | ||
| import com.sun.tools.javac.code.Symbol; | ||
|
|
||
| /** A collection of methods to enhance the use of {@link Matcher}s. */ |
There was a problem hiding this comment.
Here we are missing a link to Matchers (not to mix up with Matchers that is used in the line above 😅).
Will add it.
rickie
left a comment
There was a problem hiding this comment.
Re-requested review because I think we should also update the usages where we can. Will add a commit.
Added a commit with some other changes. LMKWYT , happy to discuss :).
Oh w.r.t. the state.findEnclosing(ClassTree.class).getMembers(), we could add a checkState there if we want to 👀.
| * A set of JUnit Jupiter-specific helper methods and {@link Matcher Matchers}, adding on to the | ||
| * ones from {@link com.google.errorprone.matchers.JUnitMatchers}. | ||
| */ | ||
| public final class MoreJUnitJupiterMatchers { |
There was a problem hiding this comment.
I'm a bit on the fence about the naming.
Although within the class it's a good idea to make explicit it's about JUnit Jupiter things, it makes sense to me to name the class inline with the upstream one, meaning we call it MoreJUnitMatchers. WDYT?
Additionally, in the near future expect more things will be added not necessarily specific to JUnit Jupiter.
| * @param methodName The method name to search for. | ||
| * @return Whether there are any methods with the given name in the given class. | ||
| */ | ||
| public static boolean isMethodInEnclosingClass(ClassTree enclosingClass, String methodName) { |
There was a problem hiding this comment.
Changed this back to a VisitorState as that is how it is used in the other place. That also holds for the usage we are introducing in #319. As passing VisitorState to ASTHelpers's methods is quite common, I think it does make sense here. WDYT?
Sorry @eric-picnic can you maybe repeat here why this was changed to a ClassTree :)?
| * Matches methods that have a {@link org.junit.jupiter.params.provider.MethodSource} annotation. | ||
| */ | ||
| public static final Matcher<MethodTree> HAS_METHOD_SOURCE = | ||
| allOf(annotations(AT_LEAST_ONE, isType("org.junit.jupiter.params.provider.MethodSource"))); |
There was a problem hiding this comment.
| allOf(annotations(AT_LEAST_ONE, isType("org.junit.jupiter.params.provider.MethodSource"))); | |
| annotations(AT_LEAST_ONE, isType("org.junit.jupiter.params.provider.MethodSource")); |
Wouldn't this be sufficient?
54e7619 to
a1c3556
Compare
|
Latest changes LGTM! Will definitely make the other review easier 🚀 ! The PR is ready for your 👁️ 👁️ @Stephan202. |
6443b04 to
ff44df7
Compare
5e2ab41 to
c841481
Compare
|
Rebased and added a commit. We discussed offline that we also want to have tests for the new utilities. I created the first one, will work on the rest as well ~soon. Haven't written many of these types of tests, so let's discuss what / how I can improve them 😄. |
541df42 to
6bcc3bd
Compare
Stephan202
left a comment
There was a problem hiding this comment.
Rebased and added a commit. Test review is TBD.
| /** | ||
| * Extracts the name of the JUnit factory method from a {@link | ||
| * org.junit.jupiter.params.provider.MethodSource} annotation. | ||
| * | ||
| * @param methodSourceAnnotation The {@link org.junit.jupiter.params.provider.MethodSource} | ||
| * annotation to extract a method name from. | ||
| * @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( | ||
| AnnotationTree methodSourceAnnotation) { | ||
| ExpressionTree attributeExpression = | ||
| ((AssignmentTree) Iterables.getOnlyElement(methodSourceAnnotation.getArguments())) | ||
| .getExpression(); | ||
| Type attributeType = ASTHelpers.getType(attributeExpression); | ||
| return attributeType.getKind() == TypeKind.ARRAY | ||
| ? Optional.empty() | ||
| : Optional.of(attributeType.stringValue()); | ||
| } |
There was a problem hiding this comment.
I flagged and resolved several issues with this implementation in #188. IMHO we should port the full functionality here:
- Proper support for multiple factory method names.
- Defaulting to the name of the annotated method if no factory method name is explicitly specified.
| * to be found. | ||
| * @return Whether there are any methods with the given name in the enclosing class. | ||
| */ | ||
| public static boolean isMethodInEnclosingClass(String methodName, VisitorState state) { |
There was a problem hiding this comment.
I guess methodExitsInEnclosingClass would be a clearer name.
(Even so: I wonder whether this method sound exist as-is: in general we'd also want to match the arguments.)
There was a problem hiding this comment.
Updated the name. Ah yes that makes sense, but let's do that once we need it.
| * @param state A {@link VisitorState} describing the context in which the given {@link Tree} is | ||
| * to be found. |
Stephan202
left a comment
There was a problem hiding this comment.
Added another commit. Did not yet review MoreASTHelpersTest; as discussed offline we should likely have separate test BugCheckers for each exposed method.
| /** | ||
| * Matches methods that have a {@link org.junit.jupiter.params.provider.MethodSource} annotation. | ||
| */ | ||
| public static final Matcher<MethodTree> HAS_METHOD_SOURCE = |
There was a problem hiding this comment.
| public static final Matcher<MethodTree> HAS_METHOD_SOURCE = | |
| public static final MultiMatcher<MethodTree, AnnotationTree> HAS_METHOD_SOURCE = |
|
|
||
| return diagnosticsMessages.isEmpty() | ||
| ? Description.NO_MATCH | ||
| : buildDescription(tree).setMessage(String.join(", ", diagnosticsMessages)).build(); |
There was a problem hiding this comment.
Instead of String.join we should just do .toString(), so that with BUG: Diagnostic contains we can verify that the enumeration doesn't contain unexpected additional checks.
| /** | ||
| * Returns the names of the JUnit value factory methods specified by the given {@link | ||
| * org.junit.jupiter.params.provider.MethodSource} annotation. | ||
| * | ||
| * @param methodSourceAnnotation The annotation from which to extract value factory method names. | ||
| * @return One or more value factory names. | ||
| */ | ||
| static ImmutableSet<String> getMethodSourceFactoryNames( | ||
| AnnotationTree methodSourceAnnotation, MethodTree method) { | ||
| ExpressionTree value = AnnotationMatcherUtils.getArgument(methodSourceAnnotation, "value"); | ||
|
|
||
| if (!(value instanceof NewArrayTree)) { | ||
| return ImmutableSet.of(ASTHelpers.constValue(value, String.class)); | ||
| } | ||
|
|
||
| NewArrayTree arrayTree = (NewArrayTree) value; | ||
| return arrayTree.getInitializers().isEmpty() | ||
| ? ImmutableSet.of(method.getName().toString()) | ||
| : arrayTree.getInitializers().stream() | ||
| .map(name -> ASTHelpers.constValue(name, String.class)) | ||
| .collect(toImmutableSet()); | ||
| } |
There was a problem hiding this comment.
Turns out this logic still isn't correct. Will fix and add tests for all cases.
3494a88 to
36c710d
Compare
There was a problem hiding this comment.
Splitted the test cases in MoreASTHelpersTest. The changes LGTM @Stephan202 , nice improvements. Let's get this one in 😄.
Rebased and added a commit.
7f1dd5e to
0e7276b
Compare
Stephan202
left a comment
There was a problem hiding this comment.
Rebased and added three commits (over-all diff is small).
Suggested commit message LGTM :)
| * <p>This includes annotations inherited from superclasses due to {@link | ||
| * java.lang.annotation.Inherited}. |
There was a problem hiding this comment.
This claim should be tested.
Edit: added a test; it fails. The issue is that this method is misnamed: it's about annotations, not meta-annotations. The single use case we have does look for a meta-annotation, but that's only because this method is apply to a tree that itself represents an annotation. Ideally we directly use Matchers#hasAnnotation, but for some reason that doesn't work (didn't check why).
Edit 2: but arguably this Matcher should apply only to AnnotationTrees, in which case @Inherited does not apply. Reverted test, dropped the comment.
| * | ||
| * @param <T> The type of tree to match against. | ||
| * @param annotationType The binary type name of the annotation (e.g. | ||
| * "org.jspecify.nullness.Nullable", or "some.package.OuterClassName$InnerClassName") |
There was a problem hiding this comment.
The JSpecify annotations were recently moved (not yet in the version we're depending on here).
| * "org.jspecify.nullness.Nullable", or "some.package.OuterClassName$InnerClassName") | |
| * "org.jspecify.annotations.Nullable", or "some.package.OuterClassName$InnerClassName") |
|
|
||
| private static String createDiagnosticsMessage( | ||
| BiFunction<String, VisitorState, Object> function, VisitorState state) { | ||
| return ImmutableSet.of("foo", "bar", "baz").stream() | ||
| .map( | ||
| methodName -> | ||
| String.join(": ", methodName, String.valueOf(function.apply(methodName, state)))) | ||
| .collect(joining(", ")); | ||
| } |
There was a problem hiding this comment.
Although we generally "call down", in case of inner classes calling methods on the outer class, we place the inner classes at the bottom.
| .addSourceLines( | ||
| "A.java", | ||
| "class A {", | ||
| " // BUG: Diagnostic contains: foo: 1, bar: 2, baz: 0", |
There was a problem hiding this comment.
I guess we can make sure that if any new method name is added, that all tests are updated by adding start/end markers. This also makes createDiagnosticsMessage simpler; will push a proposal.
|
Last changes LGTM. Nice to see that adding the tests improved the quality and the validity of the code 👍🏻. |
More{ASTHelpers,JUnitMatchers,Matchers} utility classes
This PR breaks out common changes from #319, #214, and #188, to allow them to be merged in any order.