-
Notifications
You must be signed in to change notification settings - Fork 770
Writing a check
Once you know what you want to check for, you can write a BugChecker to do it
automatically. In this tutorial we will suppose that we want to ban the use of
return null; statements.
It is well known that misuse of null can cause a staggering variety of bugs.
You can find more details in this document. Let us
suppose that we agree with the arguments in favor of avoiding null, so as a
first step we decide to avoid any return null; statements. Put plainly, we
want to avoid code like this:
public Person findUser(String name) {
if (database.contains(name)) {
return new Person(name);
}
return null; // Code to avoid
}Suppose that some of our colleagues have strong arguments against this policy. A
lot of our code would break if we just decided to ban return null; statements!
After much discussion, we all agree that if a developer explicitly annotates a
method with @Nullable, the return null; statements will be allowed:
@Nullable
public Person findUser(String name) {
if (database.contains(name)) {
return new Person(name);
}
return null; // this is okay because of the @Nullable annotation
}We now have a policy that we can attempt to enforce: The use of return null;
is banned, except in methods annotated with @Nullable.
How do we enforce our policy? By writing a BugChecker! Here's some skeleton code that will get us started in writing our BugChecker.
package com.google.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.MaturityLevel.EXPERIMENTAL;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.MethodTree;
/**
* Bug checker to detect usage of {@code return null;}.
*/
@BugPattern(
name = "DoNotReturnNull",
summary = "Do not return null.",
category = JDK,
severity = SUGGESTION,
maturity = EXPERIMENTAL)
public class DoNotReturnNull extends BugChecker implements MethodTreeMatcher {
// your code here
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
// your code here
return Description.NO_MATCH;
}
}First, the annotation on the BugChecker contains the name, a short summary, the
category, severity of the bug, and the maturity of the bug. We have chosen to
fill these out for you; since our check is new, the maturity will be
EXPERIMENTAL and since we don't want to necessarily force everyone to use this
check, the severity has been listed as SUGGESTION.
Next, the BugChecker itself is an implementation of MethodTreeMatcher because
we want to match methods that return null. Lastly, the logic in
matchMethod should return a match if the method tree does not have a
@Nullable annotation and the method body contains a return null; statement.
However, nothing has been implemented! Let's fix that, making liberal use of
utility methods from the Matchers class.
Let's start with writing code for a matcher that will determine whether a method is annotated with @Nullable. We want to fill in the right hand side of the following statement:
...
import javax.annotation.Nullable;
...
private static final Matcher<Tree> HAS_NULLABLE_ANNOTATION = //your code goes hereWrite code for a matcher that will determine whether a method has a @Nullable annotation.
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
...
import javax.annotation.Nullable;
...
private static final Matcher<Tree> HAS_NULLABLE_ANNOTATION =
hasAnnotation(Nullable.class.getCanonicalName());Now that we can detect when a method has a @Nullable annotation, we want to
write a matcher that returns true when it encounters a method with a return null; statement and returns false otherwise. Below is the definition of the
matcher class that we need to implement.
private static final Matcher<Tree> CONTAINS_RETURN_NULL = //your code goes here
...
private static class ReturnNullMatcher implements Matcher<Tree> {
@Override
public boolean matches(Tree tree, VisitorState state) {
// your code goes here
}
}Again, the com.google.errorprone.matchers.Matchers class will have the methods
we need. Let's fill in the right hand side of the second statement, with the
assumption that ReturnNullMatcher will match return null; statements once
implemented:
...
private static final Matcher<Tree> RETURN_NULL = new ReturnNullMatcher();
private static final Matcher<Tree> CONTAINS_RETURN_NULL = //your code goes here
...import static com.google.errorprone.matchers.Matchers.contains;
...
private static final Matcher<Tree> RETURN_NULL = new ReturnNullMatcher();
private static final Matcher<Tree> CONTAINS_RETURN_NULL = contains(RETURN_NULL);
...Now, let's implement ReturnNullMatcher. Remember that you will need to
override the matches method.
Note: There is more than one way to complete this exercise, but in search of the simplest solution, you can take a look at
com.sun.source.tree.Tree.Kind.NULL_LITERAL, as that will allow you to directly match a null literal expression instead of having to match usingLiteralTree.
public class DoNotReturnNull extends BugChecker implements MethodTreeMatcher {
...
private static class ReturnNullMatcher implements Matcher<Tree> {
@Override
public boolean matches(Tree tree, VisitorState state) {
// your code goes here
}
}
}...
import static com.sun.source.tree.Tree.Kind.NULL_LITERAL;
...
import com.sun.source.tree.ReturnTree;
...
private static class ReturnNullMatcher implements Matcher<Tree> {
@Override
public boolean matches(Tree tree, VisitorState state) {
if (tree instanceof ReturnTree) {
return ((ReturnTree) tree).getExpression().getKind() == NULL_LITERAL;
}
return false;
}
}We're almost there! Now that we have both the ability to check if a method has a
@Nullable annotation as well as the ability to check whether a method contains
areturn null; statement, all that's left is to put the two together to create
a matcher that will match methods that satisfy both of our criteria. Our code
should now look something like this:
...
import static com.google.errorprone.matchers.Matchers.contains;
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
...
import static com.sun.source.tree.Tree.Kind.NULL_LITERAL;
...
import com.sun.source.tree.ReturnTree;
...
import javax.annotation.Nullable;
...
@BugPattern(...)
public class DoNotReturnNull extends BugChecker implements MethodTreeMatcher {
private static final Matcher<Tree> HAS_NULLABLE_ANNOTATION =
hasAnnotation(Nullable.class.getCanonicalName());
private static final Matcher<Tree> RETURN_NULL = new ReturnNullMatcher();
private static final Matcher<Tree> CONTAINS_RETURN_NULL = contains(RETURN_NULL);
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
...
}
/**
* Matches any Tree that represents return of a null.
*/
private static class ReturnNullMatcher implements Matcher<Tree> {
@Override
public boolean matches(Tree tree, VisitorState state) {
if (tree instanceof ReturnTree) {
return ((ReturnTree) tree).getExpression().getKind() == NULL_LITERAL;
}
return false;
}
}
}Let's now fill in matchMethod that will complete our BugChecker. If the method
does not have the @Nullable annotation and the method body contains return null;, we want to return describeMatch(tree), but in all other cases we want
to return Description.NO_MATCH.
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (HAS_NULLABLE_ANNOTATION.matches(tree, state)
|| !CONTAINS_RETURN_NULL.matches(tree.getBody(), state)) {
return Description.NO_MATCH;
}
return describeMatch(tree);
}Once you've completed your BugChecker, it's time to move on to testing it.
When writing tests for a BugChecker, you should check for both positive and
negative cases. Positive cases are those that will trigger the check. In our
example, any method that includes a return null; statement is a positive case.
Negative cases are those that will not trigger the check.
To check for positive cases, create a valid java file that contains violations
of the policy being checked and place the file in a testdata subdirectory. In
our case this means that the file should include at least one return null;
statement. As in the example that follows, add a comment before each AST node
that should be identified by a BugChecker as a match.
Note: Remember that the BugChecker will match a method that returns null. Thus, the diagnostic comment should immediately precede the method definition, not the return expression.
public class DoNotReturnNullPositiveCases {
// BUG: Diagnostic contains: Do not return null.
public Object returnsNull() {
return null;
}
}For negative cases, you just need to create a valid java file that does not
contain a violation of the policy being checked, and again place it in a
testdata subdirectory.
Now, you can use com.google.errorprone.CompilationTestHelper to test your
BugChecker by creating an ordinary JUnit suite and adding a test for each
positive and negative case you want to check.
Note: As per the example, the helper expects a comment in the following format:
// BUG: Diagnostic contains: <SUMMARY>, where<SUMMARY>is the value of thesummaryparameter of the BugChecker'sBugPatternannotation.
The whole JUnit test then looks like this:
package com.google.errorprone.bugpatterns;
import com.google.errorprone.CompilationTestHelper;
import com.google.testing.testsize.MediumTest;
import com.google.testing.testsize.MediumTestAttribute;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link DoNotReturnNull}. */
@RunWith(JUnit4.class)
@MediumTest(MediumTestAttribute.FILE)
public class DoNotReturnNullTest {
private CompilationTestHelper compilationHelper;
@Before
public void setup() {
compilationHelper = CompilationTestHelper.newInstance(DoNotReturnNull.class, getClass());
}
@Test
public void doNotReturnNullPositiveCases() {
compilationHelper.addSourceFile("DoNotReturnNullPositiveCases.java").doTest();
}
@Test
public void doNotReturnNullNegativeCases() {
compilationHelper.addSourceFile("DoNotReturnNullNegativeCases.java").doTest();
}
}Write tests in DoNotReturnNullPositiveCases.java and DoNotReturnNullNegativeCases.java.
package com.google.errorprone.bugpatterns.testdata;
public class DoNotReturnNullPositiveCases {
// BUG: Diagnostic contains: Do not return null.
public Object returnsNull() {
return null;
}
// BUG: Diagnostic contains: Do not return null.
public Object sometimesReturnsNull(boolean random) {
if (random) {
return null;
}
return new Object();
}
}package com.google.errorprone.bugpatterns.testdata;
import javax.annotation.Nullable;
public class DoNotReturnNullNegativeCases {
public Object doesNotReturnNull() {
return new Object();
}
@Nullable
public Object returnsNullButAnnotatedWithNullable() {
return null;
}
}