diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de6d7513894..2024d7deca7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,16 +12,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write - steps: - - name: Setup Signing Key - run: | - gpg-agent --daemon --default-cache-ttl 7200 - echo -e "${{ secrets.GPG_SIGNING_KEY }}" | gpg --batch --import --no-tty - echo "hello world" > temp.txt - gpg --detach-sig --yes -v --output=/dev/null --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" temp.txt - rm temp.txt - gpg --list-secret-keys --keyid-format LONG - + steps: - name: Checkout uses: actions/checkout@v2.4.0 @@ -34,6 +25,8 @@ jobs: server-id: ossrh server-username: CI_DEPLOY_USERNAME server-password: CI_DEPLOY_PASSWORD + gpg-private-key: ${{ secrets.GPG_SIGNING_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE - name: Bump Version Number run: | @@ -50,6 +43,7 @@ jobs: env: CI_DEPLOY_USERNAME: ${{ secrets.CI_DEPLOY_USERNAME }} CI_DEPLOY_PASSWORD: ${{ secrets.CI_DEPLOY_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: mvn --no-transfer-progress -P release clean deploy -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" diff --git a/annotation/pom.xml b/annotation/pom.xml index 59f69a89d89..0eab043dd62 100644 --- a/annotation/pom.xml +++ b/annotation/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 @BugPattern annotation diff --git a/annotations/pom.xml b/annotations/pom.xml index 77a45ae44ea..1bdffd2eca9 100644 --- a/annotations/pom.xml +++ b/annotations/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 error-prone annotations diff --git a/annotations/src/main/java/com/google/errorprone/annotations/CompileTimeConstant.java b/annotations/src/main/java/com/google/errorprone/annotations/CompileTimeConstant.java index 8fc04f31b51..aae9ea47837 100644 --- a/annotations/src/main/java/com/google/errorprone/annotations/CompileTimeConstant.java +++ b/annotations/src/main/java/com/google/errorprone/annotations/CompileTimeConstant.java @@ -47,7 +47,7 @@ * *
{@code
  * public class C {
- *   private static final S = "Hello";
+ *   private static final String S = "Hello";
  *   void m(@CompileTimeConstant final String s) { }
  *   void n(@CompileTimeConstant final String t) {
  *     m(S + " World!");
diff --git a/check_api/pom.xml b/check_api/pom.xml
index 4a023c78089..a20e3661e93 100644
--- a/check_api/pom.xml
+++ b/check_api/pom.xml
@@ -21,7 +21,7 @@
   
     com.google.errorprone
     error_prone_parent
-    HEAD-SNAPSHOT
+    2.15.0
   
 
   error-prone check api
diff --git a/check_api/src/main/java/com/google/errorprone/RefactoringCollection.java b/check_api/src/main/java/com/google/errorprone/RefactoringCollection.java
index 62d4a76208a..ced2a69e834 100644
--- a/check_api/src/main/java/com/google/errorprone/RefactoringCollection.java
+++ b/check_api/src/main/java/com/google/errorprone/RefactoringCollection.java
@@ -160,12 +160,11 @@ public DescriptionListener getDescriptionListener(Log log, JCCompilationUnit com
 
   RefactoringResult applyChanges(URI uri) throws Exception {
     Collection listeners = foundSources.removeAll(uri);
-    if (listeners.isEmpty()) {
-      return RefactoringResult.create("", RefactoringResultType.NO_CHANGES);
+    if (doApplyProcess(fileDestination, new FsFileSource(rootPath), listeners)) {
+      return postProcess.apply(uri);
     }
 
-    doApplyProcess(fileDestination, new FsFileSource(rootPath), listeners);
-    return postProcess.apply(uri);
+    return RefactoringResult.create("", RefactoringResultType.NO_CHANGES);
   }
 
   private static void writePatchFile(
@@ -185,15 +184,21 @@ private static void writePatchFile(
     }
   }
 
-  private static void doApplyProcess(
+  private static boolean doApplyProcess(
       FileDestination fileDestination,
       FileSource fileSource,
       Collection listeners) {
+    boolean appliedDiff = false;
     for (DelegatingDescriptionListener listener : listeners) {
+      if (listener.base.isEmpty()) {
+        continue;
+      }
+
       try {
         SourceFile file = fileSource.readFile(listener.base.getRelevantFileName());
         listener.base.applyDifferences(file);
         fileDestination.writeFile(file);
+        appliedDiff = true;
       } catch (IOException e) {
         logger.log(
             Level.WARNING,
@@ -201,6 +206,8 @@ private static void doApplyProcess(
             e);
       }
     }
+
+    return appliedDiff;
   }
 
   private static final class DelegatingDescriptionListener implements DescriptionListener {
diff --git a/check_api/src/main/java/com/google/errorprone/apply/DescriptionBasedDiff.java b/check_api/src/main/java/com/google/errorprone/apply/DescriptionBasedDiff.java
index 0e704ba6040..3df86f4abdd 100644
--- a/check_api/src/main/java/com/google/errorprone/apply/DescriptionBasedDiff.java
+++ b/check_api/src/main/java/com/google/errorprone/apply/DescriptionBasedDiff.java
@@ -123,9 +123,6 @@ public void applyDifferences(SourceFile sourceFile) throws DiffNotApplicableExce
             Replacements.CoalescePolicy.REPLACEMENT_FIRST);
       }
     }
-    for (Replacement replacement : replacements.descending()) {
-      sourceFile.replaceChars(
-          replacement.startPosition(), replacement.endPosition(), replacement.replaceWith());
-    }
+    sourceFile.makeReplacements(replacements);
   }
 }
diff --git a/check_api/src/main/java/com/google/errorprone/apply/ImportOrganizer.java b/check_api/src/main/java/com/google/errorprone/apply/ImportOrganizer.java
index 509398d16ac..f63e1436a7c 100644
--- a/check_api/src/main/java/com/google/errorprone/apply/ImportOrganizer.java
+++ b/check_api/src/main/java/com/google/errorprone/apply/ImportOrganizer.java
@@ -16,6 +16,7 @@
 package com.google.errorprone.apply;
 
 import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -182,6 +183,7 @@ public String asImportBlock() {
      * @param keys the keys to add, in order, if a key is not in the groups then it is ignored.
      * @return this for chaining.
      */
+    @CanIgnoreReturnValue
     public  OrganizedImports addGroups(
         Map> groups, Iterable keys) {
       for (K key : keys) {
diff --git a/check_api/src/main/java/com/google/errorprone/apply/SourceFile.java b/check_api/src/main/java/com/google/errorprone/apply/SourceFile.java
index 5f76a929c83..b5a6d69dd0e 100644
--- a/check_api/src/main/java/com/google/errorprone/apply/SourceFile.java
+++ b/check_api/src/main/java/com/google/errorprone/apply/SourceFile.java
@@ -16,9 +16,15 @@
 
 package com.google.errorprone.apply;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.io.CharSource;
+import com.google.errorprone.fixes.Replacement;
+import com.google.errorprone.fixes.Replacements;
 import java.io.IOException;
 import java.io.LineNumberReader;
 import java.io.StringReader;
@@ -158,4 +164,51 @@ public void replaceChars(int startPosition, int endPosition, String replacement)
               path, sourceBuilder.length(), startPosition, endPosition, replacement));
     }
   }
+
+  void makeReplacements(Replacements changes) {
+    ImmutableSet replacements = changes.ascending();
+    switch (replacements.size()) {
+      case 0:
+        return;
+      case 1:
+        {
+          Replacement onlyReplacement = Iterables.getOnlyElement(replacements);
+          replaceChars(
+              onlyReplacement.startPosition(),
+              onlyReplacement.endPosition(),
+              onlyReplacement.replaceWith());
+          return;
+        }
+      default:
+        break;
+    }
+
+    // Since we have many replacements to make all at once, it's better to start off with a clean
+    // slate, rather than make multiple separate replacements which each require shifting around
+    // the tail of our sourceBuilder. If we do them all at once, we can work forward from the
+    // beginning of the tile, so that each new replacement does not affect any previous
+    // replacements.
+    StringBuilder newContent = new StringBuilder();
+    int positionInOriginal = 0;
+    for (Replacement repl : replacements) {
+      checkArgument(
+          repl.endPosition() <= sourceBuilder.length(),
+          "End [%s] should not exceed source length [%s]",
+          repl.endPosition(),
+          sourceBuilder.length());
+
+      // Write the unmodified content leading up to this change
+      newContent.append(sourceBuilder, positionInOriginal, repl.startPosition());
+      // And the modified content for this change
+      newContent.append(repl.replaceWith());
+      // Then skip everything from source between start and end
+      positionInOriginal = repl.endPosition();
+    }
+    // Flush out any remaining content after the final change
+    newContent.append(sourceBuilder, positionInOriginal, sourceBuilder.length());
+    // Overwrite the contents of our old buffer. Note we mutate the existing StringBuilder rather
+    // than replacing it, because other clients may have a view of the content via getAsSequence,
+    // and we want that view to reflect the new content.
+    setSourceText(newContent);
+  }
 }
diff --git a/check_api/src/main/java/com/google/errorprone/dataflow/AccessPathStore.java b/check_api/src/main/java/com/google/errorprone/dataflow/AccessPathStore.java
index 8d9d14f4194..a17548bc8fa 100644
--- a/check_api/src/main/java/com/google/errorprone/dataflow/AccessPathStore.java
+++ b/check_api/src/main/java/com/google/errorprone/dataflow/AccessPathStore.java
@@ -21,6 +21,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import javax.annotation.Nullable;
@@ -114,6 +115,7 @@ public static final class Builder> {
       this.heap = new LinkedHashMap<>(prototype.heap());
     }
 
+    @CanIgnoreReturnValue
     public Builder setInformation(AccessPath aPath, V value) {
       heap.put(checkNotNull(aPath), checkNotNull(value));
       return this;
diff --git a/check_api/src/main/java/com/google/errorprone/dataflow/nullnesspropagation/NullnessPropagationTransfer.java b/check_api/src/main/java/com/google/errorprone/dataflow/nullnesspropagation/NullnessPropagationTransfer.java
index 46509a70f54..3f3f0c6c861 100644
--- a/check_api/src/main/java/com/google/errorprone/dataflow/nullnesspropagation/NullnessPropagationTransfer.java
+++ b/check_api/src/main/java/com/google/errorprone/dataflow/nullnesspropagation/NullnessPropagationTransfer.java
@@ -38,6 +38,7 @@
 import com.google.common.io.Files;
 import com.google.common.primitives.UnsignedInteger;
 import com.google.common.primitives.UnsignedLong;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.errorprone.dataflow.AccessPath;
 import com.google.errorprone.dataflow.AccessPathStore;
 import com.google.errorprone.dataflow.AccessPathValues;
@@ -327,6 +328,7 @@ protected NullnessPropagationTransfer(
    * Stores the given Javac context to find and analyze field initializers. Set before analyzing a
    * method and reset after.
    */
+  @CanIgnoreReturnValue
   NullnessPropagationTransfer setContext(@Nullable Context context) {
     // This is a best-effort check (similar to ArrayList iterators, for instance), no guarantee
     Preconditions.checkArgument(
@@ -345,6 +347,7 @@ NullnessPropagationTransfer setContext(@Nullable Context context) {
    * unit. Analyzing initializers from other compilation units tends to fail because type
    * information is sometimes missing on nodes returned from {@link Trees}.
    */
+  @CanIgnoreReturnValue
   NullnessPropagationTransfer setCompilationUnit(@Nullable CompilationUnitTree compilationUnit) {
     this.compilationUnit = compilationUnit;
     return this;
diff --git a/check_api/src/main/java/com/google/errorprone/fixes/BranchedSuggestedFixes.java b/check_api/src/main/java/com/google/errorprone/fixes/BranchedSuggestedFixes.java
index b6c26e454d5..850bb2d1de5 100644
--- a/check_api/src/main/java/com/google/errorprone/fixes/BranchedSuggestedFixes.java
+++ b/check_api/src/main/java/com/google/errorprone/fixes/BranchedSuggestedFixes.java
@@ -16,6 +16,7 @@
 package com.google.errorprone.fixes;
 
 import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 
 /**
  * Helper class for accumulating a branching tree of alternative fixes designed to help build as set
@@ -64,12 +65,14 @@ public static class Builder {
     private ImmutableList.Builder builder = ImmutableList.builder();
     private ImmutableList savedList = ImmutableList.of();
 
+    @CanIgnoreReturnValue
     public Builder startWith(SuggestedFix fix) {
       savedList = ImmutableList.of();
       builder = ImmutableList.builder().add(fix);
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder addOption(SuggestedFix fix) {
       if (!savedList.isEmpty()) {
         for (SuggestedFix s : savedList) {
@@ -79,6 +82,7 @@ public Builder addOption(SuggestedFix fix) {
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder then() {
       savedList = builder.build();
       builder = ImmutableList.builder();
diff --git a/check_api/src/main/java/com/google/errorprone/fixes/Replacements.java b/check_api/src/main/java/com/google/errorprone/fixes/Replacements.java
index b3ff921353f..615495c1bce 100644
--- a/check_api/src/main/java/com/google/errorprone/fixes/Replacements.java
+++ b/check_api/src/main/java/com/google/errorprone/fixes/Replacements.java
@@ -25,6 +25,7 @@
 import com.google.common.collect.Range;
 import com.google.common.collect.RangeMap;
 import com.google.common.collect.TreeRangeMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.util.Collection;
 import java.util.Comparator;
 import java.util.LinkedHashSet;
@@ -89,10 +90,12 @@ public String coalesce(String replacement, String existing) {
     public abstract String coalesce(String replacement, String existing);
   }
 
+  @CanIgnoreReturnValue
   public Replacements add(Replacement replacement) {
     return add(replacement, CoalescePolicy.REJECT);
   }
 
+  @CanIgnoreReturnValue
   public Replacements add(Replacement replacement, CoalescePolicy coalescePolicy) {
     if (replacements.containsKey(replacement.range())) {
       Replacement existing = replacements.get(replacement.range());
@@ -143,7 +146,12 @@ private void checkOverlaps(Replacement replacement) {
     }
   }
 
-  /** Non-overlapping replacements, sorted in descending order by position. */
+  /**
+   * Non-overlapping replacements, sorted in descending order by position. Prefer using {@link
+   * #ascending} when applying changes, because applying changes in reverse tends to result in
+   * quadratic-time copying of the underlying string.
+   */
+  @Deprecated
   public Set descending() {
     // TODO(cushon): refactor SuggestedFix#getReplacements and just return a Collection,
     return new LinkedHashSet<>(replacements.values());
@@ -151,8 +159,6 @@ public Set descending() {
 
   /** Non-overlapping replacements, sorted in ascending order by position. */
   public ImmutableSet ascending() {
-    // TODO(amalloy): Encourage using this instead of descending()
-    // Applying replacements in forward order is substantially more efficient, and only a bit harder
     return ImmutableSet.copyOf(replacements.descendingMap().values());
   }
 
diff --git a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java
index 9b280f364c5..9dec16437aa 100644
--- a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java
+++ b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java
@@ -20,6 +20,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.errorprone.util.ASTHelpers;
 import com.sun.source.tree.MethodTree;
 import com.sun.source.tree.Tree;
@@ -94,7 +95,7 @@ public Set getReplacements(EndPosTable endPositions) {
       replacements.add(
           fix.getReplacement(endPositions), Replacements.CoalescePolicy.EXISTING_FIRST);
     }
-    return replacements.descending();
+    return replacements.ascending();
   }
 
   /** {@link Builder#replace(Tree, String)} */
@@ -182,6 +183,7 @@ public SuggestedFix build() {
       return new SuggestedFix(this);
     }
 
+    @CanIgnoreReturnValue
     private Builder with(FixOperation fix) {
       fixes.add(fix);
       return this;
@@ -193,11 +195,13 @@ private Builder with(FixOperation fix) {
      *
      * 

Should be limited to one sentence. */ + @CanIgnoreReturnValue public Builder setShortDescription(String shortDescription) { this.shortDescription = shortDescription; return this; } + @CanIgnoreReturnValue public Builder replace(Tree node, String replaceWith) { checkNotSyntheticConstructor(node); return with(new ReplacementFix((DiagnosticPosition) node, replaceWith)); @@ -211,6 +215,7 @@ public Builder replace(Tree node, String replaceWith) { * @param endPos The position at which to end replacing, exclusive * @param replaceWith The string to replace with */ + @CanIgnoreReturnValue public Builder replace(int startPos, int endPos, String replaceWith) { DiagnosticPosition pos = new IndexedPosition(startPos, endPos); return with(new ReplacementFix(pos, replaceWith)); @@ -230,6 +235,7 @@ public Builder replace(int startPos, int endPos, String replaceWith) { * @param startPosAdjustment The adjustment to add to the start position (negative is OK) * @param endPosAdjustment The adjustment to add to the end position (negative is OK) */ + @CanIgnoreReturnValue public Builder replace( Tree node, String replaceWith, int startPosAdjustment, int endPosAdjustment) { checkNotSyntheticConstructor(node); @@ -239,21 +245,25 @@ public Builder replace( replaceWith)); } + @CanIgnoreReturnValue public Builder prefixWith(Tree node, String prefix) { checkNotSyntheticConstructor(node); return with(new PrefixInsertion((DiagnosticPosition) node, prefix)); } + @CanIgnoreReturnValue public Builder postfixWith(Tree node, String postfix) { checkNotSyntheticConstructor(node); return with(new PostfixInsertion((DiagnosticPosition) node, postfix)); } + @CanIgnoreReturnValue public Builder delete(Tree node) { checkNotSyntheticConstructor(node); return replace(node, ""); } + @CanIgnoreReturnValue public Builder swap(Tree node1, Tree node2) { checkNotSyntheticConstructor(node1); checkNotSyntheticConstructor(node2); @@ -268,6 +278,7 @@ public Builder swap(Tree node1, Tree node2) { * Add an import statement as part of this SuggestedFix. Import string should be of the form * "foo.bar.baz". */ + @CanIgnoreReturnValue public Builder addImport(String importString) { importsToAdd.add("import " + importString); return this; @@ -277,6 +288,7 @@ public Builder addImport(String importString) { * Add a static import statement as part of this SuggestedFix. Import string should be of the * form "foo.bar.baz". */ + @CanIgnoreReturnValue public Builder addStaticImport(String importString) { importsToAdd.add("import static " + importString); return this; @@ -286,6 +298,7 @@ public Builder addStaticImport(String importString) { * Remove an import statement as part of this SuggestedFix. Import string should be of the form * "foo.bar.baz". */ + @CanIgnoreReturnValue public Builder removeImport(String importString) { importsToRemove.add("import " + importString); return this; @@ -295,6 +308,7 @@ public Builder removeImport(String importString) { * Remove a static import statement as part of this SuggestedFix. Import string should be of the * form "foo.bar.baz". */ + @CanIgnoreReturnValue public Builder removeStaticImport(String importString) { importsToRemove.add("import static " + importString); return this; @@ -303,6 +317,7 @@ public Builder removeStaticImport(String importString) { /** * Merges all edits from {@code other} into {@code this}. If {@code other} is null, do nothing. */ + @CanIgnoreReturnValue public Builder merge(@Nullable Builder other) { if (other == null) { return this; @@ -319,6 +334,7 @@ public Builder merge(@Nullable Builder other) { /** * Merges all edits from {@code other} into {@code this}. If {@code other} is null, do nothing. */ + @CanIgnoreReturnValue public Builder merge(@Nullable SuggestedFix other) { if (other == null) { return this; diff --git a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java index d2cd507ce26..e5655402cda 100644 --- a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java +++ b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java @@ -567,7 +567,7 @@ public static void qualifyDocReference( * parentheses if no elements are left. */ public static SuggestedFix removeElement( - ExpressionTree tree, List trees, VisitorState state) { + Tree tree, List trees, VisitorState state) { int indexOf = trees.indexOf(tree); checkArgument(indexOf != -1, "trees must contain tree"); if (trees.size() == 1) { @@ -926,7 +926,6 @@ private static int getThrowsPosition(MethodTree tree, VisitorState state) { * * @see #addSuppressWarnings(VisitorState, String, String) */ - @Nullable public static SuggestedFix addSuppressWarnings(VisitorState state, String warningToSuppress) { return addSuppressWarnings(state, warningToSuppress, null); } @@ -942,14 +941,16 @@ public static SuggestedFix addSuppressWarnings(VisitorState state, String warnin *

In the event that a suppressible element couldn't be found (e.g.: the state is pointing at a * CompilationUnit, or some other internal inconsistency has occurred), or the enclosing * suppressible element already has a {@code @SuppressWarnings} annotation with {@code - * warningToSuppress}, this method will return null. + * warningToSuppress}, this method will throw an {@link IllegalArgumentException}. */ - @Nullable public static SuggestedFix addSuppressWarnings( VisitorState state, String warningToSuppress, @Nullable String lineComment) { SuggestedFix.Builder fixBuilder = SuggestedFix.builder(); addSuppressWarnings(fixBuilder, state, warningToSuppress, lineComment); - return fixBuilder.isEmpty() ? null : fixBuilder.build(); + if (fixBuilder.isEmpty()) { + throw new IllegalArgumentException("Couldn't find a node to attach @SuppressWarnings."); + } + return fixBuilder.build(); } /** diff --git a/check_api/src/main/java/com/google/errorprone/matchers/Description.java b/check_api/src/main/java/com/google/errorprone/matchers/Description.java index 8ca94524dd3..0df00249505 100644 --- a/check_api/src/main/java/com/google/errorprone/matchers/Description.java +++ b/check_api/src/main/java/com/google/errorprone/matchers/Description.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.BugPattern; import com.google.errorprone.BugPattern.SeverityLevel; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.fixes.Fix; import com.google.errorprone.fixes.SuggestedFix; @@ -174,6 +175,7 @@ private Builder( * @param fix a suggested fix for this problem * @throws NullPointerException if {@code fix} is {@code null} */ + @CanIgnoreReturnValue public Builder addFix(Fix fix) { checkNotNull(fix, "fix must not be null"); if (!fix.isEmpty()) { @@ -190,6 +192,7 @@ public Builder addFix(Fix fix) { * @throws NullPointerException if {@code fix} is {@code null} * @deprecated prefer referring to empty fixes using {@link SuggestedFix#emptyFix()}. */ + @CanIgnoreReturnValue @Deprecated public Builder addFix(Optional fix) { checkNotNull(fix, "fix must not be null"); @@ -203,6 +206,7 @@ public Builder addFix(Optional fix) { * @param fixes a list of suggested fixes for this problem * @throws NullPointerException if {@code fixes} or any of its elements are {@code null} */ + @CanIgnoreReturnValue public Builder addAllFixes(List fixes) { checkNotNull(fixes, "fixes must not be null"); for (Fix fix : fixes) { @@ -217,6 +221,7 @@ public Builder addAllFixes(List fixes) { * * @param message A custom error message without the check name ("[checkname]") or link */ + @CanIgnoreReturnValue public Builder setMessage(String message) { checkNotNull(message, "message must not be null"); this.rawMessage = message; @@ -227,6 +232,7 @@ public Builder setMessage(String message) { * Set a custom link URL. The custom URL will be used instead of the default one which forms * part of the {@code @}BugPattern. */ + @CanIgnoreReturnValue public Builder setLinkUrl(String linkUrl) { checkNotNull(linkUrl, "linkUrl must not be null"); this.linkUrl = linkUrl; diff --git a/check_api/src/main/java/com/google/errorprone/matchers/JUnitMatchers.java b/check_api/src/main/java/com/google/errorprone/matchers/JUnitMatchers.java index 80fc2a7b3e0..85b98bb1bac 100644 --- a/check_api/src/main/java/com/google/errorprone/matchers/JUnitMatchers.java +++ b/check_api/src/main/java/com/google/errorprone/matchers/JUnitMatchers.java @@ -37,8 +37,8 @@ import static com.google.errorprone.matchers.Matchers.nestingKind; import static com.google.errorprone.matchers.Matchers.not; import static com.google.errorprone.suppliers.Suppliers.VOID_TYPE; -import static com.google.errorprone.util.ASTHelpers.findSuperMethods; import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.streamSuperMethods; import static javax.lang.model.element.NestingKind.TOP_LEVEL; import com.google.common.collect.ImmutableList; @@ -86,8 +86,7 @@ public static boolean hasJUnitAnnotation(MethodTree tree, VisitorState state) { if (hasJUnitAttr(methodSym)) { return true; } - return findSuperMethods(methodSym, state.getTypes()).stream() - .anyMatch(JUnitMatchers::hasJUnitAttr); + return streamSuperMethods(methodSym, state.getTypes()).anyMatch(JUnitMatchers::hasJUnitAttr); } /** Checks if a method symbol has any attribute from the org.junit package. */ diff --git a/check_api/src/main/java/com/google/errorprone/matchers/TestNgMatchers.java b/check_api/src/main/java/com/google/errorprone/matchers/TestNgMatchers.java index e820e00e2b3..b5b0f9ca678 100644 --- a/check_api/src/main/java/com/google/errorprone/matchers/TestNgMatchers.java +++ b/check_api/src/main/java/com/google/errorprone/matchers/TestNgMatchers.java @@ -15,8 +15,8 @@ */ package com.google.errorprone.matchers; -import static com.google.errorprone.util.ASTHelpers.findSuperMethods; import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.streamSuperMethods; import com.google.errorprone.VisitorState; import com.sun.source.tree.ClassTree; @@ -44,8 +44,7 @@ public static boolean hasTestNgAnnotation(MethodTree tree, VisitorState state) { if (hasTestNgAttr(methodSym)) { return true; } - return findSuperMethods(methodSym, state.getTypes()).stream() - .anyMatch(TestNgMatchers::hasTestNgAttr); + return streamSuperMethods(methodSym, state.getTypes()).anyMatch(TestNgMatchers::hasTestNgAttr); } /** Checks if a class is annotated with any annotation from the org.testng package. */ diff --git a/check_api/src/main/java/com/google/errorprone/util/ASTHelpers.java b/check_api/src/main/java/com/google/errorprone/util/ASTHelpers.java index 7ff84c44ac9..cb14b34bc62 100644 --- a/check_api/src/main/java/com/google/errorprone/util/ASTHelpers.java +++ b/check_api/src/main/java/com/google/errorprone/util/ASTHelpers.java @@ -703,11 +703,20 @@ public static MethodSymbol findSuperMethodInType( return null; } + /** + * Finds supermethods of {@code methodSymbol}, not including {@code methodSymbol} itself, and + * including interfaces. + */ public static Set findSuperMethods(MethodSymbol methodSymbol, Types types) { return findSuperMethods(methodSymbol, types, /* skipInterfaces= */ false) .collect(toCollection(LinkedHashSet::new)); } + /** See {@link #findSuperMethods(MethodSymbol, Types)}. */ + public static Stream streamSuperMethods(MethodSymbol methodSymbol, Types types) { + return findSuperMethods(methodSymbol, types, /* skipInterfaces= */ false); + } + private static Stream findSuperMethods( MethodSymbol methodSymbol, Types types, boolean skipInterfaces) { TypeSymbol owner = (TypeSymbol) methodSymbol.owner; @@ -1752,6 +1761,7 @@ public Type visitAnnotation(AnnotationTree tree, Void unused) { return null; } + @Nullable @Override public Type visitCase(CaseTree tree, Void unused) { Tree t = parent.getParentPath().getLeaf(); @@ -1856,6 +1866,7 @@ public Type visitReturn(ReturnTree tree, Void unused) { throw new AssertionError("return not enclosed by method or lambda"); } + @Nullable @Override public Type visitSynchronized(SynchronizedTree node, Void unused) { // The null occurs if you've asked for the type of the parentheses around the expression. @@ -2146,7 +2157,6 @@ public static ImmutableSet getThrownExceptions(Tree tree, VisitorState sta /** Scanner for determining what types are thrown by a tree. */ public static final class ScanThrownTypes extends TreeScanner { - boolean inResources = false; ArrayDeque> thrownTypes = new ArrayDeque<>(); SetMultimap thrownTypesByVariable = HashMultimap.create(); @@ -2204,9 +2214,15 @@ public Void visitTry(TryTree tree, Void unused) { } public void scanResources(TryTree tree) { - inResources = true; + for (Tree resource : tree.getResources()) { + Symbol symbol = getType(resource).tsym; + + if (symbol instanceof ClassSymbol) { + getCloseMethod((ClassSymbol) symbol, state) + .ifPresent(methodSymbol -> getThrownTypes().addAll(methodSymbol.getThrownTypes())); + } + } scan(tree.getResources(), null); - inResources = false; } @Override @@ -2224,22 +2240,12 @@ public Void visitThrow(ThrowTree tree, Void unused) { @Override public Void visitNewClass(NewClassTree tree, Void unused) { - MethodSymbol symbol = getSymbol(tree); - if (symbol != null) { - getThrownTypes().addAll(symbol.getThrownTypes()); - } + getThrownTypes().addAll(getSymbol(tree).getThrownTypes()); return super.visitNewClass(tree, null); } @Override public Void visitVariable(VariableTree tree, Void unused) { - if (inResources) { - Symbol symbol = getSymbol(tree.getType()); - if (symbol instanceof ClassSymbol) { - getCloseMethod((ClassSymbol) symbol, state) - .ifPresent(methodSymbol -> getThrownTypes().addAll(methodSymbol.getThrownTypes())); - } - } return super.visitVariable(tree, null); } diff --git a/check_api/src/main/java/com/google/errorprone/util/Commented.java b/check_api/src/main/java/com/google/errorprone/util/Commented.java index 529f5a97c80..3b1282142cd 100644 --- a/check_api/src/main/java/com/google/errorprone/util/Commented.java +++ b/check_api/src/main/java/com/google/errorprone/util/Commented.java @@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.sun.source.tree.Tree; import com.sun.tools.javac.parser.Tokens.Comment; @@ -51,6 +52,7 @@ abstract static class Builder { protected abstract ImmutableList.Builder afterCommentsBuilder(); + @CanIgnoreReturnValue Builder addComment( Comment comment, int nodePosition, int tokenizingOffset, Position position) { OffsetComment offsetComment = new OffsetComment(comment, tokenizingOffset); @@ -67,6 +69,7 @@ Builder addComment( return this; } + @CanIgnoreReturnValue Builder addAllComment( Iterable comments, int nodePosition, diff --git a/check_api/src/main/java/com/google/errorprone/util/ErrorProneScope.java b/check_api/src/main/java/com/google/errorprone/util/ErrorProneScope.java index 6d2c845c6ae..0a9bc79197e 100644 --- a/check_api/src/main/java/com/google/errorprone/util/ErrorProneScope.java +++ b/check_api/src/main/java/com/google/errorprone/util/ErrorProneScope.java @@ -27,6 +27,7 @@ import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.function.Predicate; +import org.checkerframework.checker.nullness.qual.Nullable; /** A compatibility wrapper around {@code com.sun.tools.javac.util.Filter} */ public final class ErrorProneScope { @@ -59,7 +60,7 @@ public boolean anyMatch(Predicate predicate) { private static final Class FILTER_CLASS = getFilterClass(); - private static Class getFilterClass() { + private static @Nullable Class getFilterClass() { if (RuntimeVersion.isAtLeast17()) { return null; } diff --git a/check_api/src/main/java/com/google/errorprone/util/FindIdentifiers.java b/check_api/src/main/java/com/google/errorprone/util/FindIdentifiers.java index d644a5fbbb8..76dfca39cc5 100644 --- a/check_api/src/main/java/com/google/errorprone/util/FindIdentifiers.java +++ b/check_api/src/main/java/com/google/errorprone/util/FindIdentifiers.java @@ -132,7 +132,7 @@ private static ClassTree getEnclosingClass(TreePath treePath) { return (ClassTree) treePath.getLeaf(); } - while (treePath != null) { + while (true) { TreePath parent = treePath.getParentPath(); if (parent == null) { return null; @@ -144,7 +144,6 @@ private static ClassTree getEnclosingClass(TreePath treePath) { } treePath = parent; } - return null; } /** @@ -486,12 +485,6 @@ private static boolean inStaticContext(TreePath path) { break; case METHOD_INVOCATION: // JLS 8.8.7.1 explicit constructor invocation MethodSymbol methodSym = ASTHelpers.getSymbol((MethodInvocationTree) tree); - if (methodSym == null) { - // sometimes javac can't resolve the symbol. In this case just assume that we are - // in a static context - this is a safe approximation in our context (checking - // visibility) - return true; - } if (methodSym.isConstructor() && (Objects.equals(methodSym.owner, enclosingClass) || Objects.equals(methodSym.owner, directSuperClass))) { diff --git a/check_api/src/main/java/com/google/errorprone/util/Reachability.java b/check_api/src/main/java/com/google/errorprone/util/Reachability.java index 1d34f65f658..67d31bf7d9c 100644 --- a/check_api/src/main/java/com/google/errorprone/util/Reachability.java +++ b/check_api/src/main/java/com/google/errorprone/util/Reachability.java @@ -167,9 +167,6 @@ private static boolean isSystemExit(ExpressionTree expression) { return false; } MethodSymbol sym = getSymbol((MethodInvocationTree) expression); - if (sym == null) { - return false; - } return sym.owner.getQualifiedName().contentEquals("java.lang.System") && sym.getSimpleName().contentEquals("exit"); } diff --git a/check_api/src/test/java/com/google/errorprone/apply/ImportStatementsTest.java b/check_api/src/test/java/com/google/errorprone/apply/ImportStatementsTest.java index 559a874e3be..b1a28b9a528 100644 --- a/check_api/src/test/java/com/google/errorprone/apply/ImportStatementsTest.java +++ b/check_api/src/test/java/com/google/errorprone/apply/ImportStatementsTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.sun.source.tree.TreeVisitor; import com.sun.tools.javac.tree.EndPosTable; import com.sun.tools.javac.tree.JCTree; @@ -97,6 +98,7 @@ private static class StubImportBuilder { * @param typeName the fully-qualified name of the type being imported * @return a new JCImport stub */ + @CanIgnoreReturnValue StubImportBuilder addImport(String typeName) { return addImport(typeName, /* isStatic= */ false); } @@ -107,6 +109,7 @@ StubImportBuilder addImport(String typeName) { * @param typeName the fully-qualified name of the type being imported * @return a new JCImport stub */ + @CanIgnoreReturnValue StubImportBuilder addStaticImport(String typeName) { return addImport(typeName, /* isStatic= */ true); } @@ -118,6 +121,7 @@ StubImportBuilder addStaticImport(String typeName) { * @param isStatic whether the import is static * @return a new JCImport stub */ + @CanIgnoreReturnValue private StubImportBuilder addImport(String typeName, boolean isStatic) { // craft import string StringBuilder returnSB = new StringBuilder("import "); diff --git a/check_api/src/test/java/com/google/errorprone/fixes/ReplacementsTest.java b/check_api/src/test/java/com/google/errorprone/fixes/ReplacementsTest.java index 3117d66533d..8ef3a0901a6 100644 --- a/check_api/src/test/java/com/google/errorprone/fixes/ReplacementsTest.java +++ b/check_api/src/test/java/com/google/errorprone/fixes/ReplacementsTest.java @@ -69,24 +69,24 @@ public Range apply(Replacement replacement) { }; @Test - public void descending() { + public void ascending() { assertThat( Iterables.transform( new Replacements() .add(Replacement.create(0, 0, "hello")) .add(Replacement.create(0, 1, "hello")) - .descending(), + .ascending(), AS_RANGES)) - .containsExactly(Range.closedOpen(0, 1), Range.closedOpen(0, 0)) + .containsExactly(Range.closedOpen(0, 0), Range.closedOpen(0, 1)) .inOrder(); assertThat( Iterables.transform( new Replacements() .add(Replacement.create(0, 1, "hello")) .add(Replacement.create(0, 0, "hello")) - .descending(), + .ascending(), AS_RANGES)) - .containsExactly(Range.closedOpen(0, 1), Range.closedOpen(0, 0)) + .containsExactly(Range.closedOpen(0, 0), Range.closedOpen(0, 1)) .inOrder(); } diff --git a/core/pom.xml b/core/pom.xml index a968768423a..49a2d07114a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 error-prone library @@ -128,7 +128,7 @@ - com.google.gwt + org.gwtproject gwt-user ${gwt.version} test diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractJUnit4InitMethodNotRun.java b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractJUnit4InitMethodNotRun.java index cec111f45c8..7d32a5c7628 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractJUnit4InitMethodNotRun.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractJUnit4InitMethodNotRun.java @@ -37,6 +37,7 @@ import java.io.Serializable; import java.util.List; import javax.lang.model.element.Modifier; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Base class for JUnit4SetUp/TearDown not run. This will take care of the nitty-gritty about @@ -139,7 +140,7 @@ private static void makeProtectedPublic( } } - private Description tryToReplaceAnnotation( + private @Nullable Description tryToReplaceAnnotation( MethodTree methodTree, VisitorState state, String badAnnotation, String goodAnnotation) { String finalName = getUnqualifiedClassName(goodAnnotation); if (hasAnnotation(badAnnotation).matches(methodTree, state)) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractMustBeClosedChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractMustBeClosedChecker.java index 1c706eecc9f..78df7fc2be3 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractMustBeClosedChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractMustBeClosedChecker.java @@ -45,6 +45,7 @@ import com.google.errorprone.matchers.Matcher; import com.google.errorprone.matchers.UnusedReturnValueMatcher; import com.google.errorprone.util.ASTHelpers; +import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.BlockTree; import com.sun.source.tree.ConditionalExpressionTree; import com.sun.source.tree.ExpressionStatementTree; @@ -245,7 +246,7 @@ private Description emptyFix(Tree tree) { private static boolean variableInitializationCountsAsClosing(VarSymbol var) { // static final fields don't need to be closed, because they never leave scope - return var.isStatic() && var.getModifiers().contains(Modifier.FINAL); + return (var.isStatic() || var.owner.isEnum()) && var.getModifiers().contains(Modifier.FINAL); } // We allow calling @MBC methods anywhere inside of a static initializer. This is a compromise: @@ -258,8 +259,12 @@ private static boolean isInStaticInitializer(VisitorState state) { return Streams.stream(state.getPath()) .anyMatch( tree -> - tree instanceof VariableTree - && variableInitializationCountsAsClosing((VarSymbol) getSymbol(tree))); + (tree instanceof VariableTree + && variableInitializationCountsAsClosing((VarSymbol) getSymbol(tree))) + || (tree instanceof AssignmentTree + && getSymbol(((AssignmentTree) tree).getVariable()) instanceof VarSymbol + && variableInitializationCountsAsClosing( + (VarSymbol) getSymbol(((AssignmentTree) tree).getVariable())))); } /** diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractReturnValueIgnored.java b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractReturnValueIgnored.java index f8d31064911..0d391209e0d 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractReturnValueIgnored.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractReturnValueIgnored.java @@ -201,6 +201,13 @@ protected boolean allowInExceptionThrowers() { */ protected Description describeReturnValueIgnored( MethodInvocationTree methodInvocationTree, VisitorState state) { + return buildDescription(methodInvocationTree) + .addFix(makeFix(methodInvocationTree, state)) + .setMessage(getMessage(getSymbol(methodInvocationTree).getSimpleName())) + .build(); + } + + final Fix makeFix(MethodInvocationTree methodInvocationTree, VisitorState state) { // Find the root of the field access chain, i.e. a.intern().trim() ==> a. ExpressionTree identifierExpr = ASTHelpers.getRootAssignable(methodInvocationTree); Type identifierType = null; @@ -236,10 +243,7 @@ protected Description describeReturnValueIgnored( fix = SuggestedFix.delete(parent); } } - return buildDescription(methodInvocationTree) - .addFix(fix) - .setMessage(getMessage(getSymbol(methodInvocationTree).getSimpleName())) - .build(); + return fix; } /** diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AlwaysThrows.java b/core/src/main/java/com/google/errorprone/bugpatterns/AlwaysThrows.java index f1c30c268c6..28665ea5b38 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/AlwaysThrows.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/AlwaysThrows.java @@ -48,6 +48,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.UUID; import java.util.function.Consumer; +import org.checkerframework.checker.nullness.qual.Nullable; /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ @BugPattern(summary = "Detects calls that will fail at runtime", severity = ERROR) @@ -219,7 +220,7 @@ private Description checkImmutableMapOf( return checkForRepeatedKeys(tree, keys); } - private Object getConstantKey(ExpressionTree key, VisitorState state) { + private @Nullable Object getConstantKey(ExpressionTree key, VisitorState state) { return constantExpressions.constantExpression(key, state).orElse(null); } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/CannotMockFinalMethod.java b/core/src/main/java/com/google/errorprone/bugpatterns/CannotMockFinalMethod.java new file mode 100644 index 00000000000..68c1a74bb10 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CannotMockFinalMethod.java @@ -0,0 +1,64 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.errorprone.bugpatterns; + +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.matchers.Matchers.staticMethod; +import static com.google.errorprone.util.ASTHelpers.getReceiver; +import static com.google.errorprone.util.ASTHelpers.getSymbol; + +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.Symbol.MethodSymbol; + +/** A BugPattern; see the summary */ +@BugPattern( + summary = "Mockito cannot mock final methods, and can't detect this at runtime", + severity = WARNING) +public final class CannotMockFinalMethod extends BugChecker implements MethodInvocationTreeMatcher { + + private static final Matcher WHEN = + staticMethod().onClass("org.mockito.Mockito").named("when"); + + private static final Matcher VERIFY = + staticMethod().onClass("org.mockito.Mockito").named("verify"); + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + if (WHEN.matches(tree, state)) { + ExpressionTree firstArgument = tree.getArguments().get(0); + if (!(firstArgument instanceof MethodInvocationTree)) { + return NO_MATCH; + } + return describe(tree, getSymbol((MethodInvocationTree) firstArgument)); + } + var receiver = getReceiver(tree); + if (receiver != null && VERIFY.matches(receiver, state)) { + return describe(tree, getSymbol(tree)); + } + return NO_MATCH; + } + + private Description describe(MethodInvocationTree tree, MethodSymbol methodSymbol) { + return (methodSymbol.flags() & Flags.FINAL) == 0 ? NO_MATCH : describeMatch(tree); + } +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java index 1d12464638f..051f98534b1 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java @@ -17,17 +17,22 @@ package com.google.errorprone.bugpatterns; import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; +import static com.google.errorprone.bugpatterns.CheckReturnValue.MessageTrailerStyle.NONE; import static com.google.errorprone.bugpatterns.checkreturnvalue.AutoValueRules.autoBuilders; import static com.google.errorprone.bugpatterns.checkreturnvalue.AutoValueRules.autoValueBuilders; import static com.google.errorprone.bugpatterns.checkreturnvalue.AutoValueRules.autoValues; import static com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue.externalIgnoreList; +import static com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue.methodNameAndParams; +import static com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue.surroundingClass; import static com.google.errorprone.bugpatterns.checkreturnvalue.ProtoRules.mutableProtos; import static com.google.errorprone.bugpatterns.checkreturnvalue.ProtoRules.protoBuilders; import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy.EXPECTED; import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy.OPTIONAL; import static com.google.errorprone.bugpatterns.checkreturnvalue.Rules.globalDefault; import static com.google.errorprone.bugpatterns.checkreturnvalue.Rules.mapAnnotationSimpleName; +import static com.google.errorprone.fixes.SuggestedFix.emptyFix; import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.getType; import static com.google.errorprone.util.ASTHelpers.hasDirectAnnotationWithSimpleName; import com.google.common.collect.ImmutableMap; @@ -37,29 +42,35 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; +import com.google.errorprone.bugpatterns.checkreturnvalue.PackagesRule; import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy; import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicyEvaluator; +import com.google.errorprone.fixes.Fix; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.Matcher; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionStatementTree; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MemberReferenceTree; +import com.sun.source.tree.MemberReferenceTree.ReferenceMode; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.Tree; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.Type.MethodType; import java.util.Optional; import javax.lang.model.element.ElementKind; -import javax.lang.model.element.Name; /** * @author eaftan@google.com (Eddie Aftandilian) */ @BugPattern( altNames = {"ResultOfMethodCallIgnored", "ReturnValueIgnored"}, - summary = "Ignored return value of method that is annotated with @CheckReturnValue", + summary = "The result of this call must be used", severity = ERROR) public class CheckReturnValue extends AbstractReturnValueIgnored implements MethodTreeMatcher, ClassTreeMatcher { @@ -70,26 +81,45 @@ public class CheckReturnValue extends AbstractReturnValueIgnored static final String CHECK_ALL_CONSTRUCTORS = "CheckReturnValue:CheckAllConstructors"; static final String CHECK_ALL_METHODS = "CheckReturnValue:CheckAllMethods"; + static final String CRV_PACKAGES = "CheckReturnValue:Packages"; + + private final MessageTrailerStyle messageTrailerStyle; private final Optional constructorPolicy; private final Optional methodPolicy; private final ResultUsePolicyEvaluator evaluator; public CheckReturnValue(ErrorProneFlags flags) { super(flags); + this.messageTrailerStyle = + flags + .getEnum("CheckReturnValue:MessageTrailerStyle", MessageTrailerStyle.class) + .orElse(NONE); this.constructorPolicy = defaultPolicy(flags, CHECK_ALL_CONSTRUCTORS); this.methodPolicy = defaultPolicy(flags, CHECK_ALL_METHODS); - this.evaluator = - ResultUsePolicyEvaluator.create( - mapAnnotationSimpleName(CHECK_RETURN_VALUE, EXPECTED), - mapAnnotationSimpleName(CAN_IGNORE_RETURN_VALUE, OPTIONAL), - protoBuilders(), - mutableProtos(), - autoValues(), - autoValueBuilders(), - autoBuilders(), - externalIgnoreList(), - globalDefault(methodPolicy, constructorPolicy)); + ResultUsePolicyEvaluator.Builder builder = + ResultUsePolicyEvaluator.builder() + .addRules( + // The order of these rules matters somewhat because when checking a method, we'll + // evaluate them in the order they're listed here and stop as soon as one of them + // returns a result. The order shouldn't matter because most of these, with the + // exception of perhaps the external ignore list, are equivalent in importance and + // we should be checking declarations to ensure they aren't producing differing + // results (i.e. ensuring an @AutoValue.Builder setter method isn't annotated @CRV). + mapAnnotationSimpleName(CHECK_RETURN_VALUE, EXPECTED), + mapAnnotationSimpleName(CAN_IGNORE_RETURN_VALUE, OPTIONAL), + protoBuilders(), + mutableProtos(), + autoValues(), + autoValueBuilders(), + autoBuilders(), + + // This is conceptually lower precedence than the above rules. + externalIgnoreList()); + flags + .getList(CRV_PACKAGES) + .ifPresent(packagePatterns -> builder.addRule(PackagesRule.fromPatterns(packagePatterns))); + this.evaluator = builder.addRule(globalDefault(methodPolicy, constructorPolicy)).build(); } private static Optional defaultPolicy(ErrorProneFlags flags, String flag) { @@ -222,25 +252,115 @@ && hasDirectAnnotationWithSimpleName(ASTHelpers.getSymbol(tree), CAN_IGNORE_RETU return Description.NO_MATCH; } + private Description describeInvocationResultIgnored( + Tree tree, + String shortCall, + String shortCallWithoutNew, + MethodSymbol symbol, + Fix fix, + VisitorState state) { + String message = + String.format( + "The result of `%s` must be used\n" + + "If you really don't want to use the result, then assign it to a variable:" + + " `var unused = ...`.\n" + + "\n" + + "If callers of `%s` shouldn't be required to use its result," + + " then annotate it with `@CanIgnoreReturnValue`.\n" + + "%s", + shortCall, shortCallWithoutNew, apiTrailer(symbol, state)); + return buildDescription(tree).addFix(fix).setMessage(message).build(); + } + @Override - protected String getMessage(Name name) { - return String.format( - methodPolicy.orElse(OPTIONAL).equals(EXPECTED) - ? "Ignored return value of '%s', which wasn't annotated with @CanIgnoreReturnValue" - : "Ignored return value of '%s', which is annotated with @CheckReturnValue", - name); + protected Description describeReturnValueIgnored(MethodInvocationTree tree, VisitorState state) { + MethodSymbol symbol = getSymbol(tree); + String shortCall = symbol.name + (tree.getArguments().isEmpty() ? "()" : "(...)"); + String shortCallWithoutNew = shortCall; + return describeInvocationResultIgnored( + tree, shortCall, shortCallWithoutNew, symbol, makeFix(tree, state), state); } @Override - protected Description describeReturnValueIgnored(NewClassTree newClassTree, VisitorState state) { - return constructorPolicy.orElse(OPTIONAL).equals(EXPECTED) - ? buildDescription(newClassTree) - .setMessage( - String.format( - "Ignored return value of '%s', which wasn't annotated with" - + " @CanIgnoreReturnValue", - state.getSourceForNode(newClassTree.getIdentifier()))) - .build() - : super.describeReturnValueIgnored(newClassTree, state); + protected Description describeReturnValueIgnored(NewClassTree tree, VisitorState state) { + MethodSymbol symbol = getSymbol(tree); + String shortCallWithoutNew = + state.getSourceForNode(tree.getIdentifier()) + + (tree.getArguments().isEmpty() ? "()" : "(...)"); + String shortCall = "new " + shortCallWithoutNew; + return describeInvocationResultIgnored( + tree, shortCall, shortCallWithoutNew, symbol, emptyFix(), state); + } + + @Override + protected Description describeReturnValueIgnored(MemberReferenceTree tree, VisitorState state) { + MethodSymbol symbol = getSymbol(tree); + Type type = state.getTypes().memberType(getType(tree.getQualifierExpression()), symbol); + // TODO(cgdecker): There are probably other types than MethodType that we could resolve here + String parensAndMaybeEllipsis = + type instanceof MethodType && ((MethodType) type).getParameterTypes().isEmpty() + ? "()" + : "(...)"; + + String shortCallWithoutNew; + String shortCall; + if (tree.getMode() == ReferenceMode.NEW) { + shortCallWithoutNew = + state.getSourceForNode(tree.getQualifierExpression()) + parensAndMaybeEllipsis; + shortCall = "new " + shortCallWithoutNew; + } else { + shortCallWithoutNew = tree.getName() + parensAndMaybeEllipsis; + shortCall = shortCallWithoutNew; + } + + String implementedMethod = + getType(tree).asElement().getSimpleName() + + "." + + state.getTypes().findDescriptorSymbol(getType(tree).asElement()).getSimpleName(); + String methodReference = state.getSourceForNode(tree); + String message = + String.format( + "The result of `%s` must be used\n" + + "`%s` acts as an implementation of `%s`.\n" + + "— which is a `void` method, so it doesn't use the result of `%s`.\n" + + "\n" + + "To use the result, you may need to restructure your code.\n" + + "\n" + + "If you really don't want to use the result, then switch to a lambda that assigns" + + " it to a variable: `%s -> { var unused = ...; }`.\n" + + "\n" + + "If callers of `%s` shouldn't be required to use its result," + + " then annotate it with `@CanIgnoreReturnValue`.\n" + + "%s", + shortCall, + methodReference, + implementedMethod, + shortCall, + parensAndMaybeEllipsis, + shortCallWithoutNew, + apiTrailer(symbol, state)); + return buildDescription(tree).setMessage(message).build(); + } + + private String apiTrailer(MethodSymbol symbol, VisitorState state) { + if (symbol.enclClass().isAnonymous()) { + // I don't think we have a defined format for members of anonymous classes. + return ""; + } + switch (messageTrailerStyle) { + case NONE: + return ""; + case API_ERASED_SIGNATURE: + return "\n\nFull API: " + + surroundingClass(symbol) + + "#" + + methodNameAndParams(symbol, state.getTypes()); + } + throw new AssertionError(); + } + + enum MessageTrailerStyle { + NONE, + API_ERASED_SIGNATURE, } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ComplexBooleanConstant.java b/core/src/main/java/com/google/errorprone/bugpatterns/ComplexBooleanConstant.java index 1863163862a..25110d00998 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ComplexBooleanConstant.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ComplexBooleanConstant.java @@ -29,6 +29,7 @@ import com.sun.source.util.SimpleTreeVisitor; import com.sun.tools.javac.tree.JCTree.JCLiteral; import java.util.Objects; +import org.checkerframework.checker.nullness.qual.Nullable; /** * @author Sumit Bhagwani (bhagwani@google.com) @@ -53,7 +54,7 @@ public Description matchBinary(BinaryTree tree, VisitorState state) { .build(); } - Boolean booleanValue(BinaryTree tree) { + @Nullable Boolean booleanValue(BinaryTree tree) { if (tree.getLeftOperand() instanceof JCLiteral && tree.getRightOperand() instanceof JCLiteral) { return ASTHelpers.constValue(tree, Boolean.class); } @@ -62,7 +63,7 @@ Boolean booleanValue(BinaryTree tree) { SimpleTreeVisitor boolValue = new SimpleTreeVisitor() { @Override - public Boolean visitLiteral(LiteralTree node, Void unused) { + public @Nullable Boolean visitLiteral(LiteralTree node, Void unused) { if (node.getValue() instanceof Boolean) { return (Boolean) node.getValue(); } @@ -70,7 +71,7 @@ public Boolean visitLiteral(LiteralTree node, Void unused) { } @Override - public Boolean visitUnary(UnaryTree node, Void unused) { + public @Nullable Boolean visitUnary(UnaryTree node, Void unused) { Boolean r = node.getExpression().accept(this, null); if (r == null) { return null; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ConstantOverflow.java b/core/src/main/java/com/google/errorprone/bugpatterns/ConstantOverflow.java index 64a714d3975..62aaef2d276 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ConstantOverflow.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ConstantOverflow.java @@ -110,6 +110,7 @@ private static Fix longFix(ExpressionTree expr, VisitorState state) { private static final SimpleTreeVisitor CONSTANT_VISITOR = new SimpleTreeVisitor() { + @Nullable @Override public Number visitConditionalExpression(ConditionalExpressionTree node, Void p) { Number ifTrue = node.getTrueExpression().accept(this, null); @@ -126,6 +127,7 @@ public Number visitParenthesized(ParenthesizedTree node, Void p) { return node.getExpression().accept(this, null); } + @Nullable @Override public Number visitUnary(UnaryTree node, Void p) { Number value = node.getExpression().accept(this, null); @@ -139,6 +141,7 @@ public Number visitUnary(UnaryTree node, Void p) { } } + @Nullable @Override public Number visitBinary(BinaryTree node, Void p) { Number lhs = node.getLeftOperand().accept(this, null); @@ -170,6 +173,7 @@ public Number visitBinary(BinaryTree node, Void p) { } } + @Nullable @Override public Number visitTypeCast(TypeCastTree node, Void p) { Number value = node.getExpression().accept(this, null); @@ -199,6 +203,7 @@ public Number visitLiteral(LiteralTree node, Void unused) { } }; + @Nullable private static Long unop(Kind kind, long value) { switch (kind) { case UNARY_PLUS: @@ -212,6 +217,7 @@ private static Long unop(Kind kind, long value) { } } + @Nullable private static Integer unop(Kind kind, int value) { switch (kind) { case UNARY_PLUS: @@ -225,6 +231,7 @@ private static Integer unop(Kind kind, int value) { } } + @Nullable static Long binop(Kind kind, long lhs, long rhs) { switch (kind) { case MULTIPLY: @@ -254,6 +261,7 @@ static Long binop(Kind kind, long lhs, long rhs) { } } + @Nullable static Integer binop(Kind kind, int lhs, int rhs) { switch (kind) { case MULTIPLY: @@ -283,6 +291,7 @@ static Integer binop(Kind kind, int lhs, int rhs) { } } + @Nullable private static Number cast(TypeKind kind, Number value) { switch (kind) { case SHORT: @@ -300,6 +309,7 @@ private static Number cast(TypeKind kind, Number value) { } } + @Nullable private static Number getIntegralConstant(Tree node) { Number number = ASTHelpers.constValue(node, Number.class); if (number instanceof Integer || number instanceof Long) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ConstantPatternCompile.java b/core/src/main/java/com/google/errorprone/bugpatterns/ConstantPatternCompile.java index 4b3bed540cb..9ad76070276 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ConstantPatternCompile.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ConstantPatternCompile.java @@ -53,6 +53,7 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.Modifier; import javax.lang.model.element.NestingKind; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Flags variables initialized with {@link java.util.regex.Pattern#compile(String)} calls that could @@ -206,7 +207,7 @@ public Void visitMemberSelect(MemberSelectTree tree, Void unused) { } /** Infer a name when upgrading the {@code Pattern} local to a constant. */ - private static String inferName(VariableTree tree, VisitorState state) { + private static @Nullable String inferName(VariableTree tree, VisitorState state) { String name; if ((name = fromName(tree)) != null) { return name; @@ -221,7 +222,7 @@ private static String inferName(VariableTree tree, VisitorState state) { } /** Use the existing local variable's name, unless it's terrible. */ - private static String fromName(VariableTree tree) { + private static @Nullable String fromName(VariableTree tree) { String name = LOWER_CAMEL.to(UPPER_UNDERSCORE, tree.getName().toString()); if (name.length() > 1 && !name.equals("PATTERN")) { return name; @@ -235,7 +236,7 @@ private static String fromName(VariableTree tree) { *

e.g. use {@code FOO_PATTERN} for {@code Pattern.compile(FOO)} and {@code * Pattern.compile(FOO_REGEX)}. */ - private static String fromInitializer(VariableTree tree) { + private static @Nullable String fromInitializer(VariableTree tree) { ExpressionTree regex = ((MethodInvocationTree) tree.getInitializer()).getArguments().get(0); if (!(regex instanceof IdentifierTree)) { return null; @@ -256,7 +257,7 @@ private static String fromInitializer(VariableTree tree) { * If the pattern is only used once in a call to {@code matcher}, and the argument is a local, use * that local's name. For example, infer {@code FOO_PATTERN} from {@code pattern.matcher(foo)}. */ - private static String fromUse(VariableTree tree, VisitorState state) { + private static @Nullable String fromUse(VariableTree tree, VisitorState state) { VarSymbol sym = getSymbol(tree); ImmutableList.Builder usesBuilder = ImmutableList.builder(); new TreePathScanner() { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java b/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java new file mode 100644 index 00000000000..b8c51c41206 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java @@ -0,0 +1,184 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import static com.google.common.collect.Streams.stream; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.fixes.SuggestedFixes.qualifyStaticImport; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.matchers.Matchers.allOf; +import static com.google.errorprone.matchers.Matchers.anyMethod; +import static com.google.errorprone.matchers.Matchers.instanceMethod; +import static com.google.errorprone.matchers.Matchers.receiverOfInvocation; +import static com.google.errorprone.matchers.Matchers.staticMethod; +import static com.google.errorprone.util.ASTHelpers.getReceiver; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.MoreAnnotations.getAnnotationValue; +import static java.lang.String.format; + +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ExpressionStatementTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreeScanner; +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import java.util.HashSet; +import java.util.Set; + +/** A bugpattern; see the description. */ +@BugPattern( + summary = + "Methods should not be directly invoked on mocks. Should this be part of a verify(..)" + + " call?", + severity = WARNING) +public final class DirectInvocationOnMock extends BugChecker implements CompilationUnitTreeMatcher { + @Override + public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { + ImmutableSet mocks = findMocks(state); + Set methodsCallingRealImplementations = new HashSet<>(); + + new SuppressibleTreePathScanner(state) { + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) { + if (THEN_CALL_REAL_METHOD.matches(tree, state)) { + var receiver = getReceiver(tree); + if (receiver != null && WHEN.matches(receiver, state)) { + ExpressionTree firstArgument = ((MethodInvocationTree) receiver).getArguments().get(0); + var firstArgumentSymbol = getSymbol(firstArgument); + if (firstArgumentSymbol instanceof MethodSymbol) { + methodsCallingRealImplementations.add((MethodSymbol) firstArgumentSymbol); + } + } + return super.visitMethodInvocation(tree, null); + } + if (DO_CALL_REAL_METHOD.matches(tree, state)) { + var methodSymbol = getSymbol(getCurrentPath().getParentPath().getParentPath().getLeaf()); + if (methodSymbol instanceof MethodSymbol) { + methodsCallingRealImplementations.add((MethodSymbol) methodSymbol); + } + return super.visitMethodInvocation(tree, null); + } + if (methodsCallingRealImplementations.contains(getSymbol(tree))) { + return super.visitMethodInvocation(tree, null); + } + if ((getSymbol(tree).flags() & Flags.FINAL) != 0) { + return null; + } + Tree parent = + stream(getCurrentPath()) + .skip(1) + .filter(t -> !(t instanceof TypeCastTree)) + .findFirst() + .get(); + var receiver = getReceiver(tree); + if (isMock(receiver) + && !(parent instanceof ExpressionTree + && WHEN.matches((ExpressionTree) parent, state))) { + var description = + buildDescription(tree) + .setMessage( + format( + "Methods should not be directly invoked on the mock `%s`. Should this be" + + " part of a verify(..) call?", + getSymbol(receiver).getSimpleName())); + if (getCurrentPath().getParentPath().getLeaf() instanceof ExpressionStatementTree) { + var fix = SuggestedFix.builder(); + String verify = qualifyStaticImport("org.mockito.Mockito.verify", fix, state); + description.addFix( + fix.replace(receiver, format("%s(%s)", verify, state.getSourceForNode(receiver))) + .setShortDescription("turn into verify() call") + .build()); + description.addFix( + SuggestedFix.builder() + .delete(tree) + .setShortDescription("delete redundant invocation") + .build()); + } + state.reportMatch(description.build()); + } + return super.visitMethodInvocation(tree, null); + } + + private boolean isMock(ExpressionTree tree) { + var symbol = getSymbol(tree); + return symbol != null + && (mocks.contains(symbol) + || symbol.getAnnotationMirrors().stream() + .filter(am -> am.type.tsym.getQualifiedName().contentEquals("org.mockito.Mock")) + .findFirst() + .filter(am -> getAnnotationValue(am, "answer").isEmpty()) + .isPresent()); + } + }.scan(state.getPath(), null); + + return NO_MATCH; + } + + private ImmutableSet findMocks(VisitorState state) { + ImmutableSet.Builder mocks = ImmutableSet.builder(); + new TreeScanner() { + @Override + public Void visitVariable(VariableTree tree, Void unused) { + if (tree.getInitializer() != null && MOCK.matches(tree.getInitializer(), state)) { + mocks.add(getSymbol(tree)); + } + return super.visitVariable(tree, null); + } + + @Override + public Void visitAssignment(AssignmentTree tree, Void unused) { + if (MOCK.matches(tree.getExpression(), state)) { + var symbol = getSymbol(tree.getVariable()); + if (symbol instanceof VarSymbol) { + mocks.add((VarSymbol) symbol); + } + } + return super.visitAssignment(tree, null); + } + }.scan(state.getPath().getCompilationUnit(), null); + return mocks.build(); + } + + private static final Matcher MOCK = + staticMethod().onClass("org.mockito.Mockito").named("mock").withParameters("java.lang.Class"); + + private static final Matcher DO_CALL_REAL_METHOD = + allOf( + instanceMethod().onDescendantOf("org.mockito.stubbing.Stubber").named("when"), + receiverOfInvocation( + staticMethod().onClass("org.mockito.Mockito").named("doCallRealMethod"))); + + private static final Matcher WHEN = anyMethod().anyClass().named("when"); + + private static final Matcher THEN_CALL_REAL_METHOD = + instanceMethod() + .onDescendantOf("org.mockito.stubbing.OngoingStubbing") + .named("thenCallRealMethod"); +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DistinctVarargsChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/DistinctVarargsChecker.java index 13826a20823..16019469a4c 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/DistinctVarargsChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DistinctVarargsChecker.java @@ -20,17 +20,19 @@ import static com.google.errorprone.matchers.Matchers.anyOf; import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod; +import com.google.common.collect.ImmutableListMultimap; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.Matcher; -import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Map; /** * ErrorProne checker to generate warning when method expecting distinct varargs is invoked with @@ -72,14 +74,14 @@ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState return checkDistinctArgumentsWithFix(tree, state); } if (ALL_DISTINCT_ARG_MATCHER.matches(tree, state)) { - return checkDistinctArguments(tree, tree.getArguments()); + return checkDistinctArguments(state, tree.getArguments()); } if (EVEN_PARITY_DISTINCT_ARG_MATCHER.matches(tree, state)) { List arguments = new ArrayList<>(); for (int index = 0; index < tree.getArguments().size(); index += 2) { arguments.add(tree.getArguments().get(index)); } - return checkDistinctArguments(tree, arguments); + return checkDistinctArguments(state, arguments); } if (EVEN_AND_ODD_PARITY_DISTINCT_ARG_MATCHER.matches(tree, state)) { List evenParityArguments = new ArrayList<>(); @@ -91,29 +93,34 @@ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState oddParityArguments.add(tree.getArguments().get(index)); } } - return checkDistinctArguments(tree, evenParityArguments, oddParityArguments); + return checkDistinctArguments(state, evenParityArguments, oddParityArguments); } return Description.NO_MATCH; } + private static ImmutableListMultimap argumentsByString( + VisitorState state, List arguments) { + ImmutableListMultimap.Builder result = ImmutableListMultimap.builder(); + for (int i = 0; i < arguments.size(); i++) { + result.put(state.getSourceForNode(arguments.get(i)), i); + } + return result.build(); + } + private Description checkDistinctArgumentsWithFix(MethodInvocationTree tree, VisitorState state) { SuggestedFix.Builder suggestedFix = SuggestedFix.builder(); - for (int index = 1; index < tree.getArguments().size(); index++) { - boolean isDistinctArgument = true; - for (int prevElementIndex = 0; prevElementIndex < index; prevElementIndex++) { - if (ASTHelpers.sameVariable( - tree.getArguments().get(index), tree.getArguments().get(prevElementIndex))) { - isDistinctArgument = false; - break; - } - } - if (!isDistinctArgument) { - suggestedFix.merge( - SuggestedFix.replace( - state.getEndPosition(tree.getArguments().get(index - 1)), - state.getEndPosition(tree.getArguments().get(index)), - "")); - } + List arguments = tree.getArguments(); + ImmutableListMultimap argumentsByString = argumentsByString(state, arguments); + for (Map.Entry> entry : argumentsByString.asMap().entrySet()) { + entry.getValue().stream() + .skip(1) + .forEachOrdered( + index -> + suggestedFix.merge( + SuggestedFix.replace( + state.getEndPosition(arguments.get(index - 1)), + state.getEndPosition(arguments.get(index)), + ""))); } if (suggestedFix.isEmpty()) { return Description.NO_MATCH; @@ -122,19 +129,14 @@ private Description checkDistinctArgumentsWithFix(MethodInvocationTree tree, Vis } private Description checkDistinctArguments( - MethodInvocationTree tree, List... argumentsList) { + VisitorState state, List... argumentsList) { for (List arguments : argumentsList) { - for (int firstArgumentIndex = 0; - firstArgumentIndex < arguments.size(); - firstArgumentIndex++) { - for (int secondArgumentIndex = firstArgumentIndex + 1; - secondArgumentIndex < arguments.size(); - secondArgumentIndex++) { - if (ASTHelpers.sameVariable( - arguments.get(firstArgumentIndex), arguments.get(secondArgumentIndex))) { - return describeMatch(tree); - } - } + ImmutableListMultimap argumentsByString = + argumentsByString(state, arguments); + for (Map.Entry> entry : argumentsByString.asMap().entrySet()) { + entry.getValue().stream() + .skip(1) + .forEachOrdered(index -> state.reportMatch(describeMatch(arguments.get(index)))); } } return Description.NO_MATCH; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java index a92c06de47b..cbacb2e14dc 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java @@ -19,6 +19,8 @@ import static com.google.common.collect.Streams.stream; import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.matchers.Matchers.allOf; +import static com.google.errorprone.matchers.Matchers.not; import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod; import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod; import static com.google.errorprone.util.ASTHelpers.findSuperMethods; @@ -28,11 +30,15 @@ import static com.google.errorprone.util.ASTHelpers.hasAnnotation; import static com.google.errorprone.util.ASTHelpers.isConsideredFinal; import static com.google.errorprone.util.ASTHelpers.isSameType; +import static com.sun.source.tree.Tree.Kind.IDENTIFIER; +import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT; +import static com.sun.source.tree.Tree.Kind.METHOD_INVOCATION; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.errorprone.BugPattern; +import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; @@ -45,7 +51,9 @@ import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.MemberReferenceTree; +import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.VariableTree; @@ -66,6 +74,39 @@ @BugPattern(name = "DoNotCall", summary = "This method should not be called.", severity = ERROR) public class DoNotCallChecker extends BugChecker implements MethodTreeMatcher, CompilationUnitTreeMatcher { + private final boolean checkNewGetClassMethods; + private final boolean checkThreadRun; + + public DoNotCallChecker(ErrorProneFlags flags) { + checkNewGetClassMethods = + flags.getBoolean("DoNotCallChecker:CheckNewGetClassMethods").orElse(true); + checkThreadRun = flags.getBoolean("DoNotCallChecker:CheckThreadRun").orElse(true); + } + + private static final Matcher STACK_TRACE_ELEMENT_GET_CLASS = + instanceMethod().onExactClass("java.lang.StackTraceElement").named("getClass"); + + private static final Matcher ANY_GET_CLASS = + instanceMethod().anyClass().named("getClass"); + + private static final Matcher THREAD_RUN = + instanceMethod().onDescendantOf("java.lang.Thread").named("run").withNoParameters(); + + private static final Matcher CALL_ON_SUPER = + (invocation, state) -> { + if (invocation.getKind() != METHOD_INVOCATION) { + return false; + } + ExpressionTree select = ((MethodInvocationTree) invocation).getMethodSelect(); + if (select.getKind() != MEMBER_SELECT) { + return false; + } + ExpressionTree receiver = ((MemberSelectTree) select).getExpression(); + if (receiver.getKind() != IDENTIFIER) { + return false; + } + return ((IdentifierTree) receiver).getName().contentEquals("super"); + }; // If your method cannot be annotated with @DoNotCall (e.g., it's a JDK or thirdparty method), // then add it to this Map with an explanation. @@ -133,6 +174,90 @@ public class DoNotCallChecker extends BugChecker "Calling getClass on StackTraceElement returns the Class object for" + " StackTraceElement, you probably meant to retrieve the class containing the" + " execution point represented by this stack trace element using getClassName") + .put( + instanceMethod().onExactClass("java.lang.StackWalker").named("getClass"), + "Calling getClass on StackWalker returns the Class object for StackWalker, you" + + " probably meant to retrieve the class containing the execution point" + + " represented by this StackWalker using getCallerClass") + .put( + instanceMethod().onExactClass("java.lang.StackWalker$StackFrame").named("getClass"), + "Calling getClass on StackFrame returns the Class object for StackFrame, you probably" + + " meant to retrieve the class containing the execution point represented by" + + " this StackFrame using getClassName") + .put( + instanceMethod().onExactClass("java.lang.reflect.Constructor").named("getClass"), + "Calling getClass on Constructor returns the Class object for Constructor, you" + + " probably meant to retrieve the class containing the constructor represented" + + " by this Constructor using getDeclaringClass") + .put( + instanceMethod().onExactClass("java.lang.reflect.Field").named("getClass"), + "Calling getClass on Field returns the Class object for Field, you probably meant to" + + " retrieve the class containing the field represented by this Field using" + + " getDeclaringClass") + .put( + instanceMethod().onExactClass("java.lang.reflect.Method").named("getClass"), + "Calling getClass on Method returns the Class object for Method, you probably meant" + + " to retrieve the class containing the method represented by this Method using" + + " getDeclaringClass") + .put( + instanceMethod() + .onExactClass("java.lang.reflect.ParameterizedType") + .named("getClass"), + "Calling getClass on ParameterizedType returns the Class object for" + + " ParameterizedType, you probably meant to retrieve the class containing the" + + " method represented by this ParameterizedType using getRawType") + .put( + instanceMethod().onExactClass("java.beans.BeanDescriptor").named("getClass"), + "Calling getClass on BeanDescriptor returns the Class object for BeanDescriptor, you" + + " probably meant to retrieve the class described by this BeanDescriptor using" + + " getBeanClass") + .put( + /* + * LockInfo has a publicly visible subclass, MonitorInfo. It seems unlikely that + * anyone is using getClass() in an attempt to distinguish the two. (If anyone is, + * then it would make more sense to use instanceof, anyway.) + */ + instanceMethod().onDescendantOf("java.lang.management.LockInfo").named("getClass"), + "Calling getClass on LockInfo returns the Class object for LockInfo, you probably" + + " meant to retrieve the class of the object that is being locked using" + + " getClassName") + /* + * These methods are part of Guava, but we have to list them in this "thirdparty" section: + * We can't annotate them with @DoNotCall because we can't override getClass. + */ + .put( + instanceMethod() + .onExactClass("com.google.common.reflect.ClassPath$ClassInfo") + .named("getClass"), + "Calling getClass on ClassInfo returns the Class object for ClassInfo, you probably" + + " meant to retrieve the class described by this ClassInfo using getName or" + + " load") + /* + * Users of TypeToken have to create a subclass. The static type of their instance is + * probably often still "TypeToken," but that may change as we see more usage of `var`. So + * let's check subclasses, too. If anyone defines an overload of getClass on such a + * subclass, this check will give that person a bad time in one additional way. + */ + .put( + instanceMethod() + .onDescendantOf("com.google.common.reflect.TypeToken") + .named("getClass"), + "Calling getClass on TypeToken returns the Class object for TypeToken, you probably" + + " meant to retrieve the class described by this TypeToken using getRawType") + .put( + /* + * A call to super.run() from a direct subclass of Thread is a no-op. That could be + * worth telling the user about, but it's not as big a deal as "You meant to call + * start()," so we ignore it here. + * + * (And if someone defines a MyThread class with a run() method that does something, + * then a call to super.run() from a subclass of *MyThread* would *not* be a no-op, + * and we wouldn't want to flag it. Still, *usually* it's likely to be useful to + * report a warning even on subclasses of Thread, such as anonymous classes.) + */ + allOf(THREAD_RUN, not(CALL_ON_SUPER)), + "Calling run on Thread runs work on this thread, rather than the given thread, you" + + " probably meant to call start") .buildOrThrow(); static final String DO_NOT_CALL = "com.google.errorprone.annotations.DoNotCall"; @@ -201,6 +326,14 @@ public Void visitMemberReference(MemberReferenceTree tree, Void unused) { private void handleTree(ExpressionTree tree, MethodSymbol symbol) { for (Map.Entry, String> matcher : THIRD_PARTY_METHODS.entrySet()) { if (matcher.getKey().matches(tree, state)) { + if (!checkNewGetClassMethods + && ANY_GET_CLASS.matches(tree, state) + && !STACK_TRACE_ELEMENT_GET_CLASS.matches(tree, state)) { + return; + } + if (!checkThreadRun && THREAD_RUN.matches(tree, state)) { + return; + } state.reportMatch(buildDescription(tree).setMessage(matcher.getValue()).build()); return; } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ErroneousThreadPoolConstructorChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/ErroneousThreadPoolConstructorChecker.java index 4e3877392fd..e32128738da 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ErroneousThreadPoolConstructorChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ErroneousThreadPoolConstructorChecker.java @@ -64,6 +64,9 @@ public Description matchNewClass(NewClassTree tree, VisitorState state) { return Description.NO_MATCH; } List arguments = tree.getArguments(); + if (arguments.size() < 2) { + return Description.NO_MATCH; + } Integer corePoolSize = ASTHelpers.constValue(arguments.get(0), Integer.class); Integer maximumPoolSize = ASTHelpers.constValue(arguments.get(1), Integer.class); if (corePoolSize == null || maximumPoolSize == null || corePoolSize.equals(maximumPoolSize)) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ForOverrideChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/ForOverrideChecker.java index 241c47fd322..e25438fcfaf 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ForOverrideChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ForOverrideChecker.java @@ -20,8 +20,8 @@ import static com.google.common.collect.Streams.findLast; import static com.google.common.collect.Streams.stream; import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; -import static com.google.errorprone.util.ASTHelpers.findSuperMethods; import static com.google.errorprone.util.ASTHelpers.hasAnnotation; +import static com.google.errorprone.util.ASTHelpers.streamSuperMethods; import static java.util.stream.Stream.concat; import com.google.common.collect.ImmutableList; @@ -199,12 +199,13 @@ private static ImmutableList getOverriddenMethods( "getOverriddenMethods may not be called on a static method"); } - return concat(Stream.of(method), findSuperMethods(method, state.getTypes()).stream()) + return concat(Stream.of(method), streamSuperMethods(method, state.getTypes())) .filter(member -> hasAnnotation(member, FOR_OVERRIDE, state)) .collect(toImmutableList()); } /** Get the outermost class/interface/enum of an element, or null if none. */ + @Nullable private static Type getOutermostClass(VisitorState state) { return findLast( stream(state.getPath()) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/InfiniteRecursion.java b/core/src/main/java/com/google/errorprone/bugpatterns/InfiniteRecursion.java index 5056f001cb1..1fb495ef8ca 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/InfiniteRecursion.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/InfiniteRecursion.java @@ -37,6 +37,7 @@ import com.sun.source.tree.TypeCastTree; import com.sun.source.util.SimpleTreeVisitor; import com.sun.tools.javac.code.Symbol.MethodSymbol; +import org.checkerframework.checker.nullness.qual.Nullable; /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ @BugPattern( @@ -74,7 +75,7 @@ public MethodInvocationTree visitTypeCast(TypeCastTree tree, Void unused) { } @Override - protected MethodInvocationTree defaultAction(Tree tree, Void unused) { + protected @Nullable MethodInvocationTree defaultAction(Tree tree, Void unused) { return tree instanceof MethodInvocationTree ? (MethodInvocationTree) tree : null; } }, diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/IsInstanceIncompatibleType.java b/core/src/main/java/com/google/errorprone/bugpatterns/IsInstanceIncompatibleType.java index cebd9e8b9ef..8c2d9a92caa 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/IsInstanceIncompatibleType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/IsInstanceIncompatibleType.java @@ -38,6 +38,7 @@ import com.sun.source.tree.Tree; import com.sun.tools.javac.code.Type; import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; /** * @author cushon@google.com (Liam Miller-Cushon) @@ -84,7 +85,7 @@ public Description matchMemberReference(MemberReferenceTree tree, VisitorState s : buildMessage(argumentType, receiverType, tree, state); } - private static Type classTypeArgument(ExpressionTree tree) { + private static @Nullable Type classTypeArgument(ExpressionTree tree) { ExpressionTree receiver = getReceiver(tree); if (receiver == null) { return null; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/JdkObsolete.java b/core/src/main/java/com/google/errorprone/bugpatterns/JdkObsolete.java index 23176749e12..5806a744ebd 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/JdkObsolete.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/JdkObsolete.java @@ -354,6 +354,7 @@ public Void visitIdentifier(IdentifierTree tree, Void unused) { return Optional.of(fix.build()); } + @Nullable private static TreePath findEnclosingMethod(VisitorState state) { TreePath path = state.getPath(); while (path != null) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/LockNotBeforeTry.java b/core/src/main/java/com/google/errorprone/bugpatterns/LockNotBeforeTry.java index 03ccbcc379a..d82f87ae2d5 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/LockNotBeforeTry.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/LockNotBeforeTry.java @@ -39,6 +39,7 @@ import com.sun.source.tree.TryTree; import com.sun.source.util.TreePath; import com.sun.source.util.TreeScanner; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Suggests that calls to {@code Lock.lock} must be immediately followed by a {@code try-finally} @@ -171,7 +172,7 @@ private static boolean releases(TryTree tryTree, ExpressionTree lockee, VisitorS Boolean released = new TreeScanner() { @Override - public Boolean reduce(Boolean r1, Boolean r2) { + public @Nullable Boolean reduce(Boolean r1, Boolean r2) { return r1 == null ? r2 : (r2 == null ? null : r1 && r2); } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/LongDoubleConversion.java b/core/src/main/java/com/google/errorprone/bugpatterns/LongDoubleConversion.java index 24ec06586c6..4bd71ae93fe 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/LongDoubleConversion.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/LongDoubleConversion.java @@ -18,6 +18,7 @@ import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.util.ASTHelpers.constValue; import static com.google.errorprone.util.ASTHelpers.getType; import static com.google.errorprone.util.ASTHelpers.targetType; @@ -39,7 +40,7 @@ "Conversion from long to double may lose precision; use an explicit cast to double if this" + " was intentional", severity = WARNING) -public class LongDoubleConversion extends BugChecker implements MethodInvocationTreeMatcher { +public final class LongDoubleConversion extends BugChecker implements MethodInvocationTreeMatcher { @Override public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { @@ -53,6 +54,10 @@ private void checkArgument(ExpressionTree argument, VisitorState state) { if (!getType(argument).getKind().equals(TypeKind.LONG)) { return; } + Object constant = constValue(argument); + if (constant instanceof Long && constant.equals((long) ((Long) constant).doubleValue())) { + return; + } ASTHelpers.TargetType targetType = targetType(state.withPath(new TreePath(state.getPath(), argument))); if (targetType != null && targetType.type().getKind().equals(TypeKind.DOUBLE)) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MethodCanBeStatic.java b/core/src/main/java/com/google/errorprone/bugpatterns/MethodCanBeStatic.java index db42998f2da..36236efbfc7 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/MethodCanBeStatic.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/MethodCanBeStatic.java @@ -24,6 +24,7 @@ import static com.google.errorprone.util.ASTHelpers.getStartPosition; import static java.util.Collections.disjoint; import static javax.lang.model.element.Modifier.ABSTRACT; +import static javax.lang.model.element.Modifier.DEFAULT; import static javax.lang.model.element.Modifier.NATIVE; import static javax.lang.model.element.Modifier.SYNCHRONIZED; @@ -250,7 +251,7 @@ private static boolean isExcluded(MethodTree tree, VisitorState state) { } private static final ImmutableSet EXCLUDED_MODIFIERS = - immutableEnumSet(NATIVE, SYNCHRONIZED, ABSTRACT); + immutableEnumSet(NATIVE, SYNCHRONIZED, ABSTRACT, DEFAULT); /** Information about a {@link MethodSymbol} and whether it can be made static. */ private static final class MethodDetails { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MissingOverride.java b/core/src/main/java/com/google/errorprone/bugpatterns/MissingOverride.java index 92964a28a44..f9baf7b1923 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/MissingOverride.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/MissingOverride.java @@ -18,8 +18,8 @@ import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.matchers.Description.NO_MATCH; -import static com.google.errorprone.util.ASTHelpers.findSuperMethods; import static com.google.errorprone.util.ASTHelpers.hasAnnotation; +import static com.google.errorprone.util.ASTHelpers.streamSuperMethods; import com.google.errorprone.BugPattern; import com.google.errorprone.BugPattern.StandardTags; @@ -59,7 +59,7 @@ public Description matchMethod(MethodTree tree, VisitorState state) { if (ignoreInterfaceOverrides && sym.enclClass().isInterface()) { return NO_MATCH; } - return findSuperMethods(sym, state.getTypes()).stream() + return streamSuperMethods(sym, state.getTypes()) .findFirst() .filter(unused -> ASTHelpers.getGeneratedBy(state).isEmpty()) // to allow deprecated methods to be removed non-atomically, we permit overrides of diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MockNotUsedInProduction.java b/core/src/main/java/com/google/errorprone/bugpatterns/MockNotUsedInProduction.java new file mode 100644 index 00000000000..e7a32649353 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/MockNotUsedInProduction.java @@ -0,0 +1,199 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import static com.google.common.collect.Streams.stream; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.matchers.Matchers.anyMethod; +import static com.google.errorprone.matchers.Matchers.anyOf; +import static com.google.errorprone.matchers.Matchers.hasAnnotation; +import static com.google.errorprone.matchers.Matchers.staticMethod; +import static com.google.errorprone.util.ASTHelpers.canBeRemoved; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static java.util.stream.Stream.concat; +import static javax.lang.model.element.ElementKind.FIELD; + +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ExpressionStatementTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePathScanner; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +/** A BugPattern; see the summary. */ +@BugPattern( + severity = WARNING, + summary = + "This mock is instantiated and configured, but is never passed to production code. It" + + " should be either removed or used.") +public final class MockNotUsedInProduction extends BugChecker + implements CompilationUnitTreeMatcher { + @Override + public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { + ImmutableMap mocks = findMocks(state); + if (mocks.isEmpty()) { + return NO_MATCH; + } + Set usedMocks = new HashSet<>(); + new TreePathScanner() { + @Override + public Void visitMethodInvocation(MethodInvocationTree invocation, Void unused) { + // Don't count references to mocks within the arguments of a when(...) call to be a usage. + // We still need to scan the receiver for the case of + // `doReturn(someMockWhichIsAUsage).when(aMockWhichIsNotAUsage);` + if (WHEN_OR_VERIFY.matches(invocation, state)) { + scan(invocation.getMethodSelect(), null); + return null; + } + return super.visitMethodInvocation(invocation, null); + } + + @Override + public Void visitMemberSelect(MemberSelectTree memberSelect, Void unused) { + handle(memberSelect); + return super.visitMemberSelect(memberSelect, null); + } + + @Override + public Void visitIdentifier(IdentifierTree identifier, Void unused) { + handle(identifier); + return super.visitIdentifier(identifier, null); + } + + private void handle(Tree tree) { + var symbol = getSymbol(tree); + if (symbol instanceof VarSymbol) { + usedMocks.add((VarSymbol) symbol); + } + } + }.scan(state.getPath(), null); + mocks.forEach( + (sym, mockTree) -> { + if (usedMocks.contains(sym)) { + return; + } + state.reportMatch(describeMatch(mockTree, generateFix(sym, state))); + }); + return NO_MATCH; + } + + /** + * Very crudely deletes every variable or expression statement which contains a reference to + * {@code sym}. This is inefficient insofar as we scan the entire file again, but only when + * generating a fix. + */ + private static SuggestedFix generateFix(VarSymbol sym, VisitorState state) { + SuggestedFix.Builder fix = SuggestedFix.builder(); + new TreePathScanner() { + + @Override + public Void scan(Tree tree, Void unused) { + if (Objects.equals(getSymbol(tree), sym)) { + // Yes, at this point, the current path hasn't been updated to include `tree`... + concat(Stream.of(tree), stream(getCurrentPath())) + .filter(t -> t instanceof ExpressionStatementTree || t instanceof VariableTree) + .findFirst() + .ifPresent(fix::delete); + } + return super.scan(tree, null); + } + }.scan(state.getPath().getCompilationUnit(), null); + return fix.build(); + } + + private ImmutableMap findMocks(VisitorState state) { + Map mocks = new HashMap<>(); + AtomicBoolean injectMocks = new AtomicBoolean(false); + new SuppressibleTreePathScanner(state) { + @Override + public Void visitVariable(VariableTree tree, Void unused) { + VarSymbol symbol = getSymbol(tree); + if (INJECT_MOCKS_ANNOTATED.matches(tree, state)) { + injectMocks.set(true); + } + if (isEligible(symbol) + && (MOCK_OR_SPY_ANNOTATED.matches(tree, state) + || (tree.getInitializer() != null && MOCK.matches(tree.getInitializer(), state)))) { + mocks.put(symbol, tree); + } + return super.visitVariable(tree, null); + } + + @Override + public Void visitAssignment(AssignmentTree tree, Void unused) { + if (MOCK.matches(tree.getExpression(), state)) { + var symbol = getSymbol(tree.getVariable()); + if (isEligible(symbol)) { + mocks.put((VarSymbol) symbol, tree); + } + } + return super.visitAssignment(tree, null); + } + + private boolean isEligible(Symbol symbol) { + return symbol instanceof VarSymbol + && (!symbol.getKind().equals(FIELD) || canBeRemoved((VarSymbol) symbol)) + && annotatedAtMostMock(symbol); + } + + private boolean annotatedAtMostMock(Symbol symbol) { + return symbol.getAnnotationMirrors().stream() + .allMatch(a -> a.getAnnotationType().asElement().getSimpleName().contentEquals("Mock")); + } + }.scan(state.getPath().getCompilationUnit(), null); + // A bit hacky: but if we saw InjectMocks, just claim there are no potentially unused mocks. + return injectMocks.get() ? ImmutableMap.of() : ImmutableMap.copyOf(mocks); + } + + private static final Matcher MOCK = + anyOf( + staticMethod() + .onClass("org.mockito.Mockito") + .namedAnyOf("mock") + .withParameters("java.lang.Class"), + staticMethod().onClass("org.mockito.Mockito").namedAnyOf("spy")); + + private static final Matcher MOCK_OR_SPY_ANNOTATED = + anyOf(hasAnnotation("org.mockito.Mock"), hasAnnotation("org.mockito.Spy")); + + private static final Matcher INJECT_MOCKS_ANNOTATED = + hasAnnotation("org.mockito.InjectMocks"); + + private static final Matcher WHEN_OR_VERIFY = + anyMethod().anyClass().namedAnyOf("when", "verify"); +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/NoAllocationChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/NoAllocationChecker.java index 1b260511a78..611fead87fa 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/NoAllocationChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/NoAllocationChecker.java @@ -38,7 +38,7 @@ import static com.google.errorprone.matchers.Matchers.typeCast; import static com.google.errorprone.matchers.Matchers.variableInitializer; import static com.google.errorprone.matchers.Matchers.variableType; -import static com.google.errorprone.util.ASTHelpers.findSuperMethods; +import static com.google.errorprone.util.ASTHelpers.streamSuperMethods; import static com.sun.source.tree.Tree.Kind.AND_ASSIGNMENT; import static com.sun.source.tree.Tree.Kind.DIVIDE_ASSIGNMENT; import static com.sun.source.tree.Tree.Kind.LEFT_SHIFT_ASSIGNMENT; @@ -393,7 +393,7 @@ public Description matchMethod(MethodTree tree, VisitorState state) { return NO_MATCH; } MethodSymbol symbol = ASTHelpers.getSymbol(tree); - return findSuperMethods(symbol, state.getTypes()).stream() + return streamSuperMethods(symbol, state.getTypes()) .filter(s -> ASTHelpers.hasAnnotation(s, NoAllocation.class.getName(), state)) .findAny() .map( diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java b/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java index 9cc7890541f..80e12e376b3 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java @@ -18,8 +18,6 @@ import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.collect.Iterables.getLast; -import static com.google.common.collect.Streams.forEachPair; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.matchers.Description.NO_MATCH; import static com.google.errorprone.util.ASTHelpers.getStartPosition; @@ -48,11 +46,15 @@ import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.code.Symbol.VarSymbol; import com.sun.tools.javac.parser.Tokens.Comment; +import com.sun.tools.javac.parser.Tokens.Comment.CommentStyle; import com.sun.tools.javac.util.Position; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; +import java.util.Iterator; import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; import java.util.regex.Matcher; /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ @@ -80,18 +82,21 @@ public ParameterName(ErrorProneFlags errorProneFlags) { @Override public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { - checkArguments(tree, tree.getArguments(), state); + checkArguments(tree, tree.getArguments(), state.getEndPosition(tree.getMethodSelect()), state); return NO_MATCH; } @Override public Description matchNewClass(NewClassTree tree, VisitorState state) { - checkArguments(tree, tree.getArguments(), state); + checkArguments(tree, tree.getArguments(), state.getEndPosition(tree.getIdentifier()), state); return NO_MATCH; } private void checkArguments( - Tree tree, List arguments, VisitorState state) { + Tree tree, + List arguments, + int argListStartPosition, + VisitorState state) { if (arguments.isEmpty()) { return; } @@ -99,55 +104,77 @@ private void checkArguments( if (NamedParameterComment.containsSyntheticParameterName(sym)) { return; } - int start = getStartPosition(tree); - int end = state.getEndPosition(getLast(arguments)); - if (start == Position.NOPOS || end == Position.NOPOS) { + int start = argListStartPosition; + if (start == Position.NOPOS) { // best effort work-around for https://github.com/google/error-prone/issues/780 return; } - String source = state.getSourceCode().subSequence(start, end).toString(); - if (!source.contains("/*")) { - // fast path if the arguments don't contain anything that looks like a comment - return; - } String enclosingClass = ASTHelpers.enclosingClass(sym).toString(); if (exemptPackages.stream().anyMatch(enclosingClass::startsWith)) { return; } - Deque tokens = - new ArrayDeque<>(ErrorProneTokens.getTokens(source, start, state.context)); - forEachPair( - sym.getParameters().stream(), - arguments.stream(), - (p, a) -> { - if (advanceTokens(tokens, a, state)) { - checkArgument(p, a, tokens.removeFirst(), state); - } - }); + Iterator argumentIterator = arguments.iterator(); + // For each parameter/argument pair, we tokenize the characters between the end of the + // previous argument (or the start of the argument list, in the case of the first argument) + // and the start of the current argument. The `start` variable is advanced each time, stepping + // over each argument when we finish processing it. + for (VarSymbol param : sym.getParameters()) { + if (!argumentIterator.hasNext()) { + return; // A vararg parameter has zero corresponding arguments passed + } + ExpressionTree argument = argumentIterator.next(); + Optional> positions = positions(argument, state); + if (positions.isEmpty()) { + return; + } + start = + processArgument( + positions.get(), start, state, tok -> checkArgument(param, argument, tok, state)); + } // handle any varargs arguments after the first - int numParams = sym.getParameters().size(); - int numArgs = arguments.size(); - if (numParams < numArgs) { - for (ExpressionTree arg : arguments.subList(numParams, numArgs)) { - if (advanceTokens(tokens, arg, state)) { - checkComment(arg, tokens.removeFirst(), state); - } + while (argumentIterator.hasNext()) { + ExpressionTree argument = argumentIterator.next(); + Optional> positions = positions(argument, state); + if (positions.isEmpty()) { + return; } + start = + processArgument(positions.get(), start, state, tok -> checkComment(argument, tok, state)); } } - private static boolean advanceTokens( - Deque tokens, ExpressionTree actual, VisitorState state) { - while (!tokens.isEmpty() && tokens.getFirst().pos() < getStartPosition(actual)) { + /** Returns the source span for a tree, or empty if the position information is not available. */ + Optional> positions(Tree tree, VisitorState state) { + int endPosition = state.getEndPosition(tree); + if (endPosition == Position.NOPOS) { + return Optional.empty(); + } + return Optional.of(Range.closedOpen(getStartPosition(tree), endPosition)); + } + + private static int processArgument( + Range positions, + int offset, + VisitorState state, + Consumer consumer) { + String source = state.getSourceCode().subSequence(offset, positions.upperEndpoint()).toString(); + Deque tokens = + new ArrayDeque<>(ErrorProneTokens.getTokens(source, offset, state.context)); + if (advanceTokens(tokens, positions)) { + consumer.accept(tokens.removeFirst()); + } + return positions.upperEndpoint(); + } + + private static boolean advanceTokens(Deque tokens, Range actual) { + while (!tokens.isEmpty() && tokens.getFirst().pos() < actual.lowerEndpoint()) { tokens.removeFirst(); } if (tokens.isEmpty()) { return false; } - Range argRange = - Range.closedOpen(getStartPosition(actual), state.getEndPosition(actual)); - if (!argRange.contains(tokens.getFirst().pos())) { + if (!actual.contains(tokens.getFirst().pos())) { return false; } return true; @@ -173,6 +200,11 @@ private void checkArgument( VarSymbol formal, ExpressionTree actual, ErrorProneToken token, VisitorState state) { List matches = new ArrayList<>(); for (Comment comment : token.comments()) { + if (comment.getStyle().equals(CommentStyle.LINE)) { + // These are usually not intended as a parameter comment, and we don't want to flag if they + // happen to match the parameter comment format. + continue; + } Matcher m = NamedParameterComment.PARAMETER_COMMENT_PATTERN.matcher( Comments.getTextFromComment(comment)); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/PreferredInterfaceType.java b/core/src/main/java/com/google/errorprone/bugpatterns/PreferredInterfaceType.java index ba99856149a..0307c41db99 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/PreferredInterfaceType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/PreferredInterfaceType.java @@ -110,7 +110,8 @@ public final class PreferredInterfaceType extends BugChecker implements Compilat "com.google.common.collect.ImmutableSetMultimap", "com.google.common.collect.ImmutableMultimap", "com.google.common.collect.ListMultimap", - "com.google.common.collect.SetMultimap")); + "com.google.common.collect.SetMultimap"), + BetterTypes.of(isDescendantOf("java.lang.CharSequence"), "java.lang.String")); private static final Matcher INTERESTING_TYPE = anyOf( diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparison.java b/core/src/main/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparison.java index 2e6c07d122e..599bcf7ef8b 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparison.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparison.java @@ -140,7 +140,7 @@ private static boolean isNull(ExpressionTree tree) { public ProtoFieldNullComparison(ErrorProneFlags flags) { this.matchTestAssertions = - flags.getBoolean("ProtoFieldNullComparison:MatchTestAssertions").orElse(false); + flags.getBoolean("ProtoFieldNullComparison:MatchTestAssertions").orElse(true); } @Override @@ -181,6 +181,7 @@ public Void visitVariable(VariableTree variable, Void unused) { private Optional getInitializer(ExpressionTree tree) { return Optional.ofNullable( new SimpleTreeVisitor() { + @Nullable @Override public ExpressionTree visitMethodInvocation(MethodInvocationTree node, Void unused) { return PROTO_RECEIVER.matches(node, state) ? node : null; @@ -290,6 +291,7 @@ private interface Fixer { private enum GetterTypes { /** {@code proto.getFoo()} */ SCALAR { + @Nullable @Override Fixer match(ExpressionTree tree, VisitorState state) { if (tree.getKind() != Kind.METHOD_INVOCATION) { @@ -335,6 +337,7 @@ private String replaceLast(String text, String pattern, String replacement) { }, /** {@code proto.getRepeatedFoo(index)} */ VECTOR_INDEXED { + @Nullable @Override Fixer match(ExpressionTree tree, VisitorState state) { if (tree.getKind() != Kind.METHOD_INVOCATION) { @@ -365,6 +368,7 @@ private String generateFix( }, /** {@code proto.getRepeatedFooList()} */ VECTOR { + @Nullable @Override Fixer match(ExpressionTree tree, VisitorState state) { if (tree.getKind() != Kind.METHOD_INVOCATION) { @@ -391,6 +395,7 @@ private String generateFix( }, /** {@code proto.getField(f)} or {@code proto.getExtension(outer, extension)}; */ EXTENSION_METHOD { + @Nullable @Override Fixer match(ExpressionTree tree, VisitorState state) { if (EXTENSION_METHODS_WITH_NO_FIX.matches(tree, state)) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ProtoRedundantSet.java b/core/src/main/java/com/google/errorprone/bugpatterns/ProtoRedundantSet.java index 43dda75068e..b295425a6c6 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ProtoRedundantSet.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ProtoRedundantSet.java @@ -41,6 +41,7 @@ import java.util.Collection; import java.util.Map; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Checks that protocol buffers built with chained builders don't set the same field twice. @@ -167,7 +168,7 @@ interface ProtoField {} enum FieldType { SINGLE { @Override - FieldWithValue match(String name, MethodInvocationTree tree) { + @Nullable FieldWithValue match(String name, MethodInvocationTree tree) { if (name.startsWith("set") && tree.getArguments().size() == 1) { return FieldWithValue.of(SingleField.of(name), tree, tree.getArguments().get(0)); } @@ -176,7 +177,7 @@ FieldWithValue match(String name, MethodInvocationTree tree) { }, REPEATED { @Override - FieldWithValue match(String name, MethodInvocationTree tree) { + @Nullable FieldWithValue match(String name, MethodInvocationTree tree) { if (name.startsWith("set") && tree.getArguments().size() == 2) { Integer index = ASTHelpers.constValue(tree.getArguments().get(0), Integer.class); if (index != null) { @@ -189,7 +190,7 @@ FieldWithValue match(String name, MethodInvocationTree tree) { }, MAP { @Override - FieldWithValue match(String name, MethodInvocationTree tree) { + @Nullable FieldWithValue match(String name, MethodInvocationTree tree) { if (name.startsWith("put") && tree.getArguments().size() == 2) { Object key = ASTHelpers.constValue(tree.getArguments().get(0), Object.class); if (key != null) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/RestrictedApiChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/RestrictedApiChecker.java index 7762bf3f69f..4a6484e731a 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/RestrictedApiChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/RestrictedApiChecker.java @@ -21,6 +21,7 @@ import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.errorprone.matchers.Description.NO_MATCH; import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.streamSuperMethods; import com.google.common.collect.ImmutableSet; import com.google.errorprone.BugPattern; @@ -189,15 +190,14 @@ private Description checkMethodUse( } // Try each super method for @RestrictedApi - Optional superWithRestrictedApi = - ASTHelpers.findSuperMethods(method, state.getTypes()).stream() - .filter((t) -> ASTHelpers.hasAnnotation(t, RestrictedApi.class, state)) - .findFirst(); - if (!superWithRestrictedApi.isPresent()) { - return NO_MATCH; - } - return checkRestriction( - getRestrictedApiAnnotation(superWithRestrictedApi.get(), state), where, state); + return streamSuperMethods(method, state.getTypes()) + .filter((t) -> ASTHelpers.hasAnnotation(t, RestrictedApi.class, state)) + .findFirst() + .map( + superWithRestrictedApi -> + checkRestriction( + getRestrictedApiAnnotation(superWithRestrictedApi, state), where, state)) + .orElse(NO_MATCH); } @Nullable diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ReturnsNullCollection.java b/core/src/main/java/com/google/errorprone/bugpatterns/ReturnsNullCollection.java index c93831d74db..6326b8a668e 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ReturnsNullCollection.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ReturnsNullCollection.java @@ -53,7 +53,8 @@ private static boolean methodWithoutNullable(MethodTree tree, VisitorState state allOf( anyOf( methodReturns(isSubtypeOf("java.util.Collection")), - methodReturns(isSubtypeOf("java.util.Map"))), + methodReturns(isSubtypeOf("java.util.Map")), + methodReturns(isSubtypeOf("com.google.common.collect.Multimap"))), ReturnsNullCollection::methodWithoutNullable); public ReturnsNullCollection() { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/RxReturnValueIgnored.java b/core/src/main/java/com/google/errorprone/bugpatterns/RxReturnValueIgnored.java index 1fcf18ebead..e60cf045f17 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/RxReturnValueIgnored.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/RxReturnValueIgnored.java @@ -22,6 +22,7 @@ import static com.google.errorprone.matchers.Matchers.not; import static com.google.errorprone.util.ASTHelpers.getSymbol; import static com.google.errorprone.util.ASTHelpers.hasAnnotation; +import static com.google.errorprone.util.ASTHelpers.streamSuperMethods; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; @@ -66,7 +67,7 @@ private static boolean hasCirvAnnotation(ExpressionTree tree, VisitorState state // if the super-type returned the exact same type. This lets us catch issues where a // superclass was annotated with @CanIgnoreReturnValue but the parent did not intend to // return an Rx type - return ASTHelpers.findSuperMethods(sym, state.getTypes()).stream() + return streamSuperMethods(sym, state.getTypes()) .anyMatch( superSym -> hasAnnotation(superSym, CanIgnoreReturnValue.class, state) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java b/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java index 5ccba6f30ec..5163b07d59e 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java @@ -41,6 +41,7 @@ import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.TypeSymbol; +import com.sun.tools.javac.util.Position; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; @@ -48,6 +49,8 @@ import java.util.Map; import java.util.Optional; import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Name; +import org.checkerframework.checker.nullness.qual.Nullable; /** Looks for types being shadowed by other types in a way that may be confusing. */ @BugPattern( @@ -92,21 +95,38 @@ private boolean shouldIgnore() { && getSymbol(parentTree) instanceof ClassSymbol; } + private @Nullable String qualifiedName(Tree tree) { + if (state.getEndPosition(tree) == Position.NOPOS) { + return null; + } + ArrayDeque parts = new ArrayDeque<>(); + while (tree instanceof MemberSelectTree) { + MemberSelectTree select = (MemberSelectTree) tree; + parts.addFirst(select.getIdentifier()); + tree = select.getExpression(); + } + if (!(tree instanceof IdentifierTree)) { + return null; + } + parts.addFirst(((IdentifierTree) tree).getName()); + return Joiner.on('.').join(parts); + } + private void handle(Tree tree) { if (tree instanceof IdentifierTree && ((IdentifierTree) tree).getName().contentEquals("Builder")) { return; } - String treeSource = state.getSourceForNode(tree); - if (treeSource == null) { + String qualifiedName = qualifiedName(tree); + if (qualifiedName == null) { return; } Symbol symbol = getSymbol(tree); if (symbol instanceof ClassSymbol) { - List treePaths = table.get(treeSource, symbol.type.tsym); + List treePaths = table.get(qualifiedName, symbol.type.tsym); if (treePaths == null) { treePaths = new ArrayList<>(); - table.put(treeSource, symbol.type.tsym, treePaths); + table.put(qualifiedName, symbol.type.tsym, treePaths); } treePaths.add(getCurrentPath()); } @@ -183,6 +203,9 @@ private static Optional getBetterImport(TypeSymbol classSymbol, String s Symbol owner = classSymbol; long dots = simpleName.chars().filter(c -> c == '.').count(); for (long i = 0; i < dots + 1; ++i) { + if (owner == null) { + return Optional.empty(); + } owner = owner.owner; } if (owner instanceof ClassSymbol) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java b/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java new file mode 100644 index 00000000000..4da47b30279 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java @@ -0,0 +1,161 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.util.ASTHelpers.enclosingClass; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.getType; +import static com.google.errorprone.util.ASTHelpers.isConsideredFinal; +import static com.google.errorprone.util.ASTHelpers.isSameType; +import static com.google.errorprone.util.ASTHelpers.isVoidType; + +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.SimpleTreeVisitor; +import com.sun.source.util.TreePathScanner; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Non-abstract instance methods named {@code self()} that return the enclosing class must always + * {@code return this}. + */ +@BugPattern( + summary = + "Non-abstract instance methods named 'self()' that return the enclosing class must always" + + " 'return this'", + severity = WARNING) +public final class SelfAlwaysReturnsThis extends BugChecker implements MethodTreeMatcher { + @Override + public Description matchMethod(MethodTree methodTree, VisitorState state) { + MethodSymbol methodSymbol = getSymbol(methodTree); + + // The method must: + // * not be a constructor + // * be named `self` + // * have no params + // * be an instance method (not static) + // * have a body (not abstract) + if (methodSymbol.isConstructor() + || !methodSymbol.getSimpleName().contentEquals("self") + || !methodSymbol.getParameters().isEmpty() + || methodSymbol.isStatic() + || methodTree.getBody() == null) { + return NO_MATCH; + } + + // * not have a void (or Void) return type + Tree returnType = methodTree.getReturnType(); + if (isVoidType(getType(returnType), state)) { + return NO_MATCH; + } + + // * have the same return type as the enclosing type + if (!isSameType(getType(returnType), enclosingClass(methodSymbol).type, state)) { + return NO_MATCH; + } + + // TODO(kak): we should probably re-used the TreePathScanner from CanIgnoreReturnValueSuggester + + // This TreePathScanner is mostly copied from CanIgnoreReturnValueSuggester + AtomicBoolean allReturnThis = new AtomicBoolean(true); + AtomicBoolean atLeastOneReturn = new AtomicBoolean(false); + + new TreePathScanner() { + private final Set thises = new HashSet<>(); + + @Override + public Void visitVariable(VariableTree variableTree, Void unused) { + VarSymbol symbol = getSymbol(variableTree); + if (isConsideredFinal(symbol) && maybeCastThis(variableTree.getInitializer())) { + thises.add(symbol); + } + return super.visitVariable(variableTree, null); + } + + @Override + public Void visitReturn(ReturnTree returnTree, Void unused) { + atLeastOneReturn.set(true); + if (!isThis(returnTree.getExpression())) { + allReturnThis.set(false); + // once we've set allReturnThis to false, no need to descend further + return null; + } + return super.visitReturn(returnTree, null); + } + + /** Returns whether the given {@link ExpressionTree} is {@code this}. */ + private boolean isThis(ExpressionTree returnExpression) { + return maybeCastThis(returnExpression) || thises.contains(getSymbol(returnExpression)); + } + + @Override + public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) { + // don't descend into lambdas + return null; + } + + @Override + public Void visitNewClass(NewClassTree node, Void unused) { + // don't descend into declarations of anonymous classes + return null; + } + }.scan(state.getPath(), null); + + if (atLeastOneReturn.get() && allReturnThis.get()) { + return NO_MATCH; + } + + return describeMatch( + methodTree, SuggestedFix.replace(methodTree.getBody(), "{ return this; }")); + } + + private static boolean maybeCastThis(Tree tree) { + return firstNonNull( + new SimpleTreeVisitor() { + + @Override + public Boolean visitTypeCast(TypeCastTree tree, Void unused) { + return visit(tree.getExpression(), null); + } + + @Override + public Boolean visitIdentifier(IdentifierTree tree, Void unused) { + return tree.getName().contentEquals("this"); + } + }.visit(tree, null), + false); + } +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/SelfAssignment.java b/core/src/main/java/com/google/errorprone/bugpatterns/SelfAssignment.java index 019c24214cf..8d0175874b6 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/SelfAssignment.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/SelfAssignment.java @@ -49,6 +49,7 @@ import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Type; +import org.checkerframework.checker.nullness.qual.Nullable; /** * TODO(eaftan): Consider cases where the parent is not a statement or there is no parent? @@ -122,7 +123,7 @@ public ExpressionTree visitTypeCast(TypeCastTree node, Void unused) { } @Override - protected ExpressionTree defaultAction(Tree node, Void unused) { + protected @Nullable ExpressionTree defaultAction(Tree node, Void unused) { return node instanceof ExpressionTree ? (ExpressionTree) node : null; } }.visit(expression, null); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/StronglyType.java b/core/src/main/java/com/google/errorprone/bugpatterns/StronglyType.java index ec9e6063a27..03d38150575 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/StronglyType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/StronglyType.java @@ -29,6 +29,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.SetMultimap; import com.google.errorprone.VisitorState; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.fixes.SuggestedFixes; import com.google.errorprone.matchers.Description; @@ -94,6 +95,7 @@ public abstract static class Builder { abstract ImmutableSet.Builder primitiveTypesToReplaceBuilder(); /** Add a type that can be replaced with a stronger type. */ + @CanIgnoreReturnValue public final Builder addType(Type type) { primitiveTypesToReplaceBuilder().add(type); return this; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/TruthAssertExpected.java b/core/src/main/java/com/google/errorprone/bugpatterns/TruthAssertExpected.java index 4753a66e46a..a974b30ad47 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/TruthAssertExpected.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/TruthAssertExpected.java @@ -132,6 +132,7 @@ public List visitNewClass(NewClassTree node, Void unus return node.getArguments(); } + @Nullable @Override public List visitMethodInvocation( MethodInvocationTree node, Void unused) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeDirectionalityCharacters.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeDirectionalityCharacters.java index 8d1b682e8a4..f0cdf0818fd 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeDirectionalityCharacters.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeDirectionalityCharacters.java @@ -40,30 +40,28 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s for (int i = 0; i < source.length(); ++i) { char c = source.charAt(i); - if (isDangerous(c)) { - state.reportMatch( - describeMatch( - new FixedPosition(tree, i), - SuggestedFix.replace(i, i + 1, String.format("\\u%04x", (int) c)))); + // Do not extract this switch to a method. It's ugly as-is, but profiling suggests this + // checker is expensive for large files, and also that the method-call overhead would + // double the time spent in this loop. + switch (c) { + case 0x202A: // Left-to-Right Embedding + case 0x202B: // Right-to-Left Embedding + case 0x202C: // Pop Directional Formatting + case 0x202D: // Left-to-Right Override + case 0x202E: // Right-to-Left Override + case 0x2066: // Left-to-Right Isolate + case 0x2067: // Right-to-Left Isolate + case 0x2068: // First Strong Isolate + case 0x2069: // Pop Directional Isolate + state.reportMatch( + describeMatch( + new FixedPosition(tree, i), + SuggestedFix.replace(i, i + 1, String.format("\\u%04x", (int) c)))); + break; + default: + break; } } return NO_MATCH; } - - private static boolean isDangerous(char c) { - switch (c) { - case 0x202A: // Left-to-Right Embedding - case 0x202B: // Right-to-Left Embedding - case 0x202C: // Pop Directional Formatting - case 0x202D: // Left-to-Right Override - case 0x202E: // Right-to-Left Override - case 0x2066: // Left-to-Right Isolate - case 0x2067: // Right-to-Left Isolate - case 0x2068: // First Strong Isolate - case 0x2069: // Pop Directional Isolate - return true; - default: - return false; - } - } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeInCode.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeInCode.java index 2a2c0fa77df..e5c72add1d4 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeInCode.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeInCode.java @@ -21,22 +21,22 @@ import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; import static com.google.errorprone.matchers.Description.NO_MATCH; import static com.google.errorprone.util.ErrorProneTokens.getTokens; +import static com.google.errorprone.util.SourceCodeEscapers.javaCharEscaper; import static java.lang.String.format; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableRangeSet; import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; +import com.google.common.collect.TreeRangeSet; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher; import com.google.errorprone.fixes.FixedPosition; import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ErrorProneToken; -import com.google.errorprone.util.SourceCodeEscapers; import com.sun.source.tree.CompilationUnitTree; import com.sun.tools.javac.parser.Tokens.TokenKind; -import java.util.LinkedHashMap; -import java.util.Map; /** Bans using non-ASCII Unicode characters outside string literals and comments. */ @BugPattern( @@ -47,17 +47,14 @@ public final class UnicodeInCode extends BugChecker implements CompilationUnitTreeMatcher { @Override public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { - ImmutableRangeSet commentsAndLiterals = commentsAndLiterals(state); - - Map violations = new LinkedHashMap<>(); - - CharSequence sourceCode = state.getSourceCode(); + RangeSet violations = TreeRangeSet.create(); + String sourceCode = state.getSourceCode().toString(); for (int i = 0; i < sourceCode.length(); ++i) { char c = sourceCode.charAt(i); - if (!isAcceptableAscii(c) && !commentsAndLiterals.contains(i)) { - violations.put(i, c); + if (!isAcceptableAscii(c)) { + violations.add(Range.closedOpen(i, i + 1)); } } @@ -65,19 +62,21 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s return NO_MATCH; } - ImmutableRangeSet suppressedRegions = suppressedRegions(state); + ImmutableRangeSet permissibleUnicodeRegions = + suppressedRegions(state).union(commentsAndLiterals(state, sourceCode)); - for (var e : violations.entrySet()) { - int violatingLocation = e.getKey(); - char c = e.getValue(); - if (!suppressedRegions.contains(violatingLocation)) { + for (var range : violations.asDescendingSetOfRanges()) { + if (!permissibleUnicodeRegions.encloses(range)) { state.reportMatch( - buildDescription(new FixedPosition(tree, violatingLocation)) + buildDescription(new FixedPosition(tree, range.lowerEndpoint())) .setMessage( format( "Avoid using non-ASCII Unicode character (%s) outside of comments and" + " literals, as they can be confusing.", - SourceCodeEscapers.javaCharEscaper().escape(Character.toString(c)))) + javaCharEscaper() + .escape( + sourceCode.substring( + range.lowerEndpoint(), range.upperEndpoint())))) .build()); } } @@ -88,9 +87,8 @@ private static boolean isAcceptableAscii(char c) { return (c >= 0x20 && c <= 0x7E) || c == '\n' || c == '\r' || c == '\t'; } - private static ImmutableRangeSet commentsAndLiterals(VisitorState state) { - ImmutableList tokens = - getTokens(state.getSourceCode().toString(), state.context); + private static ImmutableRangeSet commentsAndLiterals(VisitorState state, String source) { + ImmutableList tokens = getTokens(source, state.context); return ImmutableRangeSet.unionOf( concat( tokens.stream() diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryAssignment.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryAssignment.java index 9e34317586a..7ea5f1d9afe 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryAssignment.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryAssignment.java @@ -121,13 +121,23 @@ public Description matchAssignment(AssignmentTree tree, VisitorState state) { @Override public Description matchVariable(VariableTree tree, VisitorState state) { + boolean hasMockAnnotation = HAS_MOCK_ANNOTATION.matches(tree, state); + boolean hasInjectyAnnotation = HAS_NON_MOCK_FRAMEWORK_ANNOTATION.matches(tree, state); + if (hasMockAnnotation && hasInjectyAnnotation) { + return buildDescription(tree) + .setMessage( + "Fields shouldn't be annotated with both @Mock and another @Inject-like annotation," + + " because both Mockito and the injector will assign to the field, and one of" + + " the values will overwrite the other") + .build(); + } if (tree.getInitializer() == null) { return NO_MATCH; } - if (HAS_MOCK_ANNOTATION.matches(tree, state)) { + if (hasMockAnnotation) { return describeMatch(tree, createMockFix(tree, state)); } - if (HAS_NON_MOCK_FRAMEWORK_ANNOTATION.matches(tree, state)) { + if (hasInjectyAnnotation) { Description.Builder description = buildDescription(tree); if (!tree.getModifiers().getFlags().contains(Modifier.FINAL)) { String source = diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java index bcb19bb6db1..abacf67f7d1 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java @@ -66,6 +66,7 @@ import java.util.function.Predicate; import javax.lang.model.element.ElementKind; import javax.lang.model.element.Modifier; +import org.checkerframework.checker.nullness.qual.Nullable; /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ @BugPattern( @@ -303,7 +304,7 @@ public LambdaExpressionTree visitLambdaExpression(LambdaExpressionTree node, Voi } @Override - public LambdaExpressionTree visitBlock(BlockTree node, Void unused) { + public @Nullable LambdaExpressionTree visitBlock(BlockTree node, Void unused) { // when processing a method body, only consider methods with a single `return` statement // that returns a method return node.getStatements().size() == 1 @@ -312,7 +313,7 @@ public LambdaExpressionTree visitBlock(BlockTree node, Void unused) { } @Override - public LambdaExpressionTree visitReturn(ReturnTree node, Void unused) { + public @Nullable LambdaExpressionTree visitReturn(ReturnTree node, Void unused) { return node.getExpression() != null ? node.getExpression().accept(this, null) : null; } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryParentheses.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryParentheses.java index ff618ae44bc..474d1b4ffa9 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryParentheses.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryParentheses.java @@ -17,7 +17,6 @@ package com.google.errorprone.bugpatterns; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; -import static com.google.errorprone.BugPattern.StandardTags.STYLE; import static com.google.errorprone.matchers.Description.NO_MATCH; import static com.google.errorprone.util.ASTHelpers.getStartPosition; @@ -36,8 +35,7 @@ summary = "These grouping parentheses are unnecessary; it is unlikely the code will" + " be misinterpreted without them", - severity = WARNING, - tags = STYLE) + severity = WARNING) public class UnnecessaryParentheses extends BugChecker implements ParenthesizedTreeMatcher { @Override diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnusedTypeParameter.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedTypeParameter.java new file mode 100644 index 00000000000..32221134d03 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedTypeParameter.java @@ -0,0 +1,132 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import static com.google.common.collect.Iterables.getLast; +import static com.google.errorprone.fixes.SuggestedFixes.removeElement; +import static com.google.errorprone.util.ASTHelpers.findSuperMethods; +import static com.google.errorprone.util.ASTHelpers.getStartPosition; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.methodCanBeOverridden; + +import com.google.common.collect.ImmutableMultiset; +import com.google.errorprone.BugPattern; +import com.google.errorprone.BugPattern.SeverityLevel; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.util.ErrorProneTokens; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeParameterTree; +import com.sun.source.util.TreeScanner; +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.Symbol.TypeVariableSymbol; +import com.sun.tools.javac.parser.Tokens.TokenKind; +import java.util.List; + +/** A BugPattern; see the summary. */ +@BugPattern( + severity = SeverityLevel.WARNING, + summary = "This type parameter is unused and can be removed.") +public final class UnusedTypeParameter extends BugChecker implements CompilationUnitTreeMatcher { + @Override + public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { + var usedIdentifiers = findUsedIdentifiers(tree); + new SuppressibleTreePathScanner(state) { + @Override + public Void visitClass(ClassTree node, Void unused) { + if ((getSymbol(node).flags() & Flags.FINAL) != 0) { + handle(node, node.getTypeParameters()); + } + return super.visitClass(node, null); + } + + @Override + public Void visitMethod(MethodTree node, Void unused) { + var symbol = getSymbol(node); + if (methodCanBeOverridden(symbol) + || !findSuperMethods(symbol, state.getTypes()).isEmpty()) { + return null; + } + handle(node, node.getTypeParameters()); + return super.visitMethod(node, null); + } + + private void handle(Tree tree, List typeParameters) { + for (TypeParameterTree typeParameter : typeParameters) { + if (usedIdentifiers.count(getSymbol(typeParameter)) == 1) { + state.reportMatch( + describeMatch( + typeParameter, + removeTypeParameter(tree, typeParameter, typeParameters, state))); + } + } + } + }.scan(state.getPath(), null); + return Description.NO_MATCH; + } + + private static ImmutableMultiset findUsedIdentifiers( + CompilationUnitTree tree) { + ImmutableMultiset.Builder identifiers = ImmutableMultiset.builder(); + new TreeScanner() { + @Override + public Void scan(Tree tree, Void unused) { + var symbol = getSymbol(tree); + if (symbol instanceof TypeVariableSymbol) { + identifiers.add((TypeVariableSymbol) symbol); + } + return super.scan(tree, unused); + } + }.scan(tree, null); + return identifiers.build(); + } + + private static SuggestedFix removeTypeParameter( + Tree tree, + TypeParameterTree typeParameter, + List typeParameters, + VisitorState state) { + if (typeParameters.size() > 1) { + return removeElement(typeParameter, typeParameters, state); + } + var tokens = + ErrorProneTokens.getTokens( + state.getSourceForNode(tree), getStartPosition(tree), state.context); + int startPos = + tokens.reverse().stream() + .filter( + t -> t.pos() <= getStartPosition(typeParameter) && t.kind().equals(TokenKind.LT)) + .findFirst() + .get() + .pos(); + int endPos = + tokens.stream() + .filter( + t -> + t.endPos() >= state.getEndPosition(getLast(typeParameters)) + && (t.kind().equals(TokenKind.GT) || t.kind().equals(TokenKind.GTGT))) + .findFirst() + .get() + .endPos(); + return SuggestedFix.replace(startPos, endPos, ""); + } +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/WildcardImport.java b/core/src/main/java/com/google/errorprone/bugpatterns/WildcardImport.java index 763b20c0219..73d550b5127 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/WildcardImport.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/WildcardImport.java @@ -32,6 +32,7 @@ import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.CaseTree; import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.ImportTree; import com.sun.source.tree.MemberSelectTree; @@ -43,10 +44,15 @@ import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; import com.sun.tools.javac.tree.JCTree.JCIdent; import com.sun.tools.javac.tree.TreeScanner; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; import javax.lang.model.element.ElementKind; @@ -59,6 +65,7 @@ tags = StandardTags.STYLE, link = "https://google.github.io/styleguide/javaguide.html?cl=head#s3.3.1-wildcard-imports") public class WildcardImport extends BugChecker implements CompilationUnitTreeMatcher { + private static final Logger logger = Logger.getLogger(WildcardImport.class.getName()); /** Maximum number of members to import before switching to qualified names. */ public static final int MAX_MEMBER_IMPORTS = 20; @@ -213,6 +220,24 @@ static Fix createFix( return fix.build(); } + private static final MethodHandle CONSTANT_CASE_LABEL_TREE_GET_EXPRESSION; + + static { + MethodHandle h; + try { + Class constantCaseLabelTree = Class.forName("com.sun.source.tree.ConstantCaseLabelTree"); + h = + MethodHandles.lookup() + .findVirtual( + constantCaseLabelTree, + "getConstantExpression", + MethodType.methodType(ExpressionTree.class)); + } catch (ReflectiveOperationException e) { + h = null; + } + CONSTANT_CASE_LABEL_TREE_GET_EXPRESSION = h; + } + /** * Add an import for {@code owner}, and qualify all on demand imported references to members of * owner by owner's simple name. @@ -228,11 +253,27 @@ public Void visitIdentifier(IdentifierTree tree, Void unused) { return null; } Tree parent = getCurrentPath().getParentPath().getLeaf(); - if (parent.getKind() == Tree.Kind.CASE - && ((CaseTree) parent).getExpression().equals(tree) - && sym.owner.getKind() == ElementKind.ENUM) { - // switch cases can refer to enum constants by simple name without importing them - return null; + if (sym.owner.getKind() == ElementKind.ENUM) { + if (parent.getKind() == Tree.Kind.CASE + && ((CaseTree) parent).getExpression().equals(tree)) { + // switch cases can refer to enum constants by simple name without importing them + return null; + } + // In JDK 19, the tree representation of enum case-labels changes. We can't reference the + // relevant API directly because then this code wouldn't compile on earlier JDK versions. + // So instead we use method handles. The straightforward code would be: + // if (parent.getKind() == Tree.Kind.CONSTANT_CASE_LABEL + // && tree.equals(((ConstantCaseLabelTree) parent).getConstantExpression())) {...} + if (parent.getKind().name().equals("CONSTANT_CASE_LABEL")) { + try { + if (tree.equals(CONSTANT_CASE_LABEL_TREE_GET_EXPRESSION.invoke(parent))) { + return null; + } + } catch (Throwable e) { + // MethodHandle.invoke obliges us to catch Throwable here. + logger.log(Level.SEVERE, "Could not compare trees", e); + } + } } if (sym.owner.equals(owner) && unit.starImportScope.includes(sym)) { fix.prefixWith(tree, owner.getSimpleName() + "."); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/android/IsLoggableTagLength.java b/core/src/main/java/com/google/errorprone/bugpatterns/android/IsLoggableTagLength.java index 20740965663..2602353ea37 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/android/IsLoggableTagLength.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/android/IsLoggableTagLength.java @@ -112,6 +112,7 @@ private static VariableTree findEnclosingIdentifier( .findEnclosing(ClassTree.class) .accept( new TreeScanner() { + @Nullable @Override public VariableTree visitVariable(VariableTree node, Void p) { return getSymbol(node).equals(identifierSymbol) ? node : null; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/apidiff/ApiDiffChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/apidiff/ApiDiffChecker.java index aaee0fb2b55..a2d1afcafe9 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/apidiff/ApiDiffChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/apidiff/ApiDiffChecker.java @@ -38,6 +38,7 @@ import com.sun.tools.javac.code.Types; import java.lang.annotation.Annotation; import java.util.Optional; +import org.checkerframework.checker.nullness.qual.Nullable; /** A base Error Prone check implementation to enforce compliance with a given API diff. */ public abstract class ApiDiffChecker extends BugChecker @@ -125,7 +126,7 @@ private boolean hasAnnotationForbiddingUse(Symbol sym, VisitorState state) { * Finds the class of the expression's receiver: the declaring class of a static member access, or * the type that an instance member is accessed on. */ - private static ClassSymbol getReceiver(ExpressionTree tree, Symbol sym) { + private static @Nullable ClassSymbol getReceiver(ExpressionTree tree, Symbol sym) { if (sym.isStatic() || sym instanceof ClassSymbol) { return sym.enclClass(); } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/ArgumentChangeFinder.java b/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/ArgumentChangeFinder.java index 4a38cb00f26..8b6bf999aa0 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/ArgumentChangeFinder.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/ArgumentChangeFinder.java @@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.function.Function; /** @@ -58,6 +59,7 @@ abstract static class Builder { * eliminating spurious findings. Heuristics are applied in order so add more expensive checks * last. */ + @CanIgnoreReturnValue Builder addHeuristic(Heuristic heuristic) { heuristicsBuilder().add(heuristic); return this; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/EnclosedByReverseHeuristic.java b/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/EnclosedByReverseHeuristic.java index 2ade769023c..54935fa59b6 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/EnclosedByReverseHeuristic.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/EnclosedByReverseHeuristic.java @@ -24,6 +24,7 @@ import com.sun.source.tree.Tree; import com.sun.tools.javac.code.Symbol.MethodSymbol; import java.util.Optional; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Detect whether the method invocation we are examining is enclosed by either a method or a class @@ -74,7 +75,7 @@ public boolean isAcceptableChange( return findReverseWordsMatchInParentNodes(state) == null; } - protected String findReverseWordsMatchInParentNodes(VisitorState state) { + protected @Nullable String findReverseWordsMatchInParentNodes(VisitorState state) { for (Tree tree : state.getPath()) { Optional name = getName(tree); if (name.isPresent()) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/LowInformationNameHeuristic.java b/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/LowInformationNameHeuristic.java index 2e0a6df3601..1aa2adaedef 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/LowInformationNameHeuristic.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/LowInformationNameHeuristic.java @@ -20,6 +20,7 @@ import com.google.errorprone.VisitorState; import com.sun.source.tree.Tree; import com.sun.tools.javac.code.Symbol.MethodSymbol; +import org.checkerframework.checker.nullness.qual.Nullable; /** * A heuristic for checking if a formal parameter matches a predefined set of words which have been @@ -58,7 +59,7 @@ public boolean isAcceptableChange( * Return the first regular expression from the list of overloaded words which matches the * parameter name. */ - protected String findMatch(Parameter parameter) { + protected @Nullable String findMatch(Parameter parameter) { for (String regex : overloadedNamesRegexs) { if (parameter.name().matches(regex)) { return regex; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/BuilderReturnThis.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/BuilderReturnThis.java new file mode 100644 index 00000000000..36d177867b5 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/BuilderReturnThis.java @@ -0,0 +1,150 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns.checkreturnvalue; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.fixes.SuggestedFixes.qualifyType; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.isSameType; +import static com.google.errorprone.util.ASTHelpers.isSubtype; +import static java.lang.Boolean.TRUE; + +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.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.ConditionalExpressionTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.ParenthesizedTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.util.TreeScanner; +import com.sun.tools.javac.code.Symbol.ClassSymbol; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Type; + +/** Discourages builder instance methods that do not return 'this'. */ +@BugPattern(summary = "Builder instance method does not return 'this'", severity = WARNING) +public class BuilderReturnThis extends BugChecker implements MethodTreeMatcher { + + private static final String CRV = "com.google.errorprone.annotations.CheckReturnValue"; + + @Override + public Description matchMethod(MethodTree tree, VisitorState state) { + MethodSymbol sym = getSymbol(tree); + if (tree.getBody() == null) { + return NO_MATCH; + } + if (!instanceReturnsBuilder(sym, state)) { + return NO_MATCH; + } + if (!nonThisReturns(tree, state)) { + return NO_MATCH; + } + SuggestedFix.Builder fix = SuggestedFix.builder(); + String crvName = qualifyType(state, fix, CRV); + fix.prefixWith(tree, "@" + crvName + "\n"); + return describeMatch(tree, fix.build()); + } + + private static boolean instanceReturnsBuilder(MethodSymbol sym, VisitorState state) { + // instance methods + if (sym.isStatic()) { + return false; + } + // declared in a class with the simple name that contains Builder + ClassSymbol enclosingClass = sym.owner.enclClass(); + if (!enclosingClass.getSimpleName().toString().endsWith("Builder")) { + return false; + } + // whose return type is the exact type of this + // or perhaps "a non-Object supertype of the this-type", for interfaces + Type returnType = sym.getReturnType(); + if (!isSubtype(enclosingClass.asType(), returnType, state) + || isSameType(returnType, state.getSymtab().objectType, state)) { + return false; + } + return true; + } + + // TODO(b/236055787): consolidate heuristics for 'return this;' + boolean nonThisReturns(MethodTree tree, VisitorState state) { + + boolean[] result = {false}; + new TreeScanner() { + @Override + public Void visitLambdaExpression(LambdaExpressionTree tree, Void unused) { + return null; + } + + @Override + public Void visitMethod(MethodTree tree, Void unused) { + return null; + } + + @Override + public Void visitReturn(ReturnTree tree, Void unused) { + if (!returnsThis(tree.getExpression())) { + result[0] = true; + } + return super.visitReturn(tree, null); + } + + private boolean returnsThis(ExpressionTree tree) { + return firstNonNull( + new TreeScanner() { + @Override + public Boolean visitIdentifier(IdentifierTree tree, Void unused) { + return tree.getName().contentEquals("this"); + } + + @Override + public Boolean visitMethodInvocation(MethodInvocationTree tree, Void unused) { + return instanceReturnsBuilder(getSymbol(tree), state); + } + + @Override + public Boolean visitConditionalExpression( + ConditionalExpressionTree tree, Void unused) { + return TRUE.equals(tree.getFalseExpression().accept(this, null)) + && TRUE.equals(tree.getTrueExpression().accept(this, null)); + } + + @Override + public Boolean visitParenthesized(ParenthesizedTree tree, Void unused) { + return tree.getExpression().accept(this, null); + } + + @Override + public Boolean visitTypeCast(TypeCastTree tree, Void unused) { + return tree.getExpression().accept(this, null); + } + }.scan(tree, null), + false); + } + }.scan(tree.getBody(), null); + return result[0]; + } +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java new file mode 100644 index 00000000000..87f861c3594 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java @@ -0,0 +1,222 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns.checkreturnvalue; + +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.fixes.SuggestedFixes.qualifyType; +import static com.google.errorprone.util.ASTHelpers.getReceiver; +import static com.google.errorprone.util.ASTHelpers.getReturnType; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.getType; +import static com.google.errorprone.util.ASTHelpers.hasAnnotation; +import static com.google.errorprone.util.ASTHelpers.isSubtype; +import static com.google.errorprone.util.ASTHelpers.isVoidType; +import static com.google.errorprone.util.ASTHelpers.stripParentheses; + +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.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.suppliers.Supplier; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.StatementTree; +import com.sun.source.util.TreeScanner; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Type; + +/** + * Checker that recommends annotating a method with {@code @CanIgnoreReturnValue} if the method + * returns {@code this} (or other methods that are likely to also just return {@code this}). + */ +@BugPattern( + summary = "Methods that always 'return this' should be annotated with @CanIgnoreReturnValue", + severity = WARNING) +public final class CanIgnoreReturnValueSuggester extends BugChecker implements MethodTreeMatcher { + private static final String CRV = "com.google.errorprone.annotations.CheckReturnValue"; + private static final String CIRV = "com.google.errorprone.annotations.CanIgnoreReturnValue"; + + private static final Supplier PROTO_BUILDER = + VisitorState.memoize(s -> s.getTypeFromString("com.google.protobuf.MessageLite.Builder")); + + // TODO(kak): catch places where an input parameter is always returned + + @Override + public Description matchMethod(MethodTree methodTree, VisitorState state) { + MethodSymbol methodSymbol = getSymbol(methodTree); + + // We have a number of preconditions we can check early to ensure that this method could + // possibly be @CIRV-suggestible, before attempting a deeper scan of the method. + if (methodSymbol.isStatic() + || methodTree.getBody() == null + // These methods should probably be @CheckReturnValue! + || isDefinitionOfZeroArgSelf(methodSymbol) + // Constructors can't "return", and generally shouldn't be @CIRV + || methodTree.getReturnType() == null + // methods whose return type is void or Void can't have @CIRV + || isVoidType(getType(methodTree.getReturnType()), state) + // b/236423646 - These methods that do nothing *but* `return this;` are likely to be + // overridden in other contexts, and we've decided that these methods shouldn't be annotated + // automatically. + || isSimpleReturnThisMethod(methodTree) + // TODO(kak): This appears to be a performance optimization for refactoring passes? + || isSubtype(methodSymbol.owner.type, PROTO_BUILDER.get(state), state) + || hasAnnotation(methodSymbol, CIRV, state)) { + return Description.NO_MATCH; + } + + // if the enclosing type is already annotated with CIRV, we could theoretically _not_ directly + // annotate the method but we're likely to discourage annotating types with CIRV: b/229776283 + // if the method is already directly annotated w/ @CRV, bail out + if (hasAnnotation(methodTree, CRV, state)) { + // TODO(kak): we might want to actually _remove_ @CRV and add @CIRV in this case! + return Description.NO_MATCH; + } + + // OK, now the real implementation: For each possible return branch, does the expression + // returned look like "this" or instance methods that are also @CanIgnoreReturnValue. + if (!methodReturnsIgnorableValues(methodTree, state)) { + return Description.NO_MATCH; + } + + SuggestedFix.Builder fix = SuggestedFix.builder(); + String cirvName = qualifyType(state, fix, CIRV); + // we could add a trailing comment (e.g., @CanIgnoreReturnValue // returns `this`), but all + // developers will become familiar with these annotations sooner or later + fix.prefixWith(methodTree, "@" + cirvName + "\n"); + + return describeMatch(methodTree, fix.build()); + } + + private static boolean isSimpleReturnThisMethod(MethodTree methodTree) { + if (methodTree.getBody().getStatements().size() == 1) { + StatementTree onlyStatement = methodTree.getBody().getStatements().get(0); + if (onlyStatement instanceof ReturnTree) { + return returnsThisOrSelf((ReturnTree) onlyStatement); + } + } + return false; + } + + private static boolean isIdentifier(ExpressionTree expr, String identifierName) { + expr = stripParentheses(expr); + if (expr instanceof IdentifierTree) { + return ((IdentifierTree) expr).getName().contentEquals(identifierName); + } + return false; + } + + /** Returns whether or not the given {@link ReturnTree} returns exactly {@code this}. */ + private static boolean returnsThisOrSelf(ReturnTree returnTree) { + return isIdentifier(returnTree.getExpression(), "this") + || (returnTree.getExpression() instanceof MethodInvocationTree + && isCallToZeroArgSelf((MethodInvocationTree) returnTree.getExpression())); + } + + // this.self() or self() + private static boolean isCallToZeroArgSelf(MethodInvocationTree mit) { + if (!mit.getArguments().isEmpty()) { + return false; + } + if (isIdentifier(mit.getMethodSelect(), "self")) { + return true; + } + if (mit.getMethodSelect() instanceof MemberSelectTree) { + MemberSelectTree methodSelect = (MemberSelectTree) mit.getMethodSelect(); + return isIdentifier(methodSelect.getExpression(), "this") + && methodSelect.getIdentifier().contentEquals("self"); + } + return false; + } + + private static boolean isDefinitionOfZeroArgSelf(MethodSymbol methodSymbol) { + return methodSymbol.getSimpleName().contentEquals("self") + && methodSymbol.getParameters().isEmpty(); + } + + private static boolean methodReturnsIgnorableValues(MethodTree tree, VisitorState state) { + class ReturnValuesFromMethodAreIgnorable extends TreeScanner { + private final VisitorState state; + private final Type enclosingClassType; + private final Type methodReturnType; + private boolean atLeastOneReturn = false; + private boolean allReturnsIgnorable = true; + + private ReturnValuesFromMethodAreIgnorable(VisitorState state, MethodSymbol methSymbol) { + this.state = state; + this.methodReturnType = methSymbol.getReturnType(); + this.enclosingClassType = methSymbol.enclClass().type; + } + + @Override + public Void visitReturn(ReturnTree returnTree, Void unused) { + atLeastOneReturn = true; + if (!returnsThisOrSelf(returnTree) + && !isIgnorableMethodCallOnSameInstance(returnTree, state)) { + allReturnsIgnorable = false; + } + // Don't descend deeper into returns, since we already checked the body of this return. + return null; + } + + private boolean isIgnorableMethodCallOnSameInstance( + ReturnTree returnTree, VisitorState state) { + if (returnTree.getExpression() instanceof MethodInvocationTree) { + MethodInvocationTree mit = (MethodInvocationTree) returnTree.getExpression(); + ExpressionTree receiver = getReceiver(mit); + MethodSymbol calledMethod = getSymbol(mit); + if ((receiver == null && !calledMethod.isStatic()) + || isIdentifier(receiver, "this") + || isIdentifier(receiver, "super")) { + // If the method we're calling is @CIRV and the enclosing class could be represented by + // the object being returned by the other method, then it's probable that the other + // method is likely to + // be an ignorable result. + return hasAnnotation(calledMethod, CIRV, state) + && isSubtype(enclosingClassType, methodReturnType, state) + && isSubtype(enclosingClassType, getReturnType(mit), state); + } + } + return false; + } + + @Override + public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) { + // don't descend into lambdas + return null; + } + + @Override + public Void visitNewClass(NewClassTree node, Void unused) { + // don't descend into declarations of anonymous classes + return null; + } + } + + var scanner = new ReturnValuesFromMethodAreIgnorable(state, getSymbol(tree)); + scanner.scan(tree, null); + return scanner.atLeastOneReturn && scanner.allReturnsIgnorable; + } +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ExternalCanIgnoreReturnValue.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ExternalCanIgnoreReturnValue.java index b64fc90de99..520d7d42f5b 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ExternalCanIgnoreReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ExternalCanIgnoreReturnValue.java @@ -61,6 +61,7 @@ private ExternalCanIgnoreReturnValue() {} .errorProneOptions() .getFlags() .get(EXTERNAL_API_EXCLUSION_LIST) + .filter(s -> !s.isEmpty()) .map( filename -> loadConfigListFromFile(filename, state.errorProneOptions().getFlags())) @@ -80,7 +81,7 @@ public Optional evaluateMethod(MethodSymbol method, VisitorStat /** Encapsulates asking "does this API match the list of APIs I care about"? */ @FunctionalInterface - private interface MethodPredicate { + interface MethodPredicate { boolean methodMatches(MethodSymbol methodSymbol, VisitorState state); } @@ -90,26 +91,25 @@ private interface MethodPredicate { enum ConfigParser { AS_STRINGS { @Override - MethodPredicate load(CharSource file) throws IOException { - return configByInterpretingMethodsAsStrings(file); + MethodPredicate load(String file, ErrorProneFlags flags) throws IOException { + return configByInterpretingMethodsAsStrings(MoreFiles.asCharSource(Paths.get(file), UTF_8)); } }, PARSE_TOKENS { @Override - MethodPredicate load(CharSource file) throws IOException { - return configByParsingApiObjects(file); + MethodPredicate load(String file, ErrorProneFlags flags) throws IOException { + return configByParsingApiObjects(MoreFiles.asCharSource(Paths.get(file), UTF_8)); } }; - abstract MethodPredicate load(CharSource file) throws IOException; + abstract MethodPredicate load(String file, ErrorProneFlags flags) throws IOException; } private static MethodPredicate loadConfigListFromFile(String filename, ErrorProneFlags flags) { ConfigParser configParser = flags.getEnum(EXCLUSION_LIST_PARSER, ConfigParser.class).orElse(ConfigParser.AS_STRINGS); try { - CharSource file = MoreFiles.asCharSource(Paths.get(filename), UTF_8); - return configParser.load(file); + return configParser.load(filename, flags); } catch (IOException e) { throw new UncheckedIOException( "Could not load external resource for CanIgnoreReturnValue", e); @@ -133,20 +133,7 @@ public boolean methodMatches(MethodSymbol methodSymbol, VisitorState state) { private String apiSignature(MethodSymbol methodSymbol, Types types) { return methodSymbol.owner.getQualifiedName() + "#" - + methodSymbol.name - + "(" - + paramsString(methodSymbol, types) - + ")"; - } - - private String paramsString(MethodSymbol symbol, Types types) { - if (symbol.params().isEmpty()) { - return ""; - } - return String.join( - ",", - Iterables.transform( - symbol.params(), p -> fullyErasedAndUnannotatedType(p.type, types))); + + methodNameAndParams(methodSymbol, types); } }; } @@ -160,7 +147,7 @@ private static MethodPredicate configByParsingApiObjects(CharSource file) throws .collect(toImmutableSetMultimap(Api::className, api -> api)); } return (methodSymbol, state) -> - apis.get(methodSymbol.enclClass().getQualifiedName().toString()).stream() + apis.get(surroundingClass(methodSymbol)).stream() .anyMatch( api -> methodSymbol.getSimpleName().contentEquals(api.methodName()) @@ -168,10 +155,26 @@ && methodParametersMatch( api.parameterTypes(), methodSymbol.params(), state.getTypes())); } + public static String surroundingClass(MethodSymbol methodSymbol) { + return methodSymbol.enclClass().getQualifiedName().toString(); + } + + public static String methodNameAndParams(MethodSymbol methodSymbol, Types types) { + return methodSymbol.name + "(" + paramsString(types, methodSymbol.params()) + ")"; + } + private static boolean methodParametersMatch( ImmutableList parameters, List methodParams, Types types) { return Iterables.elementsEqual( parameters, Iterables.transform(methodParams, p -> fullyErasedAndUnannotatedType(p.type, types))); } + + private static String paramsString(Types types, List params) { + if (params.isEmpty()) { + return ""; + } + return String.join( + ",", Iterables.transform(params, p -> fullyErasedAndUnannotatedType(p.type, types))); + } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClasses.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClasses.java new file mode 100644 index 00000000000..cfbd907270d --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClasses.java @@ -0,0 +1,247 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns.checkreturnvalue; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; +import static com.google.errorprone.fixes.SuggestedFixes.qualifyType; +import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE; +import static com.google.errorprone.matchers.Matchers.annotations; +import static com.google.errorprone.matchers.Matchers.isType; +import static com.google.errorprone.util.ASTHelpers.enclosingClass; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.getType; +import static com.google.errorprone.util.ASTHelpers.hasAnnotation; +import static com.google.errorprone.util.ASTHelpers.isConsideredFinal; +import static com.google.errorprone.util.ASTHelpers.isGeneratedConstructor; +import static com.google.errorprone.util.ASTHelpers.isVoidType; + +import com.google.common.annotations.VisibleForTesting; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.MultiMatcher; +import com.google.errorprone.matchers.MultiMatcher.MultiMatchResult; +import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.SimpleTreeVisitor; +import com.sun.source.util.TreePathScanner; +import com.sun.tools.javac.code.Symbol.ClassSymbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Checker that "pushes" the {@code @CanIgnoreReturnValue} annotation down from classes to methods. + */ +@BugPattern( + summary = + "@CanIgnoreReturnValue should not be applied to classes as it almost always overmatches (as" + + " it applies to constructors and all methods), and the CIRVness isn't conferred to" + + " its subclasses.", + severity = ERROR) +public final class NoCanIgnoreReturnValueOnClasses extends BugChecker implements ClassTreeMatcher { + private static final String CRV = "com.google.errorprone.annotations.CheckReturnValue"; + private static final String CIRV = "com.google.errorprone.annotations.CanIgnoreReturnValue"; + + private static final String EXTRA_SUFFIX = ""; + + @VisibleForTesting + static final String METHOD_COMMENT = " // pushed down from class to method;" + EXTRA_SUFFIX; + + @VisibleForTesting + static final String CTOR_COMMENT = " // pushed down from class to constructor;" + EXTRA_SUFFIX; + + private static final MultiMatcher HAS_CIRV_ANNOTATION = + annotations(AT_LEAST_ONE, isType(CIRV)); + + @Override + public Description matchClass(ClassTree tree, VisitorState state) { + MultiMatchResult cirvAnnotation = + HAS_CIRV_ANNOTATION.multiMatchResult(tree, state); + // if the class isn't directly annotated w/ @CIRV, bail out + if (!cirvAnnotation.matches()) { + return Description.NO_MATCH; + } + + SuggestedFix.Builder fix = SuggestedFix.builder(); + String cirvName = qualifyType(state, fix, CIRV); + + // remove @CIRV from the class + fix.delete(cirvAnnotation.onlyMatchingNode()); + + // theoretically, we could also add @CRV to the class, since all APIs will have CIRV pushed down + // onto them, but it's very likely that a larger enclosing scope will already be @CRV (otherwise + // why did the user annotate this class as @CIRV?) + + // scan the tree and add @CIRV to all non-void method declarations that aren't already annotated + // with @CIRV or @CRV + new TreePathScanner() { + @Override + public Void visitClass(ClassTree classTree, Void unused) { + // stop descending when we reach a class that's marked @CRV + return hasAnnotation(classTree, CRV, state) ? null : super.visitClass(classTree, unused); + } + + @Override + public Void visitMethod(MethodTree methodTree, Void unused) { + if (shouldAddCirv(methodTree, state)) { + String trailingComment = null; + + if (methodTree.getReturnType() == null) { // constructor + trailingComment = CTOR_COMMENT; + } else if (alwaysReturnsThis()) { + trailingComment = ""; + } else { + trailingComment = METHOD_COMMENT; + } + + fix.prefixWith(methodTree, "@" + cirvName + trailingComment + "\n"); + } + // TODO(kak): we could also consider removing CRV from individual methods (since the + // enclosing class is now annotated as CRV. + return null; + } + + private boolean alwaysReturnsThis() { + // TODO(b/236055787): share this TreePathScanner + AtomicBoolean allReturnThis = new AtomicBoolean(true); + AtomicBoolean atLeastOneReturn = new AtomicBoolean(false); + + new TreePathScanner() { + private final Set thises = new HashSet<>(); + + @Override + public Void visitVariable(VariableTree variableTree, Void unused) { + VarSymbol symbol = getSymbol(variableTree); + if (isConsideredFinal(symbol) && maybeCastThis(variableTree.getInitializer())) { + thises.add(symbol); + } + return super.visitVariable(variableTree, null); + } + + @Override + public Void visitReturn(ReturnTree returnTree, Void unused) { + atLeastOneReturn.set(true); + if (!isThis(returnTree.getExpression())) { + allReturnThis.set(false); + // once we've set allReturnThis to false, no need to descend further + return null; + } + return super.visitReturn(returnTree, null); + } + + /** Returns whether the given {@link ExpressionTree} is {@code this}. */ + private boolean isThis(ExpressionTree returnExpression) { + return maybeCastThis(returnExpression) || thises.contains(getSymbol(returnExpression)); + } + + @Override + public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) { + // don't descend into lambdas + return null; + } + + @Override + public Void visitNewClass(NewClassTree node, Void unused) { + // don't descend into declarations of anonymous classes + return null; + } + + private boolean maybeCastThis(Tree tree) { + return firstNonNull( + new SimpleTreeVisitor() { + + @Override + public Boolean visitTypeCast(TypeCastTree tree, Void unused) { + return visit(tree.getExpression(), null); + } + + @Override + public Boolean visitIdentifier(IdentifierTree tree, Void unused) { + return tree.getName().contentEquals("this"); + } + + @Override + public Boolean visitMethodInvocation(MethodInvocationTree tree, Void unused) { + return getSymbol(tree).getSimpleName().contentEquals("self"); + } + }.visit(tree, null), + false); + } + }.scan(getCurrentPath(), null); + + return allReturnThis.get() && atLeastOneReturn.get(); + } + + private boolean shouldAddCirv(MethodTree methodTree, VisitorState state) { + if (isVoidType(getType(methodTree.getReturnType()), state)) { // void return types + return false; + } + if (hasAnnotation(methodTree, CIRV, state)) { + return false; + } + if (hasAnnotation(methodTree, CRV, state)) { + return false; + } + // if the constructor is implicit, don't add CIRV (we can't annotate a synthetic node!) + if (isGeneratedConstructor(methodTree)) { + return false; + } + // if the method is inside an AV or AV.Builder and is abstract (no body), don't add CIRV + ClassSymbol enclosingClass = enclosingClass(getSymbol(methodTree)); + if (hasAnnotation(enclosingClass, "com.google.auto.value.AutoValue", state) + || hasAnnotation(enclosingClass, "com.google.auto.value.AutoValue.Builder", state)) { + if (methodTree.getBody() == null) { + return false; + } + } + // TODO(kak): should we also return false for private methods? I'm betting most of them are + // "accidentally" CIRV'ed by the enclosing class; any compile errors would be caught by + // building the enclosing class anyways. + return true; + } + + @Override + public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) { + // don't descend into lambdas + return null; + } + + @Override + public Void visitNewClass(NewClassTree node, Void unused) { + // don't descend into declarations of anonymous classes + return null; + } + }.scan(state.getPath(), null); + return describeMatch(tree, fix.build()); + } +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/PackagesRule.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/PackagesRule.java new file mode 100644 index 00000000000..4402771e897 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/PackagesRule.java @@ -0,0 +1,82 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns.checkreturnvalue; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.SymbolRule; +import com.google.errorprone.suppliers.Supplier; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symbol.PackageSymbol; +import com.sun.tools.javac.util.Name; +import java.util.Optional; + +/** + * A rule that enables checking for methods belonging to a set of packages or any of their + * subpackages. + */ +public final class PackagesRule extends SymbolRule { + + /** + * Returns a new rule using the given package {@code patterns}. Each pattern string must either be + * the fully qualified name of a package (to enable checking for methods in that package and its + * subpackages) or a {@code -} character followed by the fully qualified name of a package (to + * disable checking for methods in that package and its subpackages). + */ + public static PackagesRule fromPatterns(Iterable patterns) { + return new PackagesRule(ImmutableList.copyOf(patterns)); + } + + private final Supplier> packagesSupplier; + + private PackagesRule(ImmutableList patterns) { + this.packagesSupplier = + VisitorState.memoize( + state -> { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (String pattern : patterns) { + if (pattern.charAt(0) == '-') { + builder.put(state.getName(pattern.substring(1)), false); + } else { + builder.put(state.getName(pattern), true); + } + } + return builder.buildOrThrow(); + }); + } + + @Override + public final String id() { + return "Packages"; + } + + @Override + public Optional evaluate(Symbol symbol, VisitorState state) { + while (symbol instanceof PackageSymbol) { + Boolean value = packagesSupplier.get(state).get(((PackageSymbol) symbol).fullname); + if (value != null) { + return value + ? Optional.of(ResultUsePolicy.EXPECTED) + // stop evaluating if the package matched a negative pattern + : Optional.empty(); + } + symbol = symbol.owner; + } + return Optional.empty(); + } +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java index ae3c334d82b..8350f68c5a1 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java @@ -53,8 +53,10 @@ public static ResultUseRule mutableProtos() { /** Rules for methods on protos. */ private static final class ProtoRule extends MethodRule { - private static final Pattern RETURNS_THIS = - Pattern.compile("(add|clear|merge|remove|set|put).*"); + // Methods that start this way produce a modification to the proto, and either return this + // or return the parameter given, for chaining purposes. + private static final Pattern NAMED_MUTATOR_METHOD = + Pattern.compile("(add|clear|insert|merge|remove|set|put).*"); private final Supplier parentType; private final String id; @@ -73,11 +75,10 @@ public String id() { public Optional evaluateMethod(MethodSymbol method, VisitorState state) { if (isProtoSubtype(method.owner.type, state)) { String methodName = method.name.toString(); - if (RETURNS_THIS.matcher(methodName).matches()) { + if (NAMED_MUTATOR_METHOD.matcher(methodName).matches()) { return Optional.of(ResultUsePolicy.OPTIONAL); } - if (isGetterOfSubmessageBuilder(methodName) - && isProtoSubtype(method.getReturnType(), state)) { + if (isMutatingAccessorMethod(methodName) && isProtoSubtype(method.getReturnType(), state)) { return Optional.of(ResultUsePolicy.OPTIONAL); } } @@ -88,11 +89,16 @@ private boolean isProtoSubtype(Type ownerType, VisitorState state) { return isSubtype(ownerType, parentType.get(state), state); } - // fooBuilder.getBarBuilder() mutates the builder such that foo.hasBar() is now true. - private static boolean isGetterOfSubmessageBuilder(String name) { + private static boolean isMutatingAccessorMethod(String name) { // TODO(glorioso): Any other naming conventions to check? // TODO(glorioso): Maybe worth making this a regex instead? But think about performance - return name.startsWith("get") && name.endsWith("Builder") && !name.endsWith("OrBuilder"); + if (name.startsWith("get")) { + // fooBuilder.getBarBuilder() mutates the builder such that foo.hasBar() is now true. + return (name.endsWith("Builder") && !name.endsWith("OrBuilder")) + // mutableFoo.getMutableBar() mutates Foo so that mutableFoo.hasBar() is now true + || name.startsWith("getMutable"); + } + return false; } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/AbstractCollectionIncompatibleTypeMatcher.java b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/AbstractCollectionIncompatibleTypeMatcher.java index 995e9a42cdf..59184c9956f 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/AbstractCollectionIncompatibleTypeMatcher.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/AbstractCollectionIncompatibleTypeMatcher.java @@ -160,6 +160,7 @@ public MatchResult visitMemberReference( }.visit(tree, null); } + @Nullable private MatchResult getMatchResult( @Nullable ExpressionTree sourceTree, @Nullable Type sourceType, @Nullable Type targetType) { if (sourceTree == null || sourceType == null || targetType == null) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/CompatibleWithMisuse.java b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/CompatibleWithMisuse.java index d087ac9644b..e81e5eedd47 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/CompatibleWithMisuse.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/CompatibleWithMisuse.java @@ -44,6 +44,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; /** * @author glorioso@google.com (Nick Glorioso) @@ -127,7 +128,7 @@ public Description matchAnnotation(AnnotationTree annoTree, VisitorState state) // => X // This function assumes the annotation tree will only have one argument, of type String, that // is required. - private static String valueArgumentFromCompatibleWithAnnotation(AnnotationTree tree) { + private static @Nullable String valueArgumentFromCompatibleWithAnnotation(AnnotationTree tree) { ExpressionTree argumentValue = Iterables.getOnlyElement(tree.getArguments()); if (argumentValue.getKind() != Kind.ASSIGNMENT) { // :-| Annotation symbol broken. Punt? diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/IncompatibleArgumentType.java b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/IncompatibleArgumentType.java index b2e5c72032a..d1e864b5263 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/IncompatibleArgumentType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/IncompatibleArgumentType.java @@ -225,6 +225,7 @@ private static RequiredType resolveRequiredTypeForThisCall( return requiredType; } + @Nullable private static RequiredType resolveTypeFromGenericMethod( Type calledMethodType, MethodSymbol declaredMethod, String typeArgName) { int tyargIndex = findTypeArgInList(declaredMethod, typeArgName); @@ -236,6 +237,7 @@ private static RequiredType resolveTypeFromGenericMethod( // Plumb through a type which is supposed to be a Types.Subst, then find the replacement // type that the compiler resolved. + @Nullable private static Type getTypeFromTypeMapping( Type m, MethodSymbol declaredMethod, String namedTypeArg) { ImmutableListMultimap substitutions = diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerArgumentToString.java b/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerArgumentToString.java index b7a9b762c2c..08e0a325c0e 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerArgumentToString.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerArgumentToString.java @@ -355,6 +355,7 @@ Description unwrapArguments( return describeMatch(tree, fix.build()); } + @Nullable private static Parameter unwrap(ExpressionTree argument, char placeholder, VisitorState state) { for (Unwrapper unwrapper : Unwrapper.values()) { if (unwrapper.matcher.matches(argument, state)) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerRedundantIsEnabled.java b/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerRedundantIsEnabled.java index e41106dc8bb..6d42b3f0fbe 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerRedundantIsEnabled.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerRedundantIsEnabled.java @@ -47,6 +47,7 @@ import com.sun.tools.javac.code.Symbol; import java.util.List; import java.util.Optional; +import org.checkerframework.checker.nullness.qual.Nullable; /** * @author mariasam@google.com (Maria Sam) @@ -136,7 +137,7 @@ private static ExpressionTree unwrap(ExpressionTree expr) { new SimpleTreeVisitor() { @Override - protected ExpressionTree defaultAction(Tree tree, Void unused) { + protected @Nullable ExpressionTree defaultAction(Tree tree, Void unused) { return tree instanceof ExpressionTree ? (ExpressionTree) tree : null; } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/StrictFormatStringValidation.java b/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/StrictFormatStringValidation.java index 2d5ec6ab1df..43c047cb73e 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/StrictFormatStringValidation.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/StrictFormatStringValidation.java @@ -100,6 +100,7 @@ public static ValidationResult validate( } /** Helps {@code validate()} validate a format string that is declared as a method parameter. */ + @Nullable private static ValidationResult validateFormatStringParameter( ExpressionTree formatStringTree, Symbol formatStringSymbol, @@ -219,6 +220,7 @@ public ValidationResult visitVariable(VariableTree node, Void unused) { return super.visitVariable(node, unused); } + @Nullable @Override public ValidationResult reduce(ValidationResult r1, ValidationResult r2) { if (r1 == null && r2 == null) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/AndroidInjectionBeforeSuper.java b/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/AndroidInjectionBeforeSuper.java index f7168ff2738..a378f57b23c 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/AndroidInjectionBeforeSuper.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/AndroidInjectionBeforeSuper.java @@ -42,6 +42,7 @@ import com.sun.source.tree.MethodTree; import com.sun.source.tree.VariableTree; import com.sun.source.util.SimpleTreeVisitor; +import org.checkerframework.checker.nullness.qual.Nullable; /** * @author Ron Shapiro @@ -127,7 +128,7 @@ private final class LifecycleMethodVisitor extends SimpleTreeVisitor No match + if (!ASTHelpers.hasAnnotation(enclosingMethodSym, "dagger.Provides", state)) { return Description.NO_MATCH; } + // Method is annotated as Nullable -> No match + if (ASTHelpers.hasDirectAnnotationWithSimpleName(enclosingMethodSym, "Nullable")) { + return Description.NO_MATCH; + } + // Type-use annotations do *NOT* work with Dagger. See b/117251022 + // You must use *any* non-type-use Nullable annotation. Fix addNullableFix = SuggestedFix.builder() diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/inlineme/Inliner.java b/core/src/main/java/com/google/errorprone/bugpatterns/inlineme/Inliner.java index 10885ba8f3b..feb5b8ef9e4 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/inlineme/Inliner.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/inlineme/Inliner.java @@ -240,7 +240,7 @@ && stringContainsComments(state.getSourceForNode(tree), state.context)) { boolean terminalVarargsReplacement = varargsWithEmptyArguments && i == varNames.size() - 1; String capturePrefixForVarargs = terminalVarargsReplacement ? "(?:,\\s*)?" : ""; // We want to avoid replacing a method invocation with the same name as the method. - Pattern extractArgAndNextToken = + var extractArgAndNextToken = Pattern.compile( "\\b" + capturePrefixForVarargs + Pattern.quote(varNames.get(i)) + "\\b([^(])"); String replacementResult = diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTag.java b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTag.java index 4c9dc0dc9fa..d3f7e055e2f 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTag.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTag.java @@ -192,7 +192,7 @@ public Void visitUnknownBlockTag(UnknownBlockTagTree unknownBlockTagTree, Void u String message = String.format( "@%1$s is not a valid tag, but is a parameter name. " - + "Use {@code %1%s} to refer to parameter names inline.", + + "Use {@code %1$s} to refer to parameter names inline.", tagName); state.reportMatch( buildDescription(diagnosticPosition(getCurrentPath(), state)) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidInlineTag.java b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidInlineTag.java index 2523d727a3c..95eed6453f9 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidInlineTag.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidInlineTag.java @@ -74,26 +74,42 @@ public final class InvalidInlineTag extends BugChecker private static final Splitter DOT_SPLITTER = Splitter.on('.'); + private void scanTags( + VisitorState state, Context context, ImmutableSet parameters, DocTreePath path) { + new InvalidTagChecker(state, context, parameters).scan(path, null); + } + + private enum Context { + CLASS(JavadocTag.VALID_CLASS_TAGS), + METHOD(JavadocTag.VALID_METHOD_TAGS), + VARIABLE(JavadocTag.VALID_VARIABLE_TAGS); + + final ImmutableSet validTags; + final Pattern misplacedCurly; + final Pattern parensRatherThanCurly; + + Context(ImmutableSet validTags) { + this.validTags = validTags; + String validInlineTags = + validTags.stream() + .filter(tag -> tag.type() == TagType.INLINE) + .map(JavadocTag::name) + .collect(joining("|")); + this.misplacedCurly = Pattern.compile(String.format("@(%s)\\{", validInlineTags)); + this.parensRatherThanCurly = Pattern.compile(String.format("\\(@(%s)", validInlineTags)); + } + } + @Override public Description matchClass(ClassTree classTree, VisitorState state) { DocTreePath path = Utils.getDocTreePath(state); if (path != null) { ImmutableSet parameters = ImmutableSet.of(); - scanTags(state, JavadocTag.VALID_CLASS_TAGS, parameters, path); + scanTags(state, Context.CLASS, parameters, path); } return Description.NO_MATCH; } - private void scanTags( - VisitorState state, - ImmutableSet tags, - ImmutableSet parameters, - DocTreePath path) { - try (InvalidTagChecker checker = new InvalidTagChecker(state, tags, parameters)) { - checker.scan(path, null); - } - } - @Override public Description matchMethod(MethodTree methodTree, VisitorState state) { DocTreePath path = Utils.getDocTreePath(state); @@ -102,7 +118,7 @@ public Description matchMethod(MethodTree methodTree, VisitorState state) { methodTree.getParameters().stream() .map(v -> v.getName().toString()) .collect(toImmutableSet()); - scanTags(state, JavadocTag.VALID_METHOD_TAGS, parameters, path); + scanTags(state, Context.METHOD, parameters, path); } return Description.NO_MATCH; } @@ -111,7 +127,7 @@ public Description matchMethod(MethodTree methodTree, VisitorState state) { public Description matchVariable(VariableTree variableTree, VisitorState state) { DocTreePath path = Utils.getDocTreePath(state); if (path != null) { - scanTags(state, JavadocTag.VALID_VARIABLE_TAGS, /* parameters= */ ImmutableSet.of(), path); + scanTags(state, Context.VARIABLE, /* parameters= */ ImmutableSet.of(), path); } return Description.NO_MATCH; } @@ -123,29 +139,19 @@ static String getMessageForInvalidTag(String paramName) { paramName); } - final class InvalidTagChecker extends DocTreePathScanner implements AutoCloseable { + final class InvalidTagChecker extends DocTreePathScanner { private final VisitorState state; - private final ImmutableSet validTags; private final ImmutableSet parameters; - - private final Pattern misplacedCurly; - private final Pattern parensRatherThanCurly; + private final Context context; private final Set fixedTags = new HashSet<>(); private InvalidTagChecker( - VisitorState state, ImmutableSet validTags, ImmutableSet parameters) { + VisitorState state, Context context, ImmutableSet parameters) { this.state = state; - this.validTags = validTags; + this.context = context; this.parameters = parameters; - String validInlineTags = - validTags.stream() - .filter(tag -> tag.type() == TagType.INLINE) - .map(JavadocTag::name) - .collect(joining("|")); - this.misplacedCurly = Pattern.compile(String.format("@(%s)\\{", validInlineTags)); - this.parensRatherThanCurly = Pattern.compile(String.format("\\(@(%s)", validInlineTags)); } @Override @@ -180,7 +186,7 @@ public Void visitText(TextTree node, Void unused) { private void handleMalformedTags(TextTree node) { String body = node.getBody(); - Matcher matcher = misplacedCurly.matcher(body); + Matcher matcher = context.misplacedCurly.matcher(body); Comment comment = ((DCDocComment) getCurrentPath().getDocComment()).comment; while (matcher.find()) { int beforeAt = comment.getSourcePos(((DCText) node).pos + matcher.start()); @@ -198,7 +204,7 @@ private void handleMalformedTags(TextTree node) { private void handleIncorrectParens(TextTree node) { String body = node.getBody(); - Matcher matcher = parensRatherThanCurly.matcher(body); + Matcher matcher = context.parensRatherThanCurly.matcher(body); Comment comment = ((DCDocComment) getCurrentPath().getDocComment()).comment; while (matcher.find()) { int beforeAt = comment.getSourcePos(((DCText) node).pos + matcher.start()); @@ -336,7 +342,7 @@ private void reportUnknownTag(DocTree docTree, JavadocTag tag) { Utils.getBestMatch( tag.name(), /* maxEditDistance= */ 2, - validTags.stream() + context.validTags.stream() .filter(t -> t.type().equals(tag.type())) .map(JavadocTag::name) .collect(toImmutableSet())); @@ -371,7 +377,7 @@ public Void scan(DocTree docTree, Void unused) { return null; } JavadocTag tag = inlineTag(((DCInlineTag) docTree).getTagName()); - if (validTags.contains(tag) || JavadocTag.KNOWN_OTHER_TAGS.contains(tag)) { + if (context.validTags.contains(tag) || JavadocTag.KNOWN_OTHER_TAGS.contains(tag)) { return null; } String message = @@ -383,8 +389,5 @@ public Void scan(DocTree docTree, Void unused) { .build()); return null; } - - @Override - public void close() {} } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/JavadocTag.java b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/JavadocTag.java index ae69ff70a4f..d0f91b89904 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/JavadocTag.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/JavadocTag.java @@ -20,9 +20,11 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.Immutable; import java.util.stream.Stream; /** Describes Javadoc tags, and contains lists of valid tags. */ +@Immutable @AutoValue abstract class JavadocTag { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ExtendsObject.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ExtendsObject.java new file mode 100644 index 00000000000..62041841aca --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ExtendsObject.java @@ -0,0 +1,60 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns.nullness; + +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.util.ASTHelpers.getType; +import static java.lang.String.format; + +import com.google.errorprone.BugPattern; +import com.google.errorprone.BugPattern.SeverityLevel; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.TypeParameterTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.fixes.SuggestedFixes; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.AnnotatedTypeTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeParameterTree; + +/** A bugpattern: see the summary. */ +@BugPattern( + summary = + "`T extends Object` is redundant in normal Java, and does not work to describe `T` as" + + " non-null across compilation boundaries when the Checker Framework unless you" + + " compile users against bytecode generated by the Checker Framework javac. (If you" + + " are building this code with the Checker Framework javac, then disable this check.)", + severity = SeverityLevel.WARNING) +public final class ExtendsObject extends BugChecker implements TypeParameterTreeMatcher { + private static final String NON_NULL = "org.checkerframework.checker.nullness.qual.NonNull"; + + @Override + public Description matchTypeParameter(TypeParameterTree tree, VisitorState state) { + for (Tree bound : tree.getBounds()) { + if (!state.getTypes().isSameType(getType(bound), state.getSymtab().objectType)) { + continue; + } + if (!(bound instanceof AnnotatedTypeTree)) { + SuggestedFix.Builder fix = SuggestedFix.builder(); + String nonNull = SuggestedFixes.qualifyType(state, fix, NON_NULL); + return describeMatch(bound, fix.prefixWith(bound, format(" @%s ", nonNull)).build()); + } + } + return NO_MATCH; + } +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java new file mode 100644 index 00000000000..e1a42cfac25 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java @@ -0,0 +1,216 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns.nullness; + +import static com.google.common.collect.Streams.forEachPair; +import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; +import static com.google.errorprone.VisitorState.memoize; +import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.hasDefinitelyNullBranch; +import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.hasExtraParameterForEnclosingInstance; +import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.nullnessChecksShouldBeConservative; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.suppliers.Suppliers.typeFromString; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.hasAnnotation; +import static javax.lang.model.type.TypeKind.TYPEVAR; + +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.BugPattern; +import com.google.errorprone.ErrorProneFlags; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher; +import com.google.errorprone.dataflow.nullnesspropagation.Nullness; +import com.google.errorprone.dataflow.nullnesspropagation.NullnessAnnotations; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.suppliers.Supplier; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.NewClassTree; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.util.Name; +import java.util.List; + +/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ +@BugPattern(summary = "Null is not permitted for this parameter.", severity = ERROR) +public final class NullArgumentForNonNullParameter extends BugChecker + implements MethodInvocationTreeMatcher, NewClassTreeMatcher { + private static final Supplier JAVA_OPTIONAL_TYPE = typeFromString("java.util.Optional"); + private static final Supplier GUAVA_OPTIONAL_TYPE = + typeFromString("com.google.common.base.Optional"); + private static final Supplier OF_NAME = memoize(state -> state.getName("of")); + private static final Supplier COM_GOOGLE_COMMON_PREFIX_NAME = + memoize(state -> state.getName("com.google.common.")); + + private final boolean beingConservative; + + public NullArgumentForNonNullParameter(ErrorProneFlags flags) { + this.beingConservative = nullnessChecksShouldBeConservative(flags); + } + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + return match(getSymbol(tree), tree.getArguments(), state); + } + + @Override + public Description matchNewClass(NewClassTree tree, VisitorState state) { + return match(getSymbol(tree), tree.getArguments(), state); + } + + private Description match( + MethodSymbol methodSymbol, List args, VisitorState state) { + if (state.errorProneOptions().isTestOnlyTarget()) { + return NO_MATCH; // The tests of `foo` often invoke `foo(null)` to verify that it NPEs. + } + + if (hasExtraParameterForEnclosingInstance(methodSymbol)) { + // TODO(b/232103314): Figure out the right way to handle the implicit outer `this` parameter. + return NO_MATCH; + } + + if (methodSymbol.isVarArgs()) { + /* + * TODO(b/232103314): Figure out the right way to handle this, or at least handle all + * parameters but the last. + */ + return NO_MATCH; + } + + forEachPair( + args.stream(), + methodSymbol.getParameters().stream(), + (argTree, paramSymbol) -> { + if (!hasDefinitelyNullBranch( + argTree, + /* + * TODO(cpovirk): Precompute sets of definitelyNullVars and varsProvenNullByParentIf + * instead of passing empty sets. + */ + ImmutableSet.of(), + ImmutableSet.of(), + state)) { + return; + } + + if (!argumentMustBeNonNull(paramSymbol, state)) { + return; + } + + state.reportMatch(describeMatch(argTree)); + }); + + return NO_MATCH; // Any matches were reported through state.reportMatch. + } + + private boolean argumentMustBeNonNull(VarSymbol sym, VisitorState state) { + if (sym.asType().isPrimitive()) { + return true; + } + + /* + * Though we get most of our nullness information from annotations, there are technical + * obstacles to relying purely on them, including around type variables (see comments below)—not + * to mention that there are no annotations on JDK classes. + * + * As a workaround, we can hardcode specific APIs that feel worth the effort. For now, the only + * ones we hardcode are the two Optional.of methods. Those just happen to be the ones that I + * thought of and found hits for in our codebase. + */ + if (sym.owner.name.equals(OF_NAME.get(state)) + && (isParameterOfMethodOnType(sym, JAVA_OPTIONAL_TYPE, state) + || isParameterOfMethodOnType(sym, GUAVA_OPTIONAL_TYPE, state))) { + return true; + } + + /* + * TODO(b/203207989): In theory, we should program this check to exclude inner classes until we + * fix the bug in MoreAnnotations.getDeclarationAndTypeAttributes, which is used by + * fromAnnotationsOn. In practice, annotations on both inner classes and outer classes are rare + * (especially when NullableOnContainingClass is enabled!), so this code currently still looks + * at parameters that are inner types, even though we might misinterpret them. + */ + Nullness nullness = NullnessAnnotations.fromAnnotationsOn(sym).orElse(null); + + if (nullness == Nullness.NONNULL && !beingConservative) { + /* + * Much code in the wild has @NonNull annotations on parameters that are apparently + * legitimately passed null arguments. Thus, we don't trust such annotations when running in + * conservative mode. + * + * TODO(cpovirk): Instead of ignoring @NonNull annotations entirely, add special cases for + * specific badly annotated APIs. Or better yet, get the annotations fixed. + */ + return true; + } + if (nullness == Nullness.NULLABLE) { + return false; + } + + if (sym.asType().getKind() == TYPEVAR) { + /* + * TODO(cpovirk): We could sometimes return true if we looked for @NullMarked and for any + * annotations on the type-parameter bounds. But looking at type-parameter bounds is hard + * because of JDK-8225377. + */ + return false; + } + + if (enclosingAnnotationDefaultsNonTypeVariablesToNonNull(sym, state)) { + return true; + } + + return false; + } + + private static boolean isParameterOfMethodOnType( + VarSymbol sym, Supplier typeSupplier, VisitorState state) { + Type target = typeSupplier.get(state); + return target != null && state.getTypes().isSameType(sym.enclClass().type, target); + } + + private boolean enclosingAnnotationDefaultsNonTypeVariablesToNonNull( + Symbol sym, VisitorState state) { + for (; sym != null; sym = sym.getEnclosingElement()) { + if (hasAnnotation(sym, "com.google.protobuf.Internal$ProtoNonnullApi", state)) { + return true; + } + /* + * Similar to @NonNull (discussed above), the "default to non-null" annotation @NullMarked is + * sometimes used on code that hasn't had @Nullable annotations added to it where necessary. + * To avoid false positives, our conservative mode trusts @NullMarked only when it appears in + * com.google.common. + * + * TODO(cpovirk): Expand the list of packages that our conservative mode trusts @NullMarked + * on. We might be able to identify some packages that would be safe to trust today. For + * others, we could use ParameterMissingNullable, which adds missing annotations in situations + * similar to the ones identified by this check. (But note that ParameterMissingNullable + * doesn't help with calls that cross file boundaries.) + */ + if (hasAnnotation(sym, "org.jspecify.nullness.NullMarked", state) + && (!beingConservative + || sym.packge().fullname.startsWith(COM_GOOGLE_COMMON_PREFIX_NAME.get(state)))) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullnessUtils.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullnessUtils.java index 6dff9193333..c81767bcc4c 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullnessUtils.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullnessUtils.java @@ -24,6 +24,7 @@ import static com.google.errorprone.fixes.SuggestedFix.emptyFix; import static com.google.errorprone.matchers.Matchers.instanceMethod; import static com.google.errorprone.suppliers.Suppliers.JAVA_LANG_VOID_TYPE; +import static com.google.errorprone.util.ASTHelpers.enclosingClass; import static com.google.errorprone.util.ASTHelpers.getSymbol; import static com.google.errorprone.util.ASTHelpers.getType; import static com.google.errorprone.util.ASTHelpers.hasAnnotation; @@ -33,7 +34,9 @@ import static com.sun.source.tree.Tree.Kind.NULL_LITERAL; import static com.sun.source.tree.Tree.Kind.PARAMETERIZED_TYPE; import static com.sun.tools.javac.parser.Tokens.TokenKind.DOT; +import static java.lang.Boolean.TRUE; +import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableSet; import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; @@ -49,6 +52,7 @@ import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.BinaryTree; import com.sun.source.tree.BlockTree; +import com.sun.source.tree.CaseTree; import com.sun.source.tree.ConditionalExpressionTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; @@ -67,9 +71,13 @@ import com.sun.tools.javac.code.Kinds.KindSelector; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.ClassSymbol; +import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.code.Symbol.VarSymbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.processing.JavacProcessingEnvironment; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Objects; import java.util.Set; import javax.annotation.Nullable; import javax.lang.model.element.Name; @@ -231,7 +239,16 @@ static boolean isAlreadyAnnotatedNullable(Symbol symbol) { return NullnessAnnotations.fromAnnotationsOn(symbol).orElse(null) == Nullness.NULLABLE; } - @com.google.auto.value.AutoValue // fully qualified to work around JDK-7177813(?) in JDK8 build + static boolean hasExtraParameterForEnclosingInstance(MethodSymbol symbol) { + // TODO(b/232103314): Figure out which cases the implicit outer `this` parameter exists in. + if (!symbol.isConstructor()) { + return false; + } + ClassSymbol constructedClass = enclosingClass(symbol); + return enclosingClass(constructedClass) != null && !constructedClass.isStatic(); + } + + @AutoValue abstract static class NullableAnnotationToUse { static NullableAnnotationToUse annotationToBeImported(String qualifiedName, boolean isTypeUse) { return new AutoValue_NullnessUtils_NullableAnnotationToUse( @@ -323,7 +340,7 @@ private static NullableAnnotationToUse pickNullableAnnotation(VisitorState state .orElse( state.isAndroidCompatible() ? "androidx.annotation.Nullable" - : "javax.annotation.Nullable"); + : "org.jspecify.nullness.Nullable"); if (sym != null) { ClassSymbol classSym = (ClassSymbol) sym; if (classSym.isAnnotationType()) { @@ -331,7 +348,7 @@ private static NullableAnnotationToUse pickNullableAnnotation(VisitorState state return annotationWithoutImporting( "Nullable", isTypeUse(classSym.className()), /*isAlreadyInScope=*/ true); } else { - // It's not an annotation type. We have to fully-qualify the import. + // The imported `Nullable` is not an annotation type. Fully qualify the annotation. return annotationWithoutImporting( defaultType, isTypeUse(defaultType), /*isAlreadyInScope=*/ false); } @@ -493,7 +510,7 @@ public Boolean visitParenthesized(ParenthesizedTree tree, Void unused) { return visit(tree.getExpression(), unused); } - // TODO(cpovirk): visitSwitchExpression + // For visitSwitchExpression logic, see defaultAction. @Override public Boolean visitTypeCast(TypeCastTree tree, Void unused) { @@ -509,7 +526,13 @@ protected Boolean defaultAction(Tree tree, Void unused) { * null)`.) */ return isVoid(getType(tree), stateForCompilationUnit) - || definitelyNullVars.contains(getSymbol(tree)); + || definitelyNullVars.contains(getSymbol(tree)) + /* + * TODO(cpovirk): It would be nicer to report the finding on the null-returning `case` + * rather than on the `switch` as a whole. To do so, maybe we could change our visitor + * to accept `Boolean isCaseOfReturnedExpressionSwitch` instead of `Void unused`? + */ + || isSwitchExpressionWithDefinitelyNullBranch(tree); } boolean isOptionalOrNull(MethodInvocationTree tree) { @@ -521,9 +544,42 @@ boolean isOptionalOrNull(MethodInvocationTree tree) { * But consider whether that would interfere with the TODO at the top of that method. */ } + + boolean isSwitchExpressionWithDefinitelyNullBranch(Tree tree) { + return tree.getKind().name().equals("SWITCH_EXPRESSION") + && getCases(tree).stream() + .map(NullnessUtils::getBody) + .anyMatch(t -> Objects.equals(visit(t, null), TRUE)); + } }.visit(tree, null); } + private static List getCases(Tree switchExpressionTree) { + try { + if (getCasesMethod == null) { + getCasesMethod = + Class.forName("com.sun.source.tree.SwitchExpressionTree").getMethod("getCases"); + } + return (List) getCasesMethod.invoke(switchExpressionTree); + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + } + + private static Tree getBody(Object caseTree) { + try { + if (getBodyMethod == null) { + getBodyMethod = CaseTree.class.getMethod("getBody"); + } + return (Tree) getBodyMethod.invoke(caseTree); + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + } + + private static Method getCasesMethod; + private static Method getBodyMethod; + /** Returns true if this is {@code x == null ? x : ...} or similar. */ private static boolean isTernaryXIfXIsNull(ConditionalExpressionTree tree) { NullCheck nullCheck = getNullCheck(tree.getCondition()); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ParameterMissingNullable.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ParameterMissingNullable.java index ced1b26104b..33c5fb95f97 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ParameterMissingNullable.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ParameterMissingNullable.java @@ -22,10 +22,10 @@ import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.fixByAddingNullableAnnotationToType; import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.getNullCheck; import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.hasDefinitelyNullBranch; +import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.hasExtraParameterForEnclosingInstance; import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.isAlreadyAnnotatedNullable; import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.nullnessChecksShouldBeConservative; import static com.google.errorprone.matchers.Description.NO_MATCH; -import static com.google.errorprone.util.ASTHelpers.enclosingClass; import static com.google.errorprone.util.ASTHelpers.getSymbol; import static com.google.errorprone.util.ASTHelpers.getType; import static com.google.errorprone.util.ASTHelpers.hasNoExplicitType; @@ -55,7 +55,6 @@ import com.sun.source.util.TreePath; import com.sun.source.util.TreeScanner; import com.sun.tools.javac.code.Symbol; -import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; import java.util.List; @@ -241,25 +240,17 @@ public Description matchNewClass(NewClassTree tree, VisitorState state) { return matchCall(getSymbol(tree), tree.getArguments(), state); } - private static boolean hasExtraParameterForEnclosingInstance(MethodSymbol symbol) { - if (!symbol.isConstructor()) { - return false; - } - ClassSymbol constructedClass = enclosingClass(symbol); - return enclosingClass(constructedClass) != null && !constructedClass.isStatic(); - } - private Description matchCall( MethodSymbol methodSymbol, List arguments, VisitorState state) { if (hasExtraParameterForEnclosingInstance(methodSymbol)) { - // TODO(cpovirk): Figure out the right way to handle the implicit outer `this` parameter. + // TODO(b/232103314): Figure out the right way to handle the implicit outer `this` parameter. return NO_MATCH; } if (methodSymbol.isVarArgs()) { /* - * TODO(cpovirk): Figure out the right way to handle this, or at least handle all parameters - * but the last. + * TODO(b/232103314): Figure out the right way to handle this, or at least handle all + * parameters but the last. */ return NO_MATCH; } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullable.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullable.java index 861fb26e260..feff9fa1bd4 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullable.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullable.java @@ -35,9 +35,11 @@ import static com.google.errorprone.util.ASTHelpers.constValue; import static com.google.errorprone.util.ASTHelpers.findEnclosingMethod; import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.hasAnnotation; import static com.google.errorprone.util.ASTHelpers.isConsideredFinal; import static com.google.errorprone.util.ASTHelpers.methodCanBeOverridden; import static com.sun.source.tree.Tree.Kind.NULL_LITERAL; +import static com.sun.source.tree.Tree.Kind.THROW; import static java.lang.Boolean.FALSE; import static java.util.regex.Pattern.compile; import static javax.lang.model.type.TypeKind.TYPEVAR; @@ -74,6 +76,14 @@ /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ @BugPattern( summary = "Method returns a definitely null value but is not annotated @Nullable", + explanation = + "Annotating a method @Nullable communicates to tools that the method can return null. That" + + " means they can check that callers handle a returned null correctly.\n\n" + + "Adding @Nullable may require updating callers so that they deal with the" + + " possibly-null value. This can happen for example with Kotlin callers, or with Java" + + " callers that are checked for null-safety by static-analysis tools. Alternatively," + + " depending on the tool, it may be possible to annotate Java callers temporarily with" + + " @SuppressWarnings(\"nullness\").", severity = SUGGESTION) public class ReturnMissingNullable extends BugChecker implements CompilationUnitTreeMatcher { private static final Matcher METHODS_THAT_NEVER_RETURN = @@ -272,6 +282,17 @@ void doVisitMethod(MethodTree tree) { return; } + if (tree.getBody() != null + && tree.getBody().getStatements().size() == 1 + && getOnlyElement(tree.getBody().getStatements()).getKind() == THROW) { + return; + } + + if (hasAnnotation( + tree, "com.google.errorprone.annotations.DoNotCall", stateForCompilationUnit)) { + return; + } + for (MethodSymbol methodKnownToReturnNull : METHODS_KNOWN_TO_RETURN_NULL.get(stateForCompilationUnit)) { if (stateForCompilationUnit @@ -281,7 +302,13 @@ void doVisitMethod(MethodTree tree) { fixByAddingNullableAnnotationToReturnType( stateForCompilationUnit.withPath(getCurrentPath()), tree); if (!fix.isEmpty()) { - stateForCompilationUnit.reportMatch(describeMatch(tree, fix)); + stateForCompilationUnit.reportMatch( + buildDescription(tree) + .setMessage( + "Nearly all implementations of this method must return null, but it is" + + " not annotated @Nullable") + .addFix(fix) + .build()); } } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/UnsafeWildcard.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/UnsafeWildcard.java new file mode 100644 index 00000000000..08f9e93a65c --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/UnsafeWildcard.java @@ -0,0 +1,364 @@ +/* + * Copyright 2019 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns.nullness; + +import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; +import static java.lang.Math.min; + +import com.google.common.collect.ImmutableListMultimap; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.AssignmentTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.ConditionalExpressionTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.LambdaExpressionTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.ParenthesizedTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.ReturnTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.TypeCastTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.util.ASTHelpers; +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.ConditionalExpressionTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.ParenthesizedTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.tree.VariableTree; +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Symbol.TypeVariableSymbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.Type.ArrayType; +import com.sun.tools.javac.code.Type.IntersectionClassType; +import com.sun.tools.javac.code.Type.MethodType; +import com.sun.tools.javac.code.Type.UnionClassType; +import com.sun.tools.javac.code.Type.WildcardType; +import com.sun.tools.javac.tree.JCTree.JCClassDecl; +import com.sun.tools.javac.tree.JCTree.JCExpression; +import com.sun.tools.javac.tree.JCTree.JCLambda; +import com.sun.tools.javac.tree.JCTree.JCMethodDecl; +import com.sun.tools.javac.tree.JCTree.JCMethodInvocation; +import com.sun.tools.javac.tree.JCTree.JCNewClass; +import com.sun.tools.javac.tree.JCTree.JCParens; +import com.sun.tools.javac.tree.JCTree.JCTypeCast; +import com.sun.tools.javac.tree.JCTree.JCVariableDecl; +import java.util.HashSet; +import java.util.Map; +import javax.lang.model.element.ElementKind; +import javax.lang.model.type.TypeKind; + +/** Check to detect unsafe upcasts of {@code null} values to wildcard types. */ +@BugPattern(summary = "Certain wildcard types can confuse the compiler.", severity = ERROR) +public class UnsafeWildcard extends BugChecker + implements AssignmentTreeMatcher, + ClassTreeMatcher, + ConditionalExpressionTreeMatcher, + LambdaExpressionTreeMatcher, + MethodInvocationTreeMatcher, + NewClassTreeMatcher, + ParenthesizedTreeMatcher, + ReturnTreeMatcher, + TypeCastTreeMatcher, + VariableTreeMatcher { + + @Override + public Description matchAssignment(AssignmentTree tree, VisitorState state) { + return checkForUnsafeNullAssignment( + ((JCExpression) tree.getVariable()).type, tree.getExpression(), state); + } + + @Override + public Description matchClass(ClassTree tree, VisitorState state) { + JCClassDecl classDecl = (JCClassDecl) tree; + // Check "extends" and "implements" for unsafe wildcards + for (JCExpression implemented : classDecl.getImplementsClause()) { + state.reportMatch( + checkForUnsafeWildcards(implemented, "Unsafe wildcard type: ", implemented.type, state)); + } + if (classDecl.getExtendsClause() != null) { + return checkForUnsafeWildcards( + classDecl.getExtendsClause(), + "Unsafe wildcard type: ", + classDecl.getExtendsClause().type, + state); + } + return Description.NO_MATCH; + } + + @Override + public Description matchConditionalExpression( + ConditionalExpressionTree tree, VisitorState state) { + // Ternary branches are implicitly upcast, so check in case they're null + Type ternaryType = ((JCExpression) tree).type; + state.reportMatch(checkForUnsafeNullAssignment(ternaryType, tree.getTrueExpression(), state)); + return checkForUnsafeNullAssignment(ternaryType, tree.getFalseExpression(), state); + } + + @Override + public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) { + if (tree.getBody() instanceof ExpressionTree) { + Type targetType = ((JCLambda) tree).getDescriptorType(state.getTypes()).getReturnType(); + return checkForUnsafeNullAssignment(targetType, (ExpressionTree) tree.getBody(), state); + } // else covered by matchReturn + return Description.NO_MATCH; + } + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + // MethodType of this invocation gives us the args' target types with any type parameters + // substituted (unlike the method's symbol, which doesn't give us effective target types). + MethodType mtype = (MethodType) ((JCMethodInvocation) tree).meth.type; + MethodSymbol callee = ASTHelpers.getSymbol(tree); + if (!((JCMethodInvocation) tree).getTypeArguments().isEmpty()) { + // Check type arguments for problematic wildcards given in source. + for (JCExpression typearg : ((JCMethodInvocation) tree).getTypeArguments()) { + state.reportMatch( + checkForUnsafeWildcards(typearg, "Unsafe wildcard type: ", typearg.type, state)); + } + } else if (!callee.type.getTypeArguments().isEmpty()) { + // Otherwise, check any inferred type arguments + ImmutableListMultimap mapping = + ASTHelpers.getTypeSubstitution(mtype, callee); + HashSet seen = new HashSet<>(); + for (Map.Entry inferredTypearg : mapping.entries()) { + if (!seen.add(inferredTypearg.getValue())) { + continue; // avoid duplicate reports for the same type + } + state.reportMatch( + checkForUnsafeWildcards( + tree, + "Unsafe wildcard in inferred type argument for callee's type parameter " + + inferredTypearg.getKey() + + ": ", + inferredTypearg.getValue(), + state)); + } + } + + int paramIndex = 0; + for (ExpressionTree arg : tree.getArguments()) { + // Check null arguments against parameter type + // NB: this will be an array type for vararg parameters, but checkForUnsafeNullAssignment + // sees through array types, so there's no need to unwrap the type here + Type paramType = mtype.argtypes.get(paramIndex); + state.reportMatch(checkForUnsafeNullAssignment(paramType, arg, state)); + paramIndex = min(paramIndex + 1, mtype.argtypes.size() - 1); + } + return Description.NO_MATCH; + } + + @Override + public Description matchNewClass(NewClassTree tree, VisitorState state) { + // MethodType of this invocation gives us the args' target types with any type parameters + // substituted (unlike the method's symbol, which doesn't give us effective target types). + MethodType mtype = (MethodType) ((JCNewClass) tree).constructorType; + // mtype contains type of outer object in some cases, which we can skip since null enclosing + // expression would cause exception. + int paramIndex = tree.getClassBody() != null && tree.getEnclosingExpression() != null ? 1 : 0; + for (ExpressionTree arg : tree.getArguments()) { + // Check null arguments against parameter type + // NB: this will be an array type for vararg parameters, but checkForUnsafeNullAssignment + // sees through array types, so there's no need to unwrap the type here + Type paramType = mtype.argtypes.get(paramIndex); + state.reportMatch(checkForUnsafeNullAssignment(paramType, arg, state)); + paramIndex = min(paramIndex + 1, mtype.argtypes.size() - 1); + } + // Check type arguments for problematic wildcards (visiting class type will recursively visit + // its arguments + return checkForUnsafeWildcards( + tree, "Unsafe wildcard type argument: ", ((JCNewClass) tree).type, state); + } + + @Override + public Description matchParenthesized(ParenthesizedTree tree, VisitorState state) { + // Treat (null) like null + return checkForUnsafeNullAssignment(((JCParens) tree).type, tree.getExpression(), state); + } + + @Override + public Description matchReturn(ReturnTree tree, VisitorState state) { + // Check "return null" against return type + if (tree.getExpression() == null + || ((JCExpression) tree.getExpression()).type.getKind() != TypeKind.NULL) { + return Description.NO_MATCH; + } + + // Figure out return type of surrounding method or lambda and check it + Tree method = state.findEnclosing(MethodTree.class, LambdaExpressionTree.class); + if (method instanceof MethodTree) { + return checkForUnsafeNullAssignment( + ((JCMethodDecl) method).getReturnType().type, tree.getExpression(), state); + } else if (method instanceof LambdaExpressionTree) { + Type targetType = ((JCLambda) method).getDescriptorType(state.getTypes()).getReturnType(); + return checkForUnsafeNullAssignment(targetType, tree.getExpression(), state); + } + return Description.NO_MATCH; + } + + @Override + public Description matchTypeCast(TypeCastTree tree, VisitorState state) { + // Check explicit casts of null + return checkForUnsafeNullAssignment( + ((JCTypeCast) tree).getType().type, tree.getExpression(), state); + } + + @Override + public Description matchVariable(VariableTree tree, VisitorState state) { + // Check initializer like assignment + if (tree.getInitializer() != null) { + return checkForUnsafeNullAssignment( + ((JCVariableDecl) tree).type, tree.getInitializer(), state); + } + + VarSymbol symbol = ASTHelpers.getSymbol(tree); + if (symbol.getKind() == ElementKind.FIELD && (symbol.flags() & Flags.FINAL) == 0) { + // Fields start out as null, so check their type as if this was a null assignment. While all + // fields start out null, we ignore them here if they're final or we checked their + // initializer above. In either case we know we'll see at least one assignment to the field, + // and if those check out they guarantees us that the field's type is ok: non-null + // assignments guarantee that there is a concrete type compatible with any + // wildcards in this field's type, and null assignments will be checked separately (possibly + // right above). + return checkForUnsafeWildcards( + tree, + "Uninitialized field with unsafe wildcard type: ", + ((JCVariableDecl) tree).type, + state); + } + return Description.NO_MATCH; + } + + /** + * Checks for unsafe wildcards in {@code targetType} if the given expression is `null`. + * + * @return diagnostic for {@code tree} if unsafe wildcard is found, {@link Description#NO_MATCH} + * otherwise. + * @see #checkForUnsafeWildcards + */ + private Description checkForUnsafeNullAssignment( + Type targetType, ExpressionTree tree, VisitorState state) { + if (!targetType.isReference() || ((JCExpression) tree).type.getKind() != TypeKind.NULL) { + return Description.NO_MATCH; + } + return checkForUnsafeWildcards(tree, "Cast to wildcard type unsafe: ", targetType, state); + } + + /** + * Recursively looks through {@code targetType} for any wildcard whose lower bounds isn't known to + * be a subtype of the corresponding type parameter's upper bound. + * + * @return diagnostic for {@code tree} with given {@code messageHeader} if unsafe wildcard found, + * {@link Description#NO_MATCH} otherwise. + */ + private Description checkForUnsafeWildcards( + Tree tree, String messageHeader, Type targetType, VisitorState state) { + while (targetType instanceof ArrayType) { + // Check array component type + targetType = ((ArrayType) targetType).getComponentType(); + } + int i = 0; + for (Type arg : targetType.getTypeArguments()) { + // Check components of generic types (getTypeArguments() is empty for other kinds of types) + if (arg instanceof WildcardType && ((WildcardType) arg).getSuperBound() != null) { + Type lowerBound = ((WildcardType) arg).getSuperBound(); + // We only check lower bounds that are themselves type variables with trivial upper bounds. + // Javac already checks other lower bounds, namely lower bounds that are concrete types or + // type variables with non-trivial upper bounds, to be in bounds of the corresponding type + // parameter (boundVar below). + // We skip these cases because the subtype check below can spuriously fail for them because + // it doesn't correctly substitute type variables when comparing lowerBound and boundVar's + // upper bound. + // Note javax.lang.model.type.TypeVariable#getUpperBound() guarantees the result to be non- + // null for type variables, so we use null check as a proxy for whether lowerBound is a type + // variable. + // TODO(kmb): avoid counting on compiler's handling of non-trivial upper bounds here + if (lowerBound.getUpperBound() != null + && lowerBound.getUpperBound().toString().endsWith("java.lang.Object")) { + Type boundVar = targetType.tsym.type.getTypeArguments().get(i); + + if (!state.getTypes().isSubtypeNoCapture(lowerBound, boundVar.getUpperBound())) { + return buildDescription(tree) + .setMessage( + messageHeader + + targetType + + " because of type argument " + + i + + " with implicit upper bound " + + boundVar.getUpperBound()) + .build(); + } + } + // Also check the super bound itself + Description contained = + checkForUnsafeWildcards(tree, messageHeader + i + " nested: ", lowerBound, state); + if (contained != Description.NO_MATCH) { + return contained; + } + } else if (arg instanceof WildcardType && ((WildcardType) arg).getExtendsBound() != null) { + // Check the wildcard's bound + Description contained = + checkForUnsafeWildcards( + tree, + messageHeader + i + " nested: ", + ((WildcardType) arg).getExtendsBound(), + state); + if (contained != Description.NO_MATCH) { + return contained; + } + } else { + // Check for wildcards in the type argument + Description contained = + checkForUnsafeWildcards(tree, messageHeader + i + " nested: ", arg, state); + if (contained != Description.NO_MATCH) { + return contained; + } + } + ++i; + } + // For union and intersection types, check their components. + if (targetType instanceof IntersectionClassType) { + for (Type bound : ((IntersectionClassType) targetType).getExplicitComponents()) { + Description contained = + checkForUnsafeWildcards(tree, messageHeader + "bound ", bound, state); + if (contained != Description.NO_MATCH) { + return contained; + } + } + } + if (targetType instanceof UnionClassType) { + for (Type alternative : ((UnionClassType) targetType).getAlternativeTypes()) { + Description contained = + checkForUnsafeWildcards(tree, messageHeader + "alternative ", alternative, state); + if (contained != Description.NO_MATCH) { + return contained; + } + } + } + return Description.NO_MATCH; + } +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullable.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullable.java index 629746f5de2..132a7f15514 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullable.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullable.java @@ -110,9 +110,6 @@ private static boolean isInNullMarkedScope(VisitorState state) { for (Tree tree : state.getPath()) { if (tree.getKind().asInterface().equals(ClassTree.class) || tree.getKind() == METHOD) { Symbol enclosingElement = getSymbol(tree); - if (tree == null) { - continue; - } return NullnessUtils.isInNullMarkedScope(enclosingElement, state); } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/DoubleCheckedLocking.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/DoubleCheckedLocking.java index b360b0979b3..18fc8419f68 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/DoubleCheckedLocking.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/DoubleCheckedLocking.java @@ -251,6 +251,7 @@ static DCLInfo findDCL(IfTree outerIf) { /** * Matches comparisons to null (e.g. {@code foo == null}) and returns the expression being tested. */ + @Nullable private static ExpressionTree getNullCheckedExpression(ExpressionTree condition) { condition = stripParentheses(condition); if (!(condition instanceof BinaryTree)) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByBinder.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByBinder.java index c9b60f4d745..3fab23a8264 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByBinder.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByBinder.java @@ -35,6 +35,7 @@ import com.sun.tools.javac.util.Names; import java.util.Optional; import javax.lang.model.element.Name; +import org.checkerframework.checker.nullness.qual.Nullable; /** * A binder from {@code @GuardedBy} annotations to {@link GuardedByExpression}s. @@ -320,7 +321,8 @@ private GuardedByExpression normalizeBase( * Returns the owner if the given member is declared in a lexically enclosing scope, and * {@code null} otherwise. */ - private ClassSymbol isEnclosedIn(ClassSymbol startingClass, Symbol member, Types types) { + private @Nullable ClassSymbol isEnclosedIn( + ClassSymbol startingClass, Symbol member, Types types) { for (ClassSymbol scope = startingClass.owner.enclClass(); scope != null; scope = scope.owner.enclClass()) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByChecker.java index 55900c9057d..aba19a60735 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByChecker.java @@ -21,7 +21,6 @@ import com.google.common.base.Joiner; import com.google.errorprone.BugPattern; -import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.LambdaExpressionTreeMatcher; @@ -41,6 +40,7 @@ import com.sun.source.util.TreePath; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Type; +import org.checkerframework.checker.nullness.qual.Nullable; /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ @BugPattern( @@ -55,16 +55,6 @@ public class GuardedByChecker extends BugChecker private final GuardedByFlags flags = GuardedByFlags.allOn(); - private final boolean reportMissingGuards; - private final boolean checkTryWithResources; - - public GuardedByChecker(ErrorProneFlags errorProneFlags) { - reportMissingGuards = - errorProneFlags.getBoolean("GuardedByChecker:reportMissingGuards").orElse(true); - checkTryWithResources = - errorProneFlags.getBoolean("GuardedByChecker:checkTryWithResources").orElse(true); - } - @Override public Description matchMethod(MethodTree tree, VisitorState state) { // Constructors (and field initializers, instance initializers, and class initializers) are free @@ -89,9 +79,7 @@ private void analyze(VisitorState state) { (ExpressionTree tree, GuardedByExpression guard, HeldLockSet live) -> report(GuardedByChecker.this.checkGuardedAccess(tree, guard, live, state), state), tree1 -> isSuppressed(tree1, state), - flags, - reportMissingGuards, - checkTryWithResources); + flags); } @Override @@ -175,7 +163,7 @@ private static String buildMessage(GuardedByExpression guard, HeldLockSet locks) return message.toString(); } - private static Select findOuterInstance(GuardedByExpression expr) { + private static @Nullable Select findOuterInstance(GuardedByExpression expr) { while (expr.kind() == Kind.SELECT) { Select select = (Select) expr; if (select.sym().name.contentEquals(GuardedByExpression.ENCLOSING_INSTANCE_NAME)) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedBySymbolResolver.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedBySymbolResolver.java index 820920f1064..a4d83510f7e 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedBySymbolResolver.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedBySymbolResolver.java @@ -225,6 +225,7 @@ private VarSymbol getParam(@Nullable MethodInfo method, String name) { return null; } + @Nullable @Override public Symbol resolveTypeLiteral(ExpressionTree expr) { checkGuardedBy(expr instanceof IdentifierTree, "bad type literal: %s", expr); @@ -266,6 +267,7 @@ private Symbol resolveType(String name, SearchSuperTypes searchSuperTypes) { return type; } + @Nullable private Symbol getSuperType(Symbol symbol, String name) { for (Type t : types.closure(symbol.type)) { if (t.asElement().getSimpleName().contentEquals(name)) { @@ -275,6 +277,7 @@ private Symbol getSuperType(Symbol symbol, String name) { return null; } + @Nullable private static Symbol getLexicallyEnclosing(ClassSymbol symbol, String name) { Symbol current = symbol.owner; while (true) { @@ -296,6 +299,7 @@ private Symbol attribIdent(String name) { return attr.attribIdent(tm.Ident(visitorState.getName(name)), compilationUnit); } + @Nullable @Override public Symbol resolveEnclosingClass(ExpressionTree expr) { checkGuardedBy(expr instanceof IdentifierTree, "bad type literal: %s", expr); @@ -350,6 +354,7 @@ static MethodInfo create(MethodSymbol sym, ImmutableList argumen return new AutoValue_GuardedBySymbolResolver_MethodInfo(sym, arguments); } + @Nullable static MethodInfo create(Tree tree, VisitorState visitorState) { Symbol sym = ASTHelpers.getSymbol(tree); if (!(sym instanceof MethodSymbol)) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByUtils.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByUtils.java index b50ecb72ee6..6040e296098 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByUtils.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByUtils.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Optional; import java.util.stream.Stream; +import org.checkerframework.checker.nullness.qual.Nullable; /** * @author cushon@google.com (Liam Miller-Cushon) @@ -137,7 +138,7 @@ public static GuardedByValidationResult isGuardedByValid( return GuardedByValidationResult.ok(); } - public static Symbol bindGuardedByString( + public static @Nullable Symbol bindGuardedByString( Tree tree, String guard, VisitorState visitorState, GuardedByFlags flags) { Optional bound = GuardedByBinder.bindString(guard, GuardedBySymbolResolver.from(tree, visitorState), flags); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzer.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzer.java index 1daff1c5725..28dd4bdcd16 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzer.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzer.java @@ -85,14 +85,10 @@ public static void analyze( VisitorState state, LockEventListener listener, Predicate isSuppressed, - GuardedByFlags flags, - boolean reportMissingGuards, - boolean checkTryWithResources) { + GuardedByFlags flags) { HeldLockSet locks = HeldLockSet.empty(); locks = handleMonitorGuards(state, locks, flags); - new LockScanner( - state, listener, isSuppressed, flags, reportMissingGuards, checkTryWithResources) - .scan(state.getPath(), locks); + new LockScanner(state, listener, isSuppressed, flags).scan(state.getPath(), locks); } // Don't use Class#getName() for inner classes, we don't want `Monitor$Guard` @@ -127,8 +123,6 @@ private static class LockScanner extends TreePathScanner { private final LockEventListener listener; private final Predicate isSuppressed; private final GuardedByFlags flags; - private final boolean reportMissingGuards; - private final boolean checkTryWithResources; private static final GuardedByExpression.Factory F = new GuardedByExpression.Factory(); @@ -136,15 +130,11 @@ private LockScanner( VisitorState visitorState, LockEventListener listener, Predicate isSuppressed, - GuardedByFlags flags, - boolean reportMissingGuards, - boolean checkTryWithResources) { + GuardedByFlags flags) { this.visitorState = visitorState; this.listener = listener; this.isSuppressed = isSuppressed; this.flags = flags; - this.reportMissingGuards = reportMissingGuards; - this.checkTryWithResources = checkTryWithResources; } @Override @@ -187,12 +177,9 @@ public Void visitTry(TryTree tree, HeldLockSet locks) { // are held for the entirety of the try and catch statements. Collection releasedLocks = ReleasedLockFinder.find(tree.getFinallyBlock(), visitorState, flags); - // We don't know what to do with the try-with-resources block. // TODO(cushon) - recognize common try-with-resources patterns. Currently there is no // standard implementation of an AutoCloseable lock resource to detect. - if (checkTryWithResources || resources.isEmpty()) { - scan(tree.getBlock(), locks.plusAll(releasedLocks)); - } + scan(tree.getBlock(), locks.plusAll(releasedLocks)); scan(tree.getCatches(), locks.plusAll(releasedLocks)); scan(tree.getFinallyBlock(), locks); return null; @@ -254,9 +241,7 @@ private void checkMatch(ExpressionTree tree, HeldLockSet locks) { GuardedBySymbolResolver.from(tree, visitorState.withPath(getCurrentPath())), flags); if (!guard.isPresent()) { - if (reportMissingGuards) { - invalidLock(tree, locks, guardString); - } + invalidLock(tree, locks, guardString); continue; } Optional boundGuard = diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableAnalysis.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableAnalysis.java index 62c6107a9ce..496e2663b46 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableAnalysis.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableAnalysis.java @@ -47,6 +47,7 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.Modifier; import javax.lang.model.type.TypeKind; +import org.checkerframework.checker.nullness.qual.Nullable; /** Analyzes types for deep immutability. */ public class ImmutableAnalysis { @@ -323,7 +324,7 @@ AnnotationInfo getImmutableAnnotation(Symbol sym, VisitorState state) { * Gets the {@link Tree}'s {@code @Immutable} annotation info, either from an annotation on the * symbol or from the list of well-known immutable types. */ - AnnotationInfo getImmutableAnnotation(Tree tree, VisitorState state) { + @Nullable AnnotationInfo getImmutableAnnotation(Tree tree, VisitorState state) { Symbol sym = ASTHelpers.getSymbol(tree); return sym == null ? null : threadSafety.getMarkerOrAcceptedAnnotation(sym, state); } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableChecker.java index 1337cb52a9a..df41d97c401 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableChecker.java @@ -50,8 +50,6 @@ import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher; import com.google.errorprone.bugpatterns.threadsafety.ThreadSafety.Violation; -import com.google.errorprone.fixes.Fix; -import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ClassTree; @@ -84,6 +82,7 @@ import java.util.Optional; import java.util.Set; import javax.lang.model.element.ElementKind; +import org.checkerframework.checker.nullness.qual.Nullable; /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ @BugPattern( @@ -101,7 +100,6 @@ public class ImmutableChecker extends BugChecker private final WellKnownMutability wellKnownMutability; private final ImmutableSet immutableAnnotations; - private final boolean handleAnonymousClasses; ImmutableChecker(ImmutableSet immutableAnnotations) { this(ErrorProneFlags.empty(), immutableAnnotations); @@ -114,8 +112,6 @@ public ImmutableChecker(ErrorProneFlags flags) { private ImmutableChecker(ErrorProneFlags flags, ImmutableSet immutableAnnotations) { this.wellKnownMutability = WellKnownMutability.fromFlags(flags); this.immutableAnnotations = immutableAnnotations; - this.handleAnonymousClasses = - flags.getBoolean("ImmutableChecker:HandleAnonymousClasses").orElse(true); } @Override @@ -259,15 +255,16 @@ public Description matchClass(ClassTree tree, VisitorState state) { if (tree.getSimpleName().length() == 0) { // anonymous classes have empty names - // TODO(cushon): once Java 8 happens, require @Immutable on anonymous classes return handleAnonymousClass(tree, state, analysis); } - AnnotationInfo annotation = analysis.getImmutableAnnotation(tree, state); + AnnotationInfo annotation = getImmutableAnnotation(analysis, tree, state); if (annotation == null) { - // If the type isn't annotated we don't check for immutability, but we do - // report an error if it extends/implements any @Immutable-annotated types. - return checkSubtype(tree, state); + // If the type isn't annotated, and doesn't extend anything annotated, there's nothing to do. + // An earlier version of the check required an explicit annotation on classes that extended + // @Immutable classes, but didn't enforce the subtyping requirement for interfaces. We now + // don't require the explicit annotations on any subtypes. + return NO_MATCH; } // Special-case visiting declarations of known-immutable types; these uses @@ -323,7 +320,7 @@ public Description matchClass(ClassTree tree, VisitorState state) { describeClass(matched, sym, annotation, violation)); Type superType = immutableSupertype(sym, state); - if (handleAnonymousClasses && superType != null && isLocal(sym)) { + if (superType != null && isLocal(sym)) { checkClosedTypes(tree, state, superType.tsym, analysis); } @@ -381,9 +378,7 @@ private Description handleAnonymousClass( return NO_MATCH; } - if (handleAnonymousClasses) { - checkClosedTypes(tree, state, superType.tsym, analysis); - } + checkClosedTypes(tree, state, superType.tsym, analysis); // We don't need to check that the superclass has an immutable instantiation. // The anonymous instance can only be referred to using a superclass type, so // the type arguments will be validated at any type use site where we care about @@ -546,28 +541,30 @@ private Description.Builder describeAnonymous(Tree tree, Type superType, Violati // Strong behavioural subtyping - /** Check for classes without {@code @Immutable} that have immutable supertypes. */ - private Description checkSubtype(ClassTree tree, VisitorState state) { + /** + * Check for classes with {@code @Immutable}, or that inherited it from a super class or + * interface. + */ + private @Nullable AnnotationInfo getImmutableAnnotation( + ImmutableAnalysis analysis, ClassTree tree, VisitorState state) { + AnnotationInfo annotation = analysis.getImmutableAnnotation(tree, state); + if (annotation != null) { + return annotation; + } + // getImmutableAnnotation inherits annotations from classes, but not interfaces. ClassSymbol sym = getSymbol(tree); Type superType = immutableSupertype(sym, state); - if (superType == null) { - return NO_MATCH; + if (superType != null) { + return analysis.getImmutableAnnotation(superType.tsym, state); } - String message = - format("Class extends @Immutable type %s, but is not annotated as immutable", superType); - Fix fix = - SuggestedFix.builder() - .prefixWith(tree, "@Immutable ") - .addImport(Immutable.class.getName()) - .build(); - return buildDescription(tree).setMessage(message).addFix(fix).build(); + return null; } /** * Returns the type of the first superclass or superinterface in the hierarchy annotated with * {@code @Immutable}, or {@code null} if no such super type exists. */ - private Type immutableSupertype(Symbol sym, VisitorState state) { + private @Nullable Type immutableSupertype(Symbol sym, VisitorState state) { for (Type superType : state.getTypes().closure(sym.type)) { if (superType.tsym.equals(sym.type.tsym)) { continue; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java index 7231fe529ea..4519338204a 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java @@ -29,6 +29,7 @@ import com.google.common.collect.Sets; import com.google.common.collect.Streams; import com.google.errorprone.VisitorState; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.bugpatterns.CanBeStaticAnalyzer; import com.google.errorprone.suppliers.Supplier; import com.google.errorprone.util.ASTHelpers; @@ -145,12 +146,14 @@ private Builder() {} @Nullable private Class typeParameterAnnotation; /** See {@link Purpose}. */ + @CanIgnoreReturnValue public Builder setPurpose(Purpose purpose) { this.purpose = purpose; return this; } /** Information about known types and whether they're known to be safe or unsafe. */ + @CanIgnoreReturnValue public Builder knownTypes(KnownTypes knownTypes) { this.knownTypes = knownTypes; return this; @@ -160,11 +163,13 @@ public Builder knownTypes(KnownTypes knownTypes) { * Annotations that will cause a class to be tested with this {@link ThreadSafety} instance; for * example, when testing a class for immutability, this should be @Immutable. */ + @CanIgnoreReturnValue public Builder markerAnnotations(Set markerAnnotations) { return markerAnnotations(ImmutableSet.copyOf(markerAnnotations)); } // TODO(ringwalt): Remove this constructor. We need it for binary compatibility. + @CanIgnoreReturnValue public Builder markerAnnotations(ImmutableSet markerAnnotations) { checkNotNull(markerAnnotations); this.markerAnnotations = markerAnnotations; @@ -177,11 +182,13 @@ public Builder markerAnnotations(ImmutableSet markerAnnotations) { * annotation, @Immutable would be included in this list, as an immutable class is by definition * thread-safe. */ + @CanIgnoreReturnValue public Builder acceptedAnnotations(Set acceptedAnnotations) { return acceptedAnnotations(ImmutableSet.copyOf(acceptedAnnotations)); } // TODO(ringwalt): Remove this constructor. We need it for binary compatibility. + @CanIgnoreReturnValue public Builder acceptedAnnotations(ImmutableSet acceptedAnnotations) { checkNotNull(acceptedAnnotations); this.acceptedAnnotations = acceptedAnnotations; @@ -189,6 +196,7 @@ public Builder acceptedAnnotations(ImmutableSet acceptedAnnotations) { } /** An annotation which marks a generic parameter as a container type. */ + @CanIgnoreReturnValue public Builder containerOfAnnotation(Class containerOfAnnotation) { checkNotNull(containerOfAnnotation); this.containerOfAnnotation = containerOfAnnotation; @@ -196,6 +204,7 @@ public Builder containerOfAnnotation(Class containerOfAnno } /** An annotation which, when found on a class, should suppress the test */ + @CanIgnoreReturnValue public Builder suppressAnnotation(Class suppressAnnotation) { checkNotNull(suppressAnnotation); this.suppressAnnotation = suppressAnnotation; @@ -206,6 +215,7 @@ public Builder suppressAnnotation(Class suppressAnnotation * An annotation which, when found on a type parameter, indicates that the type parameter may * only be instantiated with thread-safe types. */ + @CanIgnoreReturnValue public Builder typeParameterAnnotation(Class typeParameterAnnotation) { checkNotNull(typeParameterAnnotation); checkArgument( @@ -232,47 +242,6 @@ public ThreadSafety build(VisitorState state) { } } - /** Use {@link #builder()} instead. */ - // TODO(ghm): Delete after a JB release. - @Deprecated - public ThreadSafety( - VisitorState state, - KnownTypes knownTypes, - Set markerAnnotations, - Set acceptedAnnotations, - @Nullable Class containerOfAnnotation, - @Nullable Class suppressAnnotation) { - this( - state, - knownTypes, - markerAnnotations, - acceptedAnnotations, - containerOfAnnotation, - suppressAnnotation, - /* typeParameterAnnotation= */ null); - } - - /** Use {@link #builder()} instead. */ - @Deprecated - public ThreadSafety( - VisitorState state, - KnownTypes knownTypes, - Set markerAnnotations, - Set acceptedAnnotations, - @Nullable Class containerOfAnnotation, - @Nullable Class suppressAnnotation, - @Nullable Class typeParameterAnnotation) { - this( - state, - Purpose.FOR_IMMUTABLE_CHECKER, - knownTypes, - markerAnnotations, - acceptedAnnotations, - containerOfAnnotation, - suppressAnnotation, - typeParameterAnnotation); - } - private ThreadSafety( VisitorState state, Purpose purpose, @@ -651,6 +620,7 @@ public AnnotationInfo getMarkerOrAcceptedAnnotation(Symbol sym, VisitorState sta } /** Returns an enclosing instance for the specified type if it is thread-safe. */ + @Nullable public Type mutableEnclosingInstance(Optional tree, ClassType type) { if (tree.isPresent() && !CanBeStaticAnalyzer.referencesOuter( @@ -716,6 +686,7 @@ public Set threadSafeTypeParametersInScope(Symbol sym) { return result.build(); } + @Nullable private AnnotationInfo getAnnotation( Symbol sym, ImmutableSet annotationsToCheck, VisitorState state) { for (String annotation : annotationsToCheck) { @@ -727,6 +698,7 @@ private AnnotationInfo getAnnotation( return null; } + @Nullable private AnnotationInfo getAnnotation( Symbol sym, VisitorState state, @@ -742,6 +714,7 @@ private AnnotationInfo getAnnotation( if (attr.isPresent()) { ImmutableList containerElements = containerOf(state, attr.get()); if (elementAnnotation != null && containerElements.isEmpty()) { + containerElements = sym.getTypeParameters().stream() .filter(p -> p.getAnnotation(elementAnnotation) != null) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/WellKnownMutability.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/WellKnownMutability.java index cc947940433..42352e603ae 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/WellKnownMutability.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/WellKnownMutability.java @@ -27,6 +27,7 @@ import com.google.common.primitives.Primitives; import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.Immutable; import com.google.errorprone.bugpatterns.ImmutableCollections; import com.google.errorprone.suppliers.Supplier; @@ -96,6 +97,7 @@ public Set getKnownUnsafeClasses() { static class Builder { final ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); + @CanIgnoreReturnValue public Builder addClasses(Set> clazzs) { for (Class clazz : clazzs) { add(clazz); @@ -103,6 +105,7 @@ public Builder addClasses(Set> clazzs) { return this; } + @CanIgnoreReturnValue public Builder addStrings(List classNames) { for (String className : classNames) { add(className); @@ -110,6 +113,7 @@ public Builder addStrings(List classNames) { return this; } + @CanIgnoreReturnValue public Builder add(Class clazz, String... containerOf) { ImmutableSet containerTyParams = ImmutableSet.copyOf(containerOf); HashSet actualTyParams = new HashSet<>(); @@ -129,6 +133,7 @@ public Builder add(Class clazz, String... containerOf) { return this; } + @CanIgnoreReturnValue public Builder add(String className, String... containerOf) { mapBuilder.put( className, AnnotationInfo.create(className, ImmutableList.copyOf(containerOf))); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/time/InvalidJavaTimeConstant.java b/core/src/main/java/com/google/errorprone/bugpatterns/time/InvalidJavaTimeConstant.java index 8bdbd284f14..72b3a551c78 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/time/InvalidJavaTimeConstant.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/time/InvalidJavaTimeConstant.java @@ -41,6 +41,7 @@ import com.google.common.collect.Maps; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; import com.google.errorprone.matchers.Description; @@ -111,6 +112,7 @@ public abstract static class Builder { abstract ImmutableList.Builder methodsBuilder(); + @CanIgnoreReturnValue public Builder addStaticMethod(String methodName, Param... params) { methodsBuilder() .add( @@ -123,6 +125,7 @@ public Builder addStaticMethod(String methodName, Param... params) { return this; } + @CanIgnoreReturnValue public Builder addInstanceMethod(String methodName, Param... params) { methodsBuilder() .add( diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/time/TimeUnitMismatch.java b/core/src/main/java/com/google/errorprone/bugpatterns/time/TimeUnitMismatch.java index 0d291d8bb7b..38329e38085 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/time/TimeUnitMismatch.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/time/TimeUnitMismatch.java @@ -371,6 +371,7 @@ private static String extractArgumentName(ExpressionTree expr) { isSameType("java.lang.Long"), isSameType("java.lang.Double")); + @Nullable @VisibleForTesting static TimeUnit unitSuggestedByName(String name) { // Tuple types, especially Pair, trip us up. Skip APIs that might be from them. diff --git a/core/src/main/java/com/google/errorprone/refaster/Bindings.java b/core/src/main/java/com/google/errorprone/refaster/Bindings.java index 6904e63ee69..1b4754dfdb8 100644 --- a/core/src/main/java/com/google/errorprone/refaster/Bindings.java +++ b/core/src/main/java/com/google/errorprone/refaster/Bindings.java @@ -23,6 +23,7 @@ import com.google.common.collect.ForwardingMap; import com.google.common.collect.Maps; import com.google.common.reflect.TypeToken; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -128,6 +129,8 @@ public V putBinding(Key key, V value) { return (V) super.put(key, value); } + @CanIgnoreReturnValue + @Nullable @Override public Object put(Key key, Object value) { checkNotNull(key, "key"); diff --git a/core/src/main/java/com/google/errorprone/refaster/Choice.java b/core/src/main/java/com/google/errorprone/refaster/Choice.java index 6e1c27ec600..b385314b3d0 100644 --- a/core/src/main/java/com/google/errorprone/refaster/Choice.java +++ b/core/src/main/java/com/google/errorprone/refaster/Choice.java @@ -25,6 +25,7 @@ import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.ForOverride; import java.util.Collection; import java.util.Collections; @@ -78,6 +79,7 @@ public Choice or(Choice other) { return checkNotNull(other); } + @CanIgnoreReturnValue @Override public Choice condition(Predicate predicate) { checkNotNull(predicate); diff --git a/core/src/main/java/com/google/errorprone/refaster/Refaster.java b/core/src/main/java/com/google/errorprone/refaster/Refaster.java index 44776dd461f..96b72f11f77 100644 --- a/core/src/main/java/com/google/errorprone/refaster/Refaster.java +++ b/core/src/main/java/com/google/errorprone/refaster/Refaster.java @@ -16,6 +16,8 @@ package com.google.errorprone.refaster; +import com.google.errorprone.annotations.CanIgnoreReturnValue; + /** * Static utilities to indicate special handling in Refaster templates. * @@ -68,6 +70,7 @@ public static T[] asVarargs(T arg) { * */ @SafeVarargs + @CanIgnoreReturnValue public static T anyOf(T... expressions) { throw new UnsupportedOperationException(); } diff --git a/core/src/main/java/com/google/errorprone/refaster/Template.java b/core/src/main/java/com/google/errorprone/refaster/Template.java index 0fbbc435cf5..7c1a2712e65 100644 --- a/core/src/main/java/com/google/errorprone/refaster/Template.java +++ b/core/src/main/java/com/google/errorprone/refaster/Template.java @@ -492,6 +492,7 @@ private Type infer( } /** Reflectively instantiate the package-private {@code MethodResolutionPhase} enum. */ + @Nullable private static Object newMethodResolutionPhase(boolean autoboxing) { for (Class c : Resolve.class.getDeclaredClasses()) { if (!c.getName().equals("com.sun.tools.javac.comp.Resolve$MethodResolutionPhase")) { diff --git a/core/src/main/java/com/google/errorprone/refaster/UBreak.java b/core/src/main/java/com/google/errorprone/refaster/UBreak.java index b552ac08aba..5aadd598140 100644 --- a/core/src/main/java/com/google/errorprone/refaster/UBreak.java +++ b/core/src/main/java/com/google/errorprone/refaster/UBreak.java @@ -45,6 +45,7 @@ static UBreak create(@Nullable CharSequence label) { } // TODO(b/176098078): Add @Override once compiling JDK 12+ + @Nullable public ExpressionTree getValue() { return null; } diff --git a/core/src/main/java/com/google/errorprone/refaster/URepeated.java b/core/src/main/java/com/google/errorprone/refaster/URepeated.java index a537d735666..36059637d31 100644 --- a/core/src/main/java/com/google/errorprone/refaster/URepeated.java +++ b/core/src/main/java/com/google/errorprone/refaster/URepeated.java @@ -57,6 +57,7 @@ public Kind getKind() { } /** Gets the binding of the underlying identifier in the unifier. */ + @Nullable public JCExpression getUnderlyingBinding(Unifier unifier) { return (unifier == null) ? null : unifier.getBinding(new UFreeIdent.Key(identifier())); } diff --git a/core/src/main/java/com/google/errorprone/refaster/annotation/RequiredAnnotationProcessor.java b/core/src/main/java/com/google/errorprone/refaster/annotation/RequiredAnnotationProcessor.java index 0c7b41bd5f8..012a2960916 100644 --- a/core/src/main/java/com/google/errorprone/refaster/annotation/RequiredAnnotationProcessor.java +++ b/core/src/main/java/com/google/errorprone/refaster/annotation/RequiredAnnotationProcessor.java @@ -34,6 +34,7 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.util.SimpleAnnotationValueVisitor7; import javax.tools.Diagnostic.Kind; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Enforces {@code @RequiredAnnotation} as an annotation processor. @@ -51,7 +52,8 @@ public boolean process(Set annotations, RoundEnvironment return false; } - private AnnotationMirror getAnnotationMirror(Element element, TypeMirror annotationType) { + private @Nullable AnnotationMirror getAnnotationMirror( + Element element, TypeMirror annotationType) { for (AnnotationMirror mirror : element.getAnnotationMirrors()) { if (processingEnv.getTypeUtils().isSameType(mirror.getAnnotationType(), annotationType)) { return mirror; @@ -60,7 +62,7 @@ private AnnotationMirror getAnnotationMirror(Element element, TypeMirror annotat return null; } - private AnnotationValue getAnnotationValue(AnnotationMirror mirror, String key) { + private @Nullable AnnotationValue getAnnotationValue(AnnotationMirror mirror, String key) { for (Map.Entry entry : mirror.getElementValues().entrySet()) { if (entry.getKey().getSimpleName().contentEquals(key)) { diff --git a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java index 829d5a3fdf9..184cad3cd19 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -61,6 +61,7 @@ import com.google.errorprone.bugpatterns.ByteBufferBackingArray; import com.google.errorprone.bugpatterns.CacheLoaderNull; import com.google.errorprone.bugpatterns.CannotMockFinalClass; +import com.google.errorprone.bugpatterns.CannotMockFinalMethod; import com.google.errorprone.bugpatterns.CanonicalDuration; import com.google.errorprone.bugpatterns.CatchAndPrintStackTrace; import com.google.errorprone.bugpatterns.CatchFail; @@ -100,6 +101,7 @@ import com.google.errorprone.bugpatterns.DepAnn; import com.google.errorprone.bugpatterns.DeprecatedVariable; import com.google.errorprone.bugpatterns.DifferentNameButSame; +import com.google.errorprone.bugpatterns.DirectInvocationOnMock; import com.google.errorprone.bugpatterns.DiscardedPostfixExpression; import com.google.errorprone.bugpatterns.DistinctVarargsChecker; import com.google.errorprone.bugpatterns.DoNotCallChecker; @@ -222,6 +224,7 @@ import com.google.errorprone.bugpatterns.MixedArrayDimensions; import com.google.errorprone.bugpatterns.MixedDescriptors; import com.google.errorprone.bugpatterns.MixedMutabilityReturnType; +import com.google.errorprone.bugpatterns.MockNotUsedInProduction; import com.google.errorprone.bugpatterns.MockitoUsage; import com.google.errorprone.bugpatterns.ModifiedButNotUsed; import com.google.errorprone.bugpatterns.ModifyCollectionInEnhancedForLoop; @@ -301,6 +304,7 @@ import com.google.errorprone.bugpatterns.RobolectricShadowDirectlyOn; import com.google.errorprone.bugpatterns.RxReturnValueIgnored; import com.google.errorprone.bugpatterns.SameNameButDifferent; +import com.google.errorprone.bugpatterns.SelfAlwaysReturnsThis; import com.google.errorprone.bugpatterns.SelfAssignment; import com.google.errorprone.bugpatterns.SelfComparison; import com.google.errorprone.bugpatterns.SelfEquals; @@ -385,6 +389,7 @@ import com.google.errorprone.bugpatterns.UnusedException; import com.google.errorprone.bugpatterns.UnusedMethod; import com.google.errorprone.bugpatterns.UnusedNestedClass; +import com.google.errorprone.bugpatterns.UnusedTypeParameter; import com.google.errorprone.bugpatterns.UnusedVariable; import com.google.errorprone.bugpatterns.UseCorrectAssertInTests; import com.google.errorprone.bugpatterns.UseEnumSwitch; @@ -414,6 +419,9 @@ import com.google.errorprone.bugpatterns.argumentselectiondefects.ArgumentSelectionDefectChecker; import com.google.errorprone.bugpatterns.argumentselectiondefects.AssertEqualsArgumentOrderChecker; import com.google.errorprone.bugpatterns.argumentselectiondefects.AutoValueConstructorOrderChecker; +import com.google.errorprone.bugpatterns.checkreturnvalue.BuilderReturnThis; +import com.google.errorprone.bugpatterns.checkreturnvalue.CanIgnoreReturnValueSuggester; +import com.google.errorprone.bugpatterns.checkreturnvalue.NoCanIgnoreReturnValueOnClasses; import com.google.errorprone.bugpatterns.checkreturnvalue.UsingJsr305CheckReturnValue; import com.google.errorprone.bugpatterns.collectionincompatibletype.CollectionIncompatibleType; import com.google.errorprone.bugpatterns.collectionincompatibletype.CollectionUndefinedEquality; @@ -490,10 +498,13 @@ import com.google.errorprone.bugpatterns.javadoc.UrlInSee; import com.google.errorprone.bugpatterns.nullness.EqualsBrokenForNull; import com.google.errorprone.bugpatterns.nullness.EqualsMissingNullable; +import com.google.errorprone.bugpatterns.nullness.ExtendsObject; import com.google.errorprone.bugpatterns.nullness.FieldMissingNullable; +import com.google.errorprone.bugpatterns.nullness.NullArgumentForNonNullParameter; import com.google.errorprone.bugpatterns.nullness.ParameterMissingNullable; import com.google.errorprone.bugpatterns.nullness.ReturnMissingNullable; import com.google.errorprone.bugpatterns.nullness.UnnecessaryCheckNotNull; +import com.google.errorprone.bugpatterns.nullness.UnsafeWildcard; import com.google.errorprone.bugpatterns.nullness.VoidMissingNullable; import com.google.errorprone.bugpatterns.overloading.InconsistentOverloads; import com.google.errorprone.bugpatterns.threadsafety.DoubleCheckedLocking; @@ -700,9 +711,11 @@ public static ScannerSupplier errorChecks() { MoreThanOneScopeAnnotationOnClass.class, MustBeClosedChecker.class, NCopiesOfChar.class, + NoCanIgnoreReturnValueOnClasses.class, NonCanonicalStaticImport.class, NonFinalCompileTimeConstant.class, NonRuntimeAnnotation.class, + NullArgumentForNonNullParameter.class, NullTernary.class, NullableOnContainingClass.class, OptionalEquality.class, @@ -753,6 +766,7 @@ public static ScannerSupplier errorChecks() { UnicodeInCode.class, UnnecessaryCheckNotNull.class, UnnecessaryTypeArgument.class, + UnsafeWildcard.class, UnusedAnonymousClass.class, UnusedCollectionModifiedInPlace.class, Validator.class, @@ -790,7 +804,7 @@ public static ScannerSupplier errorChecks() { BugPatternNaming.class, ByteBufferBackingArray.class, CacheLoaderNull.class, - CannotMockFinalClass.class, + CanIgnoreReturnValueSuggester.class, CanonicalDuration.class, CatchAndPrintStackTrace.class, CatchFail.class, @@ -809,6 +823,7 @@ public static ScannerSupplier errorChecks() { DefaultCharset.class, DefaultPackage.class, DeprecatedVariable.class, + DirectInvocationOnMock.class, DistinctVarargsChecker.class, DoNotCallSuggester.class, DoNotClaimAnnotations.class, @@ -825,6 +840,7 @@ public static ScannerSupplier errorChecks() { ErroneousThreadPoolConstructorChecker.class, EscapedEntity.class, ExtendingJUnitAssert.class, + ExtendsObject.class, FallThrough.class, Finally.class, FloatCast.class, @@ -899,6 +915,7 @@ public static ScannerSupplier errorChecks() { MissingOverride.class, MissingSummary.class, MixedMutabilityReturnType.class, + MockNotUsedInProduction.class, ModifiedButNotUsed.class, ModifyCollectionInEnhancedForLoop.class, ModifySourceCollectionInStream.class, @@ -944,6 +961,7 @@ public static ScannerSupplier errorChecks() { RxReturnValueIgnored.class, SameNameButDifferent.class, ScopeAnnotationOnInterfaceOrAbstractClass.class, + SelfAlwaysReturnsThis.class, ShortCircuitBoolean.class, StaticAssignmentInConstructor.class, StaticAssignmentOfThrowable.class, @@ -985,6 +1003,7 @@ public static ScannerSupplier errorChecks() { UnsynchronizedOverridesSynchronized.class, UnusedMethod.class, UnusedNestedClass.class, + UnusedTypeParameter.class, UnusedVariable.class, UseBinds.class, UseCorrectAssertInTests.class, @@ -1010,6 +1029,9 @@ public static ScannerSupplier errorChecks() { BinderIdentityRestoredDangerously.class, // TODO: enable this by default. BindingToUnqualifiedCommonType.class, BooleanParameter.class, + BuilderReturnThis.class, + CannotMockFinalClass.class, + CannotMockFinalMethod.class, CatchingUnchecked.class, CheckedExceptionNotThrown.class, ClassName.class, diff --git a/core/src/test/java/com/google/errorprone/ErrorProneTestCompiler.java b/core/src/test/java/com/google/errorprone/ErrorProneTestCompiler.java index 089d6d36b4f..ef2fb5b91f1 100644 --- a/core/src/test/java/com/google/errorprone/ErrorProneTestCompiler.java +++ b/core/src/test/java/com/google/errorprone/ErrorProneTestCompiler.java @@ -16,6 +16,7 @@ package com.google.errorprone; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.scanner.ScannerSupplier; import com.sun.tools.javac.main.Main.Result; import java.io.PrintWriter; @@ -40,16 +41,19 @@ public ErrorProneTestCompiler build() { return new ErrorProneTestCompiler(listener, scannerSupplier, printWriter); } + @CanIgnoreReturnValue public Builder listenToDiagnostics(DiagnosticListener listener) { this.listener = listener; return this; } + @CanIgnoreReturnValue public Builder report(ScannerSupplier scannerSupplier) { this.scannerSupplier = scannerSupplier; return this; } + @CanIgnoreReturnValue public Builder redirectOutputTo(PrintWriter printWriter) { this.printWriter = printWriter; return this; diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/CannotMockFinalMethodTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/CannotMockFinalMethodTest.java new file mode 100644 index 00000000000..86f5379f4b2 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/CannotMockFinalMethodTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import com.google.errorprone.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CannotMockFinalMethodTest { + private final CompilationTestHelper compilationHelper = + CompilationTestHelper.newInstance(CannotMockFinalMethod.class, getClass()); + + @Test + public void whenCall_flagged() { + compilationHelper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.when;", + "class Test {", + " final Integer foo() {", + " return 1;", + " }", + " void test() {", + " // BUG: Diagnostic contains:", + " when(this.foo());", + " }", + "}") + .doTest(); + } + + @Test + public void verifyCall_flagged() { + compilationHelper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.verify;", + "class Test {", + " final Integer foo() {", + " return 1;", + " }", + " void test() {", + " // BUG: Diagnostic contains:", + " verify(this).foo();", + " }", + "}") + .doTest(); + } + + @Test + public void negative() { + compilationHelper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.when;", + "class Test {", + " Integer foo() {", + " return 1;", + " }", + " void test() {", + " when(this.foo());", + " }", + "}") + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java index 298fa258553..aee31f86741 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java @@ -119,7 +119,7 @@ public void testPackageAnnotation() { "Test.java", "class Test {", " void m() {", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " lib.Lib.f();", " }", "}") @@ -140,7 +140,7 @@ public void testClassAnnotation() { "Test.java", "class Test {", " void m() {", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " lib.Lib.f();", " }", "}") @@ -311,7 +311,7 @@ public void testNestedClassAnnotation() { "Test.java", "class Test {", " void m() {", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " lib.Lib.Inner.InnerMost.f();", " }", "}") @@ -537,7 +537,7 @@ public void ignoreInThrowingRunnables() { " foo.f(); ", " });", " org.junit.Assert.assertThrows(IllegalStateException.class, () -> { ", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " foo.f(); ", " foo.f(); ", " });", @@ -591,13 +591,13 @@ public void onlyIgnoreWithEnclosingTryCatch() { "import static org.junit.Assert.fail;", "class Test {", " void f(Foo foo) {", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " foo.f();", " org.junit.Assert.fail();", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " foo.f();", " junit.framework.Assert.fail();", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " foo.f();", " junit.framework.TestCase.fail();", " }", @@ -663,7 +663,7 @@ public void noCRVonClasspath() { "Test.java", "class Test {", " void m() {", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " com.google.errorprone.bugpatterns.CheckReturnValueTest.CRVTest.f();", " }", "}") @@ -680,7 +680,7 @@ public void constructor() { " @com.google.errorprone.annotations.CheckReturnValue", " public Test() {}", " public static void foo() {", - " // BUG: Diagnostic contains: Ignored return value of 'Test'", + " // BUG: Diagnostic contains: ", " new Test();", " }", "}") @@ -834,7 +834,7 @@ public void constructor_reference() { "Test.java", "class Test {", " void f() {", - " // BUG: Diagnostic contains: Ignored return value of 'Foo', which is annotated", + " // BUG: Diagnostic contains: ", " Runnable ignoresResult = Foo::new;", " }", "}") @@ -849,8 +849,7 @@ public void constructor_withoutCrvAnnotation() { "class Test {", " public Test() {}", " public static void foo() {", - " // BUG: Diagnostic contains: Ignored return value of 'Test', which wasn't" - + " annotated with @CanIgnoreReturnValue", + " // BUG: Diagnostic contains: ", " new Test();", " }", "}") @@ -865,8 +864,7 @@ public void allMethods_withoutCIRVAnnotation() { "class Test {", " public int bar() { return 42; }", " public static void foo() {", - " // BUG: Diagnostic contains: Ignored return value of 'bar', which wasn't" - + " annotated with @CanIgnoreReturnValue", + " // BUG: Diagnostic contains: ", " new Test().bar();", " }", "}") @@ -882,7 +880,7 @@ public void allMethods_withExternallyConfiguredIgnoreList() { "class Test {", " public static void foo(List x) {", " x.add(42);", - " // BUG: Diagnostic contains: Ignored return value of 'get'", + " // BUG: Diagnostic contains: ", " x.get(0);", " }", "}") @@ -906,12 +904,12 @@ public void usingElementInTestExpected() { " @Test(expected = IllegalArgumentException.class) ", " public void fooWith2Statements() {", " Foo f = new Foo();", - " // BUG: Diagnostic contains: Ignored return value of 'Foo'", + " // BUG: Diagnostic contains: ", " new Foo();", // Not OK if there is more than one statement in the block. " }", " @Test(expected = Test.None.class) ", // This is a weird way to spell the default " public void fooWithNone() {", - " // BUG: Diagnostic contains: Ignored return value of 'Foo'", + " // BUG: Diagnostic contains: ", " new Foo();", " }", "}") @@ -948,7 +946,7 @@ public void testAutoValueBuilderSetterMethods() { " static void testAnimal() {", " Animal.Builder builder = Animal.builder();", " builder.setNumberOfLegs(4);", // AutoValue.Builder setters are implicitly @CIRV - " // BUG: Diagnostic contains: Ignored return value of 'build'", + " // BUG: Diagnostic contains: ", " builder.build();", " }", "}") @@ -985,12 +983,12 @@ public void testAutoBuilderSetterMethods() { "package com.google.frobber;", "public final class PersonCaller {", " static void testPersonBuilder() {", - " // BUG: Diagnostic contains: Ignored return value of 'personBuilder'", + " // BUG: Diagnostic contains: ", " PersonBuilder.personBuilder();", " PersonBuilder builder = PersonBuilder.personBuilder();", " builder.setName(\"kurt\");", // AutoBuilder setters are implicitly @CIRV " builder.setId(42);", // AutoBuilder setters are implicitly @CIRV - " // BUG: Diagnostic contains: Ignored return value of 'build'", + " // BUG: Diagnostic contains: ", " builder.build();", " }", "}") @@ -1030,7 +1028,7 @@ public void testAutoBuilderSetterMethods_withInterface() { "import java.util.logging.Level;", "public final class LogCaller {", " static void testLogCaller() {", - " // BUG: Diagnostic contains: Ignored return value of 'logCaller'", + " // BUG: Diagnostic contains: ", " Caller.logCaller();", " Caller caller = Caller.logCaller();", " caller.setMessage(\"hi\");", // AutoBuilder setters are implicitly @CIRV @@ -1042,6 +1040,64 @@ public void testAutoBuilderSetterMethods_withInterface() { .doTest(); } + @Test + public void testPackagesRule() { + compilationHelperWithPackagePatterns("java.util") + .addSourceLines( + "Test.java", + "import java.util.List;", + "import java.util.regex.Pattern;", + "class Test {", + " public static void foo(List list, Pattern pattern) {", + " // BUG: Diagnostic contains: ", + " list.get(0);", + " // BUG: Diagnostic contains: ", + " pattern.matcher(\"blah\");", + " }", + "}") + .doTest(); + } + + @Test + public void testPackagesRule_negativePattern() { + compilationHelperWithPackagePatterns("java.util", "-java.util.regex") + .addSourceLines( + "Test.java", + "import java.util.List;", + "import java.util.regex.Pattern;", + "class Test {", + " public static void foo(List list, Pattern pattern) {", + " // BUG: Diagnostic contains: ", + " list.get(0);", + " pattern.matcher(\"blah\");", + " }", + "}") + .doTest(); + } + + @Test + public void testPackagesRule_negativePattern_doesNotMakeOptional() { + // A negative pattern just makes the packages rule itself not apply to that package and its + // subpackages if it otherwise would because of a positive pattern on a superpackage. It doesn't + // make APIs in that package CIRV. + compilationHelperWithPackagePatterns("java.util", "-java.util.regex") + .addSourceLines( + "Test.java", + "import java.util.List;", + "import java.util.regex.Pattern;", + "import java.util.regex.PatternSyntaxException;", + "class Test {", + " public static void foo(List list, Pattern pattern) {", + " // BUG: Diagnostic contains: ", + " list.get(0);", + " pattern.matcher(\"blah\");", + " // BUG: Diagnostic contains: ", + " new PatternSyntaxException(\"\", \"\", 0);", + " }", + "}") + .doTest(); + } + private CompilationTestHelper compilationHelperLookingAtAllConstructors() { return compilationHelper.setArgs( "-XepOpt:" + CheckReturnValue.CHECK_ALL_CONSTRUCTORS + "=true"); @@ -1063,4 +1119,10 @@ private CompilationTestHelper compileWithExternalApis(String... apis) { throw new UncheckedIOException(e); } } + + private CompilationTestHelper compilationHelperWithPackagePatterns(String... patterns) { + return compilationHelper.setArgs( + "-XepOpt:" + CheckReturnValue.CRV_PACKAGES + "=" + Joiner.on(',').join(patterns), + "-XepOpt:" + CheckReturnValue.CHECK_ALL_CONSTRUCTORS + "=true"); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java new file mode 100644 index 00000000000..4799062d0e2 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java @@ -0,0 +1,243 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link DirectInvocationOnMock}. */ +@RunWith(JUnit4.class) +public final class DirectInvocationOnMockTest { + private final CompilationTestHelper helper = + CompilationTestHelper.newInstance(DirectInvocationOnMock.class, getClass()); + + private final BugCheckerRefactoringTestHelper refactoring = + BugCheckerRefactoringTestHelper.newInstance(DirectInvocationOnMock.class, getClass()); + + @Test + public void directInvocationOnMock() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "class Test {", + " public void test() {", + " Test test = mock(Test.class);", + " // BUG: Diagnostic contains: test", + " test.test();", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMockAssignment() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "class Test {", + " public void test() {", + " Test test;", + " test = mock(Test.class);", + " // BUG: Diagnostic contains:", + " test.test();", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMock_suggestsVerify() { + refactoring + .addInputLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.verify;", + "class Test {", + " public void test() {", + " Test test = mock(Test.class);", + " test.test();", + " }", + "}") + .addOutputLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.verify;", + "class Test {", + " public void test() {", + " Test test = mock(Test.class);", + " verify(test).test();", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMock_mockHasExtraOptions_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import org.mockito.Answers;", + "class Test {", + " public void test() {", + " Test test = mock(Test.class, Answers.RETURNS_DEEP_STUBS);", + " test.test();", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMockAnnotatedField() { + helper + .addSourceLines( + "Test.java", + "import org.mockito.Mock;", + "class Test {", + " @Mock public Test test;", + " public void test() {", + " // BUG: Diagnostic contains:", + " test.test();", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMockAnnotatedField_mockHasExtraOptions_noFinding() { + helper + .addSourceLines( + "Test.java", + "import org.mockito.Answers;", + "import org.mockito.Mock;", + "class Test {", + " @Mock(answer = Answers.RETURNS_DEEP_STUBS) public Test test;", + " public void test() {", + " test.test();", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMock_withinWhen_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "class Test {", + " public Object test() {", + " Test test = mock(Test.class);", + " when(test.test()).thenReturn(null);", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMock_setUpToCallRealMethod_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "class Test {", + " public Object test() {", + " Test test = mock(Test.class);", + " when(test.test()).thenCallRealMethod();", + " return test.test();", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMock_setUpWithDoCallRealMethod_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.doCallRealMethod;", + "class Test {", + " public Object test() {", + " Test test = mock(Test.class);", + " doCallRealMethod().when(test).test();", + " return test.test();", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMock_withinCustomWhen_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import org.mockito.stubbing.OngoingStubbing;", + "class Test {", + " public OngoingStubbing when(T t) {", + " return org.mockito.Mockito.when(t);", + " }", + " public Object test() {", + " Test test = mock(Test.class);", + " when(test.test()).thenReturn(null);", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMock_withinWhenWithCast_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "class Test {", + " public Object test() {", + " Test test = mock(Test.class);", + " when((Object) test.test()).thenReturn(null);", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void finalMethodInvoked_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "class Test {", + " public Object test() {", + " Test test = mock(Test.class);", + " return test.getClass();", + " }", + "}") + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DistinctVarargsCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DistinctVarargsCheckerTest.java index 0d78edd7d70..ce1f3379113 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/DistinctVarargsCheckerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DistinctVarargsCheckerTest.java @@ -16,8 +16,11 @@ package com.google.errorprone.bugpatterns; +import static java.util.stream.Collectors.joining; + import com.google.errorprone.BugCheckerRefactoringTestHelper; import com.google.errorprone.CompilationTestHelper; +import java.util.stream.IntStream; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -195,4 +198,25 @@ public void distinctVarargsChecker_differentVarsInImmutableSetVarargsMethod_shou "}") .doTest(); } + + @Test + public void negative_quadratic() { + + String large = + IntStream.range(0, 7000) + .mapToObj(x -> String.format("\"%s\"", x)) + .collect(joining(", ", "ImmutableSet.of(", ", \"0\");")); + + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.common.collect.ImmutableSet;", + "public class Test {", + " void testFunction() {", + " // BUG: Diagnostic contains: DistinctVarargsChecker", + large, + " }", + "}") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java index b80ffb765d1..5c811e456d0 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java @@ -552,4 +552,183 @@ public void positive_getSimpleName_refactoredToGetClassName() { "}") .doTest(); } + + @Test + public void positive_stackWalkerGetClass() { + testHelper + .addSourceLines( + "Test.java", + "class Test{", + " void f(StackWalker w) {", + " // BUG: Diagnostic contains: getCallerClass", + " w.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_stackFrameGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import java.lang.StackWalker.StackFrame;", + "class Test{", + " void f(StackFrame f) {", + " // BUG: Diagnostic contains: getClassName", + " f.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_constructorGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import java.lang.reflect.Constructor;", + "class Test{", + " void f(Constructor c) {", + " // BUG: Diagnostic contains: getDeclaringClass", + " c.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_fieldGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import java.lang.reflect.Field;", + "class Test{", + " void f(Field f) {", + " // BUG: Diagnostic contains: getDeclaringClass", + " f.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_methodGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import java.lang.reflect.Method;", + "class Test{", + " void f(Method m) {", + " // BUG: Diagnostic contains: getDeclaringClass", + " m.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_beanDescriptorGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import java.beans.BeanDescriptor;", + "class Test{", + " void f(BeanDescriptor b) {", + " // BUG: Diagnostic contains: getBeanClass", + " b.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_lockInfoGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import java.lang.management.LockInfo;", + "import java.lang.management.MonitorInfo;", + "class Test{", + " void f(LockInfo l, MonitorInfo m) {", + " // BUG: Diagnostic contains: getClassName", + " l.getClass();", + " // BUG: Diagnostic contains: getClassName", + " m.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_parameterizedTypeGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import java.lang.reflect.ParameterizedType;", + "class Test{", + " void f(ParameterizedType t) {", + " // BUG: Diagnostic contains: getRawType", + " t.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_classInfoGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import com.google.common.reflect.ClassPath.ClassInfo;", + "class Test{", + " void f(ClassInfo i) {", + " // BUG: Diagnostic contains: getName", + " i.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_typeTokenGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import com.google.common.reflect.TypeToken;", + "class Test{", + " void f(TypeToken t) {", + " // BUG: Diagnostic contains: getRawType", + " t.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_threadRun() { + testHelper + .addSourceLines( + "Test.java", + "class Test {", + " void f(Thread t) {", + " // BUG: Diagnostic contains: start", + " t.run();", + " }", + "}") + .doTest(); + } + + @Test + public void negative_threadSuperRun() { + testHelper + .addSourceLines( + "Test.java", + "class Test extends Thread {", + " @Override public void run() {", + " super.run();", + " }", + "}") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/FloatingPointLiteralPrecisionTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/FloatingPointLiteralPrecisionTest.java index 9449e736134..a3b2ee2c4fb 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/FloatingPointLiteralPrecisionTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/FloatingPointLiteralPrecisionTest.java @@ -17,6 +17,7 @@ package com.google.errorprone.bugpatterns; import static com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH; +import static org.junit.Assume.assumeTrue; import com.google.errorprone.BugCheckerRefactoringTestHelper; import com.google.errorprone.CompilationTestHelper; @@ -70,6 +71,13 @@ public void negative() { @Test public void replacementTooLong() { + // In JDK versions before 19, String.valueOf(1e23) was 9.999999999999999E22, and the logic we're + // testing here was introduced to avoid introducing strings like that in rewrites. JDK 19 fixes + // https://bugs.openjdk.org/browse/JDK-4511638 (over 20 years after it was filed) so + // we don't need the logic or its test there. + String string1e23 = String.valueOf(1e23); + assumeTrue(string1e23.length() > "1e23".length() * 3); + String[] input = { "class Test {", // " // BUG: Diagnostic contains:", diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/LongDoubleConversionTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/LongDoubleConversionTest.java index 51e1f95d1cd..467a60d81bd 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/LongDoubleConversionTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/LongDoubleConversionTest.java @@ -33,7 +33,7 @@ public class LongDoubleConversionTest { BugCheckerRefactoringTestHelper.newInstance(LongDoubleConversion.class, getClass()); @Test - public void doesNotCompile() { + public void losesPrecision() { compilationTestHelper .addSourceLines( "Test.java", // @@ -42,6 +42,21 @@ public void doesNotCompile() { " void method(double l) {}", " {", " // BUG: Diagnostic contains:", + " method(9223372036854775806L);", + " }", + "}") + .doTest(); + } + + @Test + public void doesNotActuallyLosePrecision_noFinding() { + compilationTestHelper + .addSourceLines( + "Test.java", // + "class Test {", + " void method(Long l) {}", + " void method(double l) {}", + " {", " method(0L);", " }", "}") @@ -49,7 +64,7 @@ public void doesNotCompile() { } @Test - public void compiles() { + public void explicitlyCastToDouble_noFinding() { compilationTestHelper .addSourceLines( "Test.java", // @@ -71,7 +86,7 @@ public void refactors_simpleArgument() { " void method(Long l) {}", " void method(double l) {}", " {", - " method(0L);", + " method(9223372036854775806L);", " }", "}") .addOutputLines( @@ -80,7 +95,7 @@ public void refactors_simpleArgument() { " void method(Long l) {}", " void method(double l) {}", " {", - " method((double) 0L);", + " method((double) 9223372036854775806L);", " }", "}") .doTest(); @@ -95,7 +110,7 @@ public void refactors_complexArgument() { " void method(Long l) {}", " void method(double l) {}", " {", - " method(0L + 1L);", + " method(9223372036854775805L + 1L);", " }", "}") .addOutputLines( @@ -104,7 +119,7 @@ public void refactors_complexArgument() { " void method(Long l) {}", " void method(double l) {}", " {", - " method((double) (0L + 1L));", + " method((double) (9223372036854775805L + 1L));", " }", "}") .doTest(); diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/MethodCanBeStaticTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/MethodCanBeStaticTest.java index 330fe98d904..c06a3e6c69c 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/MethodCanBeStaticTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/MethodCanBeStaticTest.java @@ -484,4 +484,12 @@ public void abstractMethod_notFlagged() { "}") .doTest(); } + + @Test + public void defaultMethodExempted() { + testHelper + .addSourceLines( + "Test.java", "class Test {", " private interface Foo { default void foo() {} }", "}") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/MockNotUsedInProductionTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/MockNotUsedInProductionTest.java new file mode 100644 index 00000000000..97214fe4b91 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/MockNotUsedInProductionTest.java @@ -0,0 +1,232 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class MockNotUsedInProductionTest { + private final CompilationTestHelper helper = + CompilationTestHelper.newInstance(MockNotUsedInProduction.class, getClass()); + + private final BugCheckerRefactoringTestHelper refactoring = + BugCheckerRefactoringTestHelper.newInstance(MockNotUsedInProduction.class, getClass()); + + @Test + public void neverUsed() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "class Test {", + " public Object test() {", + " // BUG: Diagnostic contains:", + " Test test = mock(Test.class);", + " when(test.test()).thenCallRealMethod();", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void spyNeverUsed() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.spy;", + "import static org.mockito.Mockito.verify;", + "class Test {", + " public Object test() {", + " // BUG: Diagnostic contains:", + " Test test = spy(new Test());", + " verify(test).test();", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void passedToProduction() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "class Test {", + " public Object test() {", + " Test test = mock(Test.class);", + " when(test.test()).thenCallRealMethod();", + " return test.test();", + " }", + "}") + .doTest(); + } + + @Test + public void possiblyBound() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.Mock;", + "class Test {", + " @Bind @Mock public Test test;", + " public Object test() {", + " when(test.test()).thenCallRealMethod();", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void publicField() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.Mock;", + "class Test {", + " @Mock public Test test;", + " public Object test() {", + " when(test.test()).thenCallRealMethod();", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void qualifiedWithThis_stillSeen() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.Mock;", + "class Test {", + " @Mock private Test test;", + " public Test test() {", + " return this.test;", + " }", + "}") + .doTest(); + } + + @Test + public void privateField() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.Mock;", + "class Test {", + " // BUG: Diagnostic contains:", + " @Mock private Test test;", + " public Object test() {", + " when(test.test()).thenCallRealMethod();", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void injectMocks_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.InjectMocks;", + "import org.mockito.Mock;", + "class Test {", + " @Mock private Test test;", + " @InjectMocks Test t;", + " public Object test() {", + " when(test.test()).thenCallRealMethod();", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void suppressionWorks() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.Mock;", + "class Test {", + " @SuppressWarnings(\"MockNotUsedInProduction\")", + " @Mock private Test test;", + " public Object test() {", + " when(test.test()).thenCallRealMethod();", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void refactoring() { + refactoring + .addInputLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.Mock;", + "class Test {", + " @Mock private Test test;", + " public Object test() {", + " when(test.test()).thenCallRealMethod();", + " return null;", + " }", + "}") + .addOutputLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.Mock;", + "class Test {", + " public Object test() {", + " return null;", + " }", + "}") + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/MustBeClosedCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/MustBeClosedCheckerTest.java index 344cf9238bf..035928d3f84 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/MustBeClosedCheckerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/MustBeClosedCheckerTest.java @@ -50,4 +50,26 @@ public void refactoring() { .allowBreakingChanges() // The fix is best-effort, and some variable names may clash .doTest(); } + + @Test + public void enumInitializer() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.MustBeClosed;", + "import java.io.Closeable;", + "enum Test {", + " A;", + " interface Foo extends Closeable {}", + " @MustBeClosed static Foo createResource() {", + " return null;", + " }", + " private final Foo resource;", + " private final Foo resource2 = createResource();", + " Test() {", + " this.resource = createResource();", + " }", + "}") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/ParameterNameTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/ParameterNameTest.java index 92e8ca467ee..3769df4c379 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/ParameterNameTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/ParameterNameTest.java @@ -269,6 +269,39 @@ public void namedParametersChecker_toleratesMatchingComment_lineAfter() { .doTest(); } + /** + * We allow multiple comments if any one of them is right. This helps libraries migrate to a new + * parameter name gradually without breaking any builds: update the caller to use both names, then + * update the API, then remove the old name. + */ + @Test + public void namedParametersChecker_multipleComments_allowedIfAnyMatch() { + testHelper + .addSourceLines( + "Test.java", + "class Test {", + " void test(Object x) {", + " test(/* y= */ /* x= */ x);", + " test(/* x= */ /* y= */ x);", + " }", + "}") + .doTest(); + } + + @Test + public void namedParametersChecker_multipleComments_flaggedIfNoneMatch() { + testHelper + .addSourceLines( + "Test.java", + "class Test {", + " void test(Object x) {", + " // BUG: Diagnostic contains: does not match", + " test(/* y= */ /* z= */ x);", + " }", + "}") + .doTest(); + } + @Test public void namedParametersChecker_ignoresComment_nonMatchinglineAfter() { testHelper @@ -299,6 +332,22 @@ public void namedParametersChecker_ignoresComment_markedUpDelimiter() { .doTest(); } + @Test + public void namedParametersChecker_ignoresLineComments() { + testHelper + .addSourceLines( + "Test.java", + "class Test {", + " void test(int x) {", + " test(", + " // newX =", + " // (x ^ 2)", + " x * x);", + " }", + "}") + .doTest(); + } + @Test public void namedParametersChecker_ignoresComment_wrongNameWithNoEquals() { testHelper diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/PreferredInterfaceTypeTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/PreferredInterfaceTypeTest.java index 75ce5ddf2e5..f4fdf93dc59 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/PreferredInterfaceTypeTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/PreferredInterfaceTypeTest.java @@ -869,4 +869,16 @@ public void replacementNotSubtypeOfDeclaredType_noFinding() { "}") .doTest(); } + + @Test + public void charSequences() { + testHelper + .addSourceLines( + "Test.java", + "class Test {", + " // BUG: Diagnostic contains: String", + " private final CharSequence a = \"foo\";", + "}") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparisonTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparisonTest.java index 0794f7bea7d..db81b06f823 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparisonTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparisonTest.java @@ -343,7 +343,6 @@ public void assertions() { " ProtoTruth.assertThat(message.getMessage()).isNotNull();", " }", "}") - .setArgs(ImmutableList.of("-XepOpt:ProtoFieldNullComparison:MatchTestAssertions")) .doTest(); } @@ -365,6 +364,7 @@ public void assertions_negative() { " assertThat(message.getMessage()).isNotNull();", " }", "}") + .setArgs(ImmutableList.of("-XepOpt:ProtoFieldNullComparison:MatchTestAssertions=false")) .doTest(); } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/ReturnsNullCollectionTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/ReturnsNullCollectionTest.java index 11077b3cd9c..7a9876823c1 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/ReturnsNullCollectionTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/ReturnsNullCollectionTest.java @@ -33,6 +33,7 @@ public void positive() { compilationHelper .addSourceLines( "Test.java", + "import com.google.common.collect.Multimap;", "import java.util.Collection;", "import java.util.ArrayList;", "import java.util.List;", @@ -50,6 +51,10 @@ public void positive() { " // BUG: Diagnostic contains: ReturnsNullCollection", " return null;", " }", + " Multimap methodReturnsNullMultimap() {", + " // BUG: Diagnostic contains: ReturnsNullCollection", + " return null;", + " }", " List methodReturnsNullListConditionally(boolean foo) {", " if (foo) {", " // BUG: Diagnostic contains: ReturnsNullCollection", diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java new file mode 100644 index 00000000000..d7ff160b2ed --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java @@ -0,0 +1,258 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for the {@link SelfAlwaysReturnsThis}. */ +@RunWith(JUnit4.class) +public class SelfAlwaysReturnsThisTest { + + private final BugCheckerRefactoringTestHelper helper = + BugCheckerRefactoringTestHelper.newInstance(SelfAlwaysReturnsThis.class, getClass()); + + @Test + public void testSelfReturnsThis() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " return this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelfReturnsThis_withCast() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " return (Builder) this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelfReturnsThis_withCastAndTryCatch() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " try {", + " return (Builder) this;", + " } catch (ClassCastException e) {", + // sometimes people log here? + " throw e;", + " }", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelfReturnsThis_withMultipleReturnStatements() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " if (System.currentTimeMillis() % 2 == 0) {", + " return this;", + " } else {", + " return this;", + " }", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelfReturnsThis_withTwoStatementCast() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " // sometimes people write comments here :-)", + " Builder self = (Builder) this;", + " return self;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelfReturnsThis_withImplComment() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " // this is an impl comment", + " return this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelfReturnsThis_withInlineComment() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " return /* self */ this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelfReturnsNewBuilder() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " return new Builder();", + " }", + "}") + .addOutputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void testSelf_voidReturn() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public void self() {", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelf_differentReturnType() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public String self() {", + " return \"hi\";", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelf_static() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public static Builder self() {", + " return new Builder();", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelf_notNamedSelf() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder selfie() {", + " return new Builder();", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelf_hasParams() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self(int foo) {", + " return new Builder();", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelf_abstract() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public abstract class Builder {", + " public abstract Builder self();", + "}") + .expectUnchanged() + .doTest(); + } + + // TODO(kak): add a test for the inheritance style Builder (which requires a (T) cast). +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/UnicodeInCodeTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/UnicodeInCodeTest.java index 36b10a5147d..31af8151454 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/UnicodeInCodeTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnicodeInCodeTest.java @@ -84,6 +84,18 @@ public void positive() { .doTest(); } + @Test + public void positiveMultiCharacterGivesOneFinding() { + helper + .addSourceLines( + "Test.java", // + "class Test {", + " // BUG: Diagnostic contains: Unicode character (\\u03c0\\u03c0)", + " static final double \u03C0\u03C0 = 3;", + "}") + .doTest(); + } + @Test public void suppressibleAtClassLevel() { helper diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryAssignmentTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryAssignmentTest.java index ef7210b2b72..9bbe767d6bb 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryAssignmentTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryAssignmentTest.java @@ -56,6 +56,20 @@ public void negative() { .doTest(); } + @Test + public void doubleAnnotation() { + testHelper + .addSourceLines( + "Test.java", // + "import com.google.inject.Inject;", + "import org.mockito.Mock;", + "class Test {", + " // BUG: Diagnostic contains: both", + " @Mock @Inject Object mockObject;", + "}") + .doTest(); + } + @Test public void refactoring() { refactoringHelper diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/UnusedTypeParameterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/UnusedTypeParameterTest.java new file mode 100644 index 00000000000..f1bd51972e8 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnusedTypeParameterTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class UnusedTypeParameterTest { + private final CompilationTestHelper helper = + CompilationTestHelper.newInstance(UnusedTypeParameter.class, getClass()); + + private final BugCheckerRefactoringTestHelper refactoring = + BugCheckerRefactoringTestHelper.newInstance(UnusedTypeParameter.class, getClass()); + + @Test + public void positiveOnClass() { + helper + .addSourceLines( + "Test.java", // + "// BUG: Diagnostic contains:", + "final class Test {}") + .doTest(); + } + + @Test + public void refactoring() { + refactoring + .addInputLines( + "Test.java", // + "final class Test {}") + .addOutputLines( + "Test.java", // + "final class Test {}") + .doTest(); + } + + @Test + public void refactoringWithTwoParameters() { + refactoring + .addInputLines( + "Test.java", // + "final class Test {", + " B get() { return null; }", + "}") + .addOutputLines( + "Test.java", // + "final class Test {", + " B get() { return null; }", + "}") + .doTest(); + } + + @Test + public void refactoringWithGtgt() { + refactoring + .addInputLines( + "Test.java", // + "final class Test> {}") + .addOutputLines( + "Test.java", // + "final class Test {}") + .doTest(); + } + + @Test + public void positiveOnMethod() { + helper + .addSourceLines( + "Test.java", // + "final class Test {", + " // BUG: Diagnostic contains:", + " private void test() {}", + "}") + .doTest(); + } + + @Test + public void methodCouldBeOverridden_negativeFinding() { + helper + .addSourceLines( + "Test.java", // + "class Test {", + " void test() {}", + "}") + .doTest(); + } + + @Test + public void negative() { + helper + .addSourceLines( + "Test.java", // + "class Test {", + " private boolean contains(java.util.Set set, T elem) {", + " return set.contains(elem);", + " }", + "}") + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/apidiff/CompilationBuilderHelpers.java b/core/src/test/java/com/google/errorprone/bugpatterns/apidiff/CompilationBuilderHelpers.java index 3d06e0a99a9..ec380cb6d4b 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/apidiff/CompilationBuilderHelpers.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/apidiff/CompilationBuilderHelpers.java @@ -22,6 +22,7 @@ import com.google.auto.value.AutoValue; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.sun.tools.javac.file.JavacFileManager; import java.io.File; import java.io.IOException; @@ -60,6 +61,7 @@ public SourceBuilder(File tempFolder) { this.tempFolder = tempFolder; } + @CanIgnoreReturnValue public SourceBuilder addSourceLines(String name, String... lines) throws IOException { Path filePath = Paths.get(tempFolder.getAbsolutePath(), name); sources.add(filePath); @@ -90,16 +92,19 @@ public CompilationBuilder( this.fileManager = fileManager; } + @CanIgnoreReturnValue public CompilationBuilder setSources(Collection sources) { this.sources = sources; return this; } + @CanIgnoreReturnValue public CompilationBuilder setClasspath(Collection classpath) { this.classpath = classpath; return this; } + @CanIgnoreReturnValue CompilationBuilder setJavacopts(Iterable javacopts) { this.javacopts = javacopts; return this; diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/BuilderReturnThisTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/BuilderReturnThisTest.java new file mode 100644 index 00000000000..f22784e678b --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/BuilderReturnThisTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns.checkreturnvalue; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class BuilderReturnThisTest { + + private final BugCheckerRefactoringTestHelper testHelper = + BugCheckerRefactoringTestHelper.newInstance(BuilderReturnThis.class, getClass()); + + @Test + public void negative() { + testHelper + .addInputLines( + "Test.java", + "class Test {", + " static class TestBuilder {", + " static TestBuilder builder() {", + " return new TestBuilder();", + " }", + " Test build() {", + " return new Test();", + " }", + " TestBuilder setFoo(String foo) {", + " return this;", + " }", + " TestBuilder setBar(String bar) {", + " return this;", + " }", + " TestBuilder setBaz(String baz) {", + " return setFoo(baz).setBar(baz);", + " }", + " TestBuilder setTernary(String baz) {", + " return true ? setFoo(baz) : this;", + " }", + " TestBuilder setCast(String baz) {", + " return (TestBuilder) this;", + " }", + " TestBuilder setParens(String bar) {", + " return (this);", + " }", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void positive() { + testHelper + .addInputLines( + "Test.java", + "class Test {", + " static class TestBuilder {", + " TestBuilder setBar(String bar) {", + " return new TestBuilder();", + " }", + " TestBuilder setTernary(String baz) {", + " return true ? new TestBuilder() : this;", + " }", + " }", + "}") + .addOutputLines( + "Test.java", + "import com.google.errorprone.annotations.CheckReturnValue;", + "class Test {", + " static class TestBuilder {", + " @CheckReturnValue", + " TestBuilder setBar(String bar) {", + " return new TestBuilder();", + " }", + " @CheckReturnValue", + " TestBuilder setTernary(String baz) {", + " return true ? new TestBuilder() : this;", + " }", + " }", + "}") + .doTest(BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java new file mode 100644 index 00000000000..d9fbf166552 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java @@ -0,0 +1,485 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns.checkreturnvalue; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for the {@link CanIgnoreReturnValueSuggester}. */ +@RunWith(JUnit4.class) +public class CanIgnoreReturnValueSuggesterTest { + + private final BugCheckerRefactoringTestHelper helper = + BugCheckerRefactoringTestHelper.newInstance(CanIgnoreReturnValueSuggester.class, getClass()); + + @Test + public void testSimpleCase() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " private String name;", + " public Client setName(String name) {", + " this.name = name;", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " private String name;", + " @CanIgnoreReturnValue", + " public Client setName(String name) {", + " this.name = name;", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void testReturnSelf_b234875737() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " private String name;", + " public Client setName(String name) {", + " this.name = name;", + " return self();", + " }", + " private Client self() {", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " private String name;", + " @CanIgnoreReturnValue", + " public Client setName(String name) {", + " this.name = name;", + " return self();", + " }", + " private Client self() {", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void testSimpleCaseAlreadyAnnotatedWithCirv() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " private String name;", + " @CanIgnoreReturnValue", + " public Client setName(String name) {", + " this.name = name;", + " return this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSimpleCaseAlreadyAnnotatedWithCrv() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CheckReturnValue;", + "public final class Client {", + " private String name;", + " @CheckReturnValue", // this is "wrong" -- the checker could fix it though! + " public Client setName(String name) {", + " this.name = name;", + " return this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSimpleCaseWithNestedLambda() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import java.util.function.Function;", + "public final class Client {", + " private String name;", + " public Client setName(String name) {", + " new Function() {", + " @Override", + " public String apply(String in) {", + " return \"kurt\";", + " }", + " };", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "import java.util.function.Function;", + "public final class Client {", + " private String name;", + " @CanIgnoreReturnValue", + " public Client setName(String name) {", + " new Function() {", + " @Override", + " public String apply(String in) {", + " return \"kurt\";", + " }", + " };", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void testAnotherMethodDoesntReturnThis() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " private String name;", + " public Client setName(String name) {", + " this.name = name;", + " return this;", + " }", + " public Client getValue2() {", + " return new Client();", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " private String name;", + " @CanIgnoreReturnValue", + " public Client setName(String name) {", + " this.name = name;", + " return this;", + " }", + " public Client getValue2() {", + " return new Client();", + " }", + "}") + .doTest(); + } + + @Test + public void testNestedCase() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " private String name;", + " public Client setName(String name) {", + " this.name = name;", + " if (true) {", + " return new Client();", + " }", + " return this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testNestedCaseBothReturningThis() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " private String name;", + " public Client setName(String name) {", + " this.name = name;", + " if (true) {", + " return this;", + " }", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " private String name;", + " @CanIgnoreReturnValue", + " public Client setName(String name) {", + " this.name = name;", + " if (true) {", + " return this;", + " }", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void testCapitalVoidReturnType() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Void getValue() {", + " return null;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testLowerVoidReturnType() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Void getValue() {", + " return null;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testConstructor() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Client() {", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSometimesThrows() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " private String name;", + " public Client setName(String name) {", + " this.name = name;", + " if (true) throw new UnsupportedOperationException();", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " private String name;", + " @CanIgnoreReturnValue", + " public Client setName(String name) {", + " this.name = name;", + " if (true) throw new UnsupportedOperationException();", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void testAlwaysThrows() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " private String name;", + " public Client setName(String name) {", + " throw new UnsupportedOperationException();", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSimpleCaseWithSimpleNameConflict() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " private String name;", + " public @interface CanIgnoreReturnValue {}", + " public Client setName(String name) {", + " this.name = name;", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " private String name;", + " public @interface CanIgnoreReturnValue {}", + " @com.google.errorprone.annotations.CanIgnoreReturnValue", + " public Client setName(String name) {", + " this.name = name;", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void testOnlyReturnsThis_b236423646() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Client getFoo() {", + " return this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testOnlyReturnsSelf_b236423646() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Client getFoo() {", + " return self();", + " }", + " public Client self() {", + " return this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testDelegateToCirvMethod() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "import java.util.Arrays;", + "import java.util.List;", + "public final class Client {", + " public Client setFoo(String... args) {", + " return setFoo(Arrays.asList(args));", + " }", + " public Client setFoos(String... args) {", + " return this.setFoo(Arrays.asList(args));", + " }", + " @CanIgnoreReturnValue", + " public Client setFoo(List args) {", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "import java.util.Arrays;", + "import java.util.List;", + "public final class Client {", + " @CanIgnoreReturnValue", + " public Client setFoo(String... args) {", + " return setFoo(Arrays.asList(args));", + " }", + " @CanIgnoreReturnValue", + " public Client setFoos(String... args) {", + " return this.setFoo(Arrays.asList(args));", + " }", + " @CanIgnoreReturnValue", + " public Client setFoo(List args) {", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void testConverter_b240039465() { + helper + .addInputLines( + "Parent.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "abstract class Parent {", + " @CanIgnoreReturnValue", + " X doFrom(String in) { return from(in); }", + " abstract X from(String value);", + "}") + .expectUnchanged() + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client extends Parent {", + // While doFrom(String) is @CIRV, since it returns Integer, and not Client, we don't add + // @CIRV here. + " public Integer badMethod(String value) {", + " return doFrom(value);", + " }", + " @Override", + " public Integer from(String value) {", + " return Integer.parseInt(value);", + " }", + "}") + .expectUnchanged() + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClassesTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClassesTest.java new file mode 100644 index 00000000000..71a2070ac08 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClassesTest.java @@ -0,0 +1,332 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns.checkreturnvalue; + +import static com.google.errorprone.bugpatterns.checkreturnvalue.NoCanIgnoreReturnValueOnClasses.CTOR_COMMENT; +import static com.google.errorprone.bugpatterns.checkreturnvalue.NoCanIgnoreReturnValueOnClasses.METHOD_COMMENT; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for the {@link NoCanIgnoreReturnValueOnClasses}. */ +@RunWith(JUnit4.class) +public final class NoCanIgnoreReturnValueOnClassesTest { + + private final BugCheckerRefactoringTestHelper helper = + BugCheckerRefactoringTestHelper.newInstance( + NoCanIgnoreReturnValueOnClasses.class, getClass()); + + @Test + public void testSimpleCase_returnsThis() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@CanIgnoreReturnValue", + "public final class Client {", + " public Client getValue() {", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue", + " public Client getValue() {", + " return this;", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testSimpleCase_returnsSelf() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@CanIgnoreReturnValue", + "public final class Client {", + " public Client getValue() {", + " return self();", + " }", + " private Client self() {", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue", + " public Client getValue() {", + " return self();", + " }", + " @CanIgnoreReturnValue", + " private Client self() {", + " return this;", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testSimpleCase_returnsNewInstance() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@CanIgnoreReturnValue", + "public final class Client {", + " public Client getValue() {", + " return new Client();", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue" + METHOD_COMMENT, + " public Client getValue() {", + " return new Client();", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testSimpleCase_explicitConstructor() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@CanIgnoreReturnValue", + "public final class Client {", + " Client() {}", + " public Client getValue() {", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue" + CTOR_COMMENT, + " Client() {}", + " @CanIgnoreReturnValue", + " public Client getValue() {", + " return this;", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testNestedClasses_cirvAndCrv() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "import com.google.errorprone.annotations.CheckReturnValue;", + "@CanIgnoreReturnValue", + "public final class Client {", + " public Client getValue() {", + " return this;", + " }", + " @CheckReturnValue", + " public static final class Nested {", + " public int getValue() {", + " return 42;", + " }", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "import com.google.errorprone.annotations.CheckReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue", + " public Client getValue() {", + " return this;", + " }", + " @CheckReturnValue", + " public static final class Nested {", + " public int getValue() {", + " return 42;", + " }", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testNestedClasses_bothCirv() { + helper + .addInputLines( + "User.java", + "package com.google.gaia;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@CanIgnoreReturnValue", + "public final class User {", + " public User persist() {", + " return this;", + " }", + " public static final class Builder {", + " public Builder setFirstName(String firstName) {", + " return this;", + " }", + " }", + "}") + .addOutputLines( + "User.java", + "package com.google.gaia;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class User {", + " @CanIgnoreReturnValue", + " public User persist() {", + " return this;", + " }", + " public static final class Builder {", + " @CanIgnoreReturnValue", + " public Builder setFirstName(String firstName) {", + " return this;", + " }", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testAutoValue() { + helper + .addInputLines( + "Animal.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoValue;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@AutoValue", + "@CanIgnoreReturnValue", + "abstract class Animal {", + " abstract String name();", + " @AutoValue.Builder", + " abstract static class Builder {", + " abstract Builder setName(String value);", + " abstract Animal build();", + " }", + "}") + .addOutputLines( + "Animal.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoValue;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@AutoValue", + "abstract class Animal {", + " abstract String name();", + " @AutoValue.Builder", + " abstract static class Builder {", + " abstract Builder setName(String value);", + " abstract Animal build();", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testAutoValueBuilder() { + helper + .addInputLines( + "Animal.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoValue;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@AutoValue", + "abstract class Animal {", + " abstract String name();", + " @CanIgnoreReturnValue", + " @AutoValue.Builder", + " abstract static class Builder {", + " abstract Builder setName(String value);", + " abstract Animal build();", + " }", + "}") + .addOutputLines( + "Animal.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoValue;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@AutoValue", + "abstract class Animal {", + " abstract String name();", + " @AutoValue.Builder", + " abstract static class Builder {", + " abstract Builder setName(String value);", + " abstract Animal build();", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testNestedAutoValue() { + helper + .addInputLines( + "Outer.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoValue;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@CanIgnoreReturnValue", + "public class Outer {", + " public String name() {", + " return null;", + " }", + " @AutoValue", + " abstract static class Inner {", + " abstract String id();", + " }", + "}") + .addOutputLines( + "Outer.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoValue;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public class Outer {", + " @CanIgnoreReturnValue" + METHOD_COMMENT, + " public String name() {", + " return null;", + " }", + " @AutoValue", + " abstract static class Inner {", + " abstract String id();", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/inject/dagger/ProvidesNullTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/inject/dagger/ProvidesNullTest.java index 54d44d1c4d1..9bed64321d6 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/inject/dagger/ProvidesNullTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/inject/dagger/ProvidesNullTest.java @@ -79,6 +79,43 @@ public void hasOtherNullable() { .doTest(); } + @Test + public void hasTypeUseNullableOnMethod() { + compilationHelper + .addSourceLines( + "Test.java", + "import dagger.Provides;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Test {", + " @Provides", + " @Nullable", + " public Object providesObject() {", + " // BUG: Diagnostic contains: Did you mean '@Nullable' or 'throw new" + + " RuntimeException();'", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void hasTypeUseNullableOnReturnType() { + compilationHelper + .addSourceLines( + "Test.java", + "import dagger.Provides;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Test {", + " @Provides", + " public @Nullable Object providesObject() {", + " // BUG: Diagnostic contains: Did you mean '@Nullable' or 'throw new" + + " RuntimeException();'", + " return null;", + " }", + "}") + .doTest(); + } + /** * Tests that we do not flag Guice {@code @Provides} methods. While this is also wrong, there is * no enforcement in Guice and so the incorrect usage is too common to error on. diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/inlineme/SuggesterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/inlineme/SuggesterTest.java index bde02f4c9ab..bf6d4e2638d 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/inlineme/SuggesterTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/inlineme/SuggesterTest.java @@ -118,6 +118,64 @@ public void testStaticMethodInNewClass() { .doTest(); } + @Test + public void testUnqualifiedStaticFieldReference() { + refactoringTestHelper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public static final String STR = \"kurt\";", + " @Deprecated", + " public int stringLength() {", + " return STR.length();", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.InlineMe;", + "public final class Client {", + " public static final String STR = \"kurt\";", + // TODO(b/234643232): this is a bug; it should be "Client.STR.length()" plus an import + " @InlineMe(replacement = \"STR.length()\")", + " @Deprecated", + " public int stringLength() {", + " return STR.length();", + " }", + "}") + .doTest(); + } + + @Test + public void testQualifiedStaticFieldReference() { + refactoringTestHelper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public static final String STR = \"kurt\";", + " @Deprecated", + " public int stringLength() {", + " return Client.STR.length();", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.InlineMe;", + "public final class Client {", + " public static final String STR = \"kurt\";", + " @InlineMe(replacement = \"Client.STR.length()\", " + + "imports = \"com.google.frobber.Client\")", + " @Deprecated", + " public int stringLength() {", + " return Client.STR.length();", + " }", + "}") + .doTest(); + } + @Test public void testProtectedConstructor() { refactoringTestHelper diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTagTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTagTest.java index 1349f24b05a..11ee0bf534c 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTagTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTagTest.java @@ -135,6 +135,21 @@ public void parameterBlockTag() { .doTest(TestMode.TEXT_MATCH); } + @Test + public void parameterBlockTag_finding() { + helper + .addSourceLines( + "Test.java", + "interface Test {", + " /**", + " // BUG: Diagnostic contains: {@code a}", + " * @a blah", + " */", + " void foo(int a);", + "}") + .doTest(); + } + @Test public void inheritDoc() { refactoring diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ExtendsObjectTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ExtendsObjectTest.java new file mode 100644 index 00000000000..009decb7115 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ExtendsObjectTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns.nullness; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link ExtendsObject}. */ +@RunWith(JUnit4.class) +public final class ExtendsObjectTest { + private final BugCheckerRefactoringTestHelper helper = + BugCheckerRefactoringTestHelper.newInstance(ExtendsObject.class, getClass()); + + @Test + public void positive() { + helper + .addInputLines( + "Test.java", // + "class Foo {}") + .addOutputLines( + "Test.java", // + "import org.checkerframework.checker.nullness.qual.NonNull;", + "class Foo {}") + .doTest(); + } + + @Test + public void extendsParameterWithObjectErasure_noFinding() { + helper + .addInputLines( + "Test.java", // + "class Foo {}") + .expectUnchanged() + .doTest(); + } + + @Test + public void negative() { + helper + .addInputLines( + "Test.java", // + "import org.checkerframework.checker.nullness.qual.NonNull;", + "class Foo {}") + .expectUnchanged() + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullableTest.java index a1277a3811a..da0a8bc8873 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullableTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullableTest.java @@ -457,7 +457,7 @@ public void testNonAnnotationNullable() { .addOutputLines( "out/Test.java", "class T {", - " @javax.annotation.Nullable private final Object obj2 = null;", + " private final @org.jspecify.nullness.Nullable Object obj2 = null;", " class Nullable {}", "}") .doTest(); diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java new file mode 100644 index 00000000000..1f908948e4f --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java @@ -0,0 +1,174 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns.nullness; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** {@link NullArgumentForNonNullParameter}Test */ +@RunWith(JUnit4.class) +public class NullArgumentForNonNullParameterTest { + private final CompilationTestHelper conservativeHelper = + CompilationTestHelper.newInstance(NullArgumentForNonNullParameter.class, getClass()); + private final CompilationTestHelper aggressiveHelper = + CompilationTestHelper.newInstance(NullArgumentForNonNullParameter.class, getClass()) + .setArgs("-XepOpt:Nullness:Conservative=false"); + private final BugCheckerRefactoringTestHelper aggressiveRefactoringHelper = + BugCheckerRefactoringTestHelper.newInstance(NullArgumentForNonNullParameter.class, getClass()) + .setArgs("-XepOpt:Nullness:Conservative=false"); + + @Test + public void testPositivePrimitive() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import java.util.Optional;", + "class Foo {", + " void consume(int i) {}", + " void foo(Optional o) {", + " // BUG: Diagnostic contains: ", + " consume(o.orElse(null));", + " }", + "}") + .doTest(); + } + + @Test + public void testPositiveAnnotatedNonnullAggressive() { + aggressiveHelper + .addSourceLines( + "Foo.java", + "import javax.annotation.Nonnull;", + "class Foo {", + " void consume(@Nonnull String s) {}", + " void foo() {", + " // BUG: Diagnostic contains: ", + " consume(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testNegativeAnnotatedNonnullConservative() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import javax.annotation.Nonnull;", + "class Foo {", + " void consume(@Nonnull String s) {}", + " void foo() {", + " consume(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testPositiveJavaOptionalOf() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import java.util.Optional;", + "class Foo {", + " void foo() {", + " // BUG: Diagnostic contains: ", + " Optional.of(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testPositiveGuavaOptionalOf() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import com.google.common.base.Optional;", + "class Foo {", + " void foo() {", + " // BUG: Diagnostic contains: ", + " Optional.of(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testNegativeNullMarkedComGoogleCommonButNullable() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import com.google.common.collect.ImmutableSet;", + "class Foo {", + " void foo() {", + " ImmutableSet.of().contains(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testPositiveNullMarkedOtherPackageAggressive() { + aggressiveHelper + .addSourceLines( + "Foo.java", + "import org.jspecify.nullness.NullMarked;", + "@NullMarked", + "class Foo {", + " void consume(String s) {}", + " void foo() {", + " // BUG: Diagnostic contains: ", + " consume(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testNegativeNullMarkedNonComGoogleCommonPackageConservative() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import org.jspecify.nullness.NullMarked;", + "@NullMarked", + "class Foo {", + " void consume(String s) {}", + " void foo() {", + " consume(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testNegativeNullMarkedTypeVariable() { + aggressiveHelper + .addSourceLines( + "Foo.java", + "import com.google.common.collect.ImmutableSet;", + "class Foo {", + " void foo() {", + " ImmutableSet.of(null);", + " }", + "}") + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java index 440303b7b4c..64592ffda59 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java @@ -17,9 +17,11 @@ package com.google.errorprone.bugpatterns.nullness; import static com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH; +import static org.junit.Assume.assumeTrue; import com.google.errorprone.BugCheckerRefactoringTestHelper; import com.google.errorprone.CompilationTestHelper; +import com.google.errorprone.util.RuntimeVersion; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -135,6 +137,84 @@ public void testParenthesizedConditionalLiteralNullReturn() { .doTest(); } + @Test + public void testSwitchExpressionTree() { + assumeTrue(RuntimeVersion.isAtLeast12()); + + createCompilationTestHelper() + .addSourceLines( + "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java", + "package com.google.errorprone.bugpatterns.nullness;", + "public class LiteralNullReturnTest {", + " public String getMessage(int x) {", + " // BUG: Diagnostic contains: @Nullable", + " return switch (x) {", + " case 0 -> null;", + " default -> \"non-zero\";", + " };", + " }", + "}") + .doTest(); + } + + @Test + public void testSwitchExpressionTree_negative() { + assumeTrue(RuntimeVersion.isAtLeast12()); + + createCompilationTestHelper() + .addSourceLines( + "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java", + "package com.google.errorprone.bugpatterns.nullness;", + "public class LiteralNullReturnTest {", + " public String getMessage(int x) {", + " return switch (x) {", + " case 0 -> \"zero\";", + " default -> \"non-zero\";", + " };", + " }", + "}") + .doTest(); + } + + @Test + public void testSwitchStatement() { + createCompilationTestHelper() + .addSourceLines( + "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java", + "package com.google.errorprone.bugpatterns.nullness;", + "public class LiteralNullReturnTest {", + " public String getMessage(int x) {", + " switch (x) {", + " case 0:", + " // BUG: Diagnostic contains: @Nullable", + " return null;", + " default:", + " return \"non-zero\";", + " }", + " }", + "}") + .doTest(); + } + + @Test + public void testSwitchStatement_negative() { + createCompilationTestHelper() + .addSourceLines( + "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java", + "package com.google.errorprone.bugpatterns.nullness;", + "public class LiteralNullReturnTest {", + " public String getMessage(int x) {", + " switch (x) {", + " case 0:", + " return \"zero\";", + " default:", + " return \"non-zero\";", + " }", + " }", + "}") + .doTest(); + } + @Test public void testVoidReturn() { createCompilationTestHelper() @@ -494,6 +574,36 @@ public void testImplementsMap() { .doTest(); } + @Test + public void testImplementsMapButAlwaysThrows() { + createCompilationTestHelper() + .addSourceLines( + "MyMap.java", + "import java.util.Map;", + "abstract class MyMap implements Map {", + " @Override", + " public V put(K k, V v) {", + " throw new UnsupportedOperationException();", + " }", + "}") + .doTest(); + } + + @Test + public void testImplementsMapButDoNotCall() { + createCompilationTestHelper() + .addSourceLines( + "MyMap.java", + "import com.google.errorprone.annotations.DoNotCall;", + "import java.util.Map;", + "interface MyMap extends Map {", + " @DoNotCall", + " @Override", + " V put(K k, V v);", + "}") + .doTest(); + } + @Test public void testOnlyIfAlreadyInScopeAndItIs() { createCompilationTestHelper() @@ -720,10 +830,10 @@ public void testRemoveSuppressWarnings_removeNullnessReturnWarning() { .addOutputLines( "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java", "package com.google.errorprone.bugpatterns.nullness;", - "import javax.annotation.Nullable;", + "import org.jspecify.nullness.Nullable;", "public class LiteralNullReturnTest {", - " @Nullable ", - " public String getMessage(boolean b) {", + "", + " public @Nullable String getMessage(boolean b) {", " if (b) {", " return null;", " } else {", @@ -1541,8 +1651,8 @@ public void testNonAnnotationNullable() { .addOutputLines( "out/Test.java", "class T {", - " @javax.annotation.Nullable private final Object method(boolean b) { return b ? null" - + " : 0; }", + " @org.jspecify.nullness.Nullable private final Object method(boolean b) { return b ?" + + " null : 0; }", " class Nullable {}", "}") .doTest(); @@ -1564,9 +1674,9 @@ public void testMultipleNullReturns() { "}") .addOutputLines( "out/Test.java", - "import javax.annotation.Nullable;", + "import org.jspecify.nullness.Nullable;", "class T {", - " @Nullable private final Object method(boolean b) {", + " private final @Nullable Object method(boolean b) {", " if (b) {", " return null;", " } else {", diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnnecessaryCheckNotNullTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnnecessaryCheckNotNullTest.java index e3ff1c667cd..2b43061b2fa 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnnecessaryCheckNotNullTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnnecessaryCheckNotNullTest.java @@ -24,6 +24,7 @@ import com.google.common.collect.Lists; import com.google.errorprone.CompilationTestHelper; import com.google.errorprone.VisitorState; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.matchers.CompilerBasedAbstractTest; import com.google.errorprone.scanner.Scanner; import com.sun.source.tree.ExpressionStatementTree; @@ -226,6 +227,7 @@ private Match(String... expected) { private static class Builder { private final ImmutableMap.Builder builder = ImmutableMap.builder(); + @CanIgnoreReturnValue public Builder add(String expression, String... expected) { builder.put(expression, new Match(expected)); return this; diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnsafeWildcardTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnsafeWildcardTest.java new file mode 100644 index 00000000000..71c53bfdd16 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnsafeWildcardTest.java @@ -0,0 +1,558 @@ +/* + * Copyright 2018 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns.nullness; + +import com.google.errorprone.CompilationTestHelper; +import com.sun.tools.javac.main.Main.Result; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class UnsafeWildcardTest { + + private final CompilationTestHelper compilationHelper = + CompilationTestHelper.newInstance(UnsafeWildcard.class, getClass()); + + @Test + public void positiveExpressions() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "class Test {", + " static class WithBound {}", + " public WithBound basic() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return null;", + " }", + " public WithBound inParens() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return (null);", + " }", + " public WithBound cast() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return (WithBound) null;", + " }", + " public WithBound inTernary(boolean x, WithBound dflt) {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return x ? null : dflt;", + " }", + " public WithBound allNullTernary(boolean x) {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return x ? null : null;", + " }", + " public WithBound parensInTernary(boolean x) {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return x ? (null) : null;", + " }", + " public WithBound parensAroundTernary(boolean x) {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return (x ? null : null);", + " }", + " public List> nestedWildcard() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return null;", + " }", + " public List> extendsWildcard() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return null;", + " }", + " public List> superWildcard() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void negativeReturns() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "class Test {", + " public String basic() {", + " return null;", + " }", + " public String inParens() {", + " return (null);", + " }", + " public String inTernary(boolean x) {", + " return x ? null : \"foo\";", + " }", + " public String allNullTernary(boolean x) {", + " return x ? null : null;", + " }", + " public String parensInTernary(boolean x) {", + " return x ? (null) : \"foo\";", + " }", + " public String parensAroundTernary(boolean x) {", + " return (x ? null : \"foo\");", + " }", + " public List typearg() {", + " return null;", + " }", + " public List extendsWildcard() {", + " return null;", + " }", + " public List superWildcardNoImplicitBound() {", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void negativeLambdas() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "import java.util.function.Function;", + "class Test {", + " public Function basic() {", + " return x -> null;", + " }", + " public Function inParens() {", + " return x -> (null);", + " }", + " public Function inTernary() {", + " return x -> x ? null : \"foo\";", + " }", + " public Function returnInLambda() {", + " return x -> { return null; };", + " }", + "}") + .doTest(); + } + + @Test + public void lambdasWithTypeParameters() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "import java.util.function.Function;", + "class Test {", + " class WithBound {}", + " public Function> contra() {", + " return s -> null;", + " }", + " public Function> implicitOk() {", + " return i -> null;", + " }", + " public Function> implicitPositive() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return u -> null;", + " }", + " public Function> returnInLambda() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return u -> { return null; };", + " }", + " public Function> nestedWildcard() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void typeParameters() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "class Test {", + " static class WithBound {}", + " class WildcardBound> {", + " T bad() {", + " // We allow this and instead check instantiations below", + " return null;", + " }", + " }", + " WildcardBound> diamond() {", + " // BUG: Diagnostic contains: Unsafe wildcard type argument", + " return new WildcardBound<>();", + " }", + " WildcardBound> create() {", + " // BUG: Diagnostic contains: Unsafe wildcard type argument", + " return new WildcardBound>();", + " }", + " WildcardBound> none() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void variables() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "class Test {", + " class WithBound {}", + " private String s;", + " private List xs = null;", + " private List ys;", + " private WithBound zs = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " private WithBound initialized = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " private final WithBound initializedFinal = null;", + " // BUG: Diagnostic contains: Uninitialized field with unsafe wildcard", + " private WithBound uninitialized;", + " private final WithBound uninitializedFinal;", + " Test() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " uninitializedFinal = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " uninitialized = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " initialized = null;", + " }", + " public void foo() {", + " List covariant = null;", + " List contravariant = null;", + " WithBound inBounds = null;", + " WithBound uninitializedLocal;", + " final WithBound uninitializedFinalLocal;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " WithBound implicitBounds = null;", + " covariant = null;", + " contravariant = null;", + " inBounds = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " uninitializedLocal = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " uninitializedFinalLocal = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " implicitBounds = null;", + " }", + "}") + .doTest(); + } + + @Test + public void calls() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "class Test {", + " class WithBound {}", + " public void foo(String s, List xs, List contra) {", + " foo(null, null, null);", + " }", + " public void negative(WithBound xs, WithBound contra) {", + " negative(null, null);", + " }", + " public void positive(WithBound implicit) {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " this.positive(null);", + " positive(null);", // ok b/c compiler uses U = Object and ? super Object is ok + " }", + "}") + .doTest(); + } + + @Test + public void inferredParamType_flaggedIfProblematic() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "class Test {", + " static class WithBound {}", + " public List> positive(WithBound safe) {", + " // BUG: Diagnostic contains: Unsafe wildcard in inferred type argument", + " return List.of(safe,", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " null);", // implicitly upcast to WithBound + " }", + " public List> negative(WithBound safe) {", + " return List.of(safe, null);", + " }", + "}") + .doTest(); + } + + @Test + public void constructors() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " class WithBound {}", + " public Test() { this(null, null); }", + " public Test(WithBound implicit) {}", + " public Test(WithBound xs, WithBound contra) {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " this(null);", + " }", + " class Sub extends Test {", + " Sub(WithBound implicit) {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " super(null);", + " }", + " }", + " static Test newClass() {", + " new Test(null, null);", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " new Test(null);", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return new Test<>(null);", + " }", + " static Test anonymous() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " new Test(null) {};", + " return null;", + " }", + " void inner() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " new Sub(null);", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " new Sub(null) {};", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " this.new Sub(null) {};", + " }", + "}") + .doTest(); + } + + @Test + public void supertypes_problematicWildcards_flagged() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.io.Serializable;", + "import java.util.AbstractList;", + "import java.util.List;", + "class Test {", + " class WithBound {}", + " // BUG: Diagnostic contains: Unsafe wildcard type", + " abstract class BadList extends AbstractList> {}", + " abstract class BadListImpl implements Serializable,", + " // BUG: Diagnostic contains: Unsafe wildcard type", + " List> {}", + " interface BadListItf extends Serializable,", + " // BUG: Diagnostic contains: Unsafe wildcard type", + " List> {}", + "}") + .doTest(); + } + + @Test + public void varargs() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " static class WithBound {}", + " Test(WithBound xs, WithBound... args) {}", + " static void hasVararg(WithBound xs, WithBound... args) {}", + " static void nullVarargs(WithBound xs) {", + " Test.hasVararg(", + " null,", // fine: target type is safe + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " null,", + " xs,", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " null);", + " new Test(", + " null,", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " null,", + " xs,", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " null);", + " }", + "}") + .doTest(); + } + + @Test + public void arrays() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " class WithBound {}", + // Generic array creation is a compilation error, and non-generic arrays are ok + " Object[] simpleInitializer = { null };", + " Object[][] nestedInitializer = { { null }, { null } };", + " void nulls() {", + " String[][] stringMatrix = null;", + " WithBound[] implicitBound = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " WithBound[] simpleNull = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " WithBound[][] nestedNull = null;", + " }", + "}") + .doTest(); + } + + /** + * Regresion test demonstrating that generic array creation is a compiler error. If it wasn't, + * we'd want to check element types. + */ + @Test + public void genericArrays_isCompilerError() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " class WithBound {}", + " // BUG: Diagnostic matches: X", + " WithBound[] simpleInitializer = { null };", + " // BUG: Diagnostic matches: X", + " WithBound[][] nestedInitializer = { { null }, { null } };", + " // BUG: Diagnostic matches: X", + " WithBound[][] emptyInitializer = {};", + " void newArrays() {", + " // BUG: Diagnostic matches: X", + " Object[] a1 = new WithBound[] {};", + " // BUG: Diagnostic matches: X", + " Object[] a2 = new WithBound[0];", + " // BUG: Diagnostic matches: X", + " Object[] a3 = new WithBound[][] {};", + " // BUG: Diagnostic matches: X", + " Object[] a4 = new WithBound[0][];", + " }", + "}") + .expectResult(Result.ERROR) + .matchAllDiagnostics() + .expectErrorMessage("X", msg -> msg.contains("generic array creation")) + .doTest(); + } + + @Test + public void arrays_rawTypes_futureWork() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " class WithBound {}", + " void problematic() {", + // The following implicitly create problematic types even absent null values (though + // problematic non-empty arrays containing all-null values can be created just as + // easily with [N] where N > 0). The compiler issues raw and unchecked warnings here, + // but we might want to flag assignments as well. + " WithBound raw = new WithBound();", + " WithBound[] array = new WithBound[0];", + " WithBound[][] nested = new WithBound[0][];", + " }", + "}") + .doTest(); + } + + /** + * Regression test to ignore {@code null} assignment to wildcard whose lower bound is a type + * variable with non-trivial upper bound. The compiler rejects potentially dangerous wildcards on + * its own in this case, but simple subtype checks between lower and upper bound can fail and lead + * to false positives if involved type variables' upper bounds capture another type variable. + */ + @Test + public void boundedTypeVar_validLowerBound_isIgnored() { + compilationHelper + .addSourceLines( + "MyIterable.java", + "import java.util.List;", + "interface MyIterable> {", + " static class Test> implements MyIterable {", + " MyIterable parent;", + " public Test() {", + " this.parent = null;", + " }", + " }", + "}") + .doTest(); + } + + @Test + public void boundedTypeVar_questionableLowerBound_isCompilerError() { + compilationHelper + .addSourceLines( + "MyIterable.java", + "import java.util.List;", + "interface MyIterable> {", + " // BUG: Diagnostic matches: X", + " static class Test> implements MyIterable {", + " MyIterable parent;", + " public Test() {", + " this.parent = null;", + " }", + " }", + "}") + .expectResult(Result.ERROR) + .matchAllDiagnostics() + .expectErrorMessage( + "X", msg -> msg.contains("type argument S is not within bounds of type-variable T")) + .doTest(); + } + + /** + * Regression test to ignore {@code null} assignment to wildcard whose lower bound is a concrete + * type and whose implicit upper bound is F-bounded. The compiler rejects potentially dangerous + * wildcards on its own in this case, but simple subtype checks between lower and upper bound can + * fail and lead to false positives. + */ + @Test + public void fBoundedImplicitUpperBound_validLowerBound_isIgnored() { + compilationHelper + .addSourceLines( + "FBounded.java", + "abstract class FBounded> {", + " public static final class Coll extends FBounded> {}", + " public interface Listener> {}", + " public static void shouldWork() {", + " Listener> validListener = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " Listener invalidListener = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " Iterable> invalidListeners = java.util.List.of(null, null);", + " }", + "}") + .doTest(); + } + + @Test + public void fBoundedImplicitUpperBound_invalidLowerBound_isCompilerError() { + compilationHelper + .addSourceLines( + "FBounded.java", + "abstract class FBounded> {", + " public static final class Coll extends FBounded> {}", + " public interface Listener> {}", + " public static void shouldWork() {", + " // BUG: Diagnostic matches: X", + " Listener listener = null;", + " }", + "}") + .expectResult(Result.ERROR) + .matchAllDiagnostics() + .expectErrorMessage( + "X", msg -> msg.contains("String is not within bounds of type-variable U")) + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValueNegativeCases.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValueNegativeCases.java index c9d41052a56..18c872fbb53 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValueNegativeCases.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValueNegativeCases.java @@ -36,6 +36,10 @@ private int mustCheck() { return 5; } + private int nothingToCheck() { + return 42; + } + private void callRunnable(Runnable runnable) { runnable.run(); } @@ -51,11 +55,13 @@ private void callSupplier(Supplier supplier) { supplier.get(); } - public void testResolvedToVoidLambda() { + public void testResolvedToIntLambda(boolean predicate) { callSupplier(() -> mustCheck()); + callSupplier(predicate ? () -> mustCheck() : () -> nothingToCheck()); } - public void testMethodReference() { + public void testMethodReference(boolean predicate) { callSupplier(this::mustCheck); + callSupplier(predicate ? this::mustCheck : this::nothingToCheck); } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValuePositiveCases.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValuePositiveCases.java index e51e83c9e32..1b0ebbfa20f 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValuePositiveCases.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValuePositiveCases.java @@ -31,7 +31,12 @@ private int increment(int bar) { public void foo() { int i = 1; - // BUG: Diagnostic contains: + // BUG: Diagnostic contains: The result of `increment(...)` must be used + // + // If you really don't want to use the result, then assign it to a variable: `var unused = ...`. + // + // If callers of `increment(...)` shouldn't be required to use its result, then annotate it with + // `@CanIgnoreReturnValue`. increment(i); System.out.println(i); } @@ -52,19 +57,48 @@ private void callRunnable(Runnable runnable) { } public void testResolvedToVoidLambda() { - // BUG: Diagnostic contains: Ignored return value + // BUG: Diagnostic contains: callRunnable(() -> this.intValue.increment()); } - public void testResolvedToVoidMethodReference() { - // BUG: Diagnostic contains: Ignored return value + public void testResolvedToVoidMethodReference(boolean predicate) { + // BUG: Diagnostic contains: The result of `increment()` must be used + // + // `this.intValue::increment` acts as an implementation of `Runnable.run`. + // — which is a `void` method, so it doesn't use the result of `increment()`. + // + // To use the result, you may need to restructure your code. + // + // If you really don't want to use the result, then switch to a lambda that assigns it to a + // variable: `() -> { var unused = ...; }`. + // + // If callers of `increment()` shouldn't be required to use its result, then annotate it with + // `@CanIgnoreReturnValue`. callRunnable(this.intValue::increment); + // BUG: Diagnostic contains: The result of `increment()` must be used + callRunnable(predicate ? this.intValue::increment : this.intValue::increment2); + } + + public void testConstructorResolvedToVoidMethodReference() { + // BUG: Diagnostic contains: The result of `new MyObject()` must be used + // + // `MyObject::new` acts as an implementation of `Runnable.run`. + // — which is a `void` method, so it doesn't use the result of `new MyObject()`. + // + // To use the result, you may need to restructure your code. + // + // If you really don't want to use the result, then switch to a lambda that assigns it to a + // variable: `() -> { var unused = ...; }`. + // + // If callers of `MyObject()` shouldn't be required to use its result, then annotate it with + // `@CanIgnoreReturnValue`. + callRunnable(MyObject::new); } public void testRegularLambda() { callRunnable( () -> { - // BUG: Diagnostic contains: Ignored return value + // BUG: Diagnostic contains: this.intValue.increment(); }); } @@ -77,7 +111,7 @@ public void testBeforeAndAfterRule() { } public void constructor() { - // BUG: Diagnostic contains: Ignored return value + // BUG: Diagnostic contains: The result of `new MyObject()` must be used new MyObject() {}; class MySubObject1 extends MyObject {} @@ -92,7 +126,12 @@ class MySubObject3 extends MyObject { } } - // BUG: Diagnostic contains: Ignored return value + // BUG: Diagnostic contains: The result of `new MyObject()` must be used + // + // If you really don't want to use the result, then assign it to a variable: `var unused = ...`. + // + // If callers of `MyObject()` shouldn't be required to use its result, then annotate it with + // `@CanIgnoreReturnValue`. new MyObject(); } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerNegativeCases.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerNegativeCases.java index c22f8539064..d5b66520761 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerNegativeCases.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerNegativeCases.java @@ -80,6 +80,11 @@ Closeable positiveCase7() { return new Foo().mustBeClosedAnnotatedMethod(); } + @MustBeClosed + Closeable ternary(boolean condition) { + return condition ? new Foo().mustBeClosedAnnotatedMethod() : null; + } + void tryWithResources() { Foo foo = new Foo(); Closeable closeable = foo.mustBeClosedAnnotatedMethod(); @@ -138,4 +143,8 @@ void statementLambdaReturningCloseable() { return new MustBeClosedAnnotatedConstructor(); }); } + + void methodReferenceReturningCloseable() { + consumeCloseable(MustBeClosedAnnotatedConstructor::new); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases.java index e25fb620534..627809e0095 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases.java @@ -19,6 +19,7 @@ import com.google.errorprone.annotations.MustBeClosed; import java.util.stream.Stream; +@SuppressWarnings({"UnusedNestedClass", "UnusedVariable"}) class MustBeClosedCheckerPositiveCases { class DoesNotImplementAutoCloseable { @@ -112,6 +113,7 @@ Closeable positiveCase7() { } void positiveCase8() { + // Lambda has a fixless finding because no reasonable fix can be suggested Lambda expression = () -> { // BUG: Diagnostic contains: @@ -120,6 +122,11 @@ void positiveCase8() { } void positiveCase9() { + // TODO(b/218377318): BUG: Diagnostic contains: + Lambda expression = new Foo()::mustBeClosedAnnotatedMethod; + } + + void positiveCase10() { new Foo() { @Override public Closeable mustBeClosedAnnotatedMethod() { @@ -129,12 +136,38 @@ public Closeable mustBeClosedAnnotatedMethod() { }; } - int expressionDeclaredVariable() { + void subexpression() { + // BUG: Diagnostic contains: + new Foo().mustBeClosedAnnotatedMethod().method(); + } + + void ternary(boolean condition) { + // BUG: Diagnostic contains: + int result = condition ? new Foo().mustBeClosedAnnotatedMethod().method() : 0; + } + + int variableDeclaration() { // BUG: Diagnostic contains: int result = new Foo().mustBeClosedAnnotatedMethod().method(); return result; } + void forLoopInitialization() { + // TODO(b/236715080): fix results in invalid code. BUG: Diagnostic contains: + // for (int i = new Foo().mustBeClosedAnnotatedMethod().method(); i > 0; --i) { } + } + + void forLoopConditionUnfixable() { + // TODO(b/236715080): suggested fix changes behavior. + // BUG: Diagnostic contains: + for (int i = 0; i < new Foo().mustBeClosedAnnotatedMethod().method(); ++i) {} + } + + void forLoopUpdateUnfixable() { + // TODO(b/236715080): fix results in invalid code. BUG: Diagnostic contains: + // for (int i = 0; i < 100; i += new Foo().mustBeClosedAnnotatedMethod().method()) {} + } + void tryWithResources_nonFinal() { Foo foo = new Foo(); // BUG: Diagnostic contains: diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases_expected.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases_expected.java index 85da29e0a84..171d444587a 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases_expected.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases_expected.java @@ -19,10 +19,12 @@ import com.google.errorprone.annotations.MustBeClosed; import java.util.stream.Stream; +@SuppressWarnings({"UnusedNestedClass", "UnusedVariable"}) class MustBeClosedCheckerPositiveCases { class DoesNotImplementAutoCloseable { @MustBeClosed + // BUG: Diagnostic contains: MustBeClosed should only annotate constructors of AutoCloseables. DoesNotImplementAutoCloseable() {} @MustBeClosed @@ -124,6 +126,11 @@ void positiveCase8() { } void positiveCase9() { + // TODO(b/218377318): BUG: Diagnostic contains: + Lambda expression = new Foo()::mustBeClosedAnnotatedMethod; + } + + void positiveCase10() { new Foo() { @MustBeClosed @Override @@ -134,7 +141,23 @@ public Closeable mustBeClosedAnnotatedMethod() { }; } - int expressionDeclaredVariable() { + void subexpression() { + // BUG: Diagnostic contains: + try (Closeable closeable = new Foo().mustBeClosedAnnotatedMethod()) { + closeable.method(); + } + } + + void ternary(boolean condition) { + // BUG: Diagnostic contains: + int result; + try (Closeable closeable = new Foo().mustBeClosedAnnotatedMethod()) { + result = condition ? closeable.method() : 0; + } + } + + int variableDeclaration() { + // BUG: Diagnostic contains: int result; try (Closeable closeable = new Foo().mustBeClosedAnnotatedMethod()) { result = closeable.method(); @@ -142,6 +165,24 @@ int expressionDeclaredVariable() { return result; } + void forLoopInitialization() { + // TODO(b/236715080): fix results in invalid code. BUG: Diagnostic contains: + // for (int i = new Foo().mustBeClosedAnnotatedMethod().method(); i > 0; --i) {} + } + + void forLoopConditionUnfixable() { + // TODO(b/236715080): suggested fix changes behavior. + // BUG: Diagnostic contains: + try (final Closeable closeable = new Foo().mustBeClosedAnnotatedMethod()) { + for (int i = 0; i < closeable.method(); ++i) {} + } + } + + void forLoopUpdateUnfixable() { + // TODO(b/236715080): fix results in invalid code. BUG: Diagnostic contains: + // for (int i = 0; i < 100; i += new Foo().mustBeClosedAnnotatedMethod().method()) {} + } + void tryWithResources_nonFinal() { Foo foo = new Foo(); // BUG: Diagnostic contains: diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByCheckerTest.java index 16a977fc19a..1eb246cfabe 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByCheckerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByCheckerTest.java @@ -1673,7 +1673,6 @@ public void testStaticMemberClass_enclosingInstanceLock() { " }", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1770,7 +1769,6 @@ public void testMissingGuard() { " lib.doSomething();", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1818,7 +1816,6 @@ public void parameterGuard() { " worker.f(work);", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1846,7 +1843,6 @@ public void parameterGuardNegative() { " }", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1873,7 +1869,6 @@ public void parameterGuardNegativeSimpleName() { " }", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1896,7 +1891,6 @@ public void varargsArity() { " f(0);", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java index 4eb7d33dd62..710c7e164ae 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java @@ -20,7 +20,6 @@ import com.google.errorprone.BugPattern; import com.google.errorprone.CompilationTestHelper; -import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; import com.google.errorprone.matchers.Description; import com.sun.source.tree.Tree; @@ -247,10 +246,6 @@ public void testLockMethodEnclosingAccess() { @BugPattern(name = "GuardedByLockSet", summary = "", explanation = "", severity = ERROR) public static class GuardedByLockSetAnalyzer extends GuardedByChecker { - public GuardedByLockSetAnalyzer(ErrorProneFlags errorProneFlags) { - super(errorProneFlags); - } - @Override protected Description checkGuardedAccess( Tree tree, GuardedByExpression guard, HeldLockSet live, VisitorState state) { diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableCheckerTest.java index 864524e5e8e..82dd6e1b109 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableCheckerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableCheckerTest.java @@ -133,9 +133,8 @@ public void customAnnotationsSubtype() { .addSourceLines( "MyTest.java", "import java.lang.annotation.Annotation;", - "// BUG: Diagnostic contains:", - "// extends @Immutable type Test, but is not annotated as immutable", "final class MyTest implements Test {", + " // BUG: Diagnostic contains: non-final field 'xs'", " public Object[] xs = {};", " public Class annotationType() {", " return null;", @@ -1062,14 +1061,13 @@ public void transitive() { .addSourceLines( "threadsafety/Test.java", "package threadsafety;", - "// BUG: Diagnostic contains: extends @Immutable", "class Test implements J {", + " // BUG: Diagnostic contains: non-final field 'x'", " public int x = 0;", "}") .addSourceLines( "threadsafety/J.java", // "package threadsafety;", - "// BUG: Diagnostic contains: extends @Immutable", "interface J extends I {", "}") .doTest(); diff --git a/docgen/pom.xml b/docgen/pom.xml index a57b821986f..5a6e090a4b1 100644 --- a/docgen/pom.xml +++ b/docgen/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 Documentation tool for generating Error Prone bugpattern documentation diff --git a/docgen_processor/pom.xml b/docgen_processor/pom.xml index 42915547130..d4c01478c20 100644 --- a/docgen_processor/pom.xml +++ b/docgen_processor/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 JSR-269 annotation processor for @BugPattern annotation diff --git a/docs/bugpattern/AnnotationPosition.md b/docs/bugpattern/AnnotationPosition.md new file mode 100644 index 00000000000..9153a491a94 --- /dev/null +++ b/docs/bugpattern/AnnotationPosition.md @@ -0,0 +1,29 @@ +Per the [style guide](style-guide), `TYPE_USE` annotations should appear +immediately before the type being annotated, and after any modifiers: + +```java +public @Nullable V getOrNull(final Map map, final @Nullable K key) { + return map.get(key); +} +``` + +Non-`TYPE_USE` annotations should appear before modifiers, as they annotate the +entire element (method, variable, class): + +```java +@VisibleForTesting +public void reset() { + // ... +} +``` + +Javadoc must appear before any annotations, or the compiler will fail to +recognise it as Javadoc: + +```java +@Nullable +/** Might return a frobnicator. */ +Frobnicator getFrobnicator(); +``` + +[style-guide]: https://google.github.io/styleguide/javaguide.html#s4.8.5.1-type-use-annotation-style diff --git a/docs/bugpattern/BooleanParameter.md b/docs/bugpattern/BooleanParameter.md index e69de29bb2d..b75477c2843 100644 --- a/docs/bugpattern/BooleanParameter.md +++ b/docs/bugpattern/BooleanParameter.md @@ -0,0 +1,7 @@ +Providing parameter comments for boolean literals has some advantages: + +* Readability is generally improved, as the parameter name will likely provide + some context on what the boolean literal means +* [https://errorprone.info/bugpattern/ParameterName](ParameterName) checks at compile-time that the + comments match the formal argument names to avoid accidentally transposing + parameters diff --git a/docs/bugpattern/BuilderReturnThis.md b/docs/bugpattern/BuilderReturnThis.md new file mode 100644 index 00000000000..491e289f498 --- /dev/null +++ b/docs/bugpattern/BuilderReturnThis.md @@ -0,0 +1,34 @@ +This check identifies instance methods in builder classes, and requires that +they either `return this;`, or are explicitly annotated with +`@CheckReturnValue`. + +Instance methods in builders typically return `this`, to allow chaining. +Ignoring this result does not indicate a https://errorprone.info/bugpattern/CheckReturnValue bug. For +example, both of the following are fine: + +```java +Foo.Builder builder = Foo.builder(); +builder.setBar("bar"); // return value is deliberately unused +return builder.build(); +``` + +```java +Foo.Builder builder = + Foo.builder().setBar("bar").build(); +``` + +Rarely, a builder method may return a new instance, which should not be ignored. +This check requires these methods to be annotated with `@CheckReturnValue`: + +```java +class Builder { + @CheckReturnValue + Builder setFoo(String foo) { + return new Builder(foo); // returns a new builder instead of this! + } +} +``` + +This check allows the https://errorprone.info/bugpattern/CheckReturnValue enforcement to assume the +return value of instance methods in builders can safely be ignored, unless the +method is explicitly annotated with `@CheckReturnValue`. diff --git a/docs/bugpattern/CannotMockFinalMethod.md b/docs/bugpattern/CannotMockFinalMethod.md new file mode 100644 index 00000000000..d47bc98d725 --- /dev/null +++ b/docs/bugpattern/CannotMockFinalMethod.md @@ -0,0 +1,12 @@ +Mockito cannot mock `final` methods, and cannot tell at runtime that this is +attempted and fail with an error (as mocking `final` classes does). + +`when(mock.finalMethod())` will invoke the real implementation of `finalMethod`. +In some cases, this may wind up accidentally doing what's intended: + +```java +when(converter.convert(a)).thenReturn(b); +``` + +`convert` is final, but under the hood, calls `doForward`, so we wind up mocking +that method instead. diff --git a/docs/bugpattern/CheckReturnValue.md b/docs/bugpattern/CheckReturnValue.md index ea879070312..4dfa1b29847 100644 --- a/docs/bugpattern/CheckReturnValue.md +++ b/docs/bugpattern/CheckReturnValue.md @@ -1,3 +1,40 @@ +When code calls a non-`void` method, it should usually use the value that the +method returns. + +Consider the following code, which ignores the return value of `concat`: + +```java +string.concat("\n"); +``` + +That code is a no-op because `concat` doesn't modify `string`; it returns a new +string for the caller to use, as in: + +```java +string = string.concat("\n"); +``` + +To avoid this bug, Error Prone requires callers to use the return value of +`concat` and some other well-known methods. + +Additionally, Error Prone can be configured to require callers to use the return +value of any methods that you choose. + +### How to tell Error Prone which methods to check + +Most methods are like `concat`: Calls to those methods should use their return +values. + +However, there are exceptions. For example, `set.add(element)` returns a +`boolean`: The return value is `false` if `element` was *already* contained in +`set`. Typically, callers don't need to know this, so they don't need to use the +return value. + +For Error Prone's `CheckReturnValue` check to be useful, it needs to know which +methods are like `concat` and which are like `add`. + +#### `@CheckReturnValue` and `@CanIgnoreReturnValue` + The `@CheckReturnValue` annotation (available in JSR-305[^jsr] or in [Error Prone][epcrv]) marks methods whose return values should be checked. This error is triggered when one of these methods is called but the result is not diff --git a/docs/bugpattern/DirectInvocationOnMock.md b/docs/bugpattern/DirectInvocationOnMock.md new file mode 100644 index 00000000000..7a981b8ac06 --- /dev/null +++ b/docs/bugpattern/DirectInvocationOnMock.md @@ -0,0 +1,48 @@ +Direct invocations on [mocks](mockito) should be avoided in tests. + +When you call a method on a mock, the call normally does only what you have +configured it to do (through calls to `when(...).thenReturn/thenAnswer`, etc.) +and makes a record of the call that can be read by later `verify(...)` calls. +Both of these are rarely what you want: + +- The reason to configure a mock's behavior is so that the code under test + will react to that behavior. If you want for the test itself to do + something, then do it directly instead of by calling a method on the mock. +- If a future reader of your test sees a call to `verify(foo).bar()`, then the + reader will expect the test to succeed only because the code under test + called `bar()`, not because the test itself did. + +Sometimes, test authors, especially those familiar with other mocking frameworks +(like EasyMock), will call a method on a mock for one of two reasons: + +1. By default, EasyMock requires the test setup to call every method that the + code under test will call. Mockito's defaults do *not* require this. +2. Many EasyMock tests choose to have EasyMock require that the code under test + call *all* the methods that the test setup calls. Mockito tests that want to + verify calls to methods must verify each call individually. To check that + any particular method has been called, a Mockito test must call + `verify(foo).bar()`, and it must do so *after* the code under test has run. + +```java +@Test +public void balanceIsChecked() { + Account account = mock(Account.class); + LoanChecker loanChecker = new LoanChecker(account); + + assertThat(loanChecker.checkEligibility()).isFalse(); + + // Should be verify(account).checkBalance();, or be removed if the call to + // `checkEligibility` is sufficient proof the code is behaving as intended. + account.checkBalance(); +} +``` + +There is at least one edge case in which a call to a mock has different effects: +A call to a `final` method will normally *not* be intercepted by Mockito, so it +will run the implementation of that method in the code under test. Sometimes, +that method will call other methods on the mock object, producing effects +similar to if the test had called those methods directly. Sometimes, the method +will have other effects. Both kinds of effects can be confusing, so prefer to +avoid such calls when possible. + +[mockito]: https://site.mockito.org/ diff --git a/docs/bugpattern/ExtendsObject.md b/docs/bugpattern/ExtendsObject.md new file mode 100644 index 00000000000..c06c5369a6d --- /dev/null +++ b/docs/bugpattern/ExtendsObject.md @@ -0,0 +1,6 @@ +`T extends Object` is redundant when using normal (non-Checker Framework +checked) code. + +However, `T extends Object` compiles to the same bytecode as `T` when using +vanilla javac. So, when using Checker on vanilla javac's bytecode, `T extends +Object` does not imply non-null bounds *outside the same compilation unit*. diff --git a/docs/bugpattern/SelfAlwaysReturnsThis.md b/docs/bugpattern/SelfAlwaysReturnsThis.md new file mode 100644 index 00000000000..6b589797dcc --- /dev/null +++ b/docs/bugpattern/SelfAlwaysReturnsThis.md @@ -0,0 +1,29 @@ +A common pattern for abstract `Builders` is to declare an instance method named +`self()`, which subtypes override and implement as `return this` (see Effective +Java 3rd Edition, Item 2). + +Returning anything other than `this` from an instance method named `self()` with +a return type that matches the enclosing class will be confusing for readers and +callers. + +## Casting + +If an unchecked cast is required, prefer a single-statement cast, with the +suppression on the method (rather than the statement). For example: + +```java + @SuppressWarnings("unchecked") + default U self() { + return (U) this; + } +``` + +Instead of: + +```java + default U self() { + @SuppressWarnings("unchecked") + U self = (U) this; + return self; + } +``` diff --git a/docs/bugpattern/StaticProtoFuzzer.md b/docs/bugpattern/StaticProtoFuzzer.md new file mode 100644 index 00000000000..a38781d349d --- /dev/null +++ b/docs/bugpattern/StaticProtoFuzzer.md @@ -0,0 +1,63 @@ +ProtoFuzzer is a mutable class, even when seeded by a compile-time constant. +We're trying to avoid the following pitfalls which can arise when assigning a +ProtoFuzzer to a static field: + +* Accidental state leakage between unit tests, causing non-deterministic or + flaky behavior. In this scenario, it's recommended to instantiate a distinct + ProtoFuzzer for each relevant unit test, possibly by using a `@Before` + method. + +``` {.good} + private ProtoFuzzer protoFuzzer; + ... + @Before + public void setUp() { + ... + // Customize as appropriate + protoFuzzer = ProtoFuzzer.newBuilder().setSeed(...).build(); + ... + } +``` + +* If a static ProtoFuzzer is used to initialize other static fields, then this + initialization process can have program-order dependency; for example, + re-ordering two such initialized fields can cause their values to change. + This problem can be avoided by using static builder methods to initialize. + +``` {.bad} +private static final ProtoFuzzer protoFuzzer = + ProtoFuzzer.newBuilder() + .setSeed(...) + .build(); +... +// Re-ordering myFirstCustomProto and mySecondCustomProto can change their values! +private static final MyCustomProto myFirstCustomProto = + protoFuzzer.makeMessageOfType( + MyCustomProto.getDefaultInstance() + ); +private static final MyCustomProto mySecondCustomProto = + protoFuzzer.makeMessageOfType( + MyCustomProto.getDefaultInstance() + ); + +``` + +Instead, create a static builder method and replace references to the static +ProtoFuzzer field with calls to the builder: + +``` {.good} + +private static final MyCustomProto myFirstCustomProto = + buildMyCustomProtoFuzzer().makeMessageOfType( + MyCustomProto.getDefaultInstance() + ); +private static final MyCustomProto mySecondCustomProto = + buildMyCustomProtoFuzzer().makeMessageOfType( + MyCustomProto.getDefaultInstance() + ); +... +private static ProtoFuzzer buildMyCustomProtoFuzzer() { + // Customize as appropriate + return ProtoFuzzer.newBuilder().setSeed(...).build(); +} +``` diff --git a/docs/bugpattern/javadoc/InvalidInlineTag.md b/docs/bugpattern/javadoc/InvalidInlineTag.md index 97d0da6cf8e..9cba566ed68 100644 --- a/docs/bugpattern/javadoc/InvalidInlineTag.md +++ b/docs/bugpattern/javadoc/InvalidInlineTag.md @@ -21,6 +21,24 @@ int twoTimes(int n) { } ``` +If the `@` symbol occurrs inside a code excerpt, the fix is to escape the code +excerpt using `
{@code ... }
`: + +```java +/** + * Summary fragment. + * + *
{@code
+ * Your code here.
+ * Can include .
+ * You can even include snippets that contain annotations, e.g.:
+ * @Override public String toString() { ... }
+ * }
+ * + *

Following paragraph. + */ +``` + ## Suppression Suppress by applying `@SuppressWarnings("InvalidInlineTag")` to the element diff --git a/pom.xml b/pom.xml index f6e3708749b..870d37b63a1 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ Error Prone parent POM com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 pom Error Prone is a static analysis tool for Java that catches common programming mistakes at compile-time. @@ -30,7 +30,7 @@ UTF-8 31.0.1-jre - 2.8.2 + 2.10.0 1.1.3 1.0.1 1.9 @@ -289,6 +289,12 @@ sign + + + --pinentry-mode + loopback + + diff --git a/refaster/pom.xml b/refaster/pom.xml index d7951f0e492..e6c4ea5e414 100644 --- a/refaster/pom.xml +++ b/refaster/pom.xml @@ -19,7 +19,7 @@ error_prone_parent com.google.errorprone - HEAD-SNAPSHOT + 2.15.0 4.0.0 diff --git a/test_helpers/pom.xml b/test_helpers/pom.xml index 6d798ac870c..48b1f96c812 100644 --- a/test_helpers/pom.xml +++ b/test_helpers/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 error-prone test helpers diff --git a/test_helpers/src/main/java/com/google/errorprone/BugCheckerRefactoringTestHelper.java b/test_helpers/src/main/java/com/google/errorprone/BugCheckerRefactoringTestHelper.java index b18b57ffabd..09220a4fa16 100644 --- a/test_helpers/src/main/java/com/google/errorprone/BugCheckerRefactoringTestHelper.java +++ b/test_helpers/src/main/java/com/google/errorprone/BugCheckerRefactoringTestHelper.java @@ -198,11 +198,13 @@ public BugCheckerRefactoringTestHelper.ExpectOutput addInputLines(String path, S return new ExpectOutput(forSourceLines(path, input)); } + @CanIgnoreReturnValue public BugCheckerRefactoringTestHelper setFixChooser(FixChooser chooser) { this.fixChooser = chooser; return this; } + @CanIgnoreReturnValue public BugCheckerRefactoringTestHelper addModules(String... modules) { return setArgs( Arrays.stream(modules) @@ -210,23 +212,27 @@ public BugCheckerRefactoringTestHelper addModules(String... modules) { .collect(toImmutableList())); } + @CanIgnoreReturnValue public BugCheckerRefactoringTestHelper setArgs(ImmutableList args) { checkState(options.isEmpty()); this.options = args; return this; } + @CanIgnoreReturnValue public BugCheckerRefactoringTestHelper setArgs(String... args) { this.options = ImmutableList.copyOf(args); return this; } /** If set, fixes that produce output that doesn't compile are allowed. Off by default. */ + @CanIgnoreReturnValue public BugCheckerRefactoringTestHelper allowBreakingChanges() { allowBreakingChanges = true; return this; } + @CanIgnoreReturnValue public BugCheckerRefactoringTestHelper setImportOrder(String importOrder) { this.importOrder = importOrder; return this; @@ -248,6 +254,7 @@ public void doTest(TestMode testMode) { } } + @CanIgnoreReturnValue private BugCheckerRefactoringTestHelper addInputAndOutput( JavaFileObject input, JavaFileObject output) { sources.put(input, output); diff --git a/test_helpers/src/main/java/com/google/errorprone/CompilationTestHelper.java b/test_helpers/src/main/java/com/google/errorprone/CompilationTestHelper.java index 8d46b5a5460..fb639ef9aee 100644 --- a/test_helpers/src/main/java/com/google/errorprone/CompilationTestHelper.java +++ b/test_helpers/src/main/java/com/google/errorprone/CompilationTestHelper.java @@ -32,6 +32,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; import com.google.errorprone.DiagnosticTestHelper.LookForCheckNameInDiagnostic; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.scanner.ScannerSupplier; @@ -181,6 +182,7 @@ private static Optional getOverrideClasspath(@Nullable List> over */ // TODO(eaftan): We could eliminate this path parameter and just infer the path from the // package and class name + @CanIgnoreReturnValue public CompilationTestHelper addSourceLines(String path, String... lines) { this.sources.add(forSourceLines(path, lines)); return this; @@ -193,6 +195,7 @@ public CompilationTestHelper addSourceLines(String path, String... lines) { * * @param path the path to the source file */ + @CanIgnoreReturnValue public CompilationTestHelper addSourceFile(String path) { this.sources.add(forResource(clazz, path)); return this; @@ -205,11 +208,13 @@ public CompilationTestHelper addSourceFile(String path) { * * @param classes the class(es) to use as the classpath */ + @CanIgnoreReturnValue public CompilationTestHelper withClasspath(Class... classes) { this.overrideClasspath = ImmutableList.copyOf(classes); return this; } + @CanIgnoreReturnValue public CompilationTestHelper addModules(String... modules) { return setArgs( stream(modules) @@ -221,6 +226,7 @@ public CompilationTestHelper addModules(String... modules) { * Sets custom command-line arguments for the compilation. These will be appended to the default * compilation arguments. */ + @CanIgnoreReturnValue public CompilationTestHelper setArgs(String... args) { return setArgs(asList(args)); } @@ -229,6 +235,7 @@ public CompilationTestHelper setArgs(String... args) { * Sets custom command-line arguments for the compilation. These will be appended to the default * compilation arguments. */ + @CanIgnoreReturnValue public CompilationTestHelper setArgs(List args) { checkState( extraArgs.isEmpty(), @@ -244,6 +251,7 @@ public CompilationTestHelper setArgs(List args) { * source file contains bug markers. Useful for testing that a check is actually disabled when the * proper command-line argument is passed. */ + @CanIgnoreReturnValue public CompilationTestHelper expectNoDiagnostics() { this.expectNoDiagnostics = true; return this; @@ -254,6 +262,7 @@ public CompilationTestHelper expectNoDiagnostics() { * tested. This behaviour can be disabled to test the interaction between Error Prone checks and * javac diagnostics. */ + @CanIgnoreReturnValue public CompilationTestHelper matchAllDiagnostics() { this.lookForCheckNameInDiagnostic = LookForCheckNameInDiagnostic.NO; return this; @@ -263,6 +272,7 @@ public CompilationTestHelper matchAllDiagnostics() { * Tells the compilation helper to expect a specific result from the compilation, e.g. success or * failure. */ + @CanIgnoreReturnValue public CompilationTestHelper expectResult(Result result) { expectedResult = Optional.of(result); return this; @@ -282,6 +292,7 @@ public CompilationTestHelper expectResult(Result result) { * *

Error message keys that don't match any diagnostics will cause test to fail. */ + @CanIgnoreReturnValue public CompilationTestHelper expectErrorMessage(String key, Predicate matcher) { diagnosticHelper.expectErrorMessage(key, matcher); return this; diff --git a/type_annotations/pom.xml b/type_annotations/pom.xml index b2495a2167f..50fa69762ee 100644 --- a/type_annotations/pom.xml +++ b/type_annotations/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 error-prone type annotations