diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000000..86867dfc432 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,66 @@ +name: Release Error Prone + +on: + workflow_dispatch: + inputs: + version: + description: "version number for this release." + required: true + +jobs: + build-maven-jars: + 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 + + - name: Checkout + uses: actions/checkout@v2.4.0 + + - name: Set up JDK + uses: actions/setup-java@v2.5.0 + with: + java-version: 11 + distribution: 'zulu' + cache: 'maven' + server-id: ossrh + server-username: CI_DEPLOY_USERNAME + server-password: CI_DEPLOY_PASSWORD + + - name: Bump Version Number + run: | + mvn --no-transfer-progress versions:set versions:commit -DnewVersion="${{ github.event.inputs.version }}" + git ls-files | grep 'pom.xml$' | xargs git add + git config --global user.email "${{ github.actor }}@users.noreply.github.com" + git config --global user.name "${{ github.actor }}" + git commit -m "Release Error Prone ${{ github.event.inputs.version }}" + git tag "v${{ github.event.inputs.version }}" + echo "TARGET_COMMITISH=$(git rev-parse HEAD)" >> $GITHUB_ENV + git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/google/error-prone.git + + - name: Deploy to Sonatype staging + env: + CI_DEPLOY_USERNAME: ${{ secrets.CI_DEPLOY_USERNAME }} + CI_DEPLOY_PASSWORD: ${{ secrets.CI_DEPLOY_PASSWORD }} + run: + mvn --no-transfer-progress -P release clean deploy -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" + + - name: Push tag + run: | + git push origin "v${{ github.event.inputs.version }}" + + - name: Draft Release Entry + uses: softprops/action-gh-release@v0.1.14 + with: + draft: true + name: ${{ github.event.input.version }} + tag_name: "v${{ github.event.inputs.version }}" + target_commitish: ${{ env.TARGET_COMMITISH }} diff --git a/annotation/pom.xml b/annotation/pom.xml index 59f69a89d89..9afb703379e 100644 --- a/annotation/pom.xml +++ b/annotation/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.13.0 @BugPattern annotation diff --git a/annotations/pom.xml b/annotations/pom.xml index 77a45ae44ea..46c10bf86ea 100644 --- a/annotations/pom.xml +++ b/annotations/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.13.0 error-prone annotations diff --git a/check_api/pom.xml b/check_api/pom.xml index 4a023c78089..b4d8e4286a7 100644 --- a/check_api/pom.xml +++ b/check_api/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.13.0 error-prone check api diff --git a/check_api/src/main/java/com/google/errorprone/SuppressionInfo.java b/check_api/src/main/java/com/google/errorprone/SuppressionInfo.java index 8ef94ca6456..cb23c08dea3 100644 --- a/check_api/src/main/java/com/google/errorprone/SuppressionInfo.java +++ b/check_api/src/main/java/com/google/errorprone/SuppressionInfo.java @@ -16,8 +16,6 @@ package com.google.errorprone; -import static com.google.common.collect.ImmutableSet.toImmutableSet; - import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; @@ -37,7 +35,6 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Stream; /** * Immutable container of "suppression signals" - annotations or other information gathered from @@ -57,12 +54,6 @@ public class SuppressionInfo { private static final Supplier ANDROID_SUPPRESS_LINT = VisitorState.memoize(state -> state.getName("android.annotation.SuppressLint")); private static final Supplier VALUE = VisitorState.memoize(state -> state.getName("value")); - private static final Supplier> GENERATED_ANNOTATIONS = - VisitorState.memoize( - state -> - Stream.of("javax.annotation.Generated", "javax.annotation.processing.Generated") - .map(state::getName) - .collect(toImmutableSet())); private final ImmutableSet suppressWarningsStrings; @SuppressWarnings("Immutable") /* Name is javac's interned version of a string. */ @@ -78,7 +69,7 @@ private SuppressionInfo( } private static boolean isGenerated(Symbol sym, VisitorState state) { - return !ASTHelpers.annotationsAmong(sym, GENERATED_ANNOTATIONS.get(state), state).isEmpty(); + return !ASTHelpers.getGeneratedBy(sym, state).isEmpty(); } /** diff --git a/check_api/src/main/java/com/google/errorprone/VisitorState.java b/check_api/src/main/java/com/google/errorprone/VisitorState.java index 81d707c91d4..c4312004526 100644 --- a/check_api/src/main/java/com/google/errorprone/VisitorState.java +++ b/check_api/src/main/java/com/google/errorprone/VisitorState.java @@ -262,6 +262,10 @@ public ErrorProneOptions errorProneOptions() { return sharedState.errorProneOptions; } + public Map severityMap() { + return sharedState.severityMap; + } + public void reportMatch(Description description) { checkNotNull(description, "Use Description.NO_MATCH to denote an absent finding."); if (description == Description.NO_MATCH) { diff --git a/check_api/src/main/java/com/google/errorprone/bugpatterns/BugChecker.java b/check_api/src/main/java/com/google/errorprone/bugpatterns/BugChecker.java index d785545c431..6aa078935a0 100644 --- a/check_api/src/main/java/com/google/errorprone/bugpatterns/BugChecker.java +++ b/check_api/src/main/java/com/google/errorprone/bugpatterns/BugChecker.java @@ -19,12 +19,15 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.errorprone.util.ASTHelpers.getModifiers; import static com.google.errorprone.util.ASTHelpers.getStartPosition; +import static com.google.errorprone.util.ASTHelpers.getSymbol; import com.google.common.collect.ImmutableRangeSet; import com.google.common.collect.Iterables; import com.google.common.collect.Range; import com.google.errorprone.BugCheckerInfo; import com.google.errorprone.BugPattern.SeverityLevel; +import com.google.errorprone.ErrorProneOptions; +import com.google.errorprone.SuppressionInfo; import com.google.errorprone.VisitorState; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.fixes.Fix; @@ -248,17 +251,17 @@ public boolean suppressedByAnyOf(Set annotations, VisitorState s) { } /** - * Returns true if the given tree is annotated with a {@code @SuppressWarnings} that disables this - * bug checker. + * @deprecated use {@link #isSuppressed(Tree, VisitorState)} instead */ + @Deprecated public boolean isSuppressed(Tree tree) { return isSuppressed(ASTHelpers.getAnnotation(tree, SuppressWarnings.class)); } /** - * Returns true if the given symbol is annotated with a {@code @SuppressWarnings} that disables - * this bug checker. + * @deprecated use {@link #isSuppressed(Symbol, VisitorState)} instead */ + @Deprecated public boolean isSuppressed(Symbol symbol) { return isSuppressed(ASTHelpers.getAnnotation(symbol, SuppressWarnings.class)); } @@ -268,13 +271,45 @@ private boolean isSuppressed(SuppressWarnings suppression) { && !Collections.disjoint(Arrays.asList(suppression.value()), allNames()); } + /** + * Returns true if the given tree is annotated with a {@code @SuppressWarnings} that disables this + * bug checker. + */ + public boolean isSuppressed(Tree tree, VisitorState state) { + Symbol sym = getSymbol(tree); + return sym != null && isSuppressed(sym, state); + } + + /** + * Returns true if the given symbol is annotated with a {@code @SuppressWarnings} or other + * annotation that disables this bug checker. + */ + public boolean isSuppressed(Symbol sym, VisitorState state) { + ErrorProneOptions errorProneOptions = state.errorProneOptions(); + boolean suppressedInGeneratedCode = + errorProneOptions.disableWarningsInGeneratedCode() + && state.severityMap().get(canonicalName()) != SeverityLevel.ERROR; + SuppressionInfo.SuppressedState suppressedState = + SuppressionInfo.EMPTY + .withExtendedSuppressions(sym, state, customSuppressionAnnotationNames.get(state)) + .suppressedState(BugChecker.this, suppressedInGeneratedCode, state); + return suppressedState == SuppressionInfo.SuppressedState.SUPPRESSED; + } + + private final Supplier> customSuppressionAnnotationNames = + VisitorState.memoize( + state -> + customSuppressionAnnotations().stream() + .map(a -> state.getName(a.getName())) + .collect(toImmutableSet())); + /** Computes a RangeSet of code regions which are suppressed by this bug checker. */ public ImmutableRangeSet suppressedRegions(VisitorState state) { ImmutableRangeSet.Builder suppressedRegions = ImmutableRangeSet.builder(); new TreeScanner() { @Override public Void scan(Tree tree, Void unused) { - if (getModifiers(tree) != null && isSuppressed(tree)) { + if (getModifiers(tree) != null && isSuppressed(tree, state)) { suppressedRegions.add(Range.closed(getStartPosition(tree), state.getEndPosition(tree))); } else { super.scan(tree, null); @@ -517,11 +552,34 @@ public int hashCode() { /** A {@link TreePathScanner} which skips trees which are suppressed for this check. */ protected class SuppressibleTreePathScanner extends TreePathScanner { + + // TODO(cushon): make this protected once it is required; currently it would shadow + // other variables named state and break checks that pass the deprecated constructor + private final VisitorState state; + + public SuppressibleTreePathScanner(VisitorState state) { + this.state = state; + } + + /** + * @deprecated use {@link #SuppressibleTreePathScanner(VisitorState)} instead + */ + @Deprecated + public SuppressibleTreePathScanner() { + this(null); + } + @Override public A scan(Tree tree, B b) { boolean isSuppressible = tree instanceof ClassTree || tree instanceof MethodTree || tree instanceof VariableTree; - return isSuppressible && isSuppressed(tree) ? null : super.scan(tree, b); + if (isSuppressible) { + boolean suppressed = state != null ? isSuppressed(tree, state) : isSuppressed(tree); + if (suppressed) { + return null; + } + } + return super.scan(tree, b); } } } diff --git a/check_api/src/main/java/com/google/errorprone/fixes/AppliedFix.java b/check_api/src/main/java/com/google/errorprone/fixes/AppliedFix.java index 928e45d9f3c..436ea15cf92 100644 --- a/check_api/src/main/java/com/google/errorprone/fixes/AppliedFix.java +++ b/check_api/src/main/java/com/google/errorprone/fixes/AppliedFix.java @@ -18,18 +18,10 @@ import static com.google.common.base.Preconditions.checkArgument; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import com.sun.tools.javac.tree.EndPosTable; -import java.io.IOException; -import java.io.LineNumberReader; -import java.io.StringReader; -import java.util.HashSet; -import java.util.NavigableMap; import java.util.Set; -import java.util.TreeMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.annotation.Nullable; /** @@ -58,12 +50,10 @@ public boolean isRemoveLine() { public static class Applier { private final CharSequence source; private final EndPosTable endPositions; - private final Supplier> lineOffsets; public Applier(CharSequence source, EndPosTable endPositions) { this.source = source; this.endPositions = endPositions; - this.lineOffsets = Suppliers.memoize(() -> lineOffsets(source.toString())); } /** @@ -72,86 +62,77 @@ public Applier(CharSequence source, EndPosTable endPositions) { */ @Nullable public AppliedFix apply(Fix suggestedFix) { - StringBuilder replaced = new StringBuilder(source); - - // We have to apply the replacements in descending order, since otherwise the positions in - // subsequent replacements are invalidated by earlier replacements. - Set replacements = descending(suggestedFix.getReplacements(endPositions)); + // We apply the replacements in ascending order here. Descending is simpler, since applying a + // replacement can't change the index for future replacements, but it leads to quadratic + // copying behavior as we constantly shift the tail of the file around in our StringBuilder. + ImmutableSet replacements = + ascending(suggestedFix.getReplacements(endPositions)); + if (replacements.isEmpty()) { + return null; + } - Set modifiedLines = new HashSet<>(); + StringBuilder replaced = new StringBuilder(); + int positionInOriginal = 0; for (Replacement repl : replacements) { checkArgument( repl.endPosition() <= source.length(), "End [%s] should not exceed source length [%s]", repl.endPosition(), source.length()); - replaced.replace(repl.startPosition(), repl.endPosition(), repl.replaceWith()); - - // Find the line number(s) being modified - modifiedLines.add(lineOffsets.get().floorEntry(repl.startPosition()).getValue()); - } - // Not sure this is really the right behavior, but otherwise we can end up with an infinite - // loop below. - if (modifiedLines.isEmpty()) { - return null; + // Write the unmodified content leading up to this change + replaced.append(source, positionInOriginal, repl.startPosition()); + // And the modified content for this change + replaced.append(repl.replaceWith()); + // Then skip everything from source between start and end + positionInOriginal = repl.endPosition(); } + // Flush out any remaining content after the final change + replaced.append(source, positionInOriginal, source.length()); - LineNumberReader lineNumberReader = - new LineNumberReader(new StringReader(replaced.toString())); - String snippet = null; - boolean isRemoveLine = false; - try { - while (!modifiedLines.contains(lineNumberReader.getLineNumber())) { - lineNumberReader.readLine(); - } - // TODO: this is over-simplified; need a failing test case - snippet = lineNumberReader.readLine(); - if (snippet == null) { - // The file's last line was removed. - snippet = ""; - } else { - snippet = snippet.trim(); - // snip comment from line - if (snippet.contains("//")) { - snippet = snippet.substring(0, snippet.indexOf("//")).trim(); - } - } - if (snippet.isEmpty()) { - isRemoveLine = true; - snippet = "to remove this line"; - } - } catch (IOException e) { - // impossible since source is in-memory + // Find the changed line containing the first edit + String snippet = firstEditedLine(replaced, Iterables.get(replacements, 0)); + if (snippet.isEmpty()) { + return new AppliedFix("to remove this line", /* isRemoveLine= */ true); } - return new AppliedFix(snippet, isRemoveLine); + return new AppliedFix(snippet, /* isRemoveLine= */ false); } /** Get the replacements in an appropriate order to apply correctly. */ - private static Set descending(Set set) { + private static ImmutableSet ascending(Set set) { Replacements replacements = new Replacements(); set.forEach(replacements::add); - return replacements.descending(); + return replacements.ascending(); + } + + /** + * Finds the full text of the first line that's changed. In this case "line" means "bracketed by + * \n characters". We don't handle \r\n specially, because the strings that javac provides to + * Error Prone have already been transformed from platform line endings to newlines (and even if + * it didn't, the dangling \r characters would be handled by a trim() call). + */ + private static String firstEditedLine(StringBuilder content, Replacement firstEdit) { + // We subtract 1 here because we want to find the first newline *before* the edit, not one + // at its beginning. + int startOfFirstEditedLine = content.lastIndexOf("\n", firstEdit.startPosition() - 1); + int endOfFirstEditedLine = content.indexOf("\n", firstEdit.startPosition()); + if (startOfFirstEditedLine == -1) { + startOfFirstEditedLine = 0; // Change to start of file with no preceding newline + } + if (endOfFirstEditedLine == -1) { + // Change to last line of file + endOfFirstEditedLine = content.length(); + } + String snippet = content.substring(startOfFirstEditedLine, endOfFirstEditedLine); + snippet = snippet.trim(); + if (snippet.contains("//")) { + snippet = snippet.substring(0, snippet.indexOf("//")).trim(); + } + return snippet; } } public static Applier fromSource(CharSequence source, EndPosTable endPositions) { return new Applier(source, endPositions); } - - private static final Pattern NEWLINE = Pattern.compile("\\R"); - - /** Returns the start offsets of the lines in the input. */ - private static NavigableMap lineOffsets(String input) { - NavigableMap lines = new TreeMap<>(); - int line = 0; - int idx = 0; - lines.put(idx, line++); - Matcher matcher = NEWLINE.matcher(input); - while (matcher.find(idx)) { - idx = matcher.end(); - lines.put(idx, line++); - } - return lines; - } } 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 1f60d51342b..b3ff921353f 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 @@ -20,6 +20,7 @@ import com.google.common.base.Joiner; import com.google.common.collect.ComparisonChain; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Ordering; import com.google.common.collect.Range; import com.google.common.collect.RangeMap; @@ -148,6 +149,13 @@ public Set descending() { return new LinkedHashSet<>(replacements.values()); } + /** 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()); + } + public boolean isEmpty() { return replacements.isEmpty(); } diff --git a/check_api/src/main/java/com/google/errorprone/matchers/UnusedReturnValueMatcher.java b/check_api/src/main/java/com/google/errorprone/matchers/UnusedReturnValueMatcher.java index 90b7e70cb4a..34222fa5f41 100644 --- a/check_api/src/main/java/com/google/errorprone/matchers/UnusedReturnValueMatcher.java +++ b/check_api/src/main/java/com/google/errorprone/matchers/UnusedReturnValueMatcher.java @@ -35,7 +35,10 @@ import static com.google.errorprone.util.ASTHelpers.getResultType; 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.isSameType; import static com.google.errorprone.util.ASTHelpers.isVoidType; +import static javax.lang.model.element.Modifier.ABSTRACT; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -54,6 +57,7 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.Tree.Kind; 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.Type; import com.sun.tools.javac.code.Type.MethodType; @@ -71,9 +75,17 @@ public final class UnusedReturnValueMatcher implements Matcher { private static final ImmutableMap> ALLOW_MATCHERS = ImmutableMap.of( - AllowReason.MOCKING_CALL, UnusedReturnValueMatcher::mockitoInvocation, - AllowReason.EXCEPTION_TESTING, UnusedReturnValueMatcher::exceptionTesting, - AllowReason.RETURNS_JAVA_LANG_VOID, UnusedReturnValueMatcher::returnsJavaLangVoid); + AllowReason.MOCKING_CALL, + UnusedReturnValueMatcher::mockitoInvocation, + AllowReason.EXCEPTION_TESTING, + UnusedReturnValueMatcher::exceptionTesting, + AllowReason.RETURNS_JAVA_LANG_VOID, + UnusedReturnValueMatcher::returnsJavaLangVoid, + // TODO(kak): this exclusion really doesn't belong here, since the context of the calling + // code doesn't matter; known builder setters are _always_ treated as CIRV, and the + // surrounding context doesn't matter. + AllowReason.KNOWN_BUILDER_SETTER, + UnusedReturnValueMatcher::isKnownBuilderSetter); private static final ImmutableSet DISALLOW_EXCEPTION_TESTING = Sets.immutableEnumSet( @@ -146,6 +158,44 @@ public Stream getAllowReasons(ExpressionTree tree, VisitorState sta .filter(reason -> ALLOW_MATCHERS.get(reason).matches(tree, state)); } + /** + * Returns {@code true} if the given tree is a method call to an abstract setter method inside of + * a known builder class. + */ + private static boolean isKnownBuilderSetter(ExpressionTree tree, VisitorState state) { + Symbol symbol = getSymbol(tree); + if (!(symbol instanceof MethodSymbol)) { + return false; + } + + // Avoid matching static Builder factory methods, like `static Builder newBuilder()` + if (!symbol.getModifiers().contains(ABSTRACT)) { + return false; + } + + MethodSymbol method = (MethodSymbol) symbol; + ClassSymbol enclosingClass = method.enclClass(); + + // Setters always have exactly 1 param + if (method.getParameters().size() != 1) { + return false; + } + + // If the enclosing class is not a known builder type, return false. + if (!hasAnnotation(enclosingClass, "com.google.auto.value.AutoValue.Builder", state) + && !hasAnnotation(enclosingClass, "com.google.auto.value.AutoBuilder", state)) { + return false; + } + + // If the method return type is not the same as the enclosing type (the builder itself), + // e.g., the abstract `build()` method on the Builder, return false. + if (!isSameType(method.getReturnType(), enclosingClass.asType(), state)) { + return false; + } + + return true; + } + private static boolean returnsJavaLangVoid(ExpressionTree tree, VisitorState state) { return tree instanceof MemberReferenceTree ? returnsJavaLangVoid((MemberReferenceTree) tree, state) @@ -277,6 +327,8 @@ public enum AllowReason { /** The context is a mocking call such as in {@code verify(foo).getBar();}. */ MOCKING_CALL, /** The method returns {@code java.lang.Void} at this use-site. */ - RETURNS_JAVA_LANG_VOID + RETURNS_JAVA_LANG_VOID, + /** The method is a known Builder setter method (that always returns "this"). */ + KNOWN_BUILDER_SETTER, } } diff --git a/check_api/src/main/java/com/google/errorprone/scanner/ErrorProneScanner.java b/check_api/src/main/java/com/google/errorprone/scanner/ErrorProneScanner.java index a85fa4a875d..90eb6a63243 100644 --- a/check_api/src/main/java/com/google/errorprone/scanner/ErrorProneScanner.java +++ b/check_api/src/main/java/com/google/errorprone/scanner/ErrorProneScanner.java @@ -448,7 +448,7 @@ private VisitorState processMatchers( reportMatch( processingFunction.process(matcher, tree, stateWithSuppressionInformation), stateWithSuppressionInformation); - } catch (Exception t) { + } catch (Exception | AssertionError t) { handleError(matcher, t); } } 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 113bf543ecb..534a30a4a2c 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 @@ -1160,15 +1160,23 @@ public static ClassSymbol enclosingClass(Symbol sym) { } /** Return the enclosing {@code PackageSymbol} of the given symbol, or {@code null}. */ + @Nullable public static PackageSymbol enclosingPackage(Symbol sym) { - return sym.packge(); + Symbol curr = sym; + while (curr != null) { + if (curr.getKind().equals(ElementKind.PACKAGE)) { + return (PackageSymbol) curr; + } + curr = curr.owner; + } + return null; } /** Return true if the given symbol is defined in the current package. */ public static boolean inSamePackage(Symbol targetSymbol, VisitorState state) { JCCompilationUnit compilationUnit = (JCCompilationUnit) state.getPath().getCompilationUnit(); PackageSymbol usePackage = compilationUnit.packge; - PackageSymbol targetPackage = targetSymbol.packge(); + PackageSymbol targetPackage = enclosingPackage(targetSymbol); return targetPackage != null && usePackage != null diff --git a/core/pom.xml b/core/pom.xml index 6046c211042..6a41a06ecea 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.13.0 error-prone library @@ -105,6 +105,13 @@ ${autovalue.version} compile + + + com.google.auto.value + auto-value + ${autovalue.version} + test + com.google.errorprone diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AlreadyChecked.java b/core/src/main/java/com/google/errorprone/bugpatterns/AlreadyChecked.java index dbd90c34094..8c8955082c8 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/AlreadyChecked.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/AlreadyChecked.java @@ -80,6 +80,7 @@ private final class IfScanner extends SuppressibleTreePathScanner { private final VisitorState state; private IfScanner(VisitorState state) { + super(state); this.state = state; } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AmbiguousMethodReference.java b/core/src/main/java/com/google/errorprone/bugpatterns/AmbiguousMethodReference.java index c23ab415eba..3cfe0d67fd5 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/AmbiguousMethodReference.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/AmbiguousMethodReference.java @@ -67,7 +67,7 @@ public Description matchClass(ClassTree tree, VisitorState state) { continue; } MethodSymbol msym = getSymbol((MethodTree) member); - if (isSuppressed(msym)) { + if (isSuppressed(msym, state)) { continue; } List clash = methods.remove(methodReferenceDescriptor(types, msym)); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AutoValueImmutableFields.java b/core/src/main/java/com/google/errorprone/bugpatterns/AutoValueImmutableFields.java index c895267309a..8ba9ad3bc4f 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/AutoValueImmutableFields.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/AutoValueImmutableFields.java @@ -131,7 +131,7 @@ private static Matcher returning(String type) { public Description matchClass(ClassTree tree, VisitorState state) { if (ASTHelpers.hasAnnotation(tree, "com.google.auto.value.AutoValue", state)) { for (Tree memberTree : tree.getMembers()) { - if (memberTree instanceof MethodTree && !isSuppressed(memberTree)) { + if (memberTree instanceof MethodTree && !isSuppressed(memberTree, state)) { MethodTree methodTree = (MethodTree) memberTree; if (ABSTRACT_MATCHER.matches(methodTree, state)) { for (Map.Entry> entry : REPLACEMENT_TO_MATCHERS.entries()) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AutoValueSubclassLeaked.java b/core/src/main/java/com/google/errorprone/bugpatterns/AutoValueSubclassLeaked.java index 7f92d371dd8..60ecfe2b9f4 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/AutoValueSubclassLeaked.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/AutoValueSubclassLeaked.java @@ -66,7 +66,7 @@ private void scanAndReportAutoValueReferences( CompilationUnitTree tree, ImmutableSet autoValueClassesFromThisFile, VisitorState state) { - new SuppressibleTreePathScanner() { + new SuppressibleTreePathScanner(state) { @Override public Void visitClass(ClassTree classTree, Void unused) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/BadImport.java b/core/src/main/java/com/google/errorprone/bugpatterns/BadImport.java index f2454d225c1..667d79065a4 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/BadImport.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/BadImport.java @@ -171,7 +171,7 @@ private Description buildDescription( CompilationUnitTree compilationUnit = state.getPath().getCompilationUnit(); TreePath path = TreePath.getPath(compilationUnit, compilationUnit); IdentifierTree firstFound = - new SuppressibleTreePathScanner() { + new SuppressibleTreePathScanner(state) { @Override public IdentifierTree reduce(IdentifierTree r1, IdentifierTree r2) { return (r2 != null) ? r2 : r1; @@ -180,7 +180,7 @@ public IdentifierTree reduce(IdentifierTree r1, IdentifierTree r2) { @Override public IdentifierTree visitIdentifier(IdentifierTree node, Void unused) { Symbol nodeSymbol = getSymbol(node); - if (symbols.contains(nodeSymbol) && !isSuppressed(node)) { + if (symbols.contains(nodeSymbol) && !isSuppressed(node, state)) { if (getCurrentPath().getParentPath().getLeaf().getKind() != Kind.CASE) { builder.prefixWith(node, enclosingReplacement); moveTypeAnnotations(node); 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 2f2c73c5129..be23462e7f1 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java @@ -17,6 +17,7 @@ package com.google.errorprone.bugpatterns; import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; +import static com.google.errorprone.util.ASTHelpers.getSymbol; import static com.google.errorprone.util.ASTHelpers.hasDirectAnnotationWithSimpleName; import com.google.auto.value.AutoValue; @@ -32,7 +33,9 @@ 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.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; import com.sun.tools.javac.code.Symbol; @@ -96,13 +99,42 @@ public CheckReturnValue(ErrorProneFlags flags) { @Override public Matcher specializedMatcher() { return (tree, state) -> { - Optional sym = methodSymbol(tree); + Optional sym = methodToInspect(tree); return sym.flatMap(CheckReturnValue::firstAnnotation) .map(found -> found.annotation().equals(CHECK_RETURN_VALUE)) .orElse(checkAllConstructors && sym.map(MethodSymbol::isConstructor).orElse(false)); }; } + private static Optional methodToInspect(ExpressionTree tree) { + // If we're in the middle of calling an anonymous class, we want to actually look at the + // corresponding constructor of the supertype (e.g.: if I extend a class with a @CIRV + // constructor that I delegate to, then my anonymous class's constructor should *also* be + // considered @CIRV). + if (tree instanceof NewClassTree) { + ClassTree anonymousClazz = ((NewClassTree) tree).getClassBody(); + if (anonymousClazz != null) { + // There should be a single defined constructor in the anonymous class body + var constructor = + anonymousClazz.getMembers().stream() + .filter(MethodTree.class::isInstance) + .map(MethodTree.class::cast) + .filter(mt -> getSymbol(mt).isConstructor()) + .findFirst(); + + // and its first statement should be a super() call to the method in question. + return constructor + .map(MethodTree::getBody) + .map(block -> block.getStatements().get(0)) + .map(ExpressionStatementTree.class::cast) + .map(ExpressionStatementTree::getExpression) + .map(MethodInvocationTree.class::cast) + .map(ASTHelpers::getSymbol); + } + } + return methodSymbol(tree); + } + private static Optional methodSymbol(ExpressionTree tree) { Symbol sym = ASTHelpers.getSymbol(tree); return sym instanceof MethodSymbol ? Optional.of((MethodSymbol) sym) : Optional.empty(); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ClassName.java b/core/src/main/java/com/google/errorprone/bugpatterns/ClassName.java index fdd0e52ec42..c232bd0873f 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ClassName.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ClassName.java @@ -52,7 +52,7 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s for (Tree member : tree.getTypeDecls()) { if (member instanceof ClassTree) { ClassTree classMember = (ClassTree) member; - if (isSuppressed(classMember)) { + if (isSuppressed(classMember, state)) { // If any top-level classes have @SuppressWarnings("ClassName"), ignore // this compilation unit. We can't rely on the normal suppression // mechanism because the only enclosing element is the package declaration, diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DefaultPackage.java b/core/src/main/java/com/google/errorprone/bugpatterns/DefaultPackage.java index e8350e70dd1..8d4d09290ba 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/DefaultPackage.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DefaultPackage.java @@ -39,7 +39,7 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s if (state.errorProneOptions().isTestOnlyTarget()) { return Description.NO_MATCH; } - if (tree.getTypeDecls().stream().anyMatch(s -> isSuppressed(s))) { + if (tree.getTypeDecls().stream().anyMatch(s -> isSuppressed(s, state))) { return Description.NO_MATCH; } if (tree.getTypeDecls().stream() 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 6d488fda57a..a92c06de47b 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java @@ -185,7 +185,7 @@ public Description matchMethod(MethodTree tree, VisitorState state) { public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { ImmutableListMultimap assignedTypes = getAssignedTypes(state); - new SuppressibleTreePathScanner() { + new SuppressibleTreePathScanner(state) { @Override public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) { handleTree(tree, getSymbol(tree)); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/EqualsHashCode.java b/core/src/main/java/com/google/errorprone/bugpatterns/EqualsHashCode.java index 2b9b0df9606..2ea8536edfc 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/EqualsHashCode.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/EqualsHashCode.java @@ -58,10 +58,8 @@ public class EqualsHashCode extends BugChecker implements ClassTreeMatcher { @Override public Description matchClass(ClassTree classTree, VisitorState state) { - MethodTree methodTree = - checkMethodPresence( - classTree, state, NON_TRIVIAL_EQUALS, /* expectedNoArgMethod= */ "hashCode"); - if (methodTree == null || isSuppressed(methodTree)) { + MethodTree methodTree = checkMethodPresence(classTree, state/* expectedNoArgMethod= */ ); + if (methodTree == null || isSuppressed(methodTree, state)) { return NO_MATCH; } return describeMatch(methodTree); @@ -76,11 +74,7 @@ public Description matchClass(ClassTree classTree, VisitorState state) { * */ @Nullable - static MethodTree checkMethodPresence( - ClassTree classTree, - VisitorState state, - Matcher requiredMethodPresenceMatcher, - String expectedNoArgMethod) { + private static MethodTree checkMethodPresence(ClassTree classTree, VisitorState state) { TypeSymbol symbol = ASTHelpers.getSymbol(classTree); if (symbol.getKind() != ElementKind.CLASS) { return null; @@ -95,7 +89,7 @@ static MethodTree checkMethodPresence( continue; } MethodTree methodTree = (MethodTree) member; - if (requiredMethodPresenceMatcher.matches(methodTree, state)) { + if (EqualsHashCode.NON_TRIVIAL_EQUALS.matches(methodTree, state)) { requiredMethod = methodTree; } } @@ -106,7 +100,7 @@ static MethodTree checkMethodPresence( ASTHelpers.resolveExistingMethod( state, symbol, - state.getName(expectedNoArgMethod), + state.getName("hashCode"), ImmutableList.of(), ImmutableList.of()); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/FieldCanBeFinal.java b/core/src/main/java/com/google/errorprone/bugpatterns/FieldCanBeFinal.java index c1ac860298e..457dd65ede0 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/FieldCanBeFinal.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/FieldCanBeFinal.java @@ -257,7 +257,7 @@ private FinalScanner(VariableAssignmentRecords writes, VisitorState compilationS @Override public Void visitVariable(VariableTree node, InitializationContext init) { VarSymbol sym = ASTHelpers.getSymbol(node); - if (sym.getKind() == ElementKind.FIELD && !isSuppressed(node)) { + if (sym.getKind() == ElementKind.FIELD && !isSuppressed(node, compilationState)) { writes.recordDeclaration(sym, node); } return super.visitVariable(node, InitializationContext.NONE); @@ -317,7 +317,7 @@ private boolean isThisAccess(Tree tree) { public Void visitClass(ClassTree node, InitializationContext init) { VisitorState state = compilationState.withPath(getCurrentPath()); - if (isSuppressed(node)) { + if (isSuppressed(node, state)) { return null; } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/FieldCanBeLocal.java b/core/src/main/java/com/google/errorprone/bugpatterns/FieldCanBeLocal.java index 57fbcd51763..239ce875fa4 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/FieldCanBeLocal.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/FieldCanBeLocal.java @@ -79,7 +79,7 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s SetMultimap uses = MultimapBuilder.linkedHashKeys().linkedHashSetValues().build(); - new SuppressibleTreePathScanner() { + new SuppressibleTreePathScanner(state) { @Override public Void visitVariable(VariableTree variableTree, Void unused) { VarSymbol symbol = getSymbol(variableTree); @@ -118,7 +118,7 @@ private boolean canBeUsedOnLocalVariable(AnnotationTree annotationTree) { @Override public Void visitClass(ClassTree classTree, Void unused) { - if (isSuppressed(classTree)) { + if (isSuppressed(classTree, state)) { return null; } inMethod = false; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/FunctionalInterfaceClash.java b/core/src/main/java/com/google/errorprone/bugpatterns/FunctionalInterfaceClash.java index 0d1b62ad4ea..c753a02932e 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/FunctionalInterfaceClash.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/FunctionalInterfaceClash.java @@ -112,7 +112,7 @@ public Description matchClass(ClassTree tree, VisitorState state) { continue; } - if (isSuppressed(member)) { + if (isSuppressed(member, state)) { continue; } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/HashCodeToString.java b/core/src/main/java/com/google/errorprone/bugpatterns/HashCodeToString.java deleted file mode 100644 index c22b115c1c2..00000000000 --- a/core/src/main/java/com/google/errorprone/bugpatterns/HashCodeToString.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2020 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.SUGGESTION; -import static com.google.errorprone.matchers.Description.NO_MATCH; -import static com.google.errorprone.matchers.Matchers.allOf; -import static com.google.errorprone.matchers.Matchers.hashCodeMethodDeclaration; -import static com.google.errorprone.matchers.Matchers.instanceHashCodeInvocation; -import static com.google.errorprone.matchers.Matchers.not; -import static com.google.errorprone.matchers.Matchers.singleStatementReturnMatcher; - -import com.google.errorprone.BugPattern; -import com.google.errorprone.BugPattern.StandardTags; -import com.google.errorprone.VisitorState; -import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; -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.MethodTree; - -/** - * Classes that override {@link Object#hashCode} should consider overriding {@link Object#toString}. - * - * @author bhagwani@google.com (Sumit Bhagwani) - */ -@BugPattern( - summary = "Classes that override hashCode should also consider overriding toString.", - severity = SUGGESTION, - tags = StandardTags.FRAGILE_CODE) -public class HashCodeToString extends BugChecker implements ClassTreeMatcher { - - private static final Matcher NON_TRIVIAL_HASHCODE = - allOf( - hashCodeMethodDeclaration(), - not(singleStatementReturnMatcher(instanceHashCodeInvocation()))); - - @Override - public Description matchClass(ClassTree classTree, VisitorState state) { - if (ASTHelpers.hasAnnotation(classTree, "com.google.auto.value.AutoValue", state)) { - return NO_MATCH; - } - MethodTree methodTree = - EqualsHashCode.checkMethodPresence( - classTree, state, NON_TRIVIAL_HASHCODE, /* expectedNoArgMethod= */ "toString"); - if (methodTree == null || isSuppressed(methodTree)) { - return NO_MATCH; - } - return describeMatch(methodTree); - } -} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/HidingField.java b/core/src/main/java/com/google/errorprone/bugpatterns/HidingField.java index a1600d15970..31709559cb3 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/HidingField.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/HidingField.java @@ -58,7 +58,11 @@ public Description matchClass(ClassTree classTree, VisitorState visitorState) { classTree.getMembers().stream() .filter(mem -> mem instanceof VariableTree) .map(mem -> (VariableTree) mem) - .filter(mem -> !isSuppressed(getSymbol(mem)) && !isIgnoredType(mem) && !isStatic(mem)) + .filter( + mem -> + !isSuppressed(getSymbol(mem), visitorState) + && !isIgnoredType(mem) + && !isStatic(mem)) .collect(toCollection(ArrayList::new)); ClassSymbol classSymbol = getSymbol(classTree); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/IgnoredPureGetter.java b/core/src/main/java/com/google/errorprone/bugpatterns/IgnoredPureGetter.java index 5090886df2a..147ee181521 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/IgnoredPureGetter.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/IgnoredPureGetter.java @@ -18,7 +18,6 @@ import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; 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.hasAnnotation; import static com.google.errorprone.util.ASTHelpers.isSameType; @@ -46,9 +45,9 @@ @BugPattern( severity = ERROR, summary = - "Getters on AutoValue classes and protos are side-effect free, so there is no point in" - + " calling them if the return value is ignored. While there are no side effects from" - + " the getter, the receiver may have side effects.") + "Getters on AutoValues, AutoBuilders, and Protobuf Messages are side-effect free, so there" + + " is no point in calling them if the return value is ignored. While there are no" + + " side effects from the getter, the receiver may have side effects.") public final class IgnoredPureGetter extends AbstractReturnValueIgnored { private static final Supplier MESSAGE_LITE = @@ -59,6 +58,7 @@ public final class IgnoredPureGetter extends AbstractReturnValueIgnored { state -> state.getTypeFromString("com.google.protobuf.MutableMessageLite")); private final boolean checkAllProtos; + private final boolean checkAutoBuilders; public IgnoredPureGetter() { this(ErrorProneFlags.empty()); @@ -67,6 +67,7 @@ public IgnoredPureGetter() { public IgnoredPureGetter(ErrorProneFlags flags) { super(flags); this.checkAllProtos = flags.getBoolean("IgnoredPureGetter:CheckAllProtos").orElse(true); + this.checkAutoBuilders = flags.getBoolean("IgnoredPureGetter:CheckAutoBuilders").orElse(true); } @Override @@ -111,29 +112,39 @@ private boolean isPureGetter(ExpressionTree tree, VisitorState state) { // TODO(b/222475003): make this static again once the flag is gone private Optional pureGetterKind(ExpressionTree tree, VisitorState state) { - Symbol symbol = getSymbol(tree); - if (!(symbol instanceof MethodSymbol)) { + Symbol rawSymbol = getSymbol(tree); + if (!(rawSymbol instanceof MethodSymbol)) { return Optional.empty(); } - if (symbol.getModifiers().contains(ABSTRACT)) { + MethodSymbol symbol = (MethodSymbol) rawSymbol; + Symbol owner = symbol.owner; + + if (symbol.getModifiers().contains(ABSTRACT) && symbol.getParameters().isEmpty()) { // The return value of any abstract method on an @AutoValue needs to be used. - if (hasAnnotation(symbol.owner, "com.google.auto.value.AutoValue", state)) { + if (hasAnnotation(owner, "com.google.auto.value.AutoValue", state)) { return Optional.of(PureGetterKind.AUTO_VALUE); } - // The return value of an abstract method on an @AutoValue.Builder (which doesn't return the + // The return value of any abstract method on an @AutoBuilder (which doesn't return the + // Builder itself) needs to be used. + if (checkAutoBuilders + && hasAnnotation(owner, "com.google.auto.value.AutoBuilder", state) + && !isSameType(symbol.getReturnType(), owner.type, state)) { + return Optional.of(PureGetterKind.AUTO_BUILDER); + } + // The return value of any abstract method on an @AutoValue.Builder (which doesn't return the // Builder itself) needs to be used. - if (hasAnnotation(symbol.owner, "com.google.auto.value.AutoValue.Builder", state) - && !isSameType(getReturnType(tree), symbol.owner.asType(), state)) { + if (hasAnnotation(owner, "com.google.auto.value.AutoValue.Builder", state) + && !isSameType(symbol.getReturnType(), owner.type, state)) { return Optional.of(PureGetterKind.AUTO_VALUE_BUILDER); } } try { - if (isSubtype(symbol.owner.type, MESSAGE_LITE.get(state), state) - && !isSubtype(symbol.owner.type, MUTABLE_MESSAGE_LITE.get(state), state)) { + if (isSubtype(owner.type, MESSAGE_LITE.get(state), state) + && !isSubtype(owner.type, MUTABLE_MESSAGE_LITE.get(state), state)) { String name = symbol.getSimpleName().toString(); if ((name.startsWith("get") || name.startsWith("has")) - && ((MethodSymbol) symbol).getParameters().isEmpty()) { + && symbol.getParameters().isEmpty()) { return Optional.of(PureGetterKind.PROTO); } if (checkAllProtos) { @@ -150,6 +161,7 @@ private Optional pureGetterKind(ExpressionTree tree, VisitorStat private enum PureGetterKind { AUTO_VALUE, AUTO_VALUE_BUILDER, + AUTO_BUILDER, PROTO } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ImmutableMemberCollection.java b/core/src/main/java/com/google/errorprone/bugpatterns/ImmutableMemberCollection.java index bc336a76b10..3c28aea1744 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ImmutableMemberCollection.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ImmutableMemberCollection.java @@ -126,7 +126,7 @@ public Description matchClass(ClassTree classTree, VisitorState state) { classTree.getMembers().stream() .filter(member -> PRIVATE_FINAL_VAR_MATCHER.matches(member, state)) .filter(member -> !EXCLUSIONS.matches(member, state)) - .filter(member -> !isSuppressed(member)) + .filter(member -> !isSuppressed(member, state)) .map(VariableTree.class::cast) .flatMap(varTree -> stream(isReplaceable(varTree, state))) .collect(toImmutableMap(ReplaceableVar::symbol, var -> var)); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ImmutableSetForContains.java b/core/src/main/java/com/google/errorprone/bugpatterns/ImmutableSetForContains.java index 41dd47a27d9..b4430a2751a 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ImmutableSetForContains.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ImmutableSetForContains.java @@ -119,7 +119,7 @@ public Description matchClass(ClassTree tree, VisitorState state) { SuggestedFix.Builder fix = SuggestedFix.builder(); Optional firstReplacement = Optional.empty(); for (VariableTree var : immutableListVar) { - if (isSuppressed(var)) { + if (isSuppressed(var, state)) { continue; } if (!usageScanner.disallowedVarUsages.get(getSymbol(var))) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/InterruptedExceptionSwallowed.java b/core/src/main/java/com/google/errorprone/bugpatterns/InterruptedExceptionSwallowed.java index fe9836146fb..07b7ebaa946 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/InterruptedExceptionSwallowed.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/InterruptedExceptionSwallowed.java @@ -165,7 +165,7 @@ public Description matchTry(TryTree tree, VisitorState state) { && !blockChecksForInterruptedException(catchTree.getBlock(), state) && !(exceptionIsRethrown(catchTree, catchTree.getParameter(), state) && methodDeclaresInterruptedException(state.findEnclosing(MethodTree.class), state)) - && !isSuppressed(catchTree.getParameter())) { + && !isSuppressed(catchTree.getParameter(), state)) { return describeMatch(catchTree, createFix(catchTree)); } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/JUnit3TestNotRun.java b/core/src/main/java/com/google/errorprone/bugpatterns/JUnit3TestNotRun.java index 549db11fe09..42edb2687b2 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/JUnit3TestNotRun.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/JUnit3TestNotRun.java @@ -93,7 +93,7 @@ public final class JUnit3TestNotRun extends BugChecker implements CompilationUni @Override public Description matchCompilationUnit(CompilationUnitTree unused, VisitorState state) { ImmutableSet calledMethods = calledMethods(state); - new SuppressibleTreePathScanner() { + new SuppressibleTreePathScanner(state) { @Override public Void visitMethod(MethodTree tree, Void unused) { checkMethod(tree, calledMethods, state.withPath(getCurrentPath())) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/JUnit4TestNotRun.java b/core/src/main/java/com/google/errorprone/bugpatterns/JUnit4TestNotRun.java index 2a07a2b3a49..ae9d6b14637 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/JUnit4TestNotRun.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/JUnit4TestNotRun.java @@ -103,11 +103,11 @@ public Description matchClass(ClassTree tree, VisitorState state) { } Map suspiciousMethods = new HashMap<>(); for (Tree member : tree.getMembers()) { - if (!(member instanceof MethodTree) || isSuppressed(member)) { + if (!(member instanceof MethodTree) || isSuppressed(member, state)) { continue; } MethodTree methodTree = (MethodTree) member; - if (possibleTestMethod.matches(methodTree, state) && !isSuppressed(tree)) { + if (possibleTestMethod.matches(methodTree, state) && !isSuppressed(tree, state)) { suspiciousMethods.put(getSymbol(methodTree), methodTree); } } 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 dd26b72b8cc..db42998f2da 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/MethodCanBeStatic.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/MethodCanBeStatic.java @@ -77,7 +77,7 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s @Override public Void visitClass(ClassTree classTree, Void unused) { - if (isSuppressed(classTree)) { + if (isSuppressed(classTree, state)) { suppressions++; super.visitClass(classTree, null); suppressions--; @@ -89,7 +89,7 @@ public Void visitClass(ClassTree classTree, Void unused) { @Override public Void visitMethod(MethodTree tree, Void unused) { - if (isSuppressed(tree)) { + if (isSuppressed(tree, state)) { suppressions++; matchMethod(tree); super.visitMethod(tree, null); @@ -103,7 +103,7 @@ public Void visitMethod(MethodTree tree, Void unused) { @Override public Void visitVariable(VariableTree variableTree, Void unused) { - if (isSuppressed(variableTree)) { + if (isSuppressed(variableTree, state)) { suppressions++; super.visitVariable(variableTree, null); suppressions--; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MixedMutabilityReturnType.java b/core/src/main/java/com/google/errorprone/bugpatterns/MixedMutabilityReturnType.java index 8baad4234a7..5ef34d412ea 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/MixedMutabilityReturnType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/MixedMutabilityReturnType.java @@ -193,6 +193,7 @@ private final class ReturnTypesScanner extends SuppressibleTreePathScanner immutable, Set mutable) { + super(state); this.state = state; this.immutable = immutable; this.mutable = mutable; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MultipleTopLevelClasses.java b/core/src/main/java/com/google/errorprone/bugpatterns/MultipleTopLevelClasses.java index ec5dbd26b7c..9c558eb6eb5 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/MultipleTopLevelClasses.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/MultipleTopLevelClasses.java @@ -60,7 +60,7 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s case INTERFACE: case ANNOTATION_TYPE: case ENUM: - if (isSuppressed(classMember)) { + if (isSuppressed(classMember, state)) { // If any top-level classes have @SuppressWarnings("TopLevel"), ignore // this compilation unit. We can't rely on the normal suppression // mechanism because the only enclosing element is the package declaration, diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/NarrowCalculation.java b/core/src/main/java/com/google/errorprone/bugpatterns/NarrowCalculation.java new file mode 100644 index 00000000000..99479a42fbe --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/NarrowCalculation.java @@ -0,0 +1,156 @@ +/* + * 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.fixes.SuggestedFix.replace; +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.isSameType; +import static com.google.errorprone.util.ASTHelpers.targetType; +import static java.lang.String.format; + +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.BinaryTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LiteralTree; +import com.sun.source.tree.Tree.Kind; +import com.sun.tools.javac.code.Symtab; +import com.sun.tools.javac.code.Type; + +/** A BugPattern; see the summary. */ +@BugPattern( + summary = "This calculation may lose precision compared to its target type.", + severity = WARNING) +public final class NarrowCalculation extends BugChecker implements BinaryTreeMatcher { + + @Override + public Description matchBinary(BinaryTree tree, VisitorState state) { + var leftType = getType(tree.getLeftOperand()); + var rightType = getType(tree.getRightOperand()); + var targetType = targetType(state); + if (leftType == null || rightType == null || targetType == null) { + return NO_MATCH; + } + if (tree.getKind().equals(Kind.DIVIDE) + && leftType.isIntegral() + && rightType.isIntegral() + && isFloatingPoint(targetType.type())) { + Object leftConst = constValue(tree.getLeftOperand()); + Object rightConst = constValue(tree.getRightOperand()); + if (leftConst != null && rightConst != null) { + long left = ((Number) leftConst).longValue(); + long right = ((Number) rightConst).longValue(); + long divided = left / right; + if (divided * right == left) { + return NO_MATCH; + } + } + return buildDescription(tree) + .setMessage( + "This division will discard the fractional part of the result, despite being assigned" + + " to a float.") + .addFix( + SuggestedFix.builder() + .setShortDescription("Perform the division using floating point arithmetic") + .merge(forceExpressionType(tree, targetType.type(), state)) + .build()) + .build(); + } + if (tree.getKind().equals(Kind.MULTIPLY) + && isInt(leftType, state) + && isInt(rightType, state) + && isLong(targetType.type(), state)) { + // Heuristic: test data often gets generated with stuff like `i * 1000`, and is known not to + // overflow. + if (state.errorProneOptions().isTestOnlyTarget()) { + return NO_MATCH; + } + var leftConst = constValue(tree.getLeftOperand()); + var rightConst = constValue(tree.getRightOperand()); + if (leftConst != null && rightConst != null) { + int leftInt = (int) leftConst; + int rightInt = (int) rightConst; + long product = ((long) leftInt) * ((long) rightInt); + if (product == (int) product) { + return NO_MATCH; + } + } + return buildDescription(tree) + .setMessage( + "This product of integers could overflow before being implicitly cast to a long.") + .addFix( + SuggestedFix.builder() + .setShortDescription("Perform the multiplication as long * long") + .merge(forceExpressionType(tree, targetType.type(), state)) + .build()) + .build(); + } + return NO_MATCH; + } + + private static SuggestedFix forceExpressionType( + BinaryTree tree, Type targetType, VisitorState state) { + if (tree.getRightOperand() instanceof LiteralTree) { + return SuggestedFix.replace( + tree.getRightOperand(), forceLiteralType(tree.getRightOperand(), targetType, state)); + } + if (tree.getLeftOperand() instanceof LiteralTree) { + return SuggestedFix.replace( + tree.getLeftOperand(), forceLiteralType(tree.getLeftOperand(), targetType, state)); + } + return replace( + tree.getRightOperand(), + format("((%s) %s)", targetType, state.getSourceForNode(tree.getRightOperand()))); + } + + private static String forceLiteralType(ExpressionTree tree, Type targetType, VisitorState state) { + return state.getSourceForNode(tree).replaceAll("[LlFfDd]$", "") + + suffixForType(targetType, state); + } + + private static String suffixForType(Type type, VisitorState state) { + Symtab symtab = state.getSymtab(); + if (isSameType(type, symtab.longType, state)) { + return "L"; + } + if (isSameType(type, symtab.floatType, state)) { + return "f"; + } + if (isSameType(type, symtab.doubleType, state)) { + return ".0"; + } + throw new AssertionError(); + } + + private static boolean isFloatingPoint(Type type) { + return type.isNumeric() && !type.isIntegral(); + } + + private static boolean isInt(Type type, VisitorState state) { + return isSameType(type, state.getSymtab().intType, state); + } + + private static boolean isLong(Type type, VisitorState state) { + return isSameType(type, state.getSymtab().longType, state); + } +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/OptionalNotPresent.java b/core/src/main/java/com/google/errorprone/bugpatterns/OptionalNotPresent.java index bdd88070378..e625a66892d 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/OptionalNotPresent.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/OptionalNotPresent.java @@ -77,6 +77,7 @@ private final class IfScanner extends SuppressibleTreePathScanner { private final VisitorState state; private IfScanner(VisitorState state) { + super(state); this.state = state; } 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 a305f0381e1..ba99856149a 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/PreferredInterfaceType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/PreferredInterfaceType.java @@ -181,7 +181,7 @@ public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) { private ImmutableMap getFixableTypes(VisitorState state) { ImmutableMap.Builder fixableTypes = ImmutableMap.builder(); - new SuppressibleTreePathScanner() { + new SuppressibleTreePathScanner(state) { @Override public Void visitVariable(VariableTree tree, Void unused) { VarSymbol symbol = getSymbol(tree); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ProtectedMembersInFinalClass.java b/core/src/main/java/com/google/errorprone/bugpatterns/ProtectedMembersInFinalClass.java index d5254a51c75..fc01b489c32 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ProtectedMembersInFinalClass.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ProtectedMembersInFinalClass.java @@ -70,7 +70,7 @@ public Description matchClass(ClassTree tree, VisitorState state) { .filter(m -> HAS_PROTECTED.matches(m, state)) .filter( m -> !(m instanceof MethodTree) || methodHasNoParentMethod((MethodTree) m, state)) - .filter(m -> !isSuppressed(m)) + .filter(m -> !isSuppressed(m, state)) .collect(toImmutableList()); if (relevantMembers.isEmpty()) { return NO_MATCH; 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 5299fe98b25..2e6c07d122e 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparison.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparison.java @@ -160,12 +160,12 @@ private ProtoNullComparisonScanner(VisitorState state) { @Override public Void visitMethod(MethodTree method, Void unused) { - return isSuppressed(method) ? null : super.visitMethod(method, unused); + return isSuppressed(method, state) ? null : super.visitMethod(method, unused); } @Override public Void visitClass(ClassTree clazz, Void unused) { - return isSuppressed(clazz) ? null : super.visitClass(clazz, unused); + return isSuppressed(clazz, state) ? null : super.visitClass(clazz, unused); } @Override @@ -175,7 +175,7 @@ public Void visitVariable(VariableTree variable, Void unused) { getInitializer(variable.getInitializer()) .ifPresent(e -> effectivelyFinalValues.put(symbol, e)); } - return isSuppressed(variable) ? null : super.visitVariable(variable, null); + return isSuppressed(variable, state) ? null : super.visitVariable(variable, null); } private Optional getInitializer(ExpressionTree tree) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/PublicConstructorForAbstractClass.java b/core/src/main/java/com/google/errorprone/bugpatterns/PublicConstructorForAbstractClass.java deleted file mode 100644 index 54f44afe0c6..00000000000 --- a/core/src/main/java/com/google/errorprone/bugpatterns/PublicConstructorForAbstractClass.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2020 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.SUGGESTION; -import static com.google.errorprone.matchers.Description.NO_MATCH; -import static com.google.errorprone.matchers.Matchers.allOf; -import static com.google.errorprone.matchers.Matchers.enclosingClass; -import static com.google.errorprone.matchers.Matchers.hasModifier; -import static com.google.errorprone.matchers.Matchers.methodHasVisibility; -import static com.google.errorprone.matchers.Matchers.methodIsConstructor; -import static javax.lang.model.element.Modifier.ABSTRACT; - -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.fixes.SuggestedFixes; -import com.google.errorprone.matchers.Description; -import com.google.errorprone.matchers.Matcher; -import com.google.errorprone.matchers.MethodVisibility; -import com.sun.source.tree.MethodTree; -import javax.lang.model.element.Modifier; - -/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ -@BugPattern( - summary = - "Constructors of an abstract class can be declared protected as there is never a need" - + " for them to be public", - explanation = - "Abstract classes' constructors are only ever called by subclasses, never directly by" - + " another class. Therefore they never need public constructors: protected is" - + " accessible enough.", - severity = SUGGESTION) -public class PublicConstructorForAbstractClass extends BugChecker implements MethodTreeMatcher { - - private static final Matcher PUBLIC_ABSTRACT_CONSTRUCTOR = - allOf( - methodIsConstructor(), - methodHasVisibility(MethodVisibility.Visibility.PUBLIC), - enclosingClass(hasModifier(ABSTRACT))); - - @Override - public Description matchMethod(MethodTree tree, VisitorState state) { - if (!PUBLIC_ABSTRACT_CONSTRUCTOR.matches(tree, state)) { - return NO_MATCH; - } - SuggestedFix.Builder fix = SuggestedFix.builder(); - SuggestedFixes.removeModifiers(tree, state, Modifier.PUBLIC).ifPresent(fix::merge); - SuggestedFixes.addModifiers(tree, state, Modifier.PROTECTED).ifPresent(fix::merge); - return describeMatch(tree, fix.build()); - } -} 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 0ff0364fc03..5ccba6f30ec 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java @@ -57,7 +57,7 @@ public final class SameNameButDifferent extends BugChecker implements Compilatio @Override public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { Table> table = HashBasedTable.create(); - new SuppressibleTreePathScanner() { + new SuppressibleTreePathScanner(state) { @Override public Void visitMemberSelect(MemberSelectTree memberSelectTree, Void unused) { if (!shouldIgnore()) { 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 e55a4acea35..ec9e6063a27 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/StronglyType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/StronglyType.java @@ -217,7 +217,7 @@ private static String getMethodSelectOrNewClass(ExpressionTree tree, VisitorStat private ImmutableMap findPathToPotentialFields( VisitorState state, Set potentialTypes) { ImmutableMap.Builder fields = ImmutableMap.builder(); - bugChecker().new SuppressibleTreePathScanner() { + bugChecker().new SuppressibleTreePathScanner(state) { @Override public Void visitVariable(VariableTree variableTree, Void unused) { VarSymbol symbol = getSymbol(variableTree); @@ -228,7 +228,7 @@ && isConsideredFinal(symbol) && variableTree.getInitializer() != null && potentialTypes.stream() .anyMatch(potentialType -> isSameType(type, potentialType, state)) - && !bugChecker().isSuppressed(variableTree)) { + && !bugChecker().isSuppressed(variableTree, state)) { fields.put(symbol, getCurrentPath()); } return super.visitVariable(variableTree, null); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/SuppressWarningsWithoutExplanation.java b/core/src/main/java/com/google/errorprone/bugpatterns/SuppressWarningsWithoutExplanation.java index a9eb02e1dbf..1c121c27c6c 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/SuppressWarningsWithoutExplanation.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/SuppressWarningsWithoutExplanation.java @@ -82,7 +82,7 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s return NO_MATCH; } ImmutableRangeSet linesWithComments = linesWithComments(state); - new SuppressibleTreePathScanner() { + new SuppressibleTreePathScanner(state) { @Override public Void visitAnnotation(AnnotationTree annotationTree, Void unused) { if (!SUPPRESS_WARNINGS.matches(annotationTree, state)) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UngroupedOverloads.java b/core/src/main/java/com/google/errorprone/bugpatterns/UngroupedOverloads.java index 0a4b4bb8027..4da4012597c 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UngroupedOverloads.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UngroupedOverloads.java @@ -137,7 +137,7 @@ private Stream checkOverloads( if (group == 0) { return Stream.empty(); } - if (overloads.stream().anyMatch(m -> isSuppressed(m.tree()))) { + if (overloads.stream().anyMatch(m -> isSuppressed(m.tree(), state))) { return Stream.empty(); } // build a fix that replaces the first overload with all the overloads grouped together diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessarilyFullyQualified.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessarilyFullyQualified.java index 2ca3f8142bf..7202ebbf7b0 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessarilyFullyQualified.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessarilyFullyQualified.java @@ -80,7 +80,7 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s } Table> table = HashBasedTable.create(); SetMultimap identifiersSeen = HashMultimap.create(); - new SuppressibleTreePathScanner() { + new SuppressibleTreePathScanner(state) { @Override public Void visitImport(ImportTree importTree, Void unused) { return null; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryBoxedVariable.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryBoxedVariable.java index 3f4af95466b..25b8b9a986e 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryBoxedVariable.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryBoxedVariable.java @@ -17,14 +17,18 @@ package com.google.errorprone.bugpatterns; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.errorprone.matchers.Description.NO_MATCH; import static com.google.errorprone.matchers.Matchers.anyOf; 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.getSymbol; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; import com.google.errorprone.BugPattern; import com.google.errorprone.BugPattern.SeverityLevel; import com.google.errorprone.VisitorState; -import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher; +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; @@ -32,10 +36,12 @@ import com.google.errorprone.util.ASTHelpers.TargetType; import com.sun.source.tree.AnnotationTree; import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MemberReferenceTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; @@ -52,10 +58,10 @@ import com.sun.tools.javac.code.Symbol.VarSymbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.TypeTag; -import java.util.ArrayList; +import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Optional; +import java.util.Set; import javax.lang.model.element.ElementKind; /** @@ -70,74 +76,85 @@ + " corresponding primitive type, which avoids the cost of constructing an unnecessary" + " object.", severity = SeverityLevel.SUGGESTION) -public class UnnecessaryBoxedVariable extends BugChecker implements VariableTreeMatcher { +public class UnnecessaryBoxedVariable extends BugChecker implements CompilationUnitTreeMatcher { private static final Matcher VALUE_OF_MATCHER = staticMethod().onClass(UnnecessaryBoxedVariable::isBoxableType).named("valueOf"); @Override - public Description matchVariable(VariableTree tree, VisitorState state) { - Optional unboxed = unboxed(tree, state); - if (!unboxed.isPresent()) { - return Description.NO_MATCH; - } + public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { + FindBoxedUsagesScanner usages = new FindBoxedUsagesScanner(state); + usages.scan(tree, null); + + new SuppressibleTreePathScanner(state) { + @Override + public Void visitVariable(VariableTree tree, Void unused) { + VisitorState innerState = state.withPath(getCurrentPath()); + unboxed(tree, innerState) + .flatMap(u -> handleVariable(u, usages, tree, innerState)) + .ifPresent(state::reportMatch); + return super.visitVariable(tree, null); + } + }.scan(tree, null); + return NO_MATCH; + } - VarSymbol varSymbol = ASTHelpers.getSymbol(tree); + private Optional handleVariable( + Type unboxed, FindBoxedUsagesScanner usages, VariableTree tree, VisitorState state) { + VarSymbol varSymbol = getSymbol(tree); switch (varSymbol.getKind()) { case PARAMETER: - if (!canChangeMethodSignature(state, (MethodSymbol) varSymbol.getEnclosingElement())) { - return Description.NO_MATCH; + if (!canChangeMethodSignature(state, (MethodSymbol) varSymbol.getEnclosingElement()) + || state.getPath().getParentPath().getLeaf() instanceof LambdaExpressionTree) { + return Optional.empty(); } // Fall through. case LOCAL_VARIABLE: if (!variableMatches(tree, state)) { - return Description.NO_MATCH; + return Optional.empty(); } break; default: - return Description.NO_MATCH; + return Optional.empty(); } - Optional enclosingMethod = getEnclosingMethod(state.getPath()); - if (!enclosingMethod.isPresent()) { - return Description.NO_MATCH; - } + return fixVariable(unboxed, usages, tree, state); + } - TreePath path = enclosingMethod.get(); - FindBoxedUsagesScanner scanner = new FindBoxedUsagesScanner(varSymbol, path, state); - scanner.scan(path, null); - if (scanner.boxedUsageFound) { - return Description.NO_MATCH; + private Optional fixVariable( + Type unboxed, FindBoxedUsagesScanner usages, VariableTree tree, VisitorState state) { + VarSymbol varSymbol = getSymbol(tree); + if (usages.boxedUsageFound.contains(varSymbol)) { + return Optional.empty(); } - if (!scanner.used && varSymbol.getKind() == ElementKind.PARAMETER) { + if (!usages.dereferenced.contains(varSymbol) && varSymbol.getKind() == ElementKind.PARAMETER) { // If it isn't used and it is a parameter, don't fix it, because this could introduce a new // NPE. - return Description.NO_MATCH; + return Optional.empty(); } SuggestedFix.Builder fixBuilder = SuggestedFix.builder(); - fixBuilder.replace(tree.getType(), unboxed.get().tsym.getSimpleName().toString()); + fixBuilder.replace(tree.getType(), unboxed.tsym.getSimpleName().toString()); - fixMethodInvocations(scanner.fixableSimpleMethodInvocations, fixBuilder, state); - fixNullCheckInvocations(scanner.fixableNullCheckInvocations, fixBuilder, state); - fixCastingInvocations( - scanner.fixableCastMethodInvocations, enclosingMethod.get(), fixBuilder, state); + fixMethodInvocations(usages.fixableSimpleMethodInvocations.get(varSymbol), fixBuilder, state); + fixNullCheckInvocations(usages.fixableNullCheckInvocations.get(varSymbol), fixBuilder, state); + fixCastingInvocations(usages.fixableCastMethodInvocations.get(varSymbol), fixBuilder, state); // Remove @Nullable annotation, if present. AnnotationTree nullableAnnotation = ASTHelpers.getAnnotationWithSimpleName(tree.getModifiers().getAnnotations(), "Nullable"); - if (nullableAnnotation != null) { - fixBuilder.replace(nullableAnnotation, ""); - return buildDescription(tree) - .setMessage( - "All usages of this @Nullable variable would result in a NullPointerException when it" - + " actually is null. Use the primitive type if this variable should never be" - + " null, or else fix the code to avoid unboxing or invoking its instance" - + " methods.") - .addFix(fixBuilder.build()) - .build(); - } else { - return describeMatch(tree, fixBuilder.build()); + if (nullableAnnotation == null) { + return Optional.of(describeMatch(tree, fixBuilder.build())); } + fixBuilder.replace(nullableAnnotation, ""); + return Optional.of( + buildDescription(tree) + .setMessage( + "All usages of this @Nullable variable would result in a NullPointerException when" + + " it actually is null. Use the primitive type if this variable should never" + + " be null, or else fix the code to avoid unboxing or invoking its instance" + + " methods.") + .addFix(fixBuilder.build()) + .build()); } private static Optional unboxed(Tree tree, VisitorState state) { @@ -196,15 +213,12 @@ private static void fixMethodInvocations( } private static void fixCastingInvocations( - List castMethodInvocations, - TreePath enclosingMethod, - SuggestedFix.Builder fixBuilder, - VisitorState state) { - for (MethodInvocationTree castInvocation : castMethodInvocations) { + List castMethodInvocations, SuggestedFix.Builder fixBuilder, VisitorState state) { + for (TreePath castPath : castMethodInvocations) { + MethodInvocationTree castInvocation = (MethodInvocationTree) castPath.getLeaf(); ExpressionTree receiver = ASTHelpers.getReceiver(castInvocation); Type expressionType = ASTHelpers.getType(castInvocation); - TreePath castPath = TreePath.getPath(enclosingMethod, castInvocation); if (castPath.getParentPath() != null && castPath.getParentPath().getLeaf().getKind() == Kind.EXPRESSION_STATEMENT) { // If we were to replace X.intValue(); with (int) x;, the code wouldn't compile because @@ -265,18 +279,6 @@ private static boolean variableMatches(VariableTree tree, VisitorState state) { return VALUE_OF_MATCHER.matches(expression, state); } - private static Optional getEnclosingMethod(TreePath path) { - while (path != null - && path.getLeaf().getKind() != Kind.CLASS - && path.getLeaf().getKind() != Kind.LAMBDA_EXPRESSION) { - if (path.getLeaf().getKind() == Kind.METHOD) { - return Optional.of(path); - } - path = path.getParentPath(); - } - return Optional.empty(); - } - private static boolean isBoxableType(Type type, VisitorState state) { Type unboxedType = state.getTypes().unboxedType(type); return unboxedType != null && unboxedType.getTag() != TypeTag.NONE; @@ -312,25 +314,25 @@ private static class FindBoxedUsagesScanner extends TreePathScanner staticMethod().onClass("com.google.common.base.Verify").named("verifyNonNull"), staticMethod().onClass("java.util.Objects").named("requireNonNull")); - private final VarSymbol varSymbol; - private final TreePath path; private final VisitorState state; - private final List fixableSimpleMethodInvocations = new ArrayList<>(); - private final List fixableNullCheckInvocations = new ArrayList<>(); - private final List fixableCastMethodInvocations = new ArrayList<>(); + private final ListMultimap fixableSimpleMethodInvocations = + ArrayListMultimap.create(); + private final ListMultimap fixableNullCheckInvocations = + ArrayListMultimap.create(); + private final ListMultimap fixableCastMethodInvocations = + ArrayListMultimap.create(); - private boolean boxedUsageFound; - private boolean used; + private final Set boxedUsageFound = new HashSet<>(); + private final Set dereferenced = new HashSet<>(); - FindBoxedUsagesScanner(VarSymbol varSymbol, TreePath path, VisitorState state) { - this.varSymbol = varSymbol; - this.path = path; + FindBoxedUsagesScanner(VisitorState state) { this.state = state; } @Override public Void scan(Tree tree, Void unused) { - if (boxedUsageFound) { + var symbol = getSymbol(tree); + if (boxedUsageFound.contains(symbol)) { return null; } return super.scan(tree, unused); @@ -338,18 +340,17 @@ public Void scan(Tree tree, Void unused) { @Override public Void visitAssignment(AssignmentTree node, Void unused) { - Symbol nodeSymbol = ASTHelpers.getSymbol(node.getVariable()); - if (!Objects.equals(nodeSymbol, varSymbol)) { + Symbol nodeSymbol = getSymbol(node.getVariable()); + if (!isBoxed(nodeSymbol, state)) { return super.visitAssignment(node, unused); } - used = true; // The variable of interest is being assigned. Check if the expression is non-primitive, // and go on to scan the expression. if (!checkAssignmentExpression(node.getExpression())) { return scan(node.getExpression(), unused); } - boxedUsageFound = true; + boxedUsageFound.add((VarSymbol) nodeSymbol); return null; } @@ -361,21 +362,19 @@ private boolean checkAssignmentExpression(ExpressionTree expression) { // If the value is assigned a non-primitive value, we need to keep it non-primitive. // Unless it's an invocation of Boxed.valueOf or new Boxed, in which case it doesn't need to // be kept boxed since we know the result of valueOf is non-null. - return !VALUE_OF_MATCHER.matches( - expression, state.withPath(TreePath.getPath(path, expression))) + return !VALUE_OF_MATCHER.matches(expression, state.withPath(getCurrentPath())) && expression.getKind() != Kind.NEW_CLASS; } @Override public Void visitIdentifier(IdentifierTree node, Void unused) { - Symbol nodeSymbol = ASTHelpers.getSymbol(node); - if (Objects.equals(nodeSymbol, varSymbol)) { - used = true; - TreePath identifierPath = TreePath.getPath(path, node); - VisitorState identifierState = state.withPath(identifierPath); + Symbol nodeSymbol = getSymbol(node); + if (isBoxed(nodeSymbol, state)) { + dereferenced.add((VarSymbol) nodeSymbol); + VisitorState identifierState = state.withPath(getCurrentPath()); TargetType targetType = ASTHelpers.targetType(identifierState); if (targetType != null && !targetType.type().isPrimitive()) { - boxedUsageFound = true; + boxedUsageFound.add((VarSymbol) nodeSymbol); return null; } } @@ -392,28 +391,27 @@ public Void visitCompoundAssignment(CompoundAssignmentTree node, Void unused) { @Override public Void visitMethodInvocation(MethodInvocationTree node, Void unused) { if (NULL_CHECK_MATCH.matches(node, state)) { - Symbol firstArgSymbol = - ASTHelpers.getSymbol(ASTHelpers.stripParentheses(node.getArguments().get(0))); - if (Objects.equals(firstArgSymbol, varSymbol)) { - used = true; - fixableNullCheckInvocations.add(getCurrentPath()); + Symbol firstArgSymbol = getSymbol(ASTHelpers.stripParentheses(node.getArguments().get(0))); + if (isBoxed(firstArgSymbol, state)) { + dereferenced.add((VarSymbol) firstArgSymbol); + fixableNullCheckInvocations.put((VarSymbol) firstArgSymbol, getCurrentPath()); return null; } } Tree receiver = ASTHelpers.getReceiver(node); - if (receiver != null && Objects.equals(ASTHelpers.getSymbol(receiver), varSymbol)) { - used = true; + Symbol receiverSymbol = getSymbol(receiver); + if (receiver != null && isBoxed(receiverSymbol, state)) { if (SIMPLE_METHOD_MATCH.matches(node, state)) { - fixableSimpleMethodInvocations.add(node); + fixableSimpleMethodInvocations.put((VarSymbol) receiverSymbol, node); return null; } if (CAST_METHOD_MATCH.matches(node, state)) { - fixableCastMethodInvocations.add(node); + fixableCastMethodInvocations.put((VarSymbol) receiverSymbol, getCurrentPath()); return null; } - boxedUsageFound = true; + boxedUsageFound.add((VarSymbol) receiverSymbol); return null; } @@ -422,20 +420,20 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void unused) { @Override public Void visitReturn(ReturnTree node, Void unused) { - Symbol nodeSymbol = ASTHelpers.getSymbol(ASTHelpers.stripParentheses(node.getExpression())); - if (!Objects.equals(nodeSymbol, varSymbol)) { + Symbol nodeSymbol = getSymbol(ASTHelpers.stripParentheses(node.getExpression())); + if (!isBoxed(nodeSymbol, state)) { return super.visitReturn(node, unused); } - used = true; + dereferenced.add((VarSymbol) nodeSymbol); // Don't count a return value as a boxed usage, except if we are returning a parameter, and // the method's return type is boxed. - if (varSymbol.getKind() == ElementKind.PARAMETER) { + if (nodeSymbol.getKind() == ElementKind.PARAMETER) { MethodTree enclosingMethod = ASTHelpers.findEnclosingNode(getCurrentPath(), MethodTree.class); Type returnType = ASTHelpers.getType(enclosingMethod.getReturnType()); if (!returnType.isPrimitive()) { - boxedUsageFound = true; + boxedUsageFound.add((VarSymbol) nodeSymbol); } } return null; @@ -445,14 +443,19 @@ public Void visitReturn(ReturnTree node, Void unused) { public Void visitMemberReference(MemberReferenceTree node, Void unused) { ExpressionTree qualifierExpression = node.getQualifierExpression(); if (qualifierExpression.getKind() == Kind.IDENTIFIER) { - Symbol symbol = ASTHelpers.getSymbol(qualifierExpression); - if (Objects.equals(symbol, varSymbol)) { - boxedUsageFound = true; - used = true; + Symbol symbol = getSymbol(qualifierExpression); + if (isBoxed(symbol, state)) { + boxedUsageFound.add((VarSymbol) symbol); + dereferenced.add((VarSymbol) symbol); return null; } } return super.visitMemberReference(node, unused); } } + + private static boolean isBoxed(Symbol symbol, VisitorState state) { + return symbol instanceof VarSymbol + && !state.getTypes().isSameType(state.getTypes().unboxedType(symbol.type), Type.noType); + } } 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 3d05772c03d..a8ae6598291 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java @@ -27,6 +27,7 @@ import static com.google.errorprone.util.ASTHelpers.getStartPosition; import static com.google.errorprone.util.ASTHelpers.getSymbol; import static com.google.errorprone.util.ASTHelpers.getType; +import static com.sun.tools.javac.util.Position.NOPOS; import static java.util.stream.Collectors.joining; import com.google.common.collect.ImmutableSet; @@ -175,7 +176,7 @@ public Void visitIdentifier(IdentifierTree node, Void unused) { * e.g. don't rewrite uses of {@link Predicate} in compilation units that call other methods like * {#link Predicate#add}. */ - boolean canFix(Tree type, Symbol sym, VisitorState state) { + private boolean canFix(Tree type, Symbol sym, VisitorState state) { Symbol descriptor; try { descriptor = state.getTypes().findDescriptorSymbol(getType(type).asElement()); @@ -246,7 +247,11 @@ private void lambdaToMethod( replacement.append(";"); replacement.append("}"); } - fix.replace(state.getEndPosition(modifiers) + 1, endPosition, replacement.toString()); + int modifiedEndPos = state.getEndPosition(modifiers); + fix.replace( + modifiedEndPos == NOPOS ? getStartPosition(tree) : modifiedEndPos + 1, + endPosition, + replacement.toString()); } private static void replaceUseWithMethodReference( diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnusedException.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedException.java index 68cb618bb11..5fd27beec03 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnusedException.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedException.java @@ -74,7 +74,7 @@ public final class UnusedException extends BugChecker implements CatchTreeMatche @Override public Description matchCatch(CatchTree tree, VisitorState state) { - if (isSuppressed(tree.getParameter()) || isSuppressedViaName(tree.getParameter())) { + if (isSuppressed(tree.getParameter(), state) || isSuppressedViaName(tree.getParameter())) { return Description.NO_MATCH; } VarSymbol exceptionSymbol = ASTHelpers.getSymbol(tree.getParameter()); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnusedMethod.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedMethod.java index 0a505457703..48a3aef518d 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnusedMethod.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedMethod.java @@ -30,6 +30,7 @@ import static com.google.errorprone.util.ASTHelpers.canBeRemoved; import static com.google.errorprone.util.ASTHelpers.getSymbol; import static com.google.errorprone.util.ASTHelpers.getType; +import static com.google.errorprone.util.ASTHelpers.isGeneratedConstructor; import static com.google.errorprone.util.ASTHelpers.isSubtype; import static com.google.errorprone.util.ASTHelpers.scope; import static com.google.errorprone.util.ASTHelpers.shouldKeep; @@ -102,6 +103,7 @@ public final class UnusedMethod extends BugChecker implements CompilationUnitTre "javax.annotation.PostConstruct", "javax.inject.Inject", "javax.persistence.PostLoad", + "org.apache.beam.sdk.transforms.DoFn.ProcessElement", "org.aspectj.lang.annotation.Pointcut", "org.aspectj.lang.annotation.Before", "org.springframework.context.annotation.Bean", @@ -130,6 +132,10 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s AtomicBoolean ignoreUnusedMethods = new AtomicBoolean(false); class MethodFinder extends SuppressibleTreePathScanner { + MethodFinder(VisitorState state) { + super(state); + } + @Override public Void visitClass(ClassTree tree, Void unused) { if (exemptedBySuperType(getType(tree), state)) { @@ -202,6 +208,7 @@ private boolean isMethodSymbolEligibleForChecking(MethodTree tree) { } MethodSymbol methodSymbol = getSymbol(tree); if (isExemptedConstructor(methodSymbol, state) + || isGeneratedConstructor(tree) || SERIALIZATION_METHODS.matches(tree, state)) { return false; } @@ -229,7 +236,7 @@ private boolean isExemptedConstructor(MethodSymbol methodSymbol, VisitorState st return false; } } - new MethodFinder().scan(state.getPath(), null); + new MethodFinder(state).scan(state.getPath(), null); class FilterUsedMethods extends TreePathScanner { @Override diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnusedNestedClass.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedNestedClass.java index d7435349fa9..2981f111f92 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnusedNestedClass.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedNestedClass.java @@ -53,7 +53,7 @@ public final class UnusedNestedClass extends BugChecker implements CompilationUn @Override public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { - PrivateNestedClassScanner privateNestedClassScanner = new PrivateNestedClassScanner(); + PrivateNestedClassScanner privateNestedClassScanner = new PrivateNestedClassScanner(state); privateNestedClassScanner.scan(state.getPath(), null); Map privateNestedClasses = privateNestedClassScanner.classes; @@ -71,11 +71,15 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s private final class PrivateNestedClassScanner extends TreePathScanner { private final Map classes = new HashMap<>(); - private PrivateNestedClassScanner() {} + private final VisitorState state; + + private PrivateNestedClassScanner(VisitorState state) { + this.state = state; + } @Override public Void visitClass(ClassTree classTree, Void unused) { - if (ignoreUnusedClass(classTree)) { + if (ignoreUnusedClass(classTree, state)) { return null; } ClassSymbol symbol = getSymbol(classTree); @@ -86,8 +90,8 @@ public Void visitClass(ClassTree classTree, Void unused) { return super.visitClass(classTree, null); } - private boolean ignoreUnusedClass(ClassTree classTree) { - return isSuppressed(classTree) || shouldKeep(classTree); + private boolean ignoreUnusedClass(ClassTree classTree, VisitorState state) { + return isSuppressed(classTree, state) || shouldKeep(classTree); } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnusedVariable.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedVariable.java index 9aa155bcea4..79174cfbd0d 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnusedVariable.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedVariable.java @@ -567,7 +567,7 @@ public Void visitVariable(VariableTree variableTree, Void unused) { if (exemptedByName(variableTree.getName())) { return null; } - if (isSuppressed(variableTree)) { + if (isSuppressed(variableTree, state)) { return null; } VarSymbol symbol = getSymbol(variableTree); @@ -658,7 +658,7 @@ public Void visitTry(TryTree node, Void unused) { @Override public Void visitClass(ClassTree tree, Void unused) { - if (isSuppressed(tree)) { + if (isSuppressed(tree, state)) { return null; } if (EXEMPTING_SUPER_TYPES.stream() @@ -679,7 +679,7 @@ public Void visitMethod(MethodTree tree, Void unused) { if (SERIALIZATION_METHODS.matches(tree, state)) { return scan(tree.getBody(), null); } - return isSuppressed(tree) ? null : super.visitMethod(tree, unused); + return isSuppressed(tree, state) ? null : super.visitMethod(tree, unused); } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/WrongOneof.java b/core/src/main/java/com/google/errorprone/bugpatterns/WrongOneof.java index 7131f8b0b58..b56ec6e8ea8 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/WrongOneof.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/WrongOneof.java @@ -159,7 +159,7 @@ private void scanForInvalidGetters( CaseTree caseTree, ImmutableList receiverSymbolChain, VisitorState state) { - new SuppressibleTreePathScanner() { + new SuppressibleTreePathScanner(state) { @Override public Void visitMethodInvocation(MethodInvocationTree methodInvocationTree, Void unused) { ExpressionTree receiver = getReceiver(methodInvocationTree); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerLogWithCause.java b/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerLogWithCause.java index b1e690c86da..a4410fb9e4a 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerLogWithCause.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerLogWithCause.java @@ -64,7 +64,7 @@ public final class FloggerLogWithCause extends BugChecker implements CatchTreeMa @Override public Description matchCatch(CatchTree tree, VisitorState state) { - if (isSuppressed(tree.getParameter())) { + if (isSuppressed(tree.getParameter(), state)) { return Description.NO_MATCH; } List statements = tree.getBlock().getStatements(); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerPassedAround.java b/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerPassedAround.java index 158762d0788..8361de2a9a1 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerPassedAround.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerPassedAround.java @@ -52,7 +52,7 @@ public final class FloggerPassedAround extends BugChecker implements MethodTreeM public Description matchMethod(MethodTree tree, VisitorState state) { for (Tree parameter : tree.getParameters()) { Type type = getType(parameter); - if (type != null && LOGGER_TYPE.apply(type, state) && !isSuppressed(parameter)) { + if (type != null && LOGGER_TYPE.apply(type, state) && !isSuppressed(parameter, state)) { state.reportMatch(describeMatch(parameter)); } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/InlineFormatString.java b/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/InlineFormatString.java index 2d76aacf41e..4df787d5fdc 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/InlineFormatString.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/InlineFormatString.java @@ -172,7 +172,7 @@ private void handle(Tree tree) { }, null); // find the field declarations - new SuppressibleTreePathScanner() { + new SuppressibleTreePathScanner(state) { @Override public Void visitVariable(VariableTree tree, Void unused) { VarSymbol sym = getSymbol(tree); 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 e458d6cf4f6..10885ba8f3b 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 @@ -311,10 +311,10 @@ static Api create(MethodSymbol method, VisitorState state) { abstract String extraMessage(); final String message() { - return "Migrate (via inlining) from " + return "Migrate (via inlining) away from " + (isDeprecated() ? "deprecated " : "") + shortName() - + " to its replacement" + + "." + extraMessage(); } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/AlmostJavadoc.java b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/AlmostJavadoc.java index aea5a552d03..e876968388f 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/AlmostJavadoc.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/AlmostJavadoc.java @@ -123,7 +123,7 @@ private static Optional generateFix(Comment comment) { private ImmutableMap getJavadocableTrees( CompilationUnitTree tree, VisitorState state) { Map javadoccablePositions = new HashMap<>(); - new SuppressibleTreePathScanner() { + new SuppressibleTreePathScanner(state) { @Override public Void visitClass(ClassTree classTree, Void unused) { if (!shouldMatch()) { 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 ee876be351f..111e9981faa 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 @@ -372,7 +372,7 @@ && methodCanBeOverridden(method)) { @Override public Void scan(Tree tree, Void unused) { - if (isSuppressed(tree)) { + if (isSuppressed(tree, stateForCompilationUnit)) { return null; } return super.scan(tree, unused); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/overloading/InconsistentOverloads.java b/core/src/main/java/com/google/errorprone/bugpatterns/overloading/InconsistentOverloads.java index 658a6172e8c..66d9072741e 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/overloading/InconsistentOverloads.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/overloading/InconsistentOverloads.java @@ -68,7 +68,7 @@ public final class InconsistentOverloads extends BugChecker implements ClassTree @Override public Description matchClass(ClassTree classTree, VisitorState state) { - processClassMethods(getClassTreeMethods(classTree), state); + processClassMethods(getClassTreeMethods(classTree, state), state); /* * We want to report inconsistencies per method group. There is no "method group" unit in the @@ -142,12 +142,12 @@ private static Collection> getMethodGroups(List cla *

Only method trees that belong to the {@code classTree} are returned, so methods declared in * nested classes are not going to be considered. */ - private ImmutableList getClassTreeMethods(ClassTree classTree) { + private ImmutableList getClassTreeMethods(ClassTree classTree, VisitorState state) { List members = classTree.getMembers(); return members.stream() .filter(MethodTree.class::isInstance) .map(MethodTree.class::cast) - .filter(m -> !isSuppressed(m)) + .filter(m -> !isSuppressed(m, state)) .collect(toImmutableList()); } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ConstantExpressions.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ConstantExpressions.java index 399cb91a0cc..d1fb518615a 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ConstantExpressions.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ConstantExpressions.java @@ -320,6 +320,7 @@ public final String toString() { return receiver + symbol().getSimpleName(); } return receiver + + (symbol().isStatic() ? symbol().owner.getSimpleName() + "." : "") + symbol().getSimpleName() + arguments().stream().map(Object::toString).collect(joining(", ", "(", ")")); } @@ -350,8 +351,9 @@ public Optional symbolizeImmutableExpression( ? getReceiver(tree) : null; + Symbol symbol = getSymbol(tree); Optional receiverConstant; - if (receiver == null) { + if (receiver == null || (symbol != null && symbol.isStatic())) { receiverConstant = Optional.empty(); } else { receiverConstant = constantExpression(receiver, state); 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 1e00315c028..cbed17f6986 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 @@ -85,7 +85,7 @@ private void analyze(VisitorState state) { state, (ExpressionTree tree, GuardedByExpression guard, HeldLockSet live) -> report(GuardedByChecker.this.checkGuardedAccess(tree, guard, live, state), state), - this::isSuppressed, + tree1 -> isSuppressed(tree1, state), flags, reportMissingGuards); } 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 9357562fef1..62c6107a9ce 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 @@ -257,14 +257,14 @@ Violation areFieldsImmutable( } /** Check a single field for immutability. */ - private Violation isFieldImmutable( + Violation isFieldImmutable( Optional tree, ImmutableSet immutableTyParams, ClassSymbol classSym, ClassType classType, VarSymbol var, ViolationReporter reporter) { - if (bugChecker.isSuppressed(var)) { + if (bugChecker.isSuppressed(var, state)) { return Violation.absent(); } if (!var.getModifiers().contains(Modifier.FINAL) 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 67d9e2cd2cf..d3fdcf1dcf6 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 @@ -19,9 +19,17 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.util.ASTHelpers.getReceiver; +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 java.lang.String.format; +import static java.util.stream.Collectors.joining; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; import com.google.common.collect.Sets.SetView; import com.google.errorprone.BugPattern; @@ -30,6 +38,7 @@ import com.google.errorprone.annotations.Immutable; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.LambdaExpressionTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MemberReferenceTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; @@ -41,22 +50,34 @@ import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ClassTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LambdaExpressionTree; 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.NewClassTree; import com.sun.source.tree.Tree; import com.sun.source.tree.TypeParameterTree; +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.ClassSymbol; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Symbol.TypeSymbol; 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.ClassType; import com.sun.tools.javac.tree.JCTree.JCMemberReference; import com.sun.tools.javac.tree.JCTree.JCNewClass; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Optional; +import java.util.Set; +import javax.lang.model.element.ElementKind; /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ @BugPattern( @@ -66,6 +87,7 @@ documentSuppression = false) public class ImmutableChecker extends BugChecker implements ClassTreeMatcher, + LambdaExpressionTreeMatcher, NewClassTreeMatcher, MethodInvocationTreeMatcher, MethodTreeMatcher, @@ -73,6 +95,7 @@ public class ImmutableChecker extends BugChecker private final WellKnownMutability wellKnownMutability; private final ImmutableSet immutableAnnotations; + private final boolean matchLambdas; ImmutableChecker(ImmutableSet immutableAnnotations) { this(ErrorProneFlags.empty(), immutableAnnotations); @@ -85,34 +108,150 @@ public ImmutableChecker(ErrorProneFlags flags) { private ImmutableChecker(ErrorProneFlags flags, ImmutableSet immutableAnnotations) { this.wellKnownMutability = WellKnownMutability.fromFlags(flags); this.immutableAnnotations = immutableAnnotations; + this.matchLambdas = flags.getBoolean("ImmutableChecker:MatchLambdas").orElse(true); + } + + @Override + public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) { + if (!matchLambdas) { + return NO_MATCH; + } + TypeSymbol lambdaType = getType(tree).tsym; + if (!hasImmutableAnnotation(lambdaType, state)) { + return NO_MATCH; + } + Set variablesClosed = new HashSet<>(); + SetMultimap typesClosed = LinkedHashMultimap.create(); + Set variablesOwnedByLambda = new HashSet<>(); + + new TreePathScanner() { + @Override + public Void visitVariable(VariableTree tree, Void unused) { + var symbol = getSymbol(tree); + variablesOwnedByLambda.add(symbol); + return super.visitVariable(tree, null); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) { + if (getReceiver(tree) == null) { + var symbol = getSymbol(tree); + if (!symbol.isStatic()) { + // TODO(b/77333859): This isn't precise. What we really want is the type of `this`, if + // the method call were qualified with it. + typesClosed.put((ClassSymbol) symbol.owner, symbol); + } + } + return super.visitMethodInvocation(tree, null); + } + + @Override + public Void visitMemberSelect(MemberSelectTree tree, Void unused) { + // Special case the access of fields to allow accessing fields which would pass an immutable + // check. + if (tree.getExpression() instanceof IdentifierTree + && getSymbol(tree) instanceof VarSymbol) { + handleIdentifier(getSymbol(tree)); + // If we're only seeing a field access, don't complain about the fact we closed around + // `this`. + if (tree.getExpression() instanceof IdentifierTree + && ((IdentifierTree) tree.getExpression()).getName().contentEquals("this")) { + return null; + } + } + return super.visitMemberSelect(tree, null); + } + + @Override + public Void visitIdentifier(IdentifierTree tree, Void unused) { + handleIdentifier(getSymbol(tree)); + return super.visitIdentifier(tree, null); + } + + private void handleIdentifier(Symbol symbol) { + if (symbol instanceof VarSymbol && !variablesOwnedByLambda.contains(symbol)) { + variablesClosed.add((VarSymbol) symbol); + } + } + }.scan(state.getPath(), null); + + ImmutableAnalysis analysis = createImmutableAnalysis(state); + ImmutableSet typarams = + immutableTypeParametersInScope(getSymbol(tree), state, analysis); + variablesClosed.stream() + .map(closedVariable -> checkClosedLambdaVariable(closedVariable, tree, typarams, analysis)) + .filter(Violation::isPresent) + .forEachOrdered( + v -> { + String message = formLambdaReason(lambdaType) + ", but " + v.message(); + state.reportMatch(buildDescription(tree).setMessage(message).build()); + }); + for (var entry : typesClosed.asMap().entrySet()) { + var classSymbol = entry.getKey(); + var methods = entry.getValue(); + if (!hasImmutableAnnotation(classSymbol.type.tsym, state)) { + String message = + format( + "%s, but accesses instance method(s) '%s' on '%s' which is not @Immutable.", + formLambdaReason(lambdaType), + methods.stream().map(Symbol::getSimpleName).collect(joining(", ")), + classSymbol.getSimpleName()); + state.reportMatch(buildDescription(tree).setMessage(message).build()); + } + } + + return NO_MATCH; + } + + private Violation checkClosedLambdaVariable( + VarSymbol closedVariable, + LambdaExpressionTree tree, + ImmutableSet typarams, + ImmutableAnalysis analysis) { + if (!closedVariable.getKind().equals(ElementKind.FIELD)) { + return analysis.isThreadSafeType(false, typarams, closedVariable.type); + } + return analysis.isFieldImmutable( + Optional.empty(), + typarams, + (ClassSymbol) closedVariable.owner, + (ClassType) closedVariable.owner.type, + closedVariable, + (t, v) -> buildDescription(tree)); + } + + private static String formLambdaReason(TypeSymbol typeSymbol) { + return "This lambda implements @Immutable interface '" + typeSymbol.getSimpleName() + "'"; + } + + private boolean hasImmutableAnnotation(TypeSymbol tsym, VisitorState state) { + return immutableAnnotations.stream() + .anyMatch(annotation -> hasAnnotation(tsym, annotation, state)); } // check instantiations of `@ImmutableTypeParameter`s in method references @Override public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) { - checkInvocation( - tree, ((JCMemberReference) tree).referentType, state, ASTHelpers.getSymbol(tree)); + checkInvocation(tree, getSymbol(tree), ((JCMemberReference) tree).referentType, state); return NO_MATCH; } // check instantiations of `@ImmutableTypeParameter`s in method invocations @Override public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { - checkInvocation( - tree, ASTHelpers.getType(tree.getMethodSelect()), state, ASTHelpers.getSymbol(tree)); + checkInvocation(tree, getSymbol(tree), ASTHelpers.getType(tree.getMethodSelect()), state); return NO_MATCH; } @Override public Description matchNewClass(NewClassTree tree, VisitorState state) { // check instantiations of `@ImmutableTypeParameter`s in generic constructor invocations - checkInvocation( - tree, ((JCNewClass) tree).constructorType, state, ((JCNewClass) tree).constructor); + checkInvocation(tree, getSymbol(tree), ((JCNewClass) tree).constructorType, state); // check instantiations of `@ImmutableTypeParameter`s in class constructor invocations checkInstantiation( tree, state, - ASTHelpers.getSymbol(tree.getIdentifier()).getTypeParameters(), + getSymbol(tree.getIdentifier()).getTypeParameters(), ASTHelpers.getType(tree).getTypeArguments()); ClassTree classBody = tree.getClassBody(); @@ -132,7 +271,7 @@ public Description matchMethod(MethodTree tree, VisitorState state) { checkInstantiation( tree, state, - ASTHelpers.getSymbol(tree).getTypeParameters(), + getSymbol(tree).getTypeParameters(), ASTHelpers.getType(tree).getTypeArguments()); return NO_MATCH; } @@ -141,7 +280,8 @@ private ImmutableAnalysis createImmutableAnalysis(VisitorState state) { return new ImmutableAnalysis(this, state, wellKnownMutability, immutableAnnotations); } - private void checkInvocation(Tree tree, Type methodType, VisitorState state, Symbol symbol) { + private void checkInvocation( + Tree tree, MethodSymbol symbol, Type methodType, VisitorState state) { ImmutableAnalysis analysis = createImmutableAnalysis(state); Violation info = analysis.checkInvocation(methodType, symbol); if (info.isPresent()) { @@ -197,14 +337,13 @@ public Description matchClass(ClassTree tree, VisitorState state) { // Check that the types in containerOf actually exist Map typarams = new HashMap<>(); for (TypeParameterTree typaram : tree.getTypeParameters()) { - typarams.put( - typaram.getName().toString(), (TypeVariableSymbol) ASTHelpers.getSymbol(typaram)); + typarams.put(typaram.getName().toString(), (TypeVariableSymbol) getSymbol(typaram)); } SetView difference = Sets.difference(annotation.containerOf(), typarams.keySet()); if (!difference.isEmpty()) { return buildDescription(tree) .setMessage( - String.format( + format( "could not find type(s) referenced by containerOf: %s", Joiner.on("', '").join(difference))) .build(); @@ -220,7 +359,7 @@ public Description matchClass(ClassTree tree, VisitorState state) { if (!immutableAndContainer.isEmpty()) { return buildDescription(tree) .setMessage( - String.format( + format( "using both @ImmutableTypeParameter and containerOf is redundant: %s", Joiner.on("', '").join(immutableAndContainer))) .build(); @@ -231,12 +370,12 @@ public Description matchClass(ClassTree tree, VisitorState state) { // Check that the fields (including inherited fields) are immutable, and // validate the type hierarchy superclass. - ClassSymbol sym = ASTHelpers.getSymbol(tree); + ClassSymbol sym = getSymbol(tree); Violation info = analysis.checkForImmutability( Optional.of(tree), - immutableTypeParametersInScope(ASTHelpers.getSymbol(tree), state, analysis), + immutableTypeParametersInScope(getSymbol(tree), state, analysis), ASTHelpers.getType(tree), (Tree matched, Violation violation) -> describeClass(matched, sym, annotation, violation)); @@ -255,7 +394,7 @@ private void checkClassTreeInstantiation( tree, state, analysis, - ASTHelpers.getSymbol(implementTree).getTypeParameters(), + getSymbol(implementTree).getTypeParameters(), ASTHelpers.getType(implementTree).getTypeArguments()); } @@ -265,7 +404,7 @@ private void checkClassTreeInstantiation( tree, state, analysis, - ASTHelpers.getSymbol(extendsClause).getTypeParameters(), + getSymbol(extendsClause).getTypeParameters(), ASTHelpers.getType(extendsClause).getTypeArguments()); } } @@ -277,7 +416,7 @@ private Description.Builder describeClass( message = "type annotated with @Immutable could not be proven immutable: " + info.message(); } else { message = - String.format( + format( "Class extends @Immutable type %s, but is not immutable: %s", annotation.typeName(), info.message()); } @@ -289,7 +428,7 @@ private Description.Builder describeClass( /** Check anonymous implementations of {@code @Immutable} types. */ private Description handleAnonymousClass( ClassTree tree, VisitorState state, ImmutableAnalysis analysis) { - ClassSymbol sym = ASTHelpers.getSymbol(tree); + ClassSymbol sym = getSymbol(tree); Type superType = immutableSupertype(sym, state); if (superType == null) { return NO_MATCH; @@ -324,7 +463,7 @@ public Description.Builder describe(Tree tree, Violation info) { private Description.Builder describeAnonymous(Tree tree, Type superType, Violation info) { String message = - String.format( + format( "Class extends @Immutable type %s, but is not immutable: %s", superType, info.message()); return buildDescription(tree).setMessage(message); @@ -334,14 +473,13 @@ private Description.Builder describeAnonymous(Tree tree, Type superType, Violati /** Check for classes without {@code @Immutable} that have immutable supertypes. */ private Description checkSubtype(ClassTree tree, VisitorState state) { - ClassSymbol sym = ASTHelpers.getSymbol(tree); + ClassSymbol sym = getSymbol(tree); Type superType = immutableSupertype(sym, state); if (superType == null) { return NO_MATCH; } String message = - String.format( - "Class extends @Immutable type %s, but is not annotated as immutable", superType); + format("Class extends @Immutable type %s, but is not annotated as immutable", superType); Fix fix = SuggestedFix.builder() .prefixWith(tree, "@Immutable ") @@ -361,8 +499,7 @@ private Type immutableSupertype(Symbol sym, VisitorState state) { } // Don't use getImmutableAnnotation here: subtypes of trusted types are // also trusted, only check for explicitly annotated supertypes. - if (immutableAnnotations.stream() - .anyMatch(annotation -> ASTHelpers.hasAnnotation(superType.tsym, annotation, state))) { + if (hasImmutableAnnotation(superType.tsym, state)) { return superType; } // We currently trust that @interface annotations are immutable, but don't enforce that 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 45d260cc62e..8f41a09b039 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -143,7 +143,6 @@ import com.google.errorprone.bugpatterns.GetClassOnAnnotation; import com.google.errorprone.bugpatterns.GetClassOnClass; import com.google.errorprone.bugpatterns.GetClassOnEnum; -import com.google.errorprone.bugpatterns.HashCodeToString; import com.google.errorprone.bugpatterns.HashtableContains; import com.google.errorprone.bugpatterns.HidingField; import com.google.errorprone.bugpatterns.IdentityBinaryExpression; @@ -233,6 +232,7 @@ import com.google.errorprone.bugpatterns.MustBeClosedChecker; import com.google.errorprone.bugpatterns.MutablePublicArray; import com.google.errorprone.bugpatterns.NCopiesOfChar; +import com.google.errorprone.bugpatterns.NarrowCalculation; import com.google.errorprone.bugpatterns.NarrowingCompoundAssignment; import com.google.errorprone.bugpatterns.NegativeCharLiteral; import com.google.errorprone.bugpatterns.NestedInstanceOfConditions; @@ -284,7 +284,6 @@ import com.google.errorprone.bugpatterns.ProtocolBufferOrdinal; import com.google.errorprone.bugpatterns.ProtosAsKeyOfSetOrMap; import com.google.errorprone.bugpatterns.PublicApiNamedStreamShouldReturnStream; -import com.google.errorprone.bugpatterns.PublicConstructorForAbstractClass; import com.google.errorprone.bugpatterns.RandomCast; import com.google.errorprone.bugpatterns.RandomModInteger; import com.google.errorprone.bugpatterns.ReachabilityFenceUsage; @@ -901,6 +900,7 @@ public static ScannerSupplier errorChecks() { MultipleParallelOrSequentialCalls.class, MultipleUnaryOperatorsInMethodCall.class, MutablePublicArray.class, + NarrowCalculation.class, NarrowingCompoundAssignment.class, NegativeCharLiteral.class, NestedInstanceOfConditions.class, @@ -1037,7 +1037,6 @@ public static ScannerSupplier errorChecks() { FunctionalInterfaceClash.class, FuzzyEqualsShouldNotBeUsedInEqualsMethod.class, HardCodedSdCardPath.class, - HashCodeToString.class, ImmutableMemberCollection.class, ImmutableRefactoring.class, ImmutableSetForContains.class, @@ -1073,7 +1072,6 @@ public static ScannerSupplier errorChecks() { PrivateConstructorForUtilityClass.class, ProtosAsKeyOfSetOrMap.class, PublicApiNamedStreamShouldReturnStream.class, - PublicConstructorForAbstractClass.class, QualifierWithTypeUse.class, RedundantOverride.class, RedundantThrows.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/AlreadyCheckedTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/AlreadyCheckedTest.java index 3cd8203fb6f..d0acacfe3f5 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/AlreadyCheckedTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/AlreadyCheckedTest.java @@ -378,6 +378,27 @@ public void durationsComparedUsingFactoryMethods() { .doTest(); } + @Test + public void durationsComparedUsingFactoryMethods_withDifferentImport() { + helper + .addSourceLines( + "Test.java", + "import static java.time.Duration.ofSeconds;", + "import java.time.Duration;", + "class Test {", + " public void test(Duration a, Duration b) {", + " if (a.equals(Duration.ofSeconds(1))) {", + " if (a.equals(Duration.ofSeconds(2))) {}", + " // BUG: Diagnostic contains:", + " if (a.equals(ofSeconds(1))) {}", + " // BUG: Diagnostic contains:", + " if (a.equals(java.time.Duration.ofSeconds(1))) {}", + " }", + " }", + "}") + .doTest(); + } + @Test public void autoValues() { helper 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 6d6d8b5cf60..65f5f6cf2b6 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java @@ -16,6 +16,9 @@ package com.google.errorprone.bugpatterns; +import com.google.auto.value.processor.AutoBuilderProcessor; +import com.google.auto.value.processor.AutoValueProcessor; +import com.google.common.collect.ImmutableList; import com.google.errorprone.CompilationTestHelper; import org.junit.Test; import org.junit.runner.RunWith; @@ -705,6 +708,89 @@ public void constructor_superCall() { .doTest(); } + @Test + public void constructor_anonymousClassInheritsCIRV() { + compilationHelperLookingAtAllConstructors() + .addSourceLines( + "Test.java", + "class Test {", + " @com.google.errorprone.annotations.CanIgnoreReturnValue", + " public Test() {}", + " public static void foo() {", + " new Test() {};", + " new Test() {{ System.out.println(\"Lookie, instance initializer\"); }};", + " }", + "}") + .doTest(); + } + + @Test + public void constructor_anonymousClassInheritsCRV() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " @com.google.errorprone.annotations.CheckReturnValue", + " public Test() {}", + " public static void foo() {", + " // BUG: Diagnostic contains: ", + " new Test() {};", + " }", + "}") + .doTest(); + } + + @Test + public void constructor_hasOuterInstance() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " class Inner {", + " @com.google.errorprone.annotations.CheckReturnValue", + " public Inner() {}", + " }", + " public static void foo() {", + " // BUG: Diagnostic contains: ", + " new Test().new Inner() {};", + " }", + "}") + .doTest(); + } + + @Test + public void constructor_anonymousClassInheritsCRV_syntheticConstructor() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " @com.google.errorprone.annotations.CheckReturnValue", + " static class Nested {}", + " public static void foo() {", + " // BUG: Diagnostic contains: ", + " new Nested() {};", // The "called" constructor is synthetic, but within @CRV Nested + " }", + "}") + .doTest(); + } + + @Test + public void constructor_inheritsFromCrvInterface() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " @com.google.errorprone.annotations.CheckReturnValue", + " static interface IFace {}", + " public static void foo() {", + // TODO(b/226203690): It's arguable that this might need to be @CRV? + // The superclass of the anonymous class is Object, not IFace, but /shrug + " new IFace() {};", + " }", + "}") + .doTest(); + } + @Test public void constructor_throwingContexts() { compilationHelper @@ -789,6 +875,130 @@ public void usingElementInTestExpected() { .doTest(); } + @Test + public void testAutoValueBuilderSetterMethods() { + compilationHelper + .addSourceLines( + "Animal.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoValue;", + "import com.google.errorprone.annotations.CheckReturnValue;", + "@AutoValue", + "@CheckReturnValue", + "abstract class Animal {", + " abstract String name();", + " abstract int numberOfLegs();", + " static Builder builder() {", + " return new AutoValue_Animal.Builder();", + " }", + " @AutoValue.Builder", + " abstract static class Builder {", + " abstract Builder setName(String value);", + " abstract Builder setNumberOfLegs(int value);", + " abstract Animal build();", + " }", + "}") + .addSourceLines( + "AnimalCaller.java", + "package com.google.frobber;", + "public final class AnimalCaller {", + " 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'", + " builder.build();", + " }", + "}") + .setArgs(ImmutableList.of("-processor", AutoValueProcessor.class.getName())) + .doTest(); + } + + @Test + public void testAutoBuilderSetterMethods() { + compilationHelper + .addSourceLines( + "Person.java", + "package com.google.frobber;", + "public final class Person {", + " public Person(String name, int id) {}", + "}") + .addSourceLines( + "PersonBuilder.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoBuilder;", + "import com.google.errorprone.annotations.CheckReturnValue;", + "@CheckReturnValue", + "@AutoBuilder(ofClass = Person.class)", + "abstract class PersonBuilder {", + " static PersonBuilder personBuilder() {", + " return new AutoBuilder_PersonBuilder();", + " }", + " abstract PersonBuilder setName(String name);", + " abstract PersonBuilder setId(int id);", + " abstract Person build();", + "}") + .addSourceLines( + "PersonCaller.java", + "package com.google.frobber;", + "public final class PersonCaller {", + " static void testPersonBuilder() {", + " // BUG: Diagnostic contains: Ignored return value of 'personBuilder'", + " 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'", + " builder.build();", + " }", + "}") + .setArgs(ImmutableList.of("-processor", AutoBuilderProcessor.class.getName())) + .doTest(); + } + + @Test + public void testAutoBuilderSetterMethods_withInterface() { + compilationHelper + .addSourceLines( + "LogUtil.java", + "package com.google.frobber;", + "import java.util.logging.Level;", + "public class LogUtil {", + " public static void log(Level severity, String message) {}", + "}") + .addSourceLines( + "Caller.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoBuilder;", + "import java.util.logging.Level;", + "import com.google.errorprone.annotations.CheckReturnValue;", + "@CheckReturnValue", + "@AutoBuilder(callMethod = \"log\", ofClass = LogUtil.class)", + "public interface Caller {", + " static Caller logCaller() {", + " return new AutoBuilder_Caller();", + " }", + " Caller setSeverity(Level level);", + " Caller setMessage(String message);", + " void call(); // calls: LogUtil.log(severity, message)", + "}") + .addSourceLines( + "LogCaller.java", + "package com.google.frobber;", + "import java.util.logging.Level;", + "public final class LogCaller {", + " static void testLogCaller() {", + " // BUG: Diagnostic contains: Ignored return value of 'logCaller'", + " Caller.logCaller();", + " Caller caller = Caller.logCaller();", + " caller.setMessage(\"hi\");", // AutoBuilder setters are implicitly @CIRV + " caller.setSeverity(Level.FINE);", // AutoBuilder setters are implicitly @CIRV + " caller.call();", + " }", + "}") + .setArgs(ImmutableList.of("-processor", AutoBuilderProcessor.class.getName())) + .doTest(); + } + private CompilationTestHelper compilationHelperLookingAtAllConstructors() { return compilationHelper.setArgs( "-XepOpt:" + CheckReturnValue.CHECK_ALL_CONSTRUCTORS + "=true"); diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/HashCodeToStringTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/HashCodeToStringTest.java deleted file mode 100644 index ce99ea14100..00000000000 --- a/core/src/test/java/com/google/errorprone/bugpatterns/HashCodeToStringTest.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2020 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; - -/** {@link HashCodeToString}Test */ -@RunWith(JUnit4.class) -public class HashCodeToStringTest { - - private final CompilationTestHelper compilationHelper = - CompilationTestHelper.newInstance(HashCodeToString.class, getClass()); - - @Test - public void testPositiveCase() { - compilationHelper - .addSourceLines( - "HashCodeOnly.java", - "public class HashCodeOnly {", - " // BUG: Diagnostic contains: HashCodeToString", - " public int hashCode() {", - " return 0;", - " }", - "}") - .doTest(); - } - - @Test - public void negative_bothHashCodeAndToString() { - compilationHelper - .addSourceLines( - "HashCodeAndToString.java", - "public class HashCodeAndToString {", - " public int hashCode() {", - " return 0;", - " }", - " public String toString() {", - " return \"42\";", - " }", - "}") - .doTest(); - } - - @Test - public void negative_toStringOnly() { - compilationHelper - .addSourceLines( - "ToStringOnly.java", - "public class ToStringOnly {", - " public String toString() {", - " return \"42\";", - " }", - "}") - .doTest(); - } - - @Test - public void negative_neitherHashCodeAndToString() { - compilationHelper - .addSourceLines( - "NeitherHashCodeAndToString.java", // - "public class NeitherHashCodeAndToString {", - "}") - .doTest(); - } - - @Test - public void superClassWithoutToString() { - compilationHelper - .addSourceLines("Super.java", "abstract class Super {}") - .addSourceLines( - "Test.java", - "class Test extends Super {", - " // BUG: Diagnostic contains: HashCodeToString", - " public int hashCode() { return 0; }", - "}") - .doTest(); - } - - @Test - public void inherited() { - compilationHelper - .addSourceLines( - "Super.java", - "class Super {", - " public String toString() {", - " return \"42\";", - " }", - "}") - .addSourceLines( - "Test.java", // - "class Test extends Super {", - " public int hashCode() { return 0; }", - "}") - .doTest(); - } - - @Test - public void interfaceHashCode() { - compilationHelper - .addSourceLines( - "I.java", // - "interface I {", - " int hashCode();", - "}") - .doTest(); - } - - @Test - public void abstractToString() { - compilationHelper - .addSourceLines( - "Super.java", - "abstract class Super {", - " public abstract int hashCode();", - " public abstract String toString();", - "}") - .doTest(); - } - - @Test - public void abstractNoToString() { - compilationHelper - .addSourceLines( - "Super.java", - "abstract class Super {", - " // BUG: Diagnostic contains:", - " public abstract int hashCode();", - "}") - .doTest(); - } - - @Test - public void suppressOnHashCode() { - compilationHelper - .addSourceLines( - "Test.java", - "class Test {", - " @SuppressWarnings(\"HashCodeToString\")", - " public int hashCode() { return 0; }", - "}") - .doTest(); - } - - @Test - public void nopHashCode() { - compilationHelper - .addSourceLines( - "Test.java", - "class Test {", - " public int hashCode() {", - " return super.hashCode();", - " }", - "}") - .doTest(); - } - - @Test - public void dontFlagAutoValue() { - compilationHelper - .addSourceLines( - "Test.java", - "import com.google.auto.value.AutoValue;", - "@AutoValue", - "abstract class Test {", - " @Override", - " public int hashCode() {", - " return 1;", - " }", - "}") - .doTest(); - } -} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/IgnoredPureGetterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/IgnoredPureGetterTest.java index 0b8021114d1..20eaaf2e943 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/IgnoredPureGetterTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/IgnoredPureGetterTest.java @@ -142,6 +142,94 @@ public void autoValue_secondFix() { .doTest(); } + @Test + public void autoBuilder_getters() { + helper + .addSourceLines( + "Named.java", + "import com.google.auto.value.AutoBuilder;", + "import java.util.Optional;", + "public class Named {", + " Named(String name, String nickname) {}", + " @AutoBuilder", + " public abstract static class Builder {", + " public abstract Builder setName(String x);", + " public abstract Builder setNickname(String x);", + " abstract String getName();", + " abstract Optional getNickname();", + " abstract Named autoBuild();", + " public Named build() {", + " if (!getNickname().isPresent()) {", + " setNickname(getName());", + " }", + " return autoBuild();", + " }", + " }", + "}") + .addSourceLines( + "B.java", + "class B {", + " void test(Named.Builder builder) {", + // The setters are OK + " builder.setName(\"Stumpy\");", + " builder.setNickname(\"Stumps\");", + " // BUG: Diagnostic contains: IgnoredPureGetter", + " builder.getName();", + " // BUG: Diagnostic contains: IgnoredPureGetter", + " builder.getNickname();", + " // BUG: Diagnostic contains: IgnoredPureGetter", + " builder.autoBuild();", + // build() isn't covered since it's non-abstract + " builder.build();", + " }", + "}") + .doTest(); + } + + @Test + public void autoBuilder_buildIsntCrv() { + helper + .addSourceLines( + "Named.java", + "import com.google.auto.value.AutoBuilder;", + "import java.time.LocalTime;", + "@AutoBuilder(callMethod = \"of\", ofClass = LocalTime.class)", + "interface LocalTimeBuilder {", + " LocalTimeBuilder setHour(int hour);", + " LocalTimeBuilder setMinute(int minute);", + " LocalTimeBuilder setSecond(int second);", + " LocalTimeBuilder setNanoOfSecond(int nanoOfSecond);", + " int getHour();", + " int getMinute();", + " int getSecond();", + " int getNanoOfSecond();", + " LocalTime build();", // calls: LocalTime.of(...) + "}") + .addSourceLines( + "B.java", + "class B {", + " void test(LocalTimeBuilder builder) {", + // the setters are treated as CIRV: + " builder.setHour(12);", + " builder.setMinute(12);", + " builder.setSecond(12);", + // the getters are treated as CRV: + " // BUG: Diagnostic contains: IgnoredPureGetter", + " builder.getHour();", + " // BUG: Diagnostic contains: IgnoredPureGetter", + " builder.getMinute();", + " // BUG: Diagnostic contains: IgnoredPureGetter", + " builder.getSecond();", + " // BUG: Diagnostic contains: IgnoredPureGetter", + " builder.getNanoOfSecond();", + // TODO(b/229107551): this shouldn't be treated as CRV + " // BUG: Diagnostic contains: IgnoredPureGetter", + " builder.build();", + " }", + "}") + .doTest(); + } + @Test public void refactoringHelper() { refactoringHelper diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/NarrowCalculationTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/NarrowCalculationTest.java new file mode 100644 index 00000000000..ad397944780 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/NarrowCalculationTest.java @@ -0,0 +1,136 @@ +/* + * 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 NarrowCalculation}. */ +@RunWith(JUnit4.class) +public final class NarrowCalculationTest { + private final CompilationTestHelper helper = + CompilationTestHelper.newInstance(NarrowCalculation.class, getClass()); + + private final BugCheckerRefactoringTestHelper refactoring = + BugCheckerRefactoringTestHelper.newInstance(NarrowCalculation.class, getClass()); + + @Test + public void integerDivision() { + helper + .addSourceLines( + "Test.java", // + "class Test {", + " // BUG: Diagnostic contains:", + " final float a = 1 / 2;", + "}") + .doTest(); + } + + @Test + public void integerDivision_actuallyInteger() { + helper + .addSourceLines( + "Test.java", // + "class Test {", + " final float a = 8 / 2;", + "}") + .doTest(); + } + + @Test + public void integerDivision_fix() { + refactoring + .addInputLines( + "Test.java", // + "class Test {", + " final float a = 1 / 2;", + "}") + .addOutputLines( + "Test.java", // + "class Test {", + " final float a = 1 / 2f;", + "}") + .doTest(); + } + + @Test + public void longDivision_fix() { + refactoring + .addInputLines( + "Test.java", // + "class Test {", + " final double a = 1 / 2L;", + "}") + .addOutputLines( + "Test.java", // + "class Test {", + " final double a = 1 / 2.0;", + "}") + .doTest(); + } + + @Test + public void targetTypeInteger_noFinding() { + helper + .addSourceLines( + "Test.java", // + "class Test {", + " final int a = 1 / 2;", + "}") + .doTest(); + } + + @Test + public void multiplication_doesNotOverflow() { + helper + .addSourceLines( + "Test.java", // + "class Test {", + " final long a = 2 * 100;", + "}") + .doTest(); + } + + @Test + public void multiplication_wouldOverflow() { + helper + .addSourceLines( + "Test.java", // + "class Test {", + " // BUG: Diagnostic contains:", + " final long a = 1_000_000_000 * 1_000_000_000;", + "}") + .doTest(); + } + + @Test + public void multiplication_couldOverflow() { + helper + .addSourceLines( + "Test.java", // + "class Test {", + " void t(int a) {", + " // BUG: Diagnostic contains: 2L * a", + " long b = 2 * a;", + " }", + "}") + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/PublicConstructorForAbstractClassTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/PublicConstructorForAbstractClassTest.java deleted file mode 100644 index a5a66160f14..00000000000 --- a/core/src/test/java/com/google/errorprone/bugpatterns/PublicConstructorForAbstractClassTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2020 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 java.io.IOException; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** {@link PublicConstructorForAbstractClass}Test */ -@RunWith(JUnit4.class) -public class PublicConstructorForAbstractClassTest { - - @Test - public void basicRefactoringTest() throws IOException { - BugCheckerRefactoringTestHelper testHelper = - BugCheckerRefactoringTestHelper.newInstance( - PublicConstructorForAbstractClass.class, getClass()); - testHelper - .addInputLines( - "in/Test.java", // - "public abstract class Test {", - " public Test() {", - " }", - "}") - .addOutputLines( - "out/Test.java", // - "public abstract class Test {", - " protected Test() {", - " }", - "}") - .doTest(); - } - - @Test - public void basicUsageTest() { - CompilationTestHelper testHelper = - CompilationTestHelper.newInstance(PublicConstructorForAbstractClass.class, getClass()); - testHelper - .addSourceLines( - "in/Test.java", - "public abstract class Test {", - " // BUG: Diagnostic contains:", - " public Test() {", - " }", - "}") - .doTest(); - } - - @Test - public void basicNegativeTest() { - CompilationTestHelper testHelper = - CompilationTestHelper.newInstance(PublicConstructorForAbstractClass.class, getClass()); - testHelper - .addSourceLines( - "in/Test.java", // - "public class Test {", - " public Test() {", - " }", - "}") - .doTest(); - } -} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryBoxedVariableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryBoxedVariableTest.java index e7dddff7a49..26629b25957 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryBoxedVariableTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryBoxedVariableTest.java @@ -17,6 +17,7 @@ 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; @@ -28,6 +29,8 @@ public class UnnecessaryBoxedVariableTest { private final BugCheckerRefactoringTestHelper helper = BugCheckerRefactoringTestHelper.newInstance(UnnecessaryBoxedVariable.class, getClass()); + private final CompilationTestHelper compilationTestHelper = + CompilationTestHelper.newInstance(UnnecessaryBoxedVariable.class, getClass()); @Test public void testCases() { @@ -36,4 +39,34 @@ public void testCases() { .addOutput("testdata/UnnecessaryBoxedVariableCases_expected.java") .doTest(); } + + @Test + public void suppression() { + helper + .addInputLines( + "Test.java", + "class Test {", + " @SuppressWarnings(\"UnnecessaryBoxedVariable\")", + " private int a(Integer o) {", + " return o;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void lambdas() { + compilationTestHelper + .addSourceLines( + "Test.java", + "class Test {", + " interface Boxed { void a(O b); }", + " void boxed(Boxed b) {}", + " private void test() {", + " boxed((Double a) -> { double b = a + 1; });", + " }", + "}") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryLambdaTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryLambdaTest.java index 6279b0e2197..ad7253ecfe7 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryLambdaTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryLambdaTest.java @@ -60,6 +60,42 @@ public void method() { .doTest(); } + @Test + public void method_effectivelyPrivate() { + testHelper + .addInputLines( + "Test.java", + "import java.util.function.Function;", + "class Test {", + " private class Inner {", + " Function f() {", + " return x -> {", + " return \"hello \" + x;", + " };", + " }", + " void g() {", + " Function f = f();", + " System.err.println(f().apply(\"world\"));", + " }", + " }", + "}") + .addOutputLines( + "Test.java", + "import java.util.function.Function;", + "class Test {", + " private class Inner {", + " String f(String x) {", + " return \"hello \" + x;", + " }", + " void g() {", + " Function f = this::f;", + " System.err.println(f(\"world\"));", + " }", + " }", + "}") + .doTest(); + } + @Test public void method_static() { testHelper 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 1f5f95f662b..e51e83c9e32 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 @@ -77,22 +77,7 @@ public void testBeforeAndAfterRule() { } public void constructor() { - /* - * We may or may not want to treat this as a bug. On the one hand, the - * subclass might be "using" the superclass, so it might not be being - * "ignored." (Plus, it would be a pain to produce a valid suggested fix - * that incorporates any subclass constructor body, which might even contain - * calls to methods in the class.) On the other hand, the more likely - * scenario may be a class like IteratorTester, which requires (a) that the - * user subclass it to implement a method and (b) that the user call test() - * on the constructed object. There, it would be nice if IteratorTester - * could be annotated with @CheckReturnValue to mean "anyone who creates an - * anonymous subclasses of this should still do something with that - * subclass." But perhaps that's an abuse of @CheckReturnValue. - * - * Anyway, these tests are here to ensure that subclasses don't don't crash - * the compiler. - */ + // BUG: Diagnostic contains: Ignored return value new MyObject() {}; class MySubObject1 extends MyObject {} 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 83f3274581b..c09afccb0c7 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 @@ -2486,4 +2486,159 @@ private CompilationTestHelper withImmutableTypeParameterGeneric() { "import com.google.errorprone.annotations.ImmutableTypeParameter;", "class GenericWithImmutableParam<@ImmutableTypeParameter T> {}"); } + + @Test + public void lambda_cannotCloseAroundMutableField() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.ArrayList;", + "import java.util.List;", + "class Test {", + " @Immutable interface ImmutableFunction { A apply(B b); }", + " private int a = 0;", + " void test(ImmutableFunction f) {", + " // BUG: Diagnostic contains:", + " test(x -> ++a);", + " }", + "}") + .doTest(); + } + + @Test + public void lambda_canCloseAroundImmutableField() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.ArrayList;", + "import java.util.List;", + "class Test {", + " @Immutable interface ImmutableFunction { A apply(B b); }", + " private final int b = 1;", + " void test(ImmutableFunction f) {", + " test(x -> b);", + " test(x -> this.b);", + " }", + "}") + .doTest(); + } + + @Test + public void lambda_cannotCloseAroundMutableLocal() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.List;", + "import java.util.ArrayList;", + "class Test {", + " @Immutable interface ImmutableFunction { A apply(B b); }", + " void test(ImmutableFunction f) {", + " List xs = new ArrayList<>();", + " // BUG: Diagnostic contains:", + " test(x -> xs.get(x));", + " }", + "}") + .doTest(); + } + + @Test + public void notImmutableAnnotatedLambda_noFinding() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.ArrayList;", + "import java.util.List;", + "import java.util.function.Function;", + "class Test {", + " void test(Function f) {", + " List xs = new ArrayList<>();", + " test(x -> xs.get(x));", + " }", + "}") + .doTest(); + } + + @Test + public void lambda_canHaveMutableVariablesWithin() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.ArrayList;", + "import java.util.List;", + "class Test {", + " @Immutable interface ImmutableFunction { A apply(B b); }", + " void test(ImmutableFunction f) {", + " test(x -> { List xs = new ArrayList<>(); return xs.get(x); });", + " }", + "}") + .doTest(); + } + + @Test + public void lambda_cannotCallMethodOnMutableClass() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "abstract class Test {", + " @Immutable interface ImmutableFunction { A apply(B b); }", + " abstract int mutable(int a);", + " void test(ImmutableFunction f) {", + " // BUG: Diagnostic contains: This lambda implements @Immutable interface" + + " 'ImmutableFunction', but accesses instance method(s) 'mutable' on 'Test' which" + + " is not @Immutable", + " test(x -> mutable(x));", + " // BUG: Diagnostic contains: This lambda implements @Immutable interface" + + " 'ImmutableFunction', but 'Test' has field 'this' of type 'Test', the" + + " declaration of type 'Test' is not annotated with" + + " @com.google.errorprone.annotations.Immutable", + " test(x -> this.mutable(x));", + " }", + "}") + .doTest(); + } + + @Test + public void lambda_canCallMethodOnImmutableClass() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "@Immutable", + "abstract class Test {", + " @Immutable interface ImmutableFunction { A apply(B b); }", + " abstract int mutable(int a);", + " void test(ImmutableFunction f) {", + " test(x -> mutable(x));", + " test(x -> this.mutable(x));", + " }", + "}") + .doTest(); + } + + @Test + public void subclassesOfMutableType() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.function.Function;", + "@Immutable", + "abstract class Test {", + " @Immutable interface ImmutableFunction extends Function {", + " default ImmutableFunction andThen(ImmutableFunction fn) {", + // TODO(ghm): this one is sad, we're really accessing an immutable class's method here, + // but the owner of the method is not @Immutable. Look for a better heuristic to find + // the receiver type. + " // BUG: Diagnostic contains:", + " return x -> fn.apply(apply(x));", + " }", + " }", + "}") + .doTest(); + } } diff --git a/docgen/pom.xml b/docgen/pom.xml index a57b821986f..1a220ab90cc 100644 --- a/docgen/pom.xml +++ b/docgen/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.13.0 Documentation tool for generating Error Prone bugpattern documentation diff --git a/docgen_processor/pom.xml b/docgen_processor/pom.xml index 42915547130..601dda4395f 100644 --- a/docgen_processor/pom.xml +++ b/docgen_processor/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.13.0 JSR-269 annotation processor for @BugPattern annotation diff --git a/docs/bugpattern/HashCodeToString.md b/docs/bugpattern/HashCodeToString.md deleted file mode 100644 index 63ea5201c6d..00000000000 --- a/docs/bugpattern/HashCodeToString.md +++ /dev/null @@ -1,11 +0,0 @@ -Classes that override `hashCode()` should consider also overriding `toString()` -with details to aid debugging and diagnostics, instead of relying on the default -`Object.toString()` implementation. - -`Object.toString()` returns a string consisting of the class' name and the -instances' hash code. When `hashCode()` is overridden this can be misleading, as -users typically expect this default `toString()` to be (semi)unique -per-instance, especially when debugging. - -See also -[`MoreObjects.toStringHelper()`](https://guava.dev/releases/snapshot/api/docs/com/google/common/base/MoreObjects.html#toStringHelper-java.lang.Object-) diff --git a/docs/bugpattern/NarrowCalculation.md b/docs/bugpattern/NarrowCalculation.md new file mode 100644 index 00000000000..0d98656be2a --- /dev/null +++ b/docs/bugpattern/NarrowCalculation.md @@ -0,0 +1,40 @@ +Integer division is suspicious when the target type of the expression is a +float. For example: + +```java +private static final double ONE_HALF = 1 / 2; // Actually 0. +``` + +If you specifically want the integer result, consider pulling out variable to +make it clear what's happening. For example, instead of: + +```java +// Deduct 10% from the grade for every week the assignment is late: +float adjustedGrade = grade - (days / 7) * .1; +``` + +Prefer: + +```java +// Deduct 10% from the grade for every week the assignment is late: +int fullWeeks = days / 7; +float adjustedGrade = grade - fullWeeks * .1; +``` + +Similarly, multiplication of two `int` values which are then cast to a `long` is +problematic, as the `int` multiplication could overflow. It's better to perform +the multiplication using `long` arithmetic. + +```java +long secondsToNanos(int seconds) { + return seconds * 1_000_000_000; // Oops; starts overflowing around 2.15 seconds. +} +``` + +Instead, prefer: + +```java +long secondsToNanos(int seconds) { + return seconds * 1_000_000_000L; // Or ((long) seconds) * 1_000_000_000. +} +``` diff --git a/docs/bugpattern/StaticAssignmentOfThrowable.md b/docs/bugpattern/StaticAssignmentOfThrowable.md index d25bd9e337d..f1894d2cdf8 100644 --- a/docs/bugpattern/StaticAssignmentOfThrowable.md +++ b/docs/bugpattern/StaticAssignmentOfThrowable.md @@ -6,7 +6,7 @@ necessary: it often isn't. Could a Throwable simply be instantiated when needed? ``` {.bad} // this always has the same stack trace -static final MY_EXCEPTION = new MyException("something terrible has happened!"); +static final MyException MY_EXCEPTION = new MyException("something terrible has happened!"); ``` ``` {.good} diff --git a/pom.xml b/pom.xml index f2161ec1eba..62734880590 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ Error Prone parent POM com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.13.0 pom Error Prone is a static analysis tool for Java that catches common programming mistakes at compile-time. @@ -248,7 +248,6 @@ ossrh https://oss.sonatype.org/ - true diff --git a/refaster/pom.xml b/refaster/pom.xml index d7951f0e492..ac35a4d8322 100644 --- a/refaster/pom.xml +++ b/refaster/pom.xml @@ -19,7 +19,7 @@ error_prone_parent com.google.errorprone - HEAD-SNAPSHOT + 2.13.0 4.0.0 diff --git a/test_helpers/pom.xml b/test_helpers/pom.xml index 6d798ac870c..883d2ac323f 100644 --- a/test_helpers/pom.xml +++ b/test_helpers/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.13.0 error-prone test helpers diff --git a/test_helpers/src/test/java/com/google/errorprone/CompilationTestHelperTest.java b/test_helpers/src/test/java/com/google/errorprone/CompilationTestHelperTest.java index bd7bdf04bde..0fede355ec4 100644 --- a/test_helpers/src/test/java/com/google/errorprone/CompilationTestHelperTest.java +++ b/test_helpers/src/test/java/com/google/errorprone/CompilationTestHelperTest.java @@ -549,4 +549,26 @@ public void onlyCallDoTestOnce() { assertThrows(IllegalStateException.class, () -> compilationHelper.doTest()); assertThat(expected).hasMessageThat().contains("doTest"); } + + @Test + public void assertionErrors_causeTestFailures() { + var compilationTestHelper = + CompilationTestHelper.newInstance(AssertionFailingChecker.class, getClass()) + .addSourceLines("test/Test.java", "package test;", "public class Test {}"); + AssertionError expected = + assertThrows(AssertionError.class, () -> compilationTestHelper.doTest()); + assertThat(expected) + .hasMessageThat() + .contains("An unhandled exception was thrown by the Error Prone static analysis plugin"); + } + + /** A BugPattern that always throws. */ + @BugPattern(summary = "A really broken checker.", severity = ERROR) + public static class AssertionFailingChecker extends BugChecker + implements CompilationUnitTreeMatcher { + @Override + public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { + throw new AssertionError(); + } + } } diff --git a/type_annotations/pom.xml b/type_annotations/pom.xml index b2495a2167f..03cd271a6d8 100644 --- a/type_annotations/pom.xml +++ b/type_annotations/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.13.0 error-prone type annotations