diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4c9e8cc024..0d1c55b93f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,30 +8,37 @@ on: branches: - master +permissions: + contents: read # to fetch code (actions/checkout) + jobs: test: + permissions: + actions: write # to cancel/stop running workflows (styfle/cancel-workflow-action) + contents: read # to fetch code (actions/checkout) + name: "JDK ${{ matrix.java }} on ${{ matrix.os }}" strategy: fail-fast: false matrix: os: [ ubuntu-latest ] - java: [ 20, 17, 11 ] + java: [ 21, 17, 11 ] experimental: [ false ] # Only test on macos and windows with a single recent JDK to avoid a # combinatorial explosion of test configurations. # Most OS-specific issues are not specific to a particular JDK version. include: - os: macos-latest - java: 20 + java: 21 experimental: false - os: windows-latest - java: 20 + java: 21 experimental: false - os: ubuntu-latest - java: 20 + java: 21 experimental: false - os: ubuntu-latest - java: 21-ea + java: 22-ea experimental: true runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental }} @@ -90,6 +97,9 @@ jobs: run: mvn source:jar deploy -B -DskipTests=true -Dinvoker.skip=true generate_docs: + permissions: + contents: write # for git push + name: 'Generate latest docs' needs: test if: github.event_name == 'push' && github.repository == 'google/error-prone' && github.ref == 'refs/heads/master' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 29f88ea4199..5001c9d0057 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,13 +39,13 @@ jobs: 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 }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - run: - mvn --no-transfer-progress -P release clean deploy -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" + #- name: Deploy to Sonatype + # env: + # CI_DEPLOY_USERNAME: ${{ secrets.CI_DEPLOY_USERNAME }} + # CI_DEPLOY_PASSWORD: ${{ secrets.CI_DEPLOY_PASSWORD }} + # GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + # run: + # mvn --no-transfer-progress -P release clean deploy -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" - name: Push tag run: | diff --git a/KEYS.txt b/KEYS.txt new file mode 100644 index 00000000000..4071eaa6a03 --- /dev/null +++ b/KEYS.txt @@ -0,0 +1,65 @@ +This file contains the PGP and GPG keys used to sign releases of Error Prone. + +gpg --list-sigs && gpg --armor --export + +************************************************************************************************************ + +pub rsa4096 2022-01-28 [SC] + EE0C A873 0740 92F8 06F5 9B65 D364 ABAA 39A4 7320 +uid [ultimate] Liam Miller-Cushon (Error Prone releases) +sig 3 D364ABAA39A47320 2022-01-28 Liam Miller-Cushon (Error Prone releases) +sub rsa4096 2022-01-28 [E] +sig D364ABAA39A47320 2022-01-28 Liam Miller-Cushon (Error Prone releases) + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGH0NlsBEACnLJ3vl/aV+4ytkJ6QSfDFHrwzSo1eEXyuFZ85mLijvgGuaKRr +c9/lKed0MuyhLJ7YD752kcFCEIyPbjeqEFsBcgU/RWa1AEfaay4eMLBzLSOwCvhD +m+1zSFswH2bOqeLSbFZPQ9sVIOzO6AInaOTOoecHChHnUztAhRIOIUYmhABJGiu5 +jCP5SStoXm8YtRWT1unJcduHQ51EztQe02k+RTratQ31OSkeJORle7k7cudCS+yp +z5gTaS1Bx02v0Y8Qaw17vY9Pn8DmsECRvXL6K7ItX6zKkSdJYVGMtiF/kp4rg94I +XodrlzrMGPGPga9fTcqMPvx/3ffwgIsgtgaKg7te++L3db/xx48XgZ2qYAU8GssE +N14xRFQmr8sg+QiCIHL0Az88v9mILYOqgxa3RvQ79tTqAKwPg0o2w/wF/WU0Rw53 +mdNy9JTUjetWKuoTmDaXVZO4LQ2g4W2dQTbgHyomiIgV7BnLFUiqOLPo+imruSCs +W31Arjpb8q6XGTwjySa8waJxHhyV2AvEdAHUIdNuhD4dmPKXszlfFZwXbo1OOuIF +tUZ9lsOQiCpuO7IpIprLc8L9d1TRnCrfM8kxMbX4KVGajWL+c8FlLnUwR4gSxT1G +qIgZZ09wL5QiTeGF3biS5mxvn+gF9ns2Ahr2QmMqA2k5AMBTJimmY/OSWwARAQAB +tD1MaWFtIE1pbGxlci1DdXNob24gKEVycm9yIFByb25lIHJlbGVhc2VzKSA8Y3Vz +aG9uQGdvb2dsZS5jb20+iQJOBBMBCgA4FiEE7gyocwdAkvgG9Ztl02SrqjmkcyAF +AmH0NlsCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ02SrqjmkcyAtqxAA +i9e8YpWNNGiRGan+5luHPK7YiXhSoCnvaTK5/EhwQt1xqwWoHuHBTllpXyeKmUa/ +np5wK97i1gewadXFpcRuAyXLZnWN61yOdOiRfq9CoDefGSZOFgJ7/bB/RbZ4Moss +ZZihN4Vz7CWBTFaVNvq4KVg0QE5uXgcsEOZPmuyJaC8XHK37qMYwawxpkxC0jGJu +qp1nqkL+wEQBY3go9u2tzyQKX0fpF2g8puPZC92ezwf0p3ctEwhalptDICl74hDD +R9xAkPk6vimozLFxi/Ld0iDpEuouK91cuFh7nZYpjiJrgBcYKyvuEjtADefhJMWl +JRWZYmexynoLIas7mYkRGlnSQfkEFGy3dX7UU8TfRn5m1Bk7JTQHCauCNs+COdM0 +nVe8yUTsv+tZhU2yE9DIbeJP+ySUVZxpUNihhWuZFoSpqxWbsaX0XyDhZk1iPLU6 +H2RpUsWXYkMRN6eCs+8iNLBGccYuP8AI0/eMa5+JbsCF+/NToLpMiEqq3ZIeOR7i +KJY/iDkGnq+hK6eNjEv5/7lYgcW+WACqoiGURm/UKiOTeHyt0AvMXRpTGiVk7DU8 +WitWapGjayQdQEO8U8TSWlVktTdVGZQJCUiUjQT+gUlRaCybyDFIUkOStMPtLqe/ +GlSo8olccB7O1J5VOURi8/17iWUtzOgsp0ZzU5t76US5Ag0EYfQ2WwEQAL2jqb4P +Yu5saM4nEtAHUGd8E6QUdp2xuRvzZAV2sI2x3jh8mJ5qplU/7pccpVEdI+S3CkTU +WeNOEEkmwvDBy/BZfAPC8QPnufhsBM+Ws8a4bvH4tFVvEUFN34tBQJwd3em6u69s +SNB1XniZuB0yoCnl1IDgVzqHaExZUFfgR0uIf/S6LeVSiphMlwHvdTX+NspxuzT5 +xW5cimYA9CkizfSnTBYs4qImqf21NmnB+e2et18u8ovcVlxFB5ZmOofVjC3jNaUJ +GoYSnvJWqErmCfid8R1JfaSjGvnc46waTY+OHOz/lckuLUVM5yeNrmSSo4+I8YH8 +HECeM8ISxKI4WYXcB/hZ1hrf5Mrz6CAFIR4uOMtrnPKp7F+EAPCBvkBJmK1QSslk +OEC+ocbB/PdU8Q3LcraQksf6ZpbA5PVlGgmfPd/HAi6AqE/HUzOXCFNyUiScrurY +I1wHrWkL2WDvQgJbT3Q2CScTYO2aOtEw42FxKS5YYtkEoGBGo3AhMkVwB2Dr599n +MYuycR37oDb092xd8tL0omqJpu+mIGDxFoABaK/lOqw271hJZRBTMFk7je7wDFO1 +OG4dhmvFqygLewIYhxHLZX15qrjzQNEn26i040y60gdQNVJ2pWI+aaK3T4/JGIJf +8M54Ee6ZoQ6b5GojE8TyHpbywetgBDsnVrcBABEBAAGJAjYEGAEKACAWIQTuDKhz +B0CS+Ab1m2XTZKuqOaRzIAUCYfQ2WwIbDAAKCRDTZKuqOaRzICx6EACmzqP6qmPI +0ZR638HpnuclyhtLGcIg/9z65Q8PWgHpS6G3o1NhZB3CYSovkEbPxY1OwF3RxBi9 +LO2syLPm0IOiIYatZyBbGus3FzURXJ2EFtPg+mIboIFYUKgRc3vr9/Sd3FluOOhs +SVNdtDqhouHbzXY5q+Ax2IlRUGBeu1+CLn+Hj1alzmj8gMzdt+6N+ufme88iR3sR +74ZorZhIJPul8rg395bWDVK6ypFDEEoJTcLkxWBWOSkFrzZTSRPFQYMVhRRxL5iO +GL/Di7KfAhdbSlKXnC/FVXE0F8YUfl+SmuSly5Ven0HpJFUzzm5ShkkVogXKgCFT +25BB9V7q6DA/0FuHNHMOkl722I5xprPDM2c/lmPfWchXpSW1m7uZVKXUnAhxbFbd +vfqnge30bmMg9BzzFL6gx3/+nyvixgUHgo5hqzW6RE2IKyGf20l96iGQqP17DHJQ +1/WtLy45Qm9kLdzddEXwIldGnGYe7ak81/RDEVWGCEtjZwlTU5YaLo3Jk3rkR1+a +RNt4PF2VI2/z1JMPhWnoyYW2sDglkNJHQGnXMQ8qJLGOkbkWl9W/qVeYVuhrmqRc +Acb35txpFihe6f7AneKhaj5xAR3L9uxfTf4wcyyazyWKSZ88gZXDvdEXcdeMnwZW +pOSpujhmmBPD/tnf58BgT+/Gq6GemXYe8Q== +=BIwO +-----END PGP PUBLIC KEY BLOCK----- diff --git a/annotation/pom.xml b/annotation/pom.xml index 59f69a89d89..a0821195431 100644 --- a/annotation/pom.xml +++ b/annotation/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.22.0 @BugPattern annotation diff --git a/annotations/pom.xml b/annotations/pom.xml index 03c522c0391..b3c0062329a 100644 --- a/annotations/pom.xml +++ b/annotations/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.22.0 error-prone annotations diff --git a/annotations/src/main/java/com/google/errorprone/annotations/CompileTimeConstant.java b/annotations/src/main/java/com/google/errorprone/annotations/CompileTimeConstant.java index aae9ea47837..c1348a89e50 100644 --- a/annotations/src/main/java/com/google/errorprone/annotations/CompileTimeConstant.java +++ b/annotations/src/main/java/com/google/errorprone/annotations/CompileTimeConstant.java @@ -37,23 +37,28 @@ *
  • the expression consists of the literal {@code null}, or *
  • the expression consists of a single identifier, where the identifier is a formal method * parameter or class field that is declared {@code final} and has the {@link - * CompileTimeConstant} annotation. + * CompileTimeConstant} annotation, or + *
  • the expression is a {@link String}, and formed from the concatenation of symbols which meet + * these conditions, or + *
  • the expression is a ternary condition, where both branches satisfy these conditions, or + *
  • the expression is an immutable collection with all values known to satisfy these conditions + * (for example, {@code ImmutableSet.of("a", "b", "c")}). * * - *

    This constraint on call sites of methods or constructors that have one or more formal - * parameters with this annotation is enforced by error-prone. - * *

    For example, the following code snippet is legal: * *

    {@code
      * public class C {
      *   private static final String S = "Hello";
    - *   void m(@CompileTimeConstant final String s) { }
    + *   void m(@CompileTimeConstant final String s) {}
      *   void n(@CompileTimeConstant final String t) {
      *     m(S + " World!");
      *     m(null);
      *     m(t);
      *   }
    + *   void o(boolean enabled) {
    + *     m(enabled ? "on" : "off");
    + *   }
      * }
      * }
    * @@ -61,7 +66,7 @@ * *
    {@code
      * public class C {
    - *   void m(@CompileTimeConstant final String s) { }
    + *   void m(@CompileTimeConstant final String s) {}
      *   void n(String t) {
      *     m(t);
      *   }
    @@ -70,31 +75,19 @@
      *
      * 

    When a class field is annotated with the {@link CompileTimeConstant} type annotation, the * field must also be declared to be {@code final}, and the corresponding initialised value must be - * an expression that satisfies one of the following conditions: - * - *

      - *
    1. The expression is one for which the Java compiler can determine a constant value at compile - * time, or - *
    2. the expression consists of the literal {@code null}, or - *
    3. the expression consists of a single identifier, where the identifier is a formal method - * parameter or class field that is declared {@code final} and has the {@link - * CompileTimeConstant} annotation. - *
    - * - *

    This constraint on fields with this annotation is enforced by error-prone. + * an expression that satisfies the conditions above. * *

    For example, the following code snippet is legal: * *

    {@code
      * public class C {
    - *   \@CompileTimeConstant final String S;
    + *   @CompileTimeConstant final String s;
      *   public C(@CompileTimeConstant String s) {
    - *     this.S = s;
    + *     this.s = s;
      *   }
    - *   void m(@CompileTimeConstant final String s) { }
    + *   void m(@CompileTimeConstant final String s) {}
      *   void n() {
    - *     m(S);
    + *     m(s);
      *   }
      * }
      * }
    @@ -103,7 +96,7 @@ * *
    {@code
      * public class C {
    - *   \@CompileTimeConstant String S;
    + *   @CompileTimeConstant String S;
      *   public C(@CompileTimeConstant String s) {
      *     this.S = s;
      *   }
    @@ -116,7 +109,7 @@
      *
      * 
    {@code
      * public class C {
    - *   \@CompileTimeConstant final String S;
    + *   @CompileTimeConstant final String S;
      *   public C(String s) {
      *     this.S = s;
      *   }
    @@ -132,6 +125,8 @@
      * compile-time-constant values in a collection. APIs will typically accommodate such use cases via
      * domain-specific types that capture domain-specific aspects of trustworthiness that arise from
      * values being under application control.
    + *
    + * 

    These constraints are enforced by error-prone. */ @Documented @Retention(CLASS) diff --git a/check_api/pom.xml b/check_api/pom.xml index 3f28f8ce050..6ac8a8d9400 100644 --- a/check_api/pom.xml +++ b/check_api/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.22.0 error-prone check api @@ -59,7 +59,7 @@ io.github.java-diff-utils java-diff-utils - 4.0 + 4.12 diff --git a/check_api/src/main/java/com/google/errorprone/BaseErrorProneJavaCompiler.java b/check_api/src/main/java/com/google/errorprone/BaseErrorProneJavaCompiler.java index 70faabf96a5..eff5d443bc9 100644 --- a/check_api/src/main/java/com/google/errorprone/BaseErrorProneJavaCompiler.java +++ b/check_api/src/main/java/com/google/errorprone/BaseErrorProneJavaCompiler.java @@ -39,9 +39,7 @@ import java.io.PrintWriter; import java.io.Writer; import java.nio.charset.Charset; -import java.util.Arrays; import java.util.EnumSet; -import java.util.List; import java.util.Locale; import java.util.ResourceBundle; import java.util.Set; @@ -78,8 +76,7 @@ public CompilationTask getTask( Iterable classes, Iterable compilationUnits) { ErrorProneOptions errorProneOptions = ErrorProneOptions.processArgs(options); - List remainingOptions = Arrays.asList(errorProneOptions.getRemainingArgs()); - ImmutableList javacOpts = ImmutableList.copyOf(remainingOptions); + ImmutableList javacOpts = errorProneOptions.getRemainingArgs(); javacOpts = defaultToLatestSupportedLanguageLevel(javacOpts); javacOpts = setCompilePolicyToByFile(javacOpts); JavacTask task = diff --git a/check_api/src/main/java/com/google/errorprone/ErrorProneFlags.java b/check_api/src/main/java/com/google/errorprone/ErrorProneFlags.java index f053cdaa6b7..8cb8e9f8c76 100644 --- a/check_api/src/main/java/com/google/errorprone/ErrorProneFlags.java +++ b/check_api/src/main/java/com/google/errorprone/ErrorProneFlags.java @@ -152,21 +152,47 @@ public Optional getInteger(String key) { * an {@link Optional}, which is empty if the flag is unset. * *

    (note: empty strings included, e.g. {@code "-XepOpt:List=,1,,2," => ["","1","","2",""]}) + * + * @deprecated prefer {@link #getListOrEmpty(String)} */ + @Deprecated public Optional> getList(String key) { return this.get(key).map(v -> ImmutableList.copyOf(Splitter.on(',').split(v))); } + /** + * Gets the flag value for the given key as a comma-separated {@link ImmutableList} of Strings, or + * an empty list if the flag is unset. + * + *

    (note: empty strings included, e.g. {@code "-XepOpt:List=,1,,2," => ["","1","","2",""]}) + */ + public ImmutableList getListOrEmpty(String key) { + return getList(key).map(ImmutableList::copyOf).orElse(ImmutableList.of()); + } + /** * Gets the flag value for the given key as a comma-separated {@link Set} of Strings, wrapped in * an {@link Optional}, which is empty if the flag is unset. * *

    (note: empty strings included, e.g. {@code "-XepOpt:Set=,1,,1,2," => ["","1","2"]}) + * + * @deprecated prefer {@link #getSetOrEmpty(String)} */ + @Deprecated public Optional> getSet(String key) { return this.get(key).map(v -> ImmutableSet.copyOf(Splitter.on(',').split(v))); } + /** + * Gets the flag value for the given key as a comma-separated {@link Set} of Strings, or an empty + * set if unset. + * + *

    (note: empty strings included, e.g. {@code "-XepOpt:Set=,1,,1,2," => ["","1","2"]}) + */ + public ImmutableSet getSetOrEmpty(String key) { + return getSet(key).map(ImmutableSet::copyOf).orElse(ImmutableSet.of()); + } + /** Whether this Flags object is empty, i.e. no flags have been set. */ public boolean isEmpty() { return this.flagsMap.isEmpty(); diff --git a/check_api/src/main/java/com/google/errorprone/ErrorProneOptions.java b/check_api/src/main/java/com/google/errorprone/ErrorProneOptions.java index a9afc76ef51..eb0f58f0cbf 100644 --- a/check_api/src/main/java/com/google/errorprone/ErrorProneOptions.java +++ b/check_api/src/main/java/com/google/errorprone/ErrorProneOptions.java @@ -140,21 +140,7 @@ abstract static class Builder { abstract Builder importOrganizer(ImportOrganizer importOrganizer); - abstract PatchingOptions autoBuild(); - - final PatchingOptions build() { - - PatchingOptions patchingOptions = autoBuild(); - - // If anything is specified, then (checkers or refaster) and output must be set. - if ((!patchingOptions.namedCheckers().isEmpty() - || patchingOptions.customRefactorer().isPresent()) - ^ patchingOptions.doRefactor()) { - throw new InvalidCommandLineOptionException( - "-XepPatchChecks and -XepPatchLocation must be specified together"); - } - return patchingOptions; - } + abstract PatchingOptions build(); } } @@ -210,8 +196,8 @@ private ErrorProneOptions( this.ignoreLargeCodeGenerators = ignoreLargeCodeGenerators; } - public String[] getRemainingArgs() { - return remainingArgs.toArray(new String[remainingArgs.size()]); + public ImmutableList getRemainingArgs() { + return remainingArgs; } public ImmutableMap getSeverityMap() { @@ -419,6 +405,8 @@ public static ErrorProneOptions processArgs(Iterable args) { * You can pass the IGNORE_UNKNOWN_CHECKS_FLAG to opt-out of that checking. This allows you to * use command lines from different versions of error-prone interchangeably. */ + boolean patchLocationSet = false; + boolean patchCheckSet = false; Builder builder = new Builder(); for (String arg : args) { switch (arg) { @@ -458,6 +446,7 @@ public static ErrorProneOptions processArgs(Iterable args) { } else if (arg.startsWith(ErrorProneFlags.PREFIX)) { builder.parseFlag(arg); } else if (arg.startsWith(PATCH_OUTPUT_LOCATION)) { + patchLocationSet = true; String remaining = arg.substring(PATCH_OUTPUT_LOCATION.length()); if (remaining.equals("IN_PLACE")) { builder.patchingOptionsBuilder().inPlace(true); @@ -468,6 +457,7 @@ public static ErrorProneOptions processArgs(Iterable args) { builder.patchingOptionsBuilder().baseDirectory(remaining); } } else if (arg.startsWith(PATCH_CHECKS_PREFIX)) { + patchCheckSet = true; String remaining = arg.substring(PATCH_CHECKS_PREFIX.length()); if (remaining.startsWith("refaster:")) { // Refaster rule, load from InputStream at file @@ -485,7 +475,8 @@ public static ErrorProneOptions processArgs(Iterable args) { } }); } else { - Iterable checks = Splitter.on(',').trimResults().split(remaining); + Iterable checks = + Splitter.on(',').trimResults().omitEmptyStrings().split(remaining); builder.patchingOptionsBuilder().namedCheckers(ImmutableSet.copyOf(checks)); } } else if (arg.startsWith(PATCH_IMPORT_ORDER_PREFIX)) { @@ -505,6 +496,11 @@ public static ErrorProneOptions processArgs(Iterable args) { } } + if (patchCheckSet && !patchLocationSet) { + throw new InvalidCommandLineOptionException( + "-XepPatchLocation must be specified when -XepPatchChecks is"); + } + return builder.build(remainingArgs.build()); } 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 cb23c08dea3..3b891b9cc5e 100644 --- a/check_api/src/main/java/com/google/errorprone/SuppressionInfo.java +++ b/check_api/src/main/java/com/google/errorprone/SuppressionInfo.java @@ -85,7 +85,8 @@ public SuppressedState suppressedState( return SuppressedState.SUPPRESSED; } if (suppressible.supportsSuppressWarnings() - && !Collections.disjoint(suppressible.allNames(), suppressWarningsStrings)) { + && (suppressWarningsStrings.contains("all") + || !Collections.disjoint(suppressible.allNames(), suppressWarningsStrings))) { return SuppressedState.SUPPRESSED; } if (suppressible.suppressedByAnyOf(customSuppressions, state)) { diff --git a/check_api/src/main/java/com/google/errorprone/apply/DescriptionBasedDiff.java b/check_api/src/main/java/com/google/errorprone/apply/DescriptionBasedDiff.java index 3df86f4abdd..1c11b94ea06 100644 --- a/check_api/src/main/java/com/google/errorprone/apply/DescriptionBasedDiff.java +++ b/check_api/src/main/java/com/google/errorprone/apply/DescriptionBasedDiff.java @@ -99,7 +99,7 @@ public void handleFix(Fix fix) { importsToRemove.addAll(fix.getImportsToRemove()); for (Replacement replacement : fix.getReplacements(endPositions)) { try { - replacements.add(replacement, Replacements.CoalescePolicy.EXISTING_FIRST); + replacements.add(replacement, fix.getCoalescePolicy()); } catch (IllegalArgumentException iae) { if (!ignoreOverlappingFixes) { throw iae; @@ -109,7 +109,7 @@ public void handleFix(Fix fix) { } @Override - public void applyDifferences(SourceFile sourceFile) throws DiffNotApplicableException { + public void applyDifferences(SourceFile sourceFile) { if (!importsToAdd.isEmpty() || !importsToRemove.isEmpty()) { ImportStatements importStatements = ImportStatements.create(compilationUnit, importOrganizer); importStatements.addAll(importsToAdd); diff --git a/check_api/src/main/java/com/google/errorprone/apply/Diff.java b/check_api/src/main/java/com/google/errorprone/apply/Diff.java index 5e469350ea4..56b287d6891 100644 --- a/check_api/src/main/java/com/google/errorprone/apply/Diff.java +++ b/check_api/src/main/java/com/google/errorprone/apply/Diff.java @@ -25,10 +25,6 @@ public interface Diff { /** Gets the name of the file this difference applies to */ String getRelevantFileName(); - /** - * Applies this difference to the supplied {@code sourceFile}. - * - * @throws DiffNotApplicableException if the diff could not be applied to the source file - */ + /** Applies this difference to the supplied {@code sourceFile}. */ void applyDifferences(SourceFile sourceFile); } diff --git a/check_api/src/main/java/com/google/errorprone/apply/DiffApplier.java b/check_api/src/main/java/com/google/errorprone/apply/DiffApplier.java index 386d79d1875..525a07d7a81 100644 --- a/check_api/src/main/java/com/google/errorprone/apply/DiffApplier.java +++ b/check_api/src/main/java/com/google/errorprone/apply/DiffApplier.java @@ -123,7 +123,7 @@ public void run() { if (completed % 100 == 0) { logger.log(Level.INFO, String.format("Completed %d files in %s", completed, stopwatch)); } - } catch (IOException | DiffNotApplicableException e) { + } catch (IOException | RuntimeException e) { logger.log(Level.WARNING, "Failed to apply diff to file " + diff.getRelevantFileName(), e); diffsFailedPaths.add(diff.getRelevantFileName()); } finally { diff --git a/check_api/src/main/java/com/google/errorprone/apply/PatchFileDestination.java b/check_api/src/main/java/com/google/errorprone/apply/PatchFileDestination.java index 98b1a86bf9c..896cea59894 100644 --- a/check_api/src/main/java/com/google/errorprone/apply/PatchFileDestination.java +++ b/check_api/src/main/java/com/google/errorprone/apply/PatchFileDestination.java @@ -20,7 +20,6 @@ import com.github.difflib.DiffUtils; import com.github.difflib.UnifiedDiffUtils; -import com.github.difflib.algorithm.DiffException; import com.github.difflib.patch.Patch; import com.google.common.base.Joiner; import com.google.common.base.Splitter; @@ -60,12 +59,7 @@ public void writeFile(SourceFile update) throws IOException { if (!oldSource.equals(newSource)) { List originalLines = LINE_SPLITTER.splitToList(oldSource); - Patch diff = null; - try { - diff = DiffUtils.diff(originalLines, LINE_SPLITTER.splitToList(newSource)); - } catch (DiffException e) { - throw new AssertionError("DiffUtils.diff should not fail", e); - } + Patch diff = DiffUtils.diff(originalLines, LINE_SPLITTER.splitToList(newSource)); String relativePath = baseDir.relativize(sourceFilePath).toString(); List unifiedDiff = UnifiedDiffUtils.generateUnifiedDiff(relativePath, relativePath, originalLines, diff, 2); 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 4693b70ef84..73cfa5b9584 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 @@ -99,6 +99,7 @@ import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.function.BiPredicate; @@ -269,8 +270,12 @@ public boolean isSuppressed(Symbol symbol) { } private boolean isSuppressed(SuppressWarnings suppression) { - return suppression != null - && !Collections.disjoint(Arrays.asList(suppression.value()), allNames()); + if (suppression == null || !supportsSuppressWarnings()) { + return false; + } + + List suppressions = Arrays.asList(suppression.value()); + return suppressions.contains("all") || !Collections.disjoint(suppressions, allNames()); } /** diff --git a/check_api/src/main/java/com/google/errorprone/fixes/Fix.java b/check_api/src/main/java/com/google/errorprone/fixes/Fix.java index 261434e986e..2a1f2100b47 100644 --- a/check_api/src/main/java/com/google/errorprone/fixes/Fix.java +++ b/check_api/src/main/java/com/google/errorprone/fixes/Fix.java @@ -16,10 +16,9 @@ package com.google.errorprone.fixes; +import com.google.common.collect.ImmutableSet; import com.sun.tools.javac.tree.EndPosTable; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; -import java.util.Collection; -import java.util.Set; /** * Represents a source code transformation, usually used to fix a bug detected by error-prone. @@ -36,15 +35,15 @@ public interface Fix { * *

    Empty string generates the default description. */ - default String getShortDescription() { - return ""; - } + String getShortDescription(); - Set getReplacements(EndPosTable endPositions); + Replacements.CoalescePolicy getCoalescePolicy(); - Collection getImportsToAdd(); + ImmutableSet getReplacements(EndPosTable endPositions); - Collection getImportsToRemove(); + ImmutableSet getImportsToAdd(); + + ImmutableSet getImportsToRemove(); boolean isEmpty(); } diff --git a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java index 365dff1c01d..7211be164e7 100644 --- a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java +++ b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java @@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.fixes.Replacements.CoalescePolicy; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; @@ -46,6 +47,7 @@ public abstract class SuggestedFix implements Fix { private static SuggestedFix create(SuggestedFix.Builder builder) { return new AutoValue_SuggestedFix( + builder.coalescePolicy, ImmutableList.copyOf(builder.fixes), ImmutableSet.copyOf(builder.importsToAdd), ImmutableSet.copyOf(builder.importsToRemove), @@ -83,15 +85,14 @@ public String toString(JCCompilationUnit compilationUnit) { public abstract int hashCode(); @Override - public Set getReplacements(EndPosTable endPositions) { + public ImmutableSet getReplacements(EndPosTable endPositions) { if (endPositions == null) { throw new IllegalArgumentException( "Cannot produce correct replacements without endPositions."); } Replacements replacements = new Replacements(); for (FixOperation fix : fixes()) { - replacements.add( - fix.getReplacement(endPositions), Replacements.CoalescePolicy.EXISTING_FIRST); + replacements.add(fix.getReplacement(endPositions), getCoalescePolicy()); } return replacements.ascending(); } @@ -169,6 +170,7 @@ public static class Builder { private final List fixes = new ArrayList<>(); private final Set importsToAdd = new LinkedHashSet<>(); private final Set importsToRemove = new LinkedHashSet<>(); + private CoalescePolicy coalescePolicy = CoalescePolicy.EXISTING_FIRST; private String shortDescription = ""; protected Builder() {} @@ -199,6 +201,12 @@ public Builder setShortDescription(String shortDescription) { return this; } + @CanIgnoreReturnValue + public Builder setCoalescePolicy(CoalescePolicy coalescePolicy) { + this.coalescePolicy = coalescePolicy; + return this; + } + @CanIgnoreReturnValue public Builder replace(Tree node, String replaceWith) { checkNotSyntheticConstructor(node); diff --git a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java index 6b2e28d3562..f448941037b 100644 --- a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java +++ b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java @@ -745,11 +745,12 @@ public Void visitMemberSelect(MemberSelectTree tree, Void unused) { /** Be warned, only changes method name at the declaration. */ public static SuggestedFix renameMethod(MethodTree tree, String replacement, VisitorState state) { - // Search tokens from beginning of method tree to beginning of method body. - int basePos = getStartPosition(tree); + // Search tokens from end of return type tree to beginning of method body. + int basePos = state.getEndPosition(tree.getReturnType()); int endPos = tree.getBody() != null ? getStartPosition(tree.getBody()) : state.getEndPosition(tree); List methodTokens = state.getOffsetTokens(basePos, endPos); + for (ErrorProneToken token : methodTokens) { if (token.kind() == TokenKind.IDENTIFIER && token.name().equals(tree.getName())) { return SuggestedFix.replace(token.pos(), token.endPos(), replacement); diff --git a/check_api/src/test/java/com/google/errorprone/ErrorProneFlagsTest.java b/check_api/src/test/java/com/google/errorprone/ErrorProneFlagsTest.java index c1f1f07d91b..89655c03eae 100644 --- a/check_api/src/test/java/com/google/errorprone/ErrorProneFlagsTest.java +++ b/check_api/src/test/java/com/google/errorprone/ErrorProneFlagsTest.java @@ -20,7 +20,6 @@ import static com.google.common.truth.Truth8.assertThat; import static org.junit.Assert.assertThrows; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.junit.Test; @@ -53,10 +52,12 @@ public void parseAndGetBoolean() { .parseFlag("-XepOpt:Arg1=tRuE") .parseFlag("-XepOpt:Arg2=FaLsE") .parseFlag("-XepOpt:Arg3=yes") + .parseFlag("-XepOpt:Arg4") .build(); assertThat(flags.getBoolean("Arg1")).hasValue(true); assertThat(flags.getBoolean("Arg2")).hasValue(false); assertThrows(IllegalArgumentException.class, () -> flags.getBoolean("Arg3")); + assertThat(flags.getBoolean("Arg4")).hasValue(true); assertThat(flags.getBoolean("absent")).isEmpty(); } @@ -90,12 +91,12 @@ public void parseAndGetList() { .parseFlag("-XepOpt:ArgD=7") .parseFlag("-XepOpt:ArgE=") .build(); - assertThat(flags.getList("ArgA")).hasValue(ImmutableList.of("1", "2", "3")); - assertThat(flags.getList("ArgB")).hasValue(ImmutableList.of("4", "")); - assertThat(flags.getList("ArgC")).hasValue(ImmutableList.of("5", "", "", "6")); - assertThat(flags.getList("ArgD")).hasValue(ImmutableList.of("7")); - assertThat(flags.getList("ArgE")).hasValue(ImmutableList.of("")); - assertThat(flags.getList("absent")).isEmpty(); + assertThat(flags.getListOrEmpty("ArgA")).containsExactly("1", "2", "3").inOrder(); + assertThat(flags.getListOrEmpty("ArgB")).containsExactly("4", "").inOrder(); + assertThat(flags.getListOrEmpty("ArgC")).containsExactly("5", "", "", "6").inOrder(); + assertThat(flags.getListOrEmpty("ArgD")).containsExactly("7"); + assertThat(flags.getListOrEmpty("ArgE")).containsExactly(""); + assertThat(flags.getListOrEmpty("absent")).isEmpty(); } @Test diff --git a/check_api/src/test/java/com/google/errorprone/ErrorProneOptionsTest.java b/check_api/src/test/java/com/google/errorprone/ErrorProneOptionsTest.java index 15dc8893dfb..6465bd2d57b 100644 --- a/check_api/src/test/java/com/google/errorprone/ErrorProneOptionsTest.java +++ b/check_api/src/test/java/com/google/errorprone/ErrorProneOptionsTest.java @@ -46,7 +46,7 @@ public class ErrorProneOptionsTest { public void nonErrorProneFlagsPlacedInRemainingArgs() { String[] args = {"-nonErrorProneFlag", "value"}; ErrorProneOptions options = ErrorProneOptions.processArgs(args); - assertThat(options.getRemainingArgs()).isEqualTo(args); + assertThat(options.getRemainingArgs()).containsExactlyElementsIn(args); } @Test @@ -111,7 +111,7 @@ public void combineErrorProneFlagsWithNonErrorProneFlags() { }; ErrorProneOptions options = ErrorProneOptions.processArgs(args); String[] expectedRemainingArgs = {"-classpath", "/this/is/classpath", "-verbose"}; - assertThat(options.getRemainingArgs()).isEqualTo(expectedRemainingArgs); + assertThat(options.getRemainingArgs()).containsExactlyElementsIn(expectedRemainingArgs); ImmutableMap expectedSeverityMap = ImmutableMap.builder() .put("Check1", Severity.WARN) @@ -238,9 +238,6 @@ public void recognizesPatch() { @Test public void throwsExceptionWithBadPatchArgs() { - assertThrows( - InvalidCommandLineOptionException.class, - () -> ErrorProneOptions.processArgs(new String[] {"-XepPatchLocation:IN_PLACE"})); assertThrows( InvalidCommandLineOptionException.class, () -> @@ -257,6 +254,24 @@ public void recognizesRefaster() { assertThat(options.patchingOptions().customRefactorer()).isPresent(); } + @Test + public void understandsEmptySetOfNamedCheckers() { + ErrorProneOptions options = + ErrorProneOptions.processArgs(new String[] {"-XepPatchLocation:IN_PLACE"}); + assertThat(options.patchingOptions().doRefactor()).isTrue(); + assertThat(options.patchingOptions().inPlace()).isTrue(); + assertThat(options.patchingOptions().namedCheckers()).isEmpty(); + assertThat(options.patchingOptions().customRefactorer()).isAbsent(); + + options = + ErrorProneOptions.processArgs( + new String[] {"-XepPatchLocation:IN_PLACE", "-XepPatchChecks:"}); + assertThat(options.patchingOptions().doRefactor()).isTrue(); + assertThat(options.patchingOptions().inPlace()).isTrue(); + assertThat(options.patchingOptions().namedCheckers()).isEmpty(); + assertThat(options.patchingOptions().customRefactorer()).isAbsent(); + } + @Test public void importOrder_staticFirst() { ErrorProneOptions options = diff --git a/check_api/src/test/java/com/google/errorprone/fixes/AppliedFixTest.java b/check_api/src/test/java/com/google/errorprone/fixes/AppliedFixTest.java index 97089b612ff..b68f8a12ce0 100644 --- a/check_api/src/test/java/com/google/errorprone/fixes/AppliedFixTest.java +++ b/check_api/src/test/java/com/google/errorprone/fixes/AppliedFixTest.java @@ -24,14 +24,13 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.google.common.collect.ImmutableSet; import com.sun.source.tree.TreeVisitor; import com.sun.tools.javac.tree.EndPosTable; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.util.Position; import java.util.HashMap; -import java.util.LinkedHashSet; import java.util.Map; -import java.util.Set; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -156,9 +155,8 @@ public void shouldSuggestToRemoveLastLineIfAsked() { @Test public void shouldApplyFixesInReverseOrder() { // Have to use a mock Fix here in order to intentionally return Replacements in wrong order. - Set replacements = new LinkedHashSet<>(); - replacements.add(Replacement.create(0, 1, "")); - replacements.add(Replacement.create(1, 1, "")); + ImmutableSet replacements = + ImmutableSet.of(Replacement.create(0, 1, ""), Replacement.create(1, 1, "")); Fix mockFix = mock(Fix.class); when(mockFix.getReplacements(any())).thenReturn(replacements); diff --git a/core/pom.xml b/core/pom.xml index a922b690eaa..da5bc278663 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.22.0 error-prone library diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AddressSelection.java b/core/src/main/java/com/google/errorprone/bugpatterns/AddressSelection.java index 3e2be15e29a..5159374333d 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/AddressSelection.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/AddressSelection.java @@ -25,6 +25,7 @@ import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod; import static com.google.errorprone.util.ASTHelpers.constValue; +import com.google.common.base.CharMatcher; import com.google.common.collect.ImmutableSet; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; @@ -37,13 +38,12 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.NewClassTree; -import java.util.Objects; import java.util.function.Supplier; /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ @BugPattern( summary = - "Prefer InetAddress.getAllName to APIs that convert a hostname to a single IP address", + "Prefer InetAddress.getAllByName to APIs that convert a hostname to a single IP address", severity = WARNING) public final class AddressSelection extends BugChecker implements NewClassTreeMatcher, MethodInvocationTreeMatcher { @@ -98,13 +98,36 @@ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState private Description handleMatch( ExpressionTree argument, ExpressionTree replacement, Supplier fix) { String value = constValue(argument, String.class); - if (Objects.equals(value, "localhost")) { + // If it's a numeric loopback address, suggest using the method for that. + if (LOOPBACK.contains(value)) { + return describeMatch(replacement, fix.get()); + } + // If it isn't a constant, or it's "localhost", or it looks like a numeric IP address, then + // we don't say anything. + if (value == null || value.equals("localhost") || isNumericIp(value)) { return NO_MATCH; } - Description.Builder description = buildDescription(replacement); - if (LOOPBACK.contains(value)) { - description.addFix(fix.get()); + // Otherwise flag it but don't suggest a fix. + return describeMatch(replacement); + } + + /** + * Returns true if this string looks like it might be a numeric IP address. The matching here is + * very approximate. We want every numeric IP address to return true, but it's OK if some strings + * return true even though they are not actually valid numeric IP addresses. Actually parsing + * numeric IPv6 addresses in all their glory is more than we need here. + */ + private static boolean isNumericIp(String value) { + if (value.isEmpty()) { + return false; + } + if (value.contains(":")) { + return true; // Every numeric IPv6 address contains a colon and no non-numeric hostname does. } - return description.build(); + return ASCII_DIGIT.matches(value.charAt(0)) + && ASCII_DIGIT.matches(value.charAt(value.length() - 1)); + // Every numeric IPv4 address begins and ends with an ASCII digit. } + + private static final CharMatcher ASCII_DIGIT = CharMatcher.inRange('0', '9'); } 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 63235b42a92..e242bd2bdd9 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java @@ -169,9 +169,10 @@ public MethodKind getMethodKind(MethodSymbol method) { // This is conceptually lower precedence than the above rules. externalIgnoreList()); - flags - .getList(CRV_PACKAGES) - .ifPresent(packagePatterns -> builder.addRule(PackagesRule.fromPatterns(packagePatterns))); + var crvPackages = flags.getListOrEmpty(CRV_PACKAGES); + if (!crvPackages.isEmpty()) { + builder.addRule(PackagesRule.fromPatterns(crvPackages)); + } this.evaluator = builder .addRule( diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ClosingStandardOutputStreams.java b/core/src/main/java/com/google/errorprone/bugpatterns/ClosingStandardOutputStreams.java new file mode 100644 index 00000000000..20a4c0275fd --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ClosingStandardOutputStreams.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.matchers.FieldMatchers.staticField; +import static com.google.errorprone.matchers.Matchers.anyOf; + +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.TryTreeMatcher; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.TryTree; + +/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ +@BugPattern( + summary = + "Don't use try-with-resources to manage standard output streams, closing the stream will" + + " cause subsequent output to standard output or standard error to be lost", + severity = WARNING) +public class ClosingStandardOutputStreams extends BugChecker implements TryTreeMatcher { + private static final Matcher MATCHER = + anyOf(staticField("java.lang.System", "err"), staticField("java.lang.System", "out")); + + @Override + public Description matchTry(TryTree tree, VisitorState state) { + new SuppressibleTreePathScanner(state) { + + @Override + public Void visitTry(TryTree tree, Void unused) { + tree.getResources().forEach(r -> r.accept(this, null)); + return null; + } + + @Override + public Void visitMemberSelect(MemberSelectTree tree, Void unused) { + if (MATCHER.matches(tree, state)) { + state.reportMatch(describeMatch(tree)); + } + return null; + } + }.scan(state.getPath(), null); + return NO_MATCH; + } +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/CompareToZero.java b/core/src/main/java/com/google/errorprone/bugpatterns/CompareToZero.java index 52863c0130d..061672bacfe 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/CompareToZero.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CompareToZero.java @@ -20,6 +20,7 @@ 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.constValue; import com.google.common.collect.ImmutableMap; @@ -74,6 +75,31 @@ public final class CompareToZero extends BugChecker implements MethodInvocationT private static final Matcher COMPARE_TO = anyOf( + staticMethod() + .onClassAny( + "com.google.common.primitives.Booleans", + "com.google.common.primitives.Chars", + "com.google.common.primitives.Doubles", + "com.google.common.primitives.Floats", + "com.google.common.primitives.Ints", + "com.google.common.primitives.Longs", + "com.google.common.primitives.Shorts", + "com.google.common.primitives.SignedBytes", + "com.google.common.primitives.UnsignedBytes", + "com.google.common.primitives.UnsignedInts", + "com.google.common.primitives.UnsignedLongs", + "com.google.protobuf.util.Durations", + "com.google.protobuf.util.Timestamps", + "java.lang.Boolean", + "java.lang.Byte", + "java.lang.Character", + "java.lang.Double", + "java.lang.Float", + "java.lang.Integer", + "java.lang.Long", + "java.lang.Short", + "java.util.Objects") + .named("compare"), instanceMethod().onDescendantOf("java.lang.Comparable").named("compareTo"), instanceMethod().onDescendantOf("java.util.Comparator").named("compare")); 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 cb955b2d886..552806e15bf 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java @@ -323,7 +323,11 @@ private void checkTree(ExpressionTree tree, MethodSymbol sym, VisitorState state private void handleDoNotCall(ExpressionTree tree, Symbol symbol, VisitorState state) { String doNotCall = getDoNotCallValue(symbol); - StringBuilder message = new StringBuilder("This method should not be called"); + StringBuilder message = + new StringBuilder(symbol.owner.toString()) + .append('.') + .append(symbol.toString()) + .append(" should not be called"); if (doNotCall.isEmpty()) { message.append(", see its documentation for details."); } else { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/EqualsIncompatibleType.java b/core/src/main/java/com/google/errorprone/bugpatterns/EqualsIncompatibleType.java index 1fc91f1fd78..9ba0b0461ac 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/EqualsIncompatibleType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/EqualsIncompatibleType.java @@ -31,11 +31,10 @@ import static com.google.errorprone.util.ASTHelpers.getType; import com.google.errorprone.BugPattern; -import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.MemberReferenceTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; -import com.google.errorprone.bugpatterns.TypeCompatibilityUtils.TypeCompatibilityReport; +import com.google.errorprone.bugpatterns.TypeCompatibility.TypeCompatibilityReport; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.Matcher; import com.google.errorprone.util.ASTHelpers; @@ -69,11 +68,11 @@ public class EqualsIncompatibleType extends BugChecker instanceMethod().anyClass().named("assertFalse"), staticMethod().anyClass().named("assertFalse"))); - private final TypeCompatibilityUtils typeCompatibilityUtils; + private final TypeCompatibility typeCompatibility; @Inject - EqualsIncompatibleType(ErrorProneFlags flags) { - this.typeCompatibilityUtils = TypeCompatibilityUtils.fromFlags(flags); + EqualsIncompatibleType(TypeCompatibility typeCompatibility) { + this.typeCompatibility = typeCompatibility; } @Override @@ -136,7 +135,7 @@ public Description matchMemberReference(MemberReferenceTree tree, VisitorState s private Description match( ExpressionTree invocationTree, Type receiverType, Type argumentType, VisitorState state) { TypeCompatibilityReport compatibilityReport = - typeCompatibilityUtils.compatibilityOfTypes(receiverType, argumentType, state); + typeCompatibility.compatibilityOfTypes(receiverType, argumentType, state); if (compatibilityReport.isCompatible()) { return NO_MATCH; } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MissingBraces.java b/core/src/main/java/com/google/errorprone/bugpatterns/MissingBraces.java index 7f1fc5c9595..9e3f27c93af 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/MissingBraces.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/MissingBraces.java @@ -16,6 +16,7 @@ package com.google.errorprone.bugpatterns; +import static com.google.errorprone.fixes.Replacements.CoalescePolicy.KEEP_ONLY_IDENTICAL_INSERTS; import static com.google.errorprone.matchers.Description.NO_MATCH; import com.google.errorprone.BugPattern; @@ -93,7 +94,12 @@ void check(StatementTree tree, VisitorState state) { if (tree != null && !(tree instanceof BlockTree)) { state.reportMatch( describeMatch( - tree, SuggestedFix.builder().prefixWith(tree, "{").postfixWith(tree, "}").build())); + tree, + SuggestedFix.builder() + .prefixWith(tree, "{") + .postfixWith(tree, "}") + .setCoalescePolicy(KEEP_ONLY_IDENTICAL_INSERTS) + .build())); } } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MissingDefault.java b/core/src/main/java/com/google/errorprone/bugpatterns/MissingDefault.java index e77472ff220..d3129dd9f19 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/MissingDefault.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/MissingDefault.java @@ -75,7 +75,7 @@ public Description matchSwitch(SwitchTree tree, VisitorState state) { } CaseTree defaultCase = maybeDefault.get(); List statements = defaultCase.getStatements(); - if (statements != null && !statements.isEmpty()) { + if (statements == null || !statements.isEmpty()) { return NO_MATCH; } // If `default` case is empty, and last in switch, add `// fall out` comment diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MultimapKeys.java b/core/src/main/java/com/google/errorprone/bugpatterns/MultimapKeys.java new file mode 100644 index 00000000000..64261a515df --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/MultimapKeys.java @@ -0,0 +1,65 @@ +/* + * Copyright 2023 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.SuggestedFixes.renameMethodInvocation; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod; + +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.sun.source.tree.EnhancedForLoopTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; + +/** A BugPattern; see the summary. */ +@BugPattern( + summary = + "Iterating over `Multimap.keys()` does not collapse duplicates. Did you mean `keySet()`?", + severity = WARNING) +public final class MultimapKeys extends BugChecker implements MethodInvocationTreeMatcher { + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + return KEYS.matches(tree, state) && isBeingIteratedOver(state) + ? describeMatch(tree, renameMethodInvocation(tree, "keySet", state)) + : NO_MATCH; + } + + private static boolean isBeingIteratedOver(VisitorState state) { + var parent = state.getPath().getParentPath().getLeaf(); + if (parent instanceof EnhancedForLoopTree) { + return true; + } + if (parent instanceof MemberSelectTree) { + var grandparent = state.getPath().getParentPath().getParentPath().getLeaf(); + return grandparent instanceof MethodInvocationTree + && FOR_EACH.matches((ExpressionTree) grandparent, state); + } + return false; + } + + private static final Matcher KEYS = + instanceMethod().onDescendantOf("com.google.common.collect.Multimap").named("keys"); + + private static final Matcher FOR_EACH = + instanceMethod().onDescendantOf("java.util.Collection").named("forEach"); +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/NamedLikeContextualKeyword.java b/core/src/main/java/com/google/errorprone/bugpatterns/NamedLikeContextualKeyword.java index 18b3249bf30..b74a72d1b33 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/NamedLikeContextualKeyword.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/NamedLikeContextualKeyword.java @@ -29,7 +29,6 @@ import com.google.common.collect.ImmutableSet; import com.google.errorprone.BugPattern; -import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; @@ -47,7 +46,6 @@ import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; import java.util.Collections; -import javax.inject.Inject; /** * Warns on classes or methods being named similarly to contextual keywords, or invoking such @@ -87,23 +85,8 @@ public final class NamedLikeContextualKeyword extends BugChecker "com.google.auto.value.processor.AutoValueProcessor", "com.google.auto.value.processor.AutoOneOfProcessor"); - private final boolean enableMethodNames; - private final boolean enableClassNames; - - @Inject - NamedLikeContextualKeyword(ErrorProneFlags flags) { - this.enableMethodNames = - flags.getBoolean("NamedLikeContextualKeyword:EnableMethodNames").orElse(false); - this.enableClassNames = - flags.getBoolean("NamedLikeContextualKeyword:EnableClassNames").orElse(false); - } - @Override public Description matchMethod(MethodTree tree, VisitorState state) { - if (!this.enableMethodNames) { - return NO_MATCH; - } - MethodSymbol methodSymbol = ASTHelpers.getSymbol(tree); // Don't alert if an @Auto... class (safe since reference always qualified). @@ -122,10 +105,6 @@ public Description matchMethod(MethodTree tree, VisitorState state) { @Override public Description matchClass(ClassTree tree, VisitorState state) { - if (!this.enableClassNames) { - return NO_MATCH; - } - if (DISALLOWED_CLASS_NAMES.contains(tree.getSimpleName().toString())) { return describeMatch(tree); } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/OperatorPrecedence.java b/core/src/main/java/com/google/errorprone/bugpatterns/OperatorPrecedence.java index 713c946d07c..1b706d8d27d 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/OperatorPrecedence.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/OperatorPrecedence.java @@ -19,6 +19,7 @@ import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.matchers.Description.NO_MATCH; import static com.google.errorprone.util.ASTHelpers.getStartPosition; +import static com.google.errorprone.util.ASTHelpers.getType; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; @@ -26,13 +27,17 @@ import com.google.errorprone.BugPattern.StandardTags; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.BinaryTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.ConditionalExpressionTreeMatcher; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.ConditionalExpressionTree; +import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.ParenthesizedTree; import com.sun.source.tree.Tree; import com.sun.source.tree.Tree.Kind; +import com.sun.tools.javac.code.TypeTag; import com.sun.tools.javac.tree.JCTree.JCBinary; import com.sun.tools.javac.tree.TreeInfo; @@ -41,7 +46,8 @@ summary = "Use grouping parenthesis to make the operator precedence explicit", severity = WARNING, tags = StandardTags.STYLE) -public class OperatorPrecedence extends BugChecker implements BinaryTreeMatcher { +public class OperatorPrecedence extends BugChecker + implements BinaryTreeMatcher, ConditionalExpressionTreeMatcher { private static final ImmutableSet CONDITIONAL = Sets.immutableEnumSet(Kind.AND, Kind.OR, Kind.XOR, Kind.CONDITIONAL_AND, Kind.CONDITIONAL_OR); @@ -71,6 +77,22 @@ public Description matchBinary(BinaryTree tree, VisitorState state) { return createAppropriateFix(tree, state); } + @Override + public Description matchConditionalExpression( + ConditionalExpressionTree tree, VisitorState state) { + ExpressionTree condition = tree.getCondition(); + if (!(condition instanceof BinaryTree)) { + return NO_MATCH; + } + if (!CONDITIONAL.contains(condition.getKind())) { + return NO_MATCH; + } + if (!state.getTypes().unboxedTypeOrType(getType(tree)).hasTag(TypeTag.BOOLEAN)) { + return NO_MATCH; + } + return basicFix((BinaryTree) condition); + } + private static boolean isConfusing(Kind thisKind, Kind parentKind) { if (CONDITIONAL.contains(thisKind) && CONDITIONAL.contains(parentKind)) { return true; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java b/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java index 4eec04fe978..baf7fd384eb 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java @@ -72,10 +72,7 @@ public class ParameterName extends BugChecker @Inject ParameterName(ErrorProneFlags errorProneFlags) { this.exemptPackages = - errorProneFlags - .getList("ParameterName:exemptPackagePrefixes") - .orElse(ImmutableList.of()) - .stream() + errorProneFlags.getListOrEmpty("ParameterName:exemptPackagePrefixes").stream() // add a trailing '.' so that e.g. com.foo matches as a prefix of com.foo.bar, but not // com.foobar .map(p -> p.endsWith(".") ? p : p + ".") diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ReturnAtTheEndOfVoidFunction.java b/core/src/main/java/com/google/errorprone/bugpatterns/ReturnAtTheEndOfVoidFunction.java new file mode 100644 index 00000000000..075d164baab --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ReturnAtTheEndOfVoidFunction.java @@ -0,0 +1,71 @@ +/* + * Copyright 2023 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.util.ASTHelpers.getType; + +import com.google.common.collect.Iterables; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.StatementTree; +import com.sun.tools.javac.code.Type; +import java.util.List; +import javax.lang.model.type.TypeKind; + +/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ +@BugPattern( + summary = "`return;` is unnecessary at the end of void methods and constructors.", + severity = WARNING) +public final class ReturnAtTheEndOfVoidFunction extends BugChecker implements MethodTreeMatcher { + + @Override + public Description matchMethod(MethodTree methodTree, VisitorState state) { + + // is a constructor or has a void return type (Void should pass, as `return null;` is required) + Type returnType = getType(methodTree.getReturnType()); + if (returnType != null && returnType.getKind() != TypeKind.VOID) { + return NO_MATCH; + } + + // has a body (not abstract or native) + BlockTree body = methodTree.getBody(); + if (body == null) { + return NO_MATCH; + } + + // body is not empty + List statements = body.getStatements(); + if (statements == null || statements.isEmpty()) { + return NO_MATCH; + } + + // last statement is a return + StatementTree lastStatement = Iterables.getLast(statements); + if (lastStatement.getKind() != StatementTree.Kind.RETURN) { + return NO_MATCH; + } + + return describeMatch(methodTree, SuggestedFix.delete(lastStatement)); + } +} 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 1e828d29602..9669aded089 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java @@ -27,6 +27,7 @@ import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; import com.google.errorprone.BugPattern; +import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher; import com.google.errorprone.fixes.SuggestedFix; @@ -48,6 +49,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import javax.inject.Inject; import javax.lang.model.element.ElementKind; import javax.lang.model.element.Name; import org.checkerframework.checker.nullness.qual.Nullable; @@ -57,6 +59,13 @@ summary = "This type name shadows another in a way that may be confusing.", severity = WARNING) public final class SameNameButDifferent extends BugChecker implements CompilationUnitTreeMatcher { + private final Boolean batchFindings; + + @Inject + SameNameButDifferent(ErrorProneFlags flags) { + batchFindings = flags.getBoolean("SameNameButDifferent:BatchFindings").orElse(false); + } + @Override public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { Table> table = HashBasedTable.create(); @@ -176,12 +185,16 @@ private void handle(Tree tree) { .map(t -> t.getQualifiedName().toString()) .collect(joining(", ", "[", "]"))); SuggestedFix fix = fixBuilder.build(); - for (List treePaths : trimmedTable.row(simpleName).values()) { - for (TreePath treePath : treePaths) { - state.reportMatch( - buildDescription(treePath.getLeaf()).setMessage(message).addFix(fix).build()); - } - } + trimmedTable.row(simpleName).values().stream() + .flatMap(List::stream) + .limit(batchFindings ? 1 : Long.MAX_VALUE) + .forEach( + treePath -> + state.reportMatch( + buildDescription(treePath.getLeaf()) + .setMessage(message) + .addFix(fix) + .build())); } } return NO_MATCH; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/TruthContainsExactlyElementsInUsage.java b/core/src/main/java/com/google/errorprone/bugpatterns/TruthContainsExactlyElementsInUsage.java new file mode 100644 index 00000000000..78e5c92aebb --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/TruthContainsExactlyElementsInUsage.java @@ -0,0 +1,110 @@ +/* + * Copyright 2023 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import static com.google.common.collect.Iterables.getLast; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.matchers.Matchers.anyOf; +import static com.google.errorprone.matchers.Matchers.instanceMethod; +import static com.google.errorprone.matchers.Matchers.staticMethod; +import static com.google.errorprone.util.ASTHelpers.getStartPosition; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.BugPattern; +import com.google.errorprone.BugPattern.StandardTags; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; +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.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.NewArrayTree; +import java.util.Optional; + +/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ +@BugPattern( + tags = {StandardTags.REFACTORING}, + summary = + "containsExactly is preferred over containsExactlyElementsIn when creating new iterables.", + severity = WARNING) +public final class TruthContainsExactlyElementsInUsage extends BugChecker + implements MethodInvocationTreeMatcher { + + private static final Matcher CONTAINS_EXACTLY_ELEMENTS_IN_METHOD_MATCHER = + instanceMethod() + .onDescendantOf("com.google.common.truth.IterableSubject") + .named("containsExactlyElementsIn"); + + // Not including Sets for the rare occasion of duplicate element declarations inside the set. If + // refactored to containsExactly, the behavior is different. + private static final Matcher NEW_ITERABLE_MATCHERS = + anyOf( + staticMethod().onClass("com.google.common.collect.Lists").named("newArrayList"), + staticMethod().onClass("com.google.common.collect.ImmutableList").named("of"), + staticMethod().onClass("java.util.Arrays").named("asList"), + staticMethod().onClass("java.util.Collections").named("singletonList"), + staticMethod().onClass("java.util.List").named("of")); + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + if (!CONTAINS_EXACTLY_ELEMENTS_IN_METHOD_MATCHER.matches(tree, state)) { + return Description.NO_MATCH; + } + + // Avoids refactoring variables and method invocations that are not creating new iterables. + // The first param from containsExactlyElementsIn should always be an Iterable. + return getArgumentsFromNewIterableExpression(tree.getArguments().get(0), state) + .map(arguments -> describeMatch(tree, refactor(arguments, tree, state))) + .orElse(Description.NO_MATCH); + } + + // Returns the arguments from the expression. If it is not a valid expression, returns empty. + private static Optional> getArgumentsFromNewIterableExpression( + ExpressionTree expression, VisitorState state) { + if (expression instanceof MethodInvocationTree + && NEW_ITERABLE_MATCHERS.matches(expression, state)) { + MethodInvocationTree paramMethodInvocationTree = (MethodInvocationTree) expression; + return Optional.of(ImmutableList.copyOf(paramMethodInvocationTree.getArguments())); + } else if (expression instanceof NewArrayTree) { + NewArrayTree newArrayTree = (NewArrayTree) expression; + return Optional.of(ImmutableList.copyOf(newArrayTree.getInitializers())); + } + return Optional.empty(); + } + + private static SuggestedFix refactor( + ImmutableList arguments, MethodInvocationTree tree, VisitorState state) { + // First we replace the containsExactlyElementsIn method with containsExactly. + SuggestedFix.Builder fix = + SuggestedFix.builder() + .merge(SuggestedFixes.renameMethodInvocation(tree, "containsExactly", state)); + // Finally, we use the arguments from the new iterable to build the containsExactly arguments. + ExpressionTree expressionToReplace = tree.getArguments().get(0); + if (!arguments.isEmpty()) { + fix.replace(getStartPosition(expressionToReplace), getStartPosition(arguments.get(0)), "") + .replace( + state.getEndPosition(getLast(arguments)), + state.getEndPosition(expressionToReplace), + ""); + } else { + fix.delete(expressionToReplace); + } + return fix.build(); + } +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/TypeCompatibilityUtils.java b/core/src/main/java/com/google/errorprone/bugpatterns/TypeCompatibility.java similarity index 95% rename from core/src/main/java/com/google/errorprone/bugpatterns/TypeCompatibilityUtils.java rename to core/src/main/java/com/google/errorprone/bugpatterns/TypeCompatibility.java index 9dfc93299ab..bf827bd6e0d 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/TypeCompatibilityUtils.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/TypeCompatibility.java @@ -44,30 +44,26 @@ import java.util.Set; import java.util.TreeSet; import javax.annotation.Nullable; +import javax.inject.Inject; import javax.lang.model.type.TypeKind; /** - * Logical utility methods to answer the question: Are these two types "compatible" with each other, - * in the context of an equality check. + * Methods to answer the question: are these two types "compatible" with each other, in the context + * of an equality check? * - *

    i.e.: It is possible that an object of one type could be equal to an object of the other type. + *

    That is, based on the types alone, is it possible for them to be equal. In general this + * requires a common superclass that overrides {@link Object#equals(Object)}, but there are + * complexities and special-cases. */ -public final class TypeCompatibilityUtils { +public final class TypeCompatibility { private static final String WITHOUT_EQUALS_REASON = ". Though these types are the same, the type doesn't implement equals."; private final boolean treatBuildersAsIncomparable; - public static TypeCompatibilityUtils fromFlags(ErrorProneFlags flags) { - return new TypeCompatibilityUtils( - flags.getBoolean("TypeCompatibility:TreatBuildersAsIncomparable").orElse(true)); - } - - public static TypeCompatibilityUtils allOn() { - return new TypeCompatibilityUtils(/* treatBuildersAsIncomparable= */ true); - } - - private TypeCompatibilityUtils(boolean treatBuildersAsIncomparable) { - this.treatBuildersAsIncomparable = treatBuildersAsIncomparable; + @Inject + TypeCompatibility(ErrorProneFlags flags) { + this.treatBuildersAsIncomparable = + flags.getBoolean("TypeCompatibility:TreatBuildersAsIncomparable").orElse(true); } public TypeCompatibilityReport compatibilityOfTypes( @@ -364,7 +360,7 @@ private static TreeSet typeSet(VisitorState state) { @AutoValue public abstract static class TypeCompatibilityReport { private static final TypeCompatibilityReport COMPATIBLE = - new AutoValue_TypeCompatibilityUtils_TypeCompatibilityReport(true, null, null, null); + new AutoValue_TypeCompatibility_TypeCompatibilityReport(true, null, null, null); public abstract boolean isCompatible(); @@ -386,8 +382,7 @@ static TypeCompatibilityReport incompatible(Type lhs, Type rhs) { } static TypeCompatibilityReport incompatible(Type lhs, Type rhs, String extraReason) { - return new AutoValue_TypeCompatibilityUtils_TypeCompatibilityReport( - false, lhs, rhs, extraReason); + return new AutoValue_TypeCompatibility_TypeCompatibilityReport(false, lhs, rhs, extraReason); } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessarilyVisible.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessarilyVisible.java index 77a60f328d7..f09be5fe033 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessarilyVisible.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessarilyVisible.java @@ -56,6 +56,8 @@ public final class UnnecessarilyVisible extends BugChecker implements MethodTree VisitorState.memoize( s -> Stream.of( + "com.google.errorprone.refaster.annotation.AfterTemplate", + "com.google.errorprone.refaster.annotation.BeforeTemplate", "com.google.inject.Inject", "com.google.inject.Provides", "com.google.inject.multibindings.ProvidesIntoMap", diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryAsync.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryAsync.java new file mode 100644 index 00000000000..29b9c176779 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryAsync.java @@ -0,0 +1,251 @@ +/* + * Copyright 2023 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.matchers.Matchers.anyOf; +import static com.google.errorprone.matchers.Matchers.constructor; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.isConsideredFinal; +import static java.lang.String.format; + +import com.google.errorprone.BugPattern; +import com.google.errorprone.BugPattern.SeverityLevel; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePathScanner; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.Types; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; +import javax.lang.model.element.ElementKind; + +/** A BugPattern; see the summary. */ +@BugPattern( + severity = SeverityLevel.WARNING, + summary = + "Variables which are initialized and do not escape the current scope do not need to worry" + + " about concurrency. Using the non-concurrent type will reduce overhead and" + + " verbosity.") +public final class UnnecessaryAsync extends BugChecker implements VariableTreeMatcher { + private static final Matcher NEW_SYNCHRONIZED_THING = + anyOf( + Stream.of( + "java.util.concurrent.atomic.AtomicBoolean", + "java.util.concurrent.atomic.AtomicReference", + "java.util.concurrent.atomic.AtomicInteger", + "java.util.concurrent.atomic.AtomicLong", + "java.util.concurrent.ConcurrentHashMap") + .map(x -> constructor().forClass(x)) + .collect(toImmutableList())); + + @Override + public Description matchVariable(VariableTree tree, VisitorState state) { + var symbol = getSymbol(tree); + if (!symbol.getKind().equals(ElementKind.LOCAL_VARIABLE) || !isConsideredFinal(symbol)) { + return NO_MATCH; + } + var initializer = tree.getInitializer(); + if (initializer == null || !NEW_SYNCHRONIZED_THING.matches(initializer, state)) { + return NO_MATCH; + } + AtomicBoolean escapes = new AtomicBoolean(false); + new TreePathScanner() { + int lambdaDepth = 0; + + @Override + public Void visitMethod(MethodTree tree, Void unused) { + lambdaDepth++; + var ret = super.visitMethod(tree, null); + lambdaDepth--; + return ret; + } + + @Override + public Void visitLambdaExpression(LambdaExpressionTree tree, Void unused) { + lambdaDepth++; + var ret = super.visitLambdaExpression(tree, null); + lambdaDepth--; + return ret; + } + + @Override + public Void visitIdentifier(IdentifierTree tree, Void unused) { + if (!getSymbol(tree).equals(symbol)) { + return super.visitIdentifier(tree, null); + } + // We're in a lambda, so our symbol implicitly escapes. + if (lambdaDepth > 0) { + escapes.set(true); + return super.visitIdentifier(tree, null); + } + var parentTree = getCurrentPath().getParentPath().getLeaf(); + // Anything other than a method invocation on our symbol constitutes a reference to it + // escaping. + if (isVariableDeclarationItself(parentTree) || parentTree instanceof MemberSelectTree) { + return super.visitIdentifier(tree, null); + } + escapes.set(true); + return super.visitIdentifier(tree, null); + } + + private boolean isVariableDeclarationItself(Tree parentTree) { + return parentTree instanceof VariableTree && getSymbol(parentTree).equals(symbol); + } + }.scan(state.getPath().getParentPath(), null); + return escapes.get() ? NO_MATCH : describeMatch(tree, attemptFix(tree, state)); + } + + private SuggestedFix attemptFix(VariableTree tree, VisitorState state) { + var symbol = getSymbol(tree); + if (!symbol.type.toString().startsWith("java.util.concurrent.atomic")) { + return SuggestedFix.emptyFix(); + } + + AtomicBoolean fixable = new AtomicBoolean(true); + SuggestedFix.Builder fix = SuggestedFix.builder(); + + var constructor = (NewClassTree) tree.getInitializer(); + + fix.replace( + tree, + format( + "%s %s = %s;", + getPrimitiveType(symbol.type, state.getTypes()), + symbol.getSimpleName(), + constructor.getArguments().isEmpty() + ? getDefaultInitializer(symbol, state.getTypes()) + : state.getSourceForNode(constructor.getArguments().get(0)))); + + new TreePathScanner() { + @Override + public Void visitIdentifier(IdentifierTree tree, Void unused) { + if (!getSymbol(tree).equals(symbol)) { + return super.visitIdentifier(tree, null); + } + var parentTree = getCurrentPath().getParentPath().getLeaf(); + if (isVariableDeclarationItself(parentTree)) { + return super.visitIdentifier(tree, null); + } + if (parentTree instanceof MemberSelectTree) { + var grandparent = + (MethodInvocationTree) getCurrentPath().getParentPath().getParentPath().getLeaf(); + if (((MemberSelectTree) parentTree).getIdentifier().contentEquals("set")) { + fix.replace( + grandparent, + format( + "%s = %s", + state.getSourceForNode(tree), + state.getSourceForNode(grandparent.getArguments().get(0)))); + } else if (((MemberSelectTree) parentTree).getIdentifier().contentEquals("get")) { + fix.replace(grandparent, state.getSourceForNode(tree)); + } else if (((MemberSelectTree) parentTree) + .getIdentifier() + .contentEquals("getAndIncrement")) { + fix.replace(grandparent, format("%s++", state.getSourceForNode(tree))); + } else if (((MemberSelectTree) parentTree) + .getIdentifier() + .contentEquals("getAndDecrement")) { + fix.replace(grandparent, format("%s--", state.getSourceForNode(tree))); + } else if (((MemberSelectTree) parentTree) + .getIdentifier() + .contentEquals("incrementAndGet")) { + fix.replace(grandparent, format("++%s", state.getSourceForNode(tree))); + } else if (((MemberSelectTree) parentTree) + .getIdentifier() + .contentEquals("decrementAndGet")) { + fix.replace(grandparent, format("--%s", state.getSourceForNode(tree))); + } else if (((MemberSelectTree) parentTree) + .getIdentifier() + .contentEquals("compareAndSet")) { + fix.replace( + grandparent, + format( + "%s = %s", + state.getSourceForNode(tree), + state.getSourceForNode(grandparent.getArguments().get(1)))); + } else if (((MemberSelectTree) parentTree).getIdentifier().contentEquals("addAndGet")) { + fix.replace( + grandparent, + format( + "%s += %s", + state.getSourceForNode(tree), + state.getSourceForNode(grandparent.getArguments().get(0)))); + } else { + fixable.set(false); + } + + } else { + fixable.set(false); + } + return super.visitIdentifier(tree, null); + } + + private boolean isVariableDeclarationItself(Tree parentTree) { + return parentTree instanceof VariableTree && getSymbol(parentTree).equals(symbol); + } + }.scan(state.getPath().getParentPath(), null); + return fixable.get() ? fix.build() : SuggestedFix.emptyFix(); + } + + private static String getPrimitiveType(Type type, Types types) { + String name = types.erasure(type).toString(); + switch (name) { + case "java.util.concurrent.atomic.AtomicBoolean": + return "boolean"; + case "java.util.concurrent.atomic.AtomicReference": + return type.allparams().isEmpty() + ? "Object" + : type.allparams().get(0).tsym.getSimpleName().toString(); + case "java.util.concurrent.atomic.AtomicInteger": + return "int"; + case "java.util.concurrent.atomic.AtomicLong": + return "long"; + default: + throw new AssertionError(name); + } + } + + private static String getDefaultInitializer(VarSymbol symbol, Types types) { + String name = types.erasure(symbol.type).toString(); + switch (name) { + case "java.util.concurrent.atomic.AtomicBoolean": + return "false"; + case "java.util.concurrent.atomic.AtomicReference": + return "null"; + case "java.util.concurrent.atomic.AtomicInteger": + case "java.util.concurrent.atomic.AtomicLong": + return "0"; + default: + throw new AssertionError(name); + } + } +} 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 be9332b9479..1cad68ba121 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java @@ -43,6 +43,7 @@ import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.BlockTree; +import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.LambdaExpressionTree; @@ -101,15 +102,26 @@ public Description matchMethod(MethodTree tree, VisitorState state) { if (state.isAndroidCompatible()) { return NO_MATCH; } + boolean[] usedInEnhancedForLoop = {false}; new TreePathScanner() { @Override public Void visitMethodInvocation(MethodInvocationTree node, Void unused) { + if (Objects.equals(getSymbol(node), sym)) { - replaceUseWithMethodReference(fix, node, name, state.withPath(getCurrentPath())); + Tree parent = getCurrentPath().getParentPath().getLeaf(); + if (parent instanceof EnhancedForLoopTree + && ((EnhancedForLoopTree) parent).getExpression().equals(node)) { + usedInEnhancedForLoop[0] = true; + } else { + replaceUseWithMethodReference(fix, node, name, state.withPath(getCurrentPath())); + } } return super.visitMethodInvocation(node, null); } }.scan(state.getPath().getCompilationUnit(), null); + if (usedInEnhancedForLoop[0]) { + return NO_MATCH; + } lambdaToMethod(state, lambda, fix, name, type); return describeMatch(tree, fix.build()); } 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 09e31eda2a2..9193319d5c2 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnusedMethod.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedMethod.java @@ -22,6 +22,7 @@ import static com.google.common.collect.Iterables.getLast; import static com.google.common.collect.Iterables.size; import static com.google.common.collect.Multimaps.asMap; +import static com.google.common.collect.Sets.union; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.fixes.SuggestedFix.emptyFix; import static com.google.errorprone.fixes.SuggestedFixes.replaceIncludingComments; @@ -30,6 +31,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.hasAnnotation; import static com.google.errorprone.util.ASTHelpers.isGeneratedConstructor; import static com.google.errorprone.util.ASTHelpers.isSubtype; import static com.google.errorprone.util.ASTHelpers.scope; @@ -99,6 +101,8 @@ public final class UnusedMethod extends BugChecker implements CompilationUnitTre private static final ImmutableSet EXEMPTING_METHOD_ANNOTATIONS = ImmutableSet.of( "com.fasterxml.jackson.annotation.JsonCreator", + "com.fasterxml.jackson.annotation.JsonProperty", + "com.fasterxml.jackson.annotation.JsonSetter", "com.fasterxml.jackson.annotation.JsonValue", "com.google.acai.AfterTest", "com.google.acai.BeforeSuite", @@ -131,6 +135,8 @@ public final class UnusedMethod extends BugChecker implements CompilationUnitTre "javax.persistence.PrePersist", "javax.persistence.PreRemove", "javax.persistence.PreUpdate", + "javax.validation.constraints.AssertFalse", + "javax.validation.constraints.AssertTrue", "org.apache.beam.sdk.transforms.DoFn.ProcessElement", "org.aspectj.lang.annotation.Pointcut", "org.aspectj.lang.annotation.After", @@ -149,18 +155,21 @@ public final class UnusedMethod extends BugChecker implements CompilationUnitTre "org.junit.jupiter.api.Test", "org.junit.jupiter.params.ParameterizedTest"); + /** Class annotations which exempt methods within the annotated class from findings. */ + private static final ImmutableSet EXEMPTING_CLASS_ANNOTATIONS = ImmutableSet.of(); + /** The set of types exempting a type that is extending or implementing them. */ private static final ImmutableSet EXEMPTING_SUPER_TYPES = ImmutableSet.of(); - private final ImmutableSet additionalExemptingMethodAnnotations; + private final ImmutableSet exemptingMethodAnnotations; @Inject UnusedMethod(ErrorProneFlags errorProneFlags) { - this.additionalExemptingMethodAnnotations = - errorProneFlags - .getList("UnusedMethod:ExemptingMethodAnnotations") - .map(ImmutableSet::copyOf) - .orElseGet(ImmutableSet::of); + this.exemptingMethodAnnotations = + union( + errorProneFlags.getSetOrEmpty("UnusedMethod:ExemptingMethodAnnotations"), + EXEMPTING_METHOD_ANNOTATIONS) + .immutableCopy(); } @Override @@ -185,7 +194,8 @@ class MethodFinder extends SuppressibleTreePathScanner { @Override public Void visitClass(ClassTree tree, Void unused) { - if (exemptedBySuperType(getType(tree), state)) { + if (exemptedBySuperType(getType(tree), state) + || EXEMPTING_CLASS_ANNOTATIONS.stream().anyMatch(a -> hasAnnotation(tree, a, state))) { return null; } return super.visitClass(tree, null); @@ -483,8 +493,7 @@ private boolean exemptedByAnnotation(List annotations) } TypeSymbol tsym = annotationType.tsym; String annotationName = tsym.getQualifiedName().toString(); - if (EXEMPTING_METHOD_ANNOTATIONS.contains(annotationName) - || additionalExemptingMethodAnnotations.contains(annotationName)) { + if (exemptingMethodAnnotations.contains(annotationName)) { return true; } } 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 99b4bc659f4..fa1742671a2 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnusedVariable.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedVariable.java @@ -125,9 +125,9 @@ severity = WARNING, documentSuppression = false) public final class UnusedVariable extends BugChecker implements CompilationUnitTreeMatcher { - private static final String EXEMPT_PREFIX = "unused"; + private final ImmutableSet exemptPrefixes; - private static final ImmutableSet EXEMPT_NAMES = ImmutableSet.of("ignored"); + private final ImmutableSet exemptNames; /** * The set of annotation full names which exempt annotated element from being reported as unused. @@ -140,15 +140,19 @@ public final class UnusedVariable extends BugChecker implements CompilationUnitT "javax.persistence.Version", "javax.xml.bind.annotation.XmlElement", "org.junit.Rule", + "org.junit.jupiter.api.extension.RegisterExtension", "org.openqa.selenium.support.FindAll", "org.openqa.selenium.support.FindBy", "org.openqa.selenium.support.FindBys", "org.apache.beam.sdk.transforms.DoFn.TimerId", - "org.apache.beam.sdk.transforms.DoFn.StateId"); + "org.apache.beam.sdk.transforms.DoFn.StateId", + "org.springframework.boot.test.mock.mockito.MockBean"); // TODO(ghm): Find a sensible place to dedupe this with UnnecessarilyVisible. private static final ImmutableSet ANNOTATIONS_INDICATING_PARAMETERS_SHOULD_BE_CHECKED = ImmutableSet.of( + "com.google.errorprone.refaster.annotation.AfterTemplate", + "com.google.errorprone.refaster.annotation.BeforeTemplate", "com.google.inject.Inject", "com.google.inject.Provides", "com.google.inject.multibindings.ProvidesIntoMap", @@ -175,13 +179,24 @@ public final class UnusedVariable extends BugChecker implements CompilationUnitT @Inject UnusedVariable(ErrorProneFlags flags) { - ImmutableSet.Builder methodAnnotationsExemptingParameters = - ImmutableSet.builder().add("org.robolectric.annotation.Implementation"); - flags - .getList("Unused:methodAnnotationsExemptingParameters") - .ifPresent(methodAnnotationsExemptingParameters::addAll); - this.methodAnnotationsExemptingParameters = methodAnnotationsExemptingParameters.build(); + this.methodAnnotationsExemptingParameters = + ImmutableSet.builder() + .add("org.robolectric.annotation.Implementation") + .addAll(flags.getListOrEmpty("Unused:methodAnnotationsExemptingParameters")) + .build(); this.reportInjectedFields = flags.getBoolean("Unused:ReportInjectedFields").orElse(false); + + this.exemptNames = + ImmutableSet.builder() + .add("ignored") + .addAll(flags.getListOrEmpty("Unused:exemptNames")) + .build(); + + this.exemptPrefixes = + ImmutableSet.builder() + .add("unused") + .addAll(flags.getSetOrEmpty("Unused:exemptPrefixes")) + .build(); } @Override @@ -563,10 +578,11 @@ private static boolean exemptedByAnnotation(List annot return false; } - private static boolean exemptedByName(Name name) { + private boolean exemptedByName(Name name) { String nameString = name.toString(); - return Ascii.toLowerCase(nameString).startsWith(EXEMPT_PREFIX) - || EXEMPT_NAMES.contains(nameString); + String nameStringLower = Ascii.toLowerCase(nameString); + return exemptPrefixes.stream().anyMatch(nameStringLower::startsWith) + || exemptNames.contains(nameString); } private class VariableFinder extends TreePathScanner { @@ -623,6 +639,13 @@ && exemptedFieldBySuperType(getType(variableTree), state)) { if (variableTree.getName().contentEquals("this")) { return null; } + // Ignore if parameter is part of canonical record constructor; tree does not seem + // to contain usage in that case, but parameter is always used implicitly + // For compact canonical constructor parameters don't have record flag so need to + // check constructor flags (`symbol.owner`) instead + if (hasRecordFlag(symbol.owner)) { + return null; + } unusedElements.put(symbol, getCurrentPath()); if (!isParameterSubjectToAnalysis(symbol)) { onlyCheckForReassignments.add(symbol); @@ -645,7 +668,7 @@ private boolean isFieldEligibleForChecking(VariableTree variableTree, VarSymbol && ASTHelpers.hasDirectAnnotationWithSimpleName(variableTree, "Inject")) { return true; } - if ((symbol.flags() & RECORD_FLAG) == RECORD_FLAG) { + if (hasRecordFlag(symbol)) { return false; } return canBeRemoved(symbol) && !SPECIAL_FIELDS.contains(symbol.getSimpleName().toString()); @@ -653,6 +676,10 @@ private boolean isFieldEligibleForChecking(VariableTree variableTree, VarSymbol private static final long RECORD_FLAG = 1L << 61; + private boolean hasRecordFlag(Symbol symbol) { + return (symbol.flags() & RECORD_FLAG) == RECORD_FLAG; + } + /** Returns whether {@code sym} can be removed without updating call sites in other files. */ private boolean isParameterSubjectToAnalysis(Symbol sym) { checkArgument(sym.getKind() == ElementKind.PARAMETER); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java index f74a48cdda8..7db01019079 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java @@ -32,7 +32,9 @@ import static com.google.errorprone.util.ASTHelpers.stripParentheses; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; import com.google.errorprone.BugPattern; +import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; @@ -59,6 +61,7 @@ import com.sun.tools.javac.code.Type; import java.util.HashSet; import java.util.Set; +import javax.inject.Inject; /** * Checker that recommends annotating a method with {@code @CanIgnoreReturnValue} if the method @@ -74,8 +77,12 @@ public final class CanIgnoreReturnValueSuggester extends BugChecker implements M private static final String AUTO_VALUE = "com.google.auto.value.AutoValue"; private static final String IMMUTABLE = "com.google.errorprone.annotations.Immutable"; - private static final String CRV = "com.google.errorprone.annotations.CheckReturnValue"; private static final String CIRV = "com.google.errorprone.annotations.CanIgnoreReturnValue"; + private static final ImmutableSet EXEMPTING_METHOD_ANNOTATIONS = + ImmutableSet.of( + CIRV, + "com.google.errorprone.annotations.CheckReturnValue", + "com.google.errorprone.refaster.annotation.AfterTemplate"); private static final Supplier PROTO_BUILDER = VisitorState.memoize(s -> s.getTypeFromString("com.google.protobuf.MessageLite.Builder")); @@ -83,12 +90,24 @@ public final class CanIgnoreReturnValueSuggester extends BugChecker implements M private static final ImmutableSet BANNED_METHOD_PREFIXES = ImmutableSet.of("get", "is", "has", "new", "clone", "copy"); + private final ImmutableSet exemptingMethodAnnotations; + + @Inject + public CanIgnoreReturnValueSuggester(ErrorProneFlags errorProneFlags) { + this.exemptingMethodAnnotations = + Sets.union( + EXEMPTING_METHOD_ANNOTATIONS, + errorProneFlags.getSetOrEmpty("CanIgnoreReturnValue:ExemptingMethodAnnotations")) + .immutableCopy(); + } + @Override public Description matchMethod(MethodTree methodTree, VisitorState state) { MethodSymbol methodSymbol = getSymbol(methodTree); - // if the method is already directly annotated w/ @CRV or @CIRV, bail out - if (hasAnnotation(methodSymbol, CRV, state) || hasAnnotation(methodSymbol, CIRV, state)) { + // If the method has an exempting annotation, then bail out. + if (exemptingMethodAnnotations.stream() + .anyMatch(annotation -> ASTHelpers.hasAnnotation(methodSymbol, annotation, state))) { return Description.NO_MATCH; } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/CollectionIncompatibleType.java b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/CollectionIncompatibleType.java index 57db1931705..370c119b15b 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/CollectionIncompatibleType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/CollectionIncompatibleType.java @@ -26,8 +26,8 @@ import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.MemberReferenceTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; -import com.google.errorprone.bugpatterns.TypeCompatibilityUtils; -import com.google.errorprone.bugpatterns.TypeCompatibilityUtils.TypeCompatibilityReport; +import com.google.errorprone.bugpatterns.TypeCompatibility; +import com.google.errorprone.bugpatterns.TypeCompatibility.TypeCompatibilityReport; import com.google.errorprone.bugpatterns.collectionincompatibletype.AbstractCollectionIncompatibleTypeMatcher.MatchResult; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; @@ -72,13 +72,13 @@ private enum FixType { } private final FixType fixType; - private final TypeCompatibilityUtils typeCompatibilityUtils; + private final TypeCompatibility typeCompatibility; @Inject - CollectionIncompatibleType(ErrorProneFlags flags) { + CollectionIncompatibleType(TypeCompatibility typeCompatibility, ErrorProneFlags flags) { this.fixType = flags.getEnum("CollectionIncompatibleType:FixType", FixType.class).orElse(FixType.NONE); - this.typeCompatibilityUtils = TypeCompatibilityUtils.fromFlags(flags); + this.typeCompatibility = typeCompatibility; } @Override @@ -99,8 +99,7 @@ public Description match(ExpressionTree tree, VisitorState state) { Types types = state.getTypes(); TypeCompatibilityReport compatibilityReport = - typeCompatibilityUtils.compatibilityOfTypes( - result.targetType(), result.sourceType(), state); + typeCompatibility.compatibilityOfTypes(result.targetType(), result.sourceType(), state); if (compatibilityReport.isCompatible()) { return NO_MATCH; } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/IncompatibleArgumentType.java b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/IncompatibleArgumentType.java index 581d316ba86..2390babf1a0 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/IncompatibleArgumentType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/IncompatibleArgumentType.java @@ -22,14 +22,13 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableListMultimap; import com.google.errorprone.BugPattern; -import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.CompatibleWith; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; -import com.google.errorprone.bugpatterns.TypeCompatibilityUtils; -import com.google.errorprone.bugpatterns.TypeCompatibilityUtils.TypeCompatibilityReport; +import com.google.errorprone.bugpatterns.TypeCompatibility; +import com.google.errorprone.bugpatterns.TypeCompatibility.TypeCompatibilityReport; import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; import com.google.errorprone.util.Signatures; @@ -58,11 +57,11 @@ severity = ERROR) public class IncompatibleArgumentType extends BugChecker implements MethodInvocationTreeMatcher { - private final TypeCompatibilityUtils typeCompatibilityUtils; + private final TypeCompatibility typeCompatibility; @Inject - IncompatibleArgumentType(ErrorProneFlags flags) { - this.typeCompatibilityUtils = TypeCompatibilityUtils.fromFlags(flags); + IncompatibleArgumentType(TypeCompatibility typeCompatibility) { + this.typeCompatibility = typeCompatibility; } // Nonnull requiredType: The type I need is bound, in requiredType @@ -134,7 +133,7 @@ private void reportAnyViolations( if (requiredType.type() != null) { // Report a violation for this type TypeCompatibilityReport report = - typeCompatibilityUtils.compatibilityOfTypes(requiredType.type(), argType, state); + typeCompatibility.compatibilityOfTypes(requiredType.type(), argType, state); if (!report.isCompatible()) { state.reportMatch( describeViolation(argument, argType, requiredType.type(), types, state)); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/TruthIncompatibleType.java b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/TruthIncompatibleType.java index 25d7bf7460e..060d1448b48 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/TruthIncompatibleType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/TruthIncompatibleType.java @@ -39,8 +39,8 @@ import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; -import com.google.errorprone.bugpatterns.TypeCompatibilityUtils; -import com.google.errorprone.bugpatterns.TypeCompatibilityUtils.TypeCompatibilityReport; +import com.google.errorprone.bugpatterns.TypeCompatibility; +import com.google.errorprone.bugpatterns.TypeCompatibility.TypeCompatibilityReport; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.Matcher; import com.google.errorprone.suppliers.Supplier; @@ -96,6 +96,16 @@ public class TruthIncompatibleType extends BugChecker implements MethodInvocatio .namedAnyOf( "contains", "containsExactly", "doesNotContain", "containsAnyOf", "containsNoneOf"); + private static final Matcher IS_ANY_OF = + instanceMethod() + .onDescendantOf("com.google.common.truth.Subject") + .namedAnyOf("isAnyOf", "isNoneOf"); + + private static final Matcher IS_IN = + instanceMethod() + .onDescendantOf("com.google.common.truth.Subject") + .namedAnyOf("isIn", "isNotIn"); + private static final Matcher VECTOR_CONTAINS = instanceMethod() .onDescendantOfAny( @@ -144,17 +154,22 @@ public class TruthIncompatibleType extends BugChecker implements MethodInvocatio private static final Supplier CORRESPONDENCE = typeFromString("com.google.common.truth.Correspondence"); - private final TypeCompatibilityUtils typeCompatibilityUtils; + private final TypeCompatibility typeCompatibility; + private final boolean flagMoreCases; @Inject - TruthIncompatibleType(ErrorProneFlags flags) { - this.typeCompatibilityUtils = TypeCompatibilityUtils.fromFlags(flags); + TruthIncompatibleType(TypeCompatibility typeCompatibility, ErrorProneFlags flags) { + this.typeCompatibility = typeCompatibility; + + this.flagMoreCases = flags.getBoolean("TruthIncompatibleType:FlagMoreCases").orElse(true); } @Override public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { Streams.concat( matchEquality(tree, state), + matchIsAnyOf(tree, state), + matchIsIn(tree, state), matchVectorContains(tree, state), matchArrayContains(tree, state), matchScalarContains(tree, state), @@ -194,6 +209,38 @@ private Stream matchEquality(MethodInvocationTree tree, VisitorStat return checkCompatibility(getOnlyElement(tree.getArguments()), targetType, sourceType, state); } + private Stream matchIsAnyOf(MethodInvocationTree tree, VisitorState state) { + if (!flagMoreCases || !IS_ANY_OF.matches(tree, state)) { + return Stream.empty(); + } + ExpressionTree receiver = getReceiver(tree); + if (!START_OF_ASSERTION.matches(receiver, state)) { + return Stream.empty(); + } + Type targetType = + getType(ignoringCasts(getOnlyElement(((MethodInvocationTree) receiver).getArguments()))); + return matchScalarContains(tree, targetType, state); + } + + private Stream matchIsIn(MethodInvocationTree tree, VisitorState state) { + if (!flagMoreCases || !IS_IN.matches(tree, state)) { + return Stream.empty(); + } + ExpressionTree receiver = getReceiver(tree); + if (!START_OF_ASSERTION.matches(receiver, state)) { + return Stream.empty(); + } + + Type targetType = + getType(ignoringCasts(getOnlyElement(((MethodInvocationTree) receiver).getArguments()))); + Type sourceType = + getIterableTypeArg( + getType(getOnlyElement(tree.getArguments())), + getOnlyElement(tree.getArguments()), + state); + return checkCompatibility(getOnlyElement(tree.getArguments()), targetType, sourceType, state); + } + private Stream matchVectorContains(MethodInvocationTree tree, VisitorState state) { if (!VECTOR_CONTAINS.matches(tree, state)) { return Stream.empty(); @@ -242,13 +289,16 @@ private Stream matchScalarContains(MethodInvocationTree tree, Visit if (!START_OF_ASSERTION.matches(receiver, state)) { return Stream.empty(); } - - Tree argument = ignoringCasts(getOnlyElement(((MethodInvocationTree) receiver).getArguments())); Type targetType = getIterableTypeArg( getOnlyElement(getSymbol((MethodInvocationTree) receiver).getParameters()).type, - argument, + ignoringCasts(getOnlyElement(((MethodInvocationTree) receiver).getArguments())), state); + return matchScalarContains(tree, targetType, state); + } + + private Stream matchScalarContains( + MethodInvocationTree tree, Type targetType, VisitorState state) { MethodSymbol methodSymbol = getSymbol(tree); return Streams.mapWithIndex( tree.getArguments().stream(), @@ -419,7 +469,7 @@ private static boolean isNonVarargsCall( private Stream checkCompatibility( ExpressionTree tree, Type targetType, Type sourceType, VisitorState state) { TypeCompatibilityReport compatibilityReport = - typeCompatibilityUtils.compatibilityOfTypes(targetType, sourceType, state); + typeCompatibility.compatibilityOfTypes(targetType, sourceType, state); if (compatibilityReport.isCompatible()) { return Stream.empty(); } @@ -453,7 +503,7 @@ protected Tree defaultAction(Tree node, Void unused) { @Override public Tree visitTypeCast(TypeCastTree node, Void unused) { - return node.getExpression().accept(this, null); + return getType(node).isPrimitive() ? node : node.getExpression().accept(this, null); } @Override 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 11c29293f11..696bac19f90 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 @@ -91,8 +91,7 @@ public final class Inliner extends BugChecker @Inject Inliner(ErrorProneFlags flags) { - this.apiPrefixes = - ImmutableSet.copyOf(flags.getSet(PREFIX_FLAG).orElse(ImmutableSet.of())); + this.apiPrefixes = flags.getSetOrEmpty(PREFIX_FLAG); this.skipCallsitesWithComments = flags.getBoolean(SKIP_COMMENTS_FLAG).orElse(true); this.checkFixCompiles = flags.getBoolean(CHECK_FIX_COMPILES).orElse(false); } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java index bca48066a80..4354141414b 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java @@ -248,7 +248,9 @@ private boolean enclosingAnnotationDefaultsNonTypeVariablesToNonNull( if (hasAnnotation(sym, "com.google.protobuf.Internal$ProtoNonnullApi", state)) { return true; } - if (hasAnnotation(sym, "org.jspecify.nullness.NullMarked", state) + if ((hasAnnotation(sym, "org.jspecify.annotations.NullMarked", state) + // We break this string to avoid having it rewritten by Copybara. + || hasAnnotation(sym, "org.jspecify.null" + "ness.NullMarked", state)) && weTrustNullMarkedOn(sym, state)) { return true; } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullnessUtils.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullnessUtils.java index 90f8ce83125..e1dbc7e815f 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullnessUtils.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullnessUtils.java @@ -129,7 +129,9 @@ static boolean nullnessChecksShouldBeConservative(ErrorProneFlags flags) { static boolean isInNullMarkedScope(Symbol sym, VisitorState state) { for (; sym != null; sym = sym.getEnclosingElement()) { - if (hasAnnotation(sym, "org.jspecify.nullness.NullMarked", state)) { + if (hasAnnotation(sym, "org.jspecify.annotations.NullMarked", state) + // We break this string to avoid having it rewritten by Copybara. + || hasAnnotation(sym, "org.jspecify.null" + "ness.NullMarked", state)) { return true; } } @@ -353,7 +355,7 @@ private static NullableAnnotationToUse pickNullableAnnotation(VisitorState state .orElse( state.isAndroidCompatible() ? "androidx.annotation.Nullable" - : "org.jspecify.nullness.Nullable"); + : "org.jspecify.annotations.Nullable"); if (sym != null) { ClassSymbol classSym = (ClassSymbol) sym; if (classSym.isAnnotationType()) { @@ -381,8 +383,9 @@ private static boolean isTypeUse(String className) { case "org.checkerframework.checker.nullness.qual.Nullable": case "org.jspecify.annotations.NonNull": case "org.jspecify.annotations.Nullable": - case "org.jspecify.nullness.NonNull": - case "org.jspecify.nullness.Nullable": + // We break these strings to avoid having them rewritten by Copybara. + case "org.jspecify.null" + "ness.NonNull": + case "org.jspecify.null" + "ness.Nullable": return true; default: // TODO(cpovirk): Detect type-use-ness from the class symbol if it's available? 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 8f066e93afd..e06bc53213b 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 @@ -257,6 +257,18 @@ public Void visitMethod(MethodTree tree, Void unused) { } void doVisitMethod(MethodTree tree) { + if (beingConservative) { + /* + * In practice, we don't see any of the cases in which a compliant implementation of a + * method like Map.get would never return null. But we do see cases in which people have + * implemented Map.get to handle non-existent keys by returning "new Foo()" or by throwing + * IllegalArgumentException. Presumably, users of such methods would not be thrilled if we + * added @Nullable to their return types, given the effort that the types have gone to not + * to ever return null. So we avoid making such changes in conservative mode. + */ + return; + } + MethodSymbol possibleOverride = getSymbol(tree); /* diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/WellKnownThreadSafety.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/WellKnownThreadSafety.java index 632f57cd62c..63c08dbda4f 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/WellKnownThreadSafety.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/WellKnownThreadSafety.java @@ -27,8 +27,7 @@ public final class WellKnownThreadSafety implements ThreadSafety.KnownTypes { @Inject WellKnownThreadSafety(ErrorProneFlags flags, WellKnownMutability wellKnownMutability) { - List knownThreadSafe = - flags.getList("ThreadSafe:KnownThreadSafe").orElse(ImmutableList.of()); + ImmutableList knownThreadSafe = flags.getListOrEmpty("ThreadSafe:KnownThreadSafe"); this.knownThreadSafeClasses = buildThreadSafeClasses(knownThreadSafe, wellKnownMutability); this.knownUnsafeClasses = wellKnownMutability.getKnownMutableClasses(); } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/time/TimeUnitConversionChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/time/TimeUnitConversionChecker.java index 142beb2a830..5be09329940 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/time/TimeUnitConversionChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/time/TimeUnitConversionChecker.java @@ -32,6 +32,7 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; +import com.sun.tools.javac.code.Symbol; import java.util.concurrent.TimeUnit; /** Check for problematic or suspicious TimeUnit conversion calls. */ @@ -73,7 +74,11 @@ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState // TimeUnit SECONDS = TimeUnit.MINUTES; // long about2500 = SECONDS.toSeconds(42); // but... I think that's bad enough to ignore here :) - String timeUnitName = ASTHelpers.getSymbol(receiverOfConversion).getSimpleName().toString(); + Symbol receiverOfConversionSymbol = ASTHelpers.getSymbol(receiverOfConversion); + if (receiverOfConversionSymbol == null) { + return Description.NO_MATCH; + } + String timeUnitName = receiverOfConversionSymbol.getSimpleName().toString(); Optional receiver = Enums.getIfPresent(TimeUnit.class, timeUnitName); if (!receiver.isPresent()) { return Description.NO_MATCH; diff --git a/core/src/main/java/com/google/errorprone/refaster/UMemberSelect.java b/core/src/main/java/com/google/errorprone/refaster/UMemberSelect.java index 6ff2092bcd8..221ff3963d5 100644 --- a/core/src/main/java/com/google/errorprone/refaster/UMemberSelect.java +++ b/core/src/main/java/com/google/errorprone/refaster/UMemberSelect.java @@ -65,7 +65,7 @@ public Choice visitMemberSelect(MemberSelectTree fieldAccess, Unifier u @Override public Choice visitIdentifier(IdentifierTree ident, Unifier unifier) { Symbol sym = ASTHelpers.getSymbol(ident); - if (sym != null && sym.owner.type != null) { + if (sym != null && sym.owner.type != null && sym.owner.type.isReference()) { JCExpression thisIdent = unifier.thisExpression(sym.owner.type); return getIdentifier() .unify(ident.getName(), unifier) 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 5efa078e05b..332c5d80a3a 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -81,6 +81,7 @@ import com.google.errorprone.bugpatterns.ClassName; import com.google.errorprone.bugpatterns.ClassNamedLikeTypeParameter; import com.google.errorprone.bugpatterns.ClassNewInstance; +import com.google.errorprone.bugpatterns.ClosingStandardOutputStreams; import com.google.errorprone.bugpatterns.CollectionToArraySafeParameter; import com.google.errorprone.bugpatterns.CollectorShouldNotUseState; import com.google.errorprone.bugpatterns.ComparableAndComparator; @@ -246,6 +247,7 @@ import com.google.errorprone.bugpatterns.ModifySourceCollectionInStream; import com.google.errorprone.bugpatterns.ModifyingCollectionWithItself; import com.google.errorprone.bugpatterns.MultiVariableDeclaration; +import com.google.errorprone.bugpatterns.MultimapKeys; import com.google.errorprone.bugpatterns.MultipleParallelOrSequentialCalls; import com.google.errorprone.bugpatterns.MultipleTopLevelClasses; import com.google.errorprone.bugpatterns.MultipleUnaryOperatorsInMethodCall; @@ -319,6 +321,7 @@ import com.google.errorprone.bugpatterns.RequiredModifiersChecker; import com.google.errorprone.bugpatterns.RestrictedApiChecker; import com.google.errorprone.bugpatterns.RethrowReflectiveOperationExceptionAsLinkageError; +import com.google.errorprone.bugpatterns.ReturnAtTheEndOfVoidFunction; import com.google.errorprone.bugpatterns.ReturnValueIgnored; import com.google.errorprone.bugpatterns.ReturnsNullCollection; import com.google.errorprone.bugpatterns.RobolectricShadowDirectlyOn; @@ -367,6 +370,7 @@ import com.google.errorprone.bugpatterns.TreeToString; import com.google.errorprone.bugpatterns.TruthAssertExpected; import com.google.errorprone.bugpatterns.TruthConstantAsserts; +import com.google.errorprone.bugpatterns.TruthContainsExactlyElementsInUsage; import com.google.errorprone.bugpatterns.TruthGetOrDefault; import com.google.errorprone.bugpatterns.TruthSelfEquals; import com.google.errorprone.bugpatterns.TryFailRefactoring; @@ -389,6 +393,7 @@ import com.google.errorprone.bugpatterns.UnnecessarilyVisible; import com.google.errorprone.bugpatterns.UnnecessaryAnonymousClass; import com.google.errorprone.bugpatterns.UnnecessaryAssignment; +import com.google.errorprone.bugpatterns.UnnecessaryAsync; import com.google.errorprone.bugpatterns.UnnecessaryBoxedAssignment; import com.google.errorprone.bugpatterns.UnnecessaryBoxedVariable; import com.google.errorprone.bugpatterns.UnnecessaryDefaultInEnumSwitch; @@ -851,6 +856,7 @@ public static ScannerSupplier warningChecks() { ClassCanBeStatic.class, ClassNewInstance.class, CloseableProvides.class, + ClosingStandardOutputStreams.class, CollectionUndefinedEquality.class, CollectorShouldNotUseState.class, ComparableAndComparator.class, @@ -966,6 +972,7 @@ public static ScannerSupplier warningChecks() { ModifiedButNotUsed.class, ModifyCollectionInEnhancedForLoop.class, ModifySourceCollectionInStream.class, + MultimapKeys.class, MultipleParallelOrSequentialCalls.class, MultipleUnaryOperatorsInMethodCall.class, MutablePublicArray.class, @@ -1009,6 +1016,7 @@ public static ScannerSupplier warningChecks() { ReachabilityFenceUsage.class, ReferenceEquality.class, RethrowReflectiveOperationExceptionAsLinkageError.class, + ReturnAtTheEndOfVoidFunction.class, ReturnFromVoid.class, RobolectricShadowDirectlyOn.class, RxReturnValueIgnored.class, @@ -1045,6 +1053,7 @@ public static ScannerSupplier warningChecks() { UndefinedEquals.class, UnicodeEscape.class, UnnecessaryAssignment.class, + UnnecessaryAsync.class, UnnecessaryLambda.class, UnnecessaryLongToIntConversion.class, UnnecessaryMethodInvocationMatcher.class, @@ -1178,6 +1187,7 @@ public static ScannerSupplier warningChecks() { TimeUnitMismatch.class, TooManyParameters.class, TransientMisuse.class, + TruthContainsExactlyElementsInUsage.class, TryFailRefactoring.class, TryWithResourcesVariable.class, TypeParameterNaming.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/AddressSelectionTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/AddressSelectionTest.java index 0d1537570ac..b2b48638c07 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/AddressSelectionTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/AddressSelectionTest.java @@ -88,6 +88,24 @@ public void negativeLocalhost() throws Exception { .doTest(); } + @Test + public void negativeNumeric() throws Exception { + compilationHelper + .addSourceLines( + "Test.java", + "import java.net.InetAddress;", + "import java.net.InetSocketAddress;", + "import java.net.Socket;", + "class Test {", + " void f() throws Exception {", + " new Socket(\"1.2.3.4\", 80);", + " InetAddress.getByName(\"2001:db8:85a3:8d3:1319:8a2e:370:7348\");", + " new InetSocketAddress(\"::ffff:192.0.2.128\", 80);", + " }", + "}") + .doTest(); + } + @Test public void refactor() throws Exception { refactoringTestHelper diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/BugCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/BugCheckerTest.java new file mode 100644 index 00000000000..ad435d73b35 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/BugCheckerTest.java @@ -0,0 +1,184 @@ +/* + * Copyright 2023 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.BugPattern; +import com.google.errorprone.CompilationTestHelper; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.VariableTree; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class BugCheckerTest { + @Test + public void isSuppressed_withoutVisitorState() { + CompilationTestHelper.newInstance(LegacySuppressionCheck.class, getClass()) + .addSourceLines( + "A.java", + "class A {", + " void m() {", + " // BUG: Diagnostic contains: []", + " int unsuppressed;", + " // BUG: Diagnostic contains: []", + " @SuppressWarnings(\"foo\") int unrelatedSuppression;", + " // BUG: Diagnostic contains: [Suppressible]", + " @SuppressWarnings(\"Suppressible\") int suppressed;", + " // BUG: Diagnostic contains: [Suppressible]", + " @SuppressWarnings(\"Alternative\") int suppressedWithAlternativeName;", + " // BUG: Diagnostic contains: [Suppressible]", + " @SuppressWarnings(\"all\") int allSuppressed;", + " // BUG: Diagnostic contains: [Suppressible]", + " @SuppressWarnings({\"foo\", \"Suppressible\"}) int alsoSuppressed;", + " // BUG: Diagnostic contains: [Suppressible]", + " @SuppressWarnings({\"all\", \"foo\"}) int redundantlySuppressed;", + " // BUG: Diagnostic contains: [Suppressible]", + " @SuppressWarnings({\"all\", \"OnlySuppressedInsideDeprecatedCode\"}) int" + + " ineffectiveSuppression;", + " // BUG: Diagnostic contains: []", + " @Deprecated int unuspportedSuppression;", + " }", + "}") + .doTest(); + } + + @Test + public void isSuppressed() { + CompilationTestHelper.newInstance(SuppressibleCheck.class, getClass()) + .addSourceLines( + "A.java", + "class A {", + " void m() {", + " // BUG: Diagnostic contains:", + " int unsuppressed;", + " // BUG: Diagnostic contains:", + " @SuppressWarnings(\"foo\") int unrelatedSuppression;", + " @SuppressWarnings(\"Suppressible\") int suppressed;", + " @SuppressWarnings(\"Alternative\") int suppressedWithAlternativeName;", + " @SuppressWarnings(\"all\") int allSuppressed;", + " @SuppressWarnings({\"foo\", \"Suppressible\"}) int alsoSuppressed;", + " @SuppressWarnings({\"all\", \"foo\"}) int redundantlySuppressed;", + " }", + "}") + .doTest(); + } + + @Test + public void isSuppressed_customSuppressionAnnotation() { + CompilationTestHelper.newInstance(CustomSuppressibilityCheck.class, getClass()) + .addSourceLines( + "A.java", + "class A {", + " void m() {", + " // BUG: Diagnostic contains:", + " int unsuppressed;", + " // BUG: Diagnostic contains:", + " @SuppressWarnings({\"all\", \"OnlySuppressedInsideDeprecatedCode\"}) int" + + " ineffectiveSuppression;", + " @Deprecated int suppressed;", + " }", + "}") + .doTest(); + } + + @Test + public void isSuppressed_disableWarningsInGeneratedCode() { + CompilationTestHelper.newInstance(CustomSuppressibilityCheck.class, getClass()) + .setArgs("-XepDisableWarningsInGeneratedCode") + .addSourceLines( + "A.java", + "import javax.annotation.processing.Generated;", + "class A {", + " void m() {", + " // BUG: Diagnostic contains:", + " @Generated(\"some-tool\") int unsuppressed;", + " }", + "}") + .doTest(); + + // The check is suppressed if its severity is downgraded to `WARNING`. + CompilationTestHelper.newInstance(CustomSuppressibilityCheck.class, getClass()) + .setArgs( + "-XepDisableWarningsInGeneratedCode", "-Xep:OnlySuppressedInsideDeprecatedCode:WARN") + .addSourceLines( + "A.java", + "import javax.annotation.processing.Generated;", + "class A {", + " void m() {", + " @Generated(\"some-tool\") int unsuppressed;", + " }", + "}") + .doTest(); + } + + @BugPattern( + name = "SuppressionReporter", + summary = + "Tells whether some other checks are suppressed according to the deprecated method " + + "`BugChecker#isSuppressed(Tree)`", + severity = ERROR, + suppressionAnnotations = {}) + public static final class LegacySuppressionCheck extends BugChecker + implements VariableTreeMatcher { + private final ImmutableList checks = + ImmutableList.of(new SuppressibleCheck(), new CustomSuppressibilityCheck()); + + @Override + @SuppressWarnings("deprecation") // testing deprecated method + public Description matchVariable(VariableTree tree, VisitorState state) { + ImmutableList suppressions = + checks.stream() + .filter(check -> check.isSuppressed(tree)) + .map(BugChecker::canonicalName) + .collect(toImmutableList()); + + return buildDescription(tree).setMessage("Suppressions: " + suppressions).build(); + } + } + + @BugPattern( + name = "Suppressible", + altNames = "Alternative", + summary = "Can be suppressed", + severity = ERROR) + public static final class SuppressibleCheck extends BugChecker implements VariableTreeMatcher { + @Override + public Description matchVariable(VariableTree tree, VisitorState state) { + return describeMatch(tree); + } + } + + @BugPattern( + name = "OnlySuppressedInsideDeprecatedCode", + summary = "Can be suppressed using `@Deprecated`, but not `@SuppressWarnings`", + severity = ERROR, + suppressionAnnotations = Deprecated.class) + public static final class CustomSuppressibilityCheck extends BugChecker + implements VariableTreeMatcher { + @Override + public Description matchVariable(VariableTree tree, VisitorState state) { + return describeMatch(tree); + } + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/ClosingStandardOutputStreamsTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/ClosingStandardOutputStreamsTest.java new file mode 100644 index 00000000000..94f3636fe7b --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/ClosingStandardOutputStreamsTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2023 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import com.google.errorprone.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ClosingStandardOutputStreamsTest { + private final CompilationTestHelper testHelper = + CompilationTestHelper.newInstance(ClosingStandardOutputStreams.class, getClass()); + + @Test + public void positive() { + testHelper + .addSourceLines( + "Test.java", + "import java.io.OutputStreamWriter;", + "import java.io.PrintWriter;", + "class Test {", + " void f() {", + " // BUG: Diagnostic contains:", + " try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.err), true))" + + " {", + " pw.println(\"hello\");", + " }", + " }", + "}") + .doTest(); + } + + @Test + public void negative() { + testHelper + .addSourceLines( + "Test.java", + "import java.io.OutputStreamWriter;", + "import java.io.PrintWriter;", + "class Test {", + " void f(OutputStreamWriter os) {", + " System.err.println(42);", + " try (PrintWriter pw = new PrintWriter(os, true)) {", + " pw.println(\"hello\");", + " }", + " }", + "}") + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/CompareToZeroTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/CompareToZeroTest.java index 7a54aa2e92c..7fccb39599a 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/CompareToZeroTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/CompareToZeroTest.java @@ -44,6 +44,20 @@ public void positive() { .doTest(); } + @Test + public void positiveStaticCompare() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " boolean test(boolean x, boolean y) {", + " // BUG: Diagnostic contains: compared", + " return Boolean.compare(x, y) == -1;", + " }", + "}") + .doTest(); + } + @Test public void positiveSuggestionForConsistency() { compilationHelper diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java index 5c811e456d0..f67d7532730 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java @@ -47,12 +47,13 @@ public void positive() { " @DoNotCall final void g() {}", " void m() {", " // BUG: Diagnostic contains:", - " // This method should not be called: satisfying explanation", + " // Test.f() should not be called: satisfying explanation", " f();", " // BUG: Diagnostic contains:", - " // This method should not be called, see its documentation for details", + " // Test.g() should not be called, see its documentation for details.", " g();", " // BUG: Diagnostic contains:", + " // Test.g() should not be called, see its documentation for details.", " Runnable r = this::g;", " }", "}") @@ -264,7 +265,9 @@ public void noDNConClasspath() { "Test.java", "class Test {", " void m() {", - " // BUG: Diagnostic contains: This method should not be called", + " // BUG: Diagnostic contains:" + + " com.google.errorprone.bugpatterns.DoNotCallCheckerTest.DNCTest.f() should not" + + " be called, see its documentation for details.", " com.google.errorprone.bugpatterns.DoNotCallCheckerTest.DNCTest.f();", " }", "}") diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/MemberNameTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/MemberNameTest.java index 6e58cfdb57a..a1da16d7349 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/MemberNameTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/MemberNameTest.java @@ -463,4 +463,32 @@ public void methodReference() { "}") .doTest(); } + + @Test + public void methodNameWithMatchingReturnType() { + refactoringHelper + .addInputLines( + "Test.java", + "class Test {", + " private Object Object() {", + " return null;", + " }", + "", + " void call() {", + " Object();", + " }", + "}") + .addOutputLines( + "Test.java", + "class Test {", + " private Object object() {", + " return null;", + " }", + "", + " void call() {", + " object();", + " }", + "}") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/MissingBracesTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/MissingBracesTest.java index 961e30a6872..dd2ca60b7ac 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/MissingBracesTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/MissingBracesTest.java @@ -76,4 +76,33 @@ public void negative() { "}") .doTest(); } + + @Test + public void suggestedFixForNestedProblemsWithOverlappingBracePostfixInsertions() { + BugCheckerRefactoringTestHelper.newInstance(MissingBraces.class, getClass()) + .addInputLines( + "Test.java", + "import java.util.List;", + "class Test {", + " private String findNotNull(List items) {", + " for (String item : items)", + " if (item != null) return item;", + " return null;", + " }", + "}") + .addOutputLines( + "Test.java", + "import java.util.List;", + "class Test {", + " private String findNotNull(List items) {", + " for (String item : items) {", + " if (item != null) {", + " return item;", + " }", + " }", + " return null;", + " }", + "}") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/MissingDefaultTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/MissingDefaultTest.java index 05f77cab6c8..9799b78fe4f 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/MissingDefaultTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/MissingDefaultTest.java @@ -263,4 +263,23 @@ public void arrowSwitchNegative() { "}") .doTest(); } + + @Test + public void arrowComment() { + assumeTrue(RuntimeVersion.isAtLeast14()); + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " void f() {}", + " void m(int i) {", + " switch (i) {", + " case 0 -> f();", + " case 1 -> f();", + " default -> f();", + " }", + " }", + "}") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/MultimapKeysTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/MultimapKeysTest.java new file mode 100644 index 00000000000..1a7b52a5227 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/MultimapKeysTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2023 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class MultimapKeysTest { + private final BugCheckerRefactoringTestHelper refactoring = + BugCheckerRefactoringTestHelper.newInstance(MultimapKeys.class, getClass()); + + @Test + public void positive() { + refactoring + .addInputLines( + "Test.java", + "import com.google.common.collect.Multimap;", + "class Test {", + " void test(Multimap m) {", + " for (String k : m.keys()) {}", + " }", + "}") + .addOutputLines( + "Test.java", + "import com.google.common.collect.Multimap;", + "class Test {", + " void test(Multimap m) {", + " for (String k : m.keySet()) {}", + " }", + "}") + .doTest(); + } + + @Test + public void positiveSubclass() { + refactoring + .addInputLines( + "Test.java", + "import com.google.common.collect.SetMultimap;", + "class Test {", + " void test(SetMultimap m) {", + " for (String k : m.keys()) {}", + " }", + "}") + .addOutputLines( + "Test.java", + "import com.google.common.collect.SetMultimap;", + "class Test {", + " void test(SetMultimap m) {", + " for (String k : m.keySet()) {}", + " }", + "}") + .doTest(); + } + + @Test + public void positiveFunctional() { + refactoring + .addInputLines( + "Test.java", + "import com.google.common.collect.Multimap;", + "class Test {", + " void test(Multimap m) {", + " m.keys().forEach(x -> {});", + " }", + "}") + .addOutputLines( + "Test.java", + "import com.google.common.collect.Multimap;", + "class Test {", + " void test(Multimap m) {", + " m.keySet().forEach(x -> {});", + " }", + "}") + .doTest(); + } + + @Test + public void negative() { + refactoring + .addInputLines( + "Test.java", + "import com.google.common.collect.Multimap;", + "import com.google.common.collect.Multiset;", + "class Test {", + " Multiset test(Multimap m) {", + " return m.keys();", + " }", + "}") + .expectUnchanged() + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/NamedLikeContextualKeywordTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/NamedLikeContextualKeywordTest.java index da56f24f412..7f2a74d52f3 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/NamedLikeContextualKeywordTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/NamedLikeContextualKeywordTest.java @@ -46,7 +46,6 @@ public void instanceMethodName_error() { " foo = new NullPointerException(\"uh oh\");", " }", "}") - .setArgs(ImmutableList.of("-XepOpt:NamedLikeContextualKeyword:EnableMethodNames")) .doTest(); } @@ -65,7 +64,6 @@ public void staticMethodName_error() { " foo = new NullPointerException(\"uh oh\");", " }", "}") - .setArgs(ImmutableList.of("-XepOpt:NamedLikeContextualKeyword:EnableMethodNames")) .doTest(); } @@ -85,7 +83,6 @@ public void autoOneOfMethodName_noError() { " foo = new NullPointerException(\"uh oh\");", " }", "}") - .setArgs("-XepOpt:NamedLikeContextualKeyword:EnableMethodNames") .doTest(); } @@ -105,7 +102,6 @@ public void autoValueMethodName_noError() { " foo = new NullPointerException(\"uh oh\");", " }", "}") - .setArgs("-XepOpt:NamedLikeContextualKeyword:EnableMethodNames") .doTest(); } @@ -126,7 +122,6 @@ public void generatedButNotAuto_error() { " foo = new NullPointerException(\"uh oh\");", " }", "}") - .setArgs("-XepOpt:NamedLikeContextualKeyword:EnableMethodNames") .doTest(); } @@ -140,7 +135,6 @@ public void className_error() { " public module() {", " }", "}") - .setArgs(ImmutableList.of("-XepOpt:NamedLikeContextualKeyword:EnableClassNames")) .doTest(); } @@ -161,10 +155,6 @@ public void yieldInSwitch_noError() { " };", " }", "}") - .setArgs( - ImmutableList.of( - "-XepOpt:NamedLikeContextualKeyword:EnableMethodNames", - "-XepOpt:NamedLikeContextualKeyword:EnableClassNames")) .doTest(); } @@ -188,10 +178,6 @@ public void interfaceImplementation_noError() { " @SuppressWarnings(\"NamedLikeContextualKeyword\")", " void yield();", "}") - .setArgs( - ImmutableList.of( - "-XepOpt:NamedLikeContextualKeyword:EnableMethodNames", - "-XepOpt:NamedLikeContextualKeyword:EnableClassNames")) .doTest(); } @@ -215,10 +201,6 @@ public void nonAnnotatedOverride_noError() { " @SuppressWarnings(\"NamedLikeContextualKeyword\")", " void yield() {}", "}") - .setArgs( - ImmutableList.of( - "-XepOpt:NamedLikeContextualKeyword:EnableMethodNames", - "-XepOpt:NamedLikeContextualKeyword:EnableClassNames")) .doTest(); } @@ -243,10 +225,6 @@ public void annotatedOverride_noError() { " @SuppressWarnings(\"NamedLikeContextualKeyword\")", " void yield() {}", "}") - .setArgs( - ImmutableList.of( - "-XepOpt:NamedLikeContextualKeyword:EnableMethodNames", - "-XepOpt:NamedLikeContextualKeyword:EnableClassNames")) .doTest(); } @@ -263,12 +241,7 @@ public void positive() { " yield();", " }", "}") - .setArgs( - ImmutableList.of( - "--release", - "11", - "-XepOpt:NamedLikeContextualKeyword:EnableMethodNames", - "-XepOpt:NamedLikeContextualKeyword:EnableClassNames")) + .setArgs(ImmutableList.of("--release", "11")) .doTest(); } @@ -291,12 +264,7 @@ public void enclosing() { " }", " }", "}") - .setArgs( - ImmutableList.of( - "--release", - "11", - "-XepOpt:NamedLikeContextualKeyword:EnableMethodNames", - "-XepOpt:NamedLikeContextualKeyword:EnableClassNames")) + .setArgs(ImmutableList.of("--release", "11")) .doTest(); } @@ -315,12 +283,7 @@ public void staticMethod() { " }", " }", "}") - .setArgs( - ImmutableList.of( - "--release", - "11", - "-XepOpt:NamedLikeContextualKeyword:EnableMethodNames", - "-XepOpt:NamedLikeContextualKeyword:EnableClassNames")) + .setArgs(ImmutableList.of("--release", "11")) .doTest(); } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/NullableOptionalTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/NullableOptionalTest.java index 8da09a598ab..f4e5ff320b4 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/NullableOptionalTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/NullableOptionalTest.java @@ -79,7 +79,7 @@ public void methodReturnsOptionalWithAnotherNullableAnnotation_showsError() { .addSourceLines( "Test.java", "import java.util.Optional;", - "import org.jspecify.nullness.Nullable;", + "import org.jspecify.annotations.Nullable;", "final class Test {", " @Nullable", " // BUG: Diagnostic contains:", @@ -136,7 +136,7 @@ public void methodReturnsNonOptionalWithAnotherNullableAnnotation_noError() { compilationHelper .addSourceLines( "Test.java", - "import org.jspecify.nullness.Nullable;", + "import org.jspecify.annotations.Nullable;", "final class Test {", " @Nullable", " private Object foo() {", @@ -151,7 +151,7 @@ public void methodHasNullableNonOptionalAsParameter_noError() { compilationHelper .addSourceLines( "Test.java", - "import org.jspecify.nullness.Nullable;", + "import org.jspecify.annotations.Nullable;", "final class Test {", " private void foo(@Nullable Object object) {}", "}") diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/OperatorPrecedenceTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/OperatorPrecedenceTest.java index 5fcb78bb0ce..4e6a25f5216 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/OperatorPrecedenceTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/OperatorPrecedenceTest.java @@ -191,4 +191,42 @@ public void aLotOfParenthesis() { "}") .doTest(); } + + @Test + public void conditionalBoolean() { + helper + .addInputLines( + "Test.java", // + "class Test {", + " void f(boolean a, boolean b, boolean c, boolean d) {", + " boolean g = a || b ? c : d;", + " g = a && b ? c : d;", + " g = a == b ? c : d;", + " }", + "}") + .addOutputLines( + "Test.java", // + "class Test {", + " void f(boolean a, boolean b, boolean c, boolean d) {", + " boolean g = (a || b) ? c : d;", + " g = (a && b) ? c : d;", + " g = a == b ? c : d;", + " }", + "}") + .doTest(); + } + + @Test + public void conditionOtherType() { + helper + .addInputLines( + "Test.java", // + "class Test {", + " void f(boolean a, boolean b, String c, String d) {", + " String g = a || b ? c : d;", + " }", + "}") + .expectUnchanged() + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/ReturnAtTheEndOfVoidFunctionTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/ReturnAtTheEndOfVoidFunctionTest.java new file mode 100644 index 00000000000..bed2653de76 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/ReturnAtTheEndOfVoidFunctionTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2023 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for the {@link ReturnAtTheEndOfVoidFunction}. */ +@RunWith(JUnit4.class) +public class ReturnAtTheEndOfVoidFunctionTest { + + private final BugCheckerRefactoringTestHelper helper = + BugCheckerRefactoringTestHelper.newInstance(ReturnAtTheEndOfVoidFunction.class, getClass()); + + @Test + public void lastReturnIsDeleted() { + helper + .addInputLines( + "Builder.java", + "package com.google.gporeba;", + "public final class Builder {", + " public void stuff() {", + " int x = 5;", + " return;", + " }", + "}") + .addOutputLines( + "Builder.java", + "package com.google.gporeba;", + "public final class Builder {", + " public void stuff() {", + " int x = 5;", + " }", + "}") + .doTest(); + } + + @Test + public void lastReturnIsNotDeleted() { + helper + .addInputLines( + "Builder.java", + "package com.google.gporeba;", + "public final class Builder {", + " public int stuff() {", + " int x = 5;", + " return x;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void returnAtDifferentPositionIsNotDeleted() { + helper + .addInputLines( + "Builder.java", + "package com.google.gporeba;", + "public final class Builder {", + " public void stuff() {", + " int x = 5;", + " if(x > 2) {", + " return;", + " }", + " int z = 2173;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void emptyFunctionIsUnchanged() { + helper + .addInputLines( + "Builder.java", + "package com.google.gporeba;", + "public final class Builder {", + " public void nothing() {", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void nullReturnInVoidIsUnchanged() { + helper + .addInputLines( + "Builder.java", + "package com.google.gporeba;", + "public final class Builder {", + " public Void nothing() {", + " return null;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void constructorIsCleaned() { + helper + .addInputLines( + "Builder.java", + "package com.google.gporeba;", + "public final class Builder {", + " public Builder() {", + " return;", + " }", + "}") + .addOutputLines( + "Builder.java", + "package com.google.gporeba;", + "public final class Builder {", + " public Builder() {", + " }", + "}") + .doTest(); + } + + @Test + public void abstractDoesntCrash() { + helper + .addInputLines( + "Builder.java", + "package com.google.gporeba;", + "public abstract class Builder {", + " public abstract void stuff();", + "}") + .expectUnchanged() + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/SameNameButDifferentTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/SameNameButDifferentTest.java index 79caad61236..66633c57068 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/SameNameButDifferentTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/SameNameButDifferentTest.java @@ -16,6 +16,7 @@ package com.google.errorprone.bugpatterns; +import com.google.common.collect.ImmutableList; import com.google.errorprone.BugCheckerRefactoringTestHelper; import com.google.errorprone.CompilationTestHelper; import org.junit.Test; @@ -207,4 +208,27 @@ public void referencesSelf() { "}") .doTest(); } + + @Test + public void ungroupedOverloadsPositiveCasesCoveringOnlyFirstOverload() { + helper + .addSourceLines( + "Test.java", + "import java.util.function.Supplier;", + "class Test {", + " class One {", + " class Clash {}", + " // BUG: Diagnostic contains:", + " Clash a;", + " Clash b;", + " }", + " class Two {", + " class Clash {}", + " Clash a;", + " Clash b;", + " }", + "}") + .setArgs(ImmutableList.of("-XepOpt:SameNameButDifferent:BatchFindings")) + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/TruthContainsExactlyElementsInUsageTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/TruthContainsExactlyElementsInUsageTest.java new file mode 100644 index 00000000000..ec0fbe43895 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/TruthContainsExactlyElementsInUsageTest.java @@ -0,0 +1,371 @@ +/* + * Copyright 2023 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class TruthContainsExactlyElementsInUsageTest { + + private final BugCheckerRefactoringTestHelper refactoringHelper = + BugCheckerRefactoringTestHelper.newInstance( + TruthContainsExactlyElementsInUsage.class, getClass()); + + private final CompilationTestHelper compilationHelper = + CompilationTestHelper.newInstance(TruthContainsExactlyElementsInUsage.class, getClass()); + + @Test + public void negativeDirectContainsExactlyUsage() { + compilationHelper + .addSourceLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3)).containsExactly(1,2,3);", + " }", + "}") + .doTest(); + } + + @Test + public void negativeVariableContainsExactlyUsage() { + compilationHelper + .addSourceLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import java.util.List;", + "public class ExampleClassTest {", + " void test() {", + " List list = ImmutableList.of(1, 2, 3);", + " assertThat(list).containsExactly(1,2,3);", + " }", + "}") + .doTest(); + } + + @Test + public void negativeVariableTruthContainsExactlyElementsInUsage() { + compilationHelper + .addSourceLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import java.util.List;", + "public class ExampleClassTest {", + " void test() {", + " List list = ImmutableList.of(1, 2, 3);", + " assertThat(list).containsExactlyElementsIn(list);", + " }", + "}") + .doTest(); + } + + @Test + public void negativeVariableTruthContainsExactlyElementsInUsageWithCopy() { + compilationHelper + .addSourceLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import java.util.List;", + "public class ExampleClassTest {", + " void test() {", + " List list = ImmutableList.of(1, 2, 3);", + " assertThat(list).containsExactlyElementsIn(ImmutableList.copyOf(list));", + " }", + "}") + .doTest(); + } + + @Test + public void negativeTruthContainsExactlyElementsInUsageWithHashSet() { + compilationHelper + .addSourceLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import com.google.common.collect.Sets;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3))", + " .containsExactlyElementsIn(Sets.newHashSet(1, 2, 3));", + " }", + "}") + .doTest(); + } + + @Test + public void negativeTruthContainsExactlyElementsInUsageWithImmutableSet() { + compilationHelper + .addSourceLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import com.google.common.collect.ImmutableSet;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3))", + " .containsExactlyElementsIn(ImmutableSet.of(1, 2, 3));", + " }", + "}") + .doTest(); + } + + @Test + public void refactoringTruthContainsExactlyElementsInUsageWithArrayList() { + refactoringHelper + .addInputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import java.util.Arrays;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3))", + " .containsExactlyElementsIn(Arrays.asList(1, 2, 3));", + " }", + "}") + .addOutputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import java.util.Arrays;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3)).containsExactly(1, 2, 3);", + " }", + "}") + .doTest(); + } + + @Test + public void refactoringTruthContainsExactlyElementsInUsageWithListOf() { + refactoringHelper + .addInputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import java.util.List;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3))", + " .containsExactlyElementsIn(List.of(1, 2, 3));", + " }", + "}") + .addOutputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import java.util.List;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3)).containsExactly(1, 2, 3);", + " }", + "}") + .doTest(); + } + + @Test + public void refactoringTruthContainsExactlyElementsInUsageWithInOrderList() { + refactoringHelper + .addInputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import java.util.Arrays;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3))", + " .containsExactlyElementsIn(Arrays.asList(1, 2, 3)).inOrder();", + " }", + "}") + .addOutputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import java.util.Arrays;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3)).containsExactly(1, 2, 3).inOrder();", + " }", + "}") + .doTest(); + } + + @Test + public void refactoringTruthContainsExactlyElementsInUsageWithStaticallyImportedAsList() { + refactoringHelper + .addInputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import static java.util.Arrays.asList;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3)).containsExactlyElementsIn(asList(1, 2, 3));", + " }", + "}") + .addOutputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import static java.util.Arrays.asList;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3)).containsExactly(1, 2, 3);", + " }", + "}") + .doTest(); + } + + @Test + public void refactoringTruthContainsExactlyElementsInUsageWithNewArrayList() { + refactoringHelper + .addInputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import com.google.common.collect.Lists;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3))", + " .containsExactlyElementsIn(Lists.newArrayList(1, 2, 3));", + " }", + "}") + .addOutputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import com.google.common.collect.Lists;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3)).containsExactly(1, 2, 3);", + " }", + "}") + .doTest(); + } + + @Test + public void refactoringTruthContainsExactlyElementsInUsageWithSingletonList() { + refactoringHelper + .addInputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import java.util.Collections;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1))", + " .containsExactlyElementsIn(Collections.singletonList(1));", + " }", + "}") + .addOutputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import java.util.Collections;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1)).containsExactly(1);", + " }", + "}") + .doTest(); + } + + @Test + public void refactoringTruthContainsExactlyElementsInUsageWithEmptyList() { + refactoringHelper + .addInputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import java.util.Arrays;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of()).containsExactlyElementsIn(Arrays.asList());", + " }", + "}") + .addOutputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "import java.util.Arrays;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of()).containsExactly();", + " }", + "}") + .doTest(); + } + + @Test + public void refactoringTruthContainsExactlyElementsInUsageWithImmutableList() { + refactoringHelper + .addInputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3))", + " .containsExactlyElementsIn(ImmutableList.of(1, 2, 3));", + " }", + "}") + .addOutputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3)).containsExactly(1, 2, 3);", + " }", + "}") + .doTest(); + } + + @Test + public void refactoringTruthContainsExactlyElementsInUsageWithArray() { + refactoringHelper + .addInputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3))", + " .containsExactlyElementsIn(new Integer[] {1, 2, 3});", + " }", + "}") + .addOutputLines( + "ExampleClassTest.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "public class ExampleClassTest {", + " void test() {", + " assertThat(ImmutableList.of(1, 2, 3)).containsExactly(1,2,3);", + " }", + "}") + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryAsyncTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryAsyncTest.java new file mode 100644 index 00000000000..a817e8504c8 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryAsyncTest.java @@ -0,0 +1,223 @@ +/* + * Copyright 2023 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class UnnecessaryAsyncTest { + private final CompilationTestHelper helper = + CompilationTestHelper.newInstance(UnnecessaryAsync.class, getClass()); + private final BugCheckerRefactoringTestHelper refactoring = + BugCheckerRefactoringTestHelper.newInstance(UnnecessaryAsync.class, getClass()); + + @Test + public void positive() { + helper + .addSourceLines( + "Test.java", + "import java.util.concurrent.atomic.AtomicInteger;", + "class Test {", + " int test() {", + " // BUG: Diagnostic contains:", + " var ai = new AtomicInteger();", + " ai.set(1);", + " return ai.get();", + " }", + "}") + .doTest(); + } + + @Test + public void refactoringInteger() { + refactoring + .addInputLines( + "Test.java", + "import java.util.concurrent.atomic.AtomicInteger;", + "class Test {", + " int test() {", + " var ai = new AtomicInteger();", + " ai.set(1);", + " return ai.get();", + " }", + "}") + .addOutputLines( + "Test.java", + "import java.util.concurrent.atomic.AtomicInteger;", + "class Test {", + " int test() {", + " int ai = 0;", + " ai = 1;", + " return ai;", + " }", + "}") + .doTest(); + } + + @Test + public void refactoringReference() { + refactoring + .addInputLines( + "Test.java", + "import java.util.concurrent.atomic.AtomicReference;", + "class Test {", + " String test() {", + " var ar = new AtomicReference(null);", + " ar.compareAndSet(null, \"foo\");", + " return ar.get();", + " }", + "}") + .addOutputLines( + "Test.java", + "import java.util.concurrent.atomic.AtomicReference;", + "class Test {", + " String test() {", + " String ar = null;", + " ar = \"foo\";", + " return ar;", + " }", + "}") + .doTest(); + } + + @Test + public void refactoring_unfixable_noAttempt() { + refactoring + .addInputLines( + "Test.java", + "import java.util.concurrent.atomic.AtomicReference;", + "class Test {", + " String test() {", + " var ar = new AtomicReference(null);", + " return ar.toString();", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void refactoring_rawType() { + refactoring + .addInputLines( + "Test.java", + "import java.util.concurrent.atomic.AtomicReference;", + "class Test {", + " Object test() {", + " var ar = new AtomicReference();", + " ar.set(\"foo\");", + " return ar.get();", + " }", + "}") + .addOutputLines( + "Test.java", + "import java.util.concurrent.atomic.AtomicReference;", + "class Test {", + " Object test() {", + " Object ar = null;", + " ar = \"foo\";", + " return ar;", + " }", + "}") + .doTest(); + } + + @Test + public void negative_escapesScope() { + helper + .addSourceLines( + "Test.java", + "import java.util.concurrent.atomic.AtomicInteger;", + "class Test {", + " AtomicInteger test() {", + " var ai = new AtomicInteger();", + " ai.set(1);", + " return ai;", + " }", + "}") + .doTest(); + } + + @Test + public void negative_passedToAnotherMethod() { + helper + .addSourceLines( + "Test.java", + "import java.util.concurrent.atomic.AtomicInteger;", + "class Test {", + " void test() {", + " var ai = new AtomicInteger();", + " ai.set(1);", + " frobnicate(ai);", + " }", + " void frobnicate(Number n) {}", + "}") + .doTest(); + } + + @Test + public void positive_uselessConcurrentMap() { + helper + .addSourceLines( + "Test.java", + "import java.util.concurrent.ConcurrentHashMap;", + "class Test {", + " int test() {", + " // BUG: Diagnostic contains:", + " var chm = new ConcurrentHashMap<>();", + " chm.put(1, 2);", + " return chm.size();", + " }", + "}") + .doTest(); + } + + @Test + public void negative_capturedByLambda() { + helper + .addSourceLines( + "Test.java", + "import java.util.concurrent.atomic.AtomicInteger;", + "import java.util.List;", + "class Test {", + " long test(List xs) {", + " var ai = new AtomicInteger();", + " return xs.stream().mapToLong(x -> ai.getAndIncrement()).sum();", + " }", + "}") + .doTest(); + } + + @Test + public void atomicReference_unfixable() { + helper + .addSourceLines( + "Test.java", + "import java.util.concurrent.atomic.AtomicReference;", + "class Test {", + " void f() {", + " // BUG: Diagnostic contains: String result = null;", + " AtomicReference result = new AtomicReference<>();", + " }", + "}") + .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 69e16564851..68b0abf5faf 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryLambdaTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryLambdaTest.java @@ -388,4 +388,24 @@ public void e() { "}") .doTest(); } + + @Test + public void iterable() { + testHelper + .addInputLines( + "Test.java", + "import java.util.stream.IntStream;", + "class Example {", + " void someLoopyCode() {", + " for (int i : someIterable()) {", + " // Do something.", + " }", + " }", + " private Iterable someIterable() {", + " return () -> IntStream.range(0, 42).boxed().iterator();", + " }", + "}") + .expectUnchanged() + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/UnusedMethodTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/UnusedMethodTest.java index b41c55fe19b..20a1d8a5b5d 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/UnusedMethodTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnusedMethodTest.java @@ -131,10 +131,17 @@ public void exemptedByName() { "Unuseds.java", "package unusedvars;", "class ExemptedByName {", - " private void unused1(int a, int unusedParam) {", + " private void unused1(" + + " int a, int unusedParam, " + + " int customUnused1, int customUnused2, " + + " int prefixUnused1Param, int prefixUnused2Param" + + " ) {", " int unusedLocal = a;", " }", "}") + .setArgs( + "-XepOpt:Unused:exemptNames=customUnused1,customUnused2", + "-XepOpt:Unused:exemptPrefixes=prefixunused1,prefixunused2") .doTest(); } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/UnusedVariableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/UnusedVariableTest.java index 6a172a4e305..6f248ed024f 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/UnusedVariableTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnusedVariableTest.java @@ -434,7 +434,14 @@ public void exemptedByName() { " private int unusedInt;", " private static final int UNUSED_CONSTANT = 5;", " private int ignored;", + " private int customUnused1;", + " private int customUnused2;", + " private int prefixUnused1Field;", + " private int prefixUnused2Field;", "}") + .setArgs( + "-XepOpt:Unused:exemptNames=customUnused1,customUnused2", + "-XepOpt:Unused:exemptPrefixes=prefixunused1,prefixunused2") .doTest(); } @@ -1398,12 +1405,105 @@ public void nestedRecord() { .addSourceLines( "SimpleClass.java", "public class SimpleClass {", - " public record SimpleRecord (Integer foo, Long bar) {}", + " public record SimpleRecord (Integer foo, Long bar) {}", + "}") + .expectNoDiagnostics() + .doTest(); + } + + @Test + public void recordWithStaticFields() { + assumeTrue(RuntimeVersion.isAtLeast16()); + helper + .addSourceLines( + "SimpleClass.java", + "public class SimpleClass {", + " public record MyRecord (int foo) {", + " private static int a = 1;", + " private static int b = 1;", + " // BUG: Diagnostic contains: is never read", + " private static int c = 1;", + " ", + " public MyRecord {", + " foo = Math.max(a, foo);", + " }", + " }", + "", + " public int b() {", + " return MyRecord.b;", + " }", + "}") + .doTest(); + } + + // Implicit canonical constructor has same access as record + // (https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.10.4) + // Therefore this case is important to test because UnusedVariable treats parameters of private + // methods differently + @Test + public void nestedPrivateRecord() { + assumeTrue(RuntimeVersion.isAtLeast16()); + helper + .addSourceLines( + "SimpleClass.java", + "public class SimpleClass {", + " private record SimpleRecord (Integer foo, Long bar) {}", + "}") + .expectNoDiagnostics() + .doTest(); + } + + @Test + public void nestedPrivateRecordCompactCanonicalConstructor() { + assumeTrue(RuntimeVersion.isAtLeast16()); + helper + .addSourceLines( + "SimpleClass.java", + "public class SimpleClass {", + " private record SimpleRecord (Integer foo, Long bar) {", + // Compact canonical constructor implicitly assigns field values at end + " private SimpleRecord {", + " System.out.println(foo);", + " }", + " }", "}") .expectNoDiagnostics() .doTest(); } + @Test + public void nestedPrivateRecordNormalCanonicalConstructor() { + assumeTrue(RuntimeVersion.isAtLeast16()); + helper + .addSourceLines( + "SimpleClass.java", + "public class SimpleClass {", + " private record SimpleRecord (Integer foo, Long bar) {", + " private SimpleRecord(Integer foo, Long bar) {", + " this.foo = foo;", + " this.bar = bar;", + " }", + " }", + "}") + .expectNoDiagnostics() + .doTest(); + } + + @Test + public void unusedRecordConstructorParameter() { + assumeTrue(RuntimeVersion.isAtLeast16()); + helper + .addSourceLines( + "SimpleRecord.java", + "public record SimpleRecord (int x) {", + " // BUG: Diagnostic contains: The parameter 'b' is never read", + " private SimpleRecord(int a, int b) {", + " this(a);", + " }", + "}") + .doTest(); + } + @Test public void unusedInRecord() { assumeTrue(RuntimeVersion.isAtLeast16()); diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java index 69690a5b5e4..7aef6264819 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java @@ -609,6 +609,24 @@ public void constructor() { .doTest(); } + @Test + public void refasterAfterTemplate() { + helper + .addInputLines( + "A.java", + "import com.google.errorprone.refaster.annotation.AfterTemplate;", + "class A {", + " static final class MethodLacksBeforeTemplateAnnotation {", + " @AfterTemplate", + " String after(String str) {", + " return str;", + " }", + " }", + "}") + .expectUnchanged() + .doTest(); + } + @Test public void sometimesThrows() { helper @@ -832,4 +850,26 @@ public void providesMethod_b267362954() { .expectUnchanged() .doTest(); } + + @Test + public void exemptedByCustomAnnotation() { + helper + .addInputLines("Foo.java", "package example;", "@interface Foo {}") + .expectUnchanged() + .addInputLines( + "ExemptedByCustomAnnotation.java", + "package example;", + "public final class ExemptedByCustomAnnotation {", + " private String name;", + "", + " @Foo", + " public ExemptedByCustomAnnotation setName(String name) {", + " this.name = name;", + " return this;", + " }", + "}") + .expectUnchanged() + .setArgs("-XepOpt:CanIgnoreReturnValue:ExemptingMethodAnnotations=example.Foo") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/collectionincompatibletype/TruthIncompatibleTypeTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/collectionincompatibletype/TruthIncompatibleTypeTest.java index d898de43888..e558e88bd01 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/collectionincompatibletype/TruthIncompatibleTypeTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/collectionincompatibletype/TruthIncompatibleTypeTest.java @@ -16,13 +16,24 @@ package com.google.errorprone.bugpatterns.collectionincompatibletype; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.TruthJUnit.assume; +import static java.lang.String.format; +import static java.util.Arrays.stream; + +import com.google.common.collect.ImmutableList; +import com.google.common.truth.Subject; import com.google.errorprone.CompilationTestHelper; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; /** {@link TruthIncompatibleType}Test */ -@RunWith(JUnit4.class) +@RunWith(TestParameterInjector.class) public class TruthIncompatibleTypeTest { private final CompilationTestHelper compilationHelper = @@ -48,7 +59,7 @@ public void positive() { } @Test - public void assume() { + public void assumeTypeCheck() { compilationHelper .addSourceLines( "Test.java", @@ -559,4 +570,70 @@ public void comparingElementsUsingRawCollection_noFinding() { "}") .doTest(); } + + @Test + public void casts() { + compilationHelper + .addSourceLines( + "Test.java", + "import static com.google.common.truth.Truth.assertThat;", + "public class Test {", + " public void f(int a, long b, long c) {", + " assertThat((long) a).isAnyOf(b, c);", + " }", + "}") + .doTest(); + } + + @Test + public void subjectExhaustiveness( + @TestParameter(valuesProvider = SubjectMethods.class) Method method) { + // TODO(ghm): isNotSameInstanceAs might be worth flagging, but the check can be even stricter. + assume().that(method.getName()).isNoneOf("isSameInstanceAs", "isNotSameInstanceAs"); + + compilationHelper + .addSourceLines( + "Test.java", + "import static com.google.common.truth.Truth.assertThat;", + "import com.google.common.collect.ImmutableList;", + "class Test {", + " public void test(String a, Long b) {", + " // BUG: Diagnostic contains:", + getOffensiveLine(method), + " }", + "}") + .doTest(); + } + + private static String getOffensiveLine(Method method) { + if (stream(method.getParameterTypes()).allMatch(p -> p.equals(Iterable.class))) { + return format(" assertThat(a).%s(ImmutableList.of(b));", method.getName()); + } else if (stream(method.getParameterTypes()).allMatch(p -> p.equals(Object.class))) { + return format(" assertThat(a).%s(b);", method.getName()); + } else if (stream(method.getParameterTypes()) + .allMatch(p -> p.equals(Object.class) || p.isArray())) { + return format(" assertThat(a).%s(b, b, b);", method.getName()); + } else if (stream(method.getParameterTypes()).allMatch(Class::isArray)) { + return format(" assertThat(a).%s(b);", method.getName()); + } else { + throw new AssertionError(); + } + } + + private static final class SubjectMethods implements TestParameterValuesProvider { + @Override + public ImmutableList provideValues() { + return stream(Subject.class.getDeclaredMethods()) + .filter( + m -> + Modifier.isPublic(m.getModifiers()) + && !m.getName().equals("equals") + && m.getParameterCount() > 0 + && (stream(m.getParameterTypes()).allMatch(p -> p.equals(Iterable.class)) + || stream(m.getParameterTypes()) + .allMatch(p -> p.equals(Object.class) || p.isArray()) + || stream(m.getParameterTypes()).allMatch(Class::isArray))) + .collect(toImmutableList()); + } + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullableTest.java index f1eb363284e..ce01e9f8039 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullableTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullableTest.java @@ -125,7 +125,7 @@ public void positiveConservativeNullMarked() { conservativeHelper .addSourceLines( "Foo.java", - "import org.jspecify.nullness.NullMarked;", + "import org.jspecify.annotations.NullMarked;", "@NullMarked", "abstract class Foo {", " // BUG: Diagnostic contains: @Nullable", diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullableTest.java index 651d854e6f0..49f85147d76 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullableTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullableTest.java @@ -457,7 +457,7 @@ public void nonAnnotationNullable() { .addOutputLines( "out/Test.java", "class T {", - " private final @org.jspecify.nullness.Nullable Object obj2 = null;", + " private final @org.jspecify.annotations.Nullable Object obj2 = null;", " class Nullable {}", "}") .doTest(); diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java index 362cb557808..8718f94aeae 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java @@ -177,7 +177,7 @@ public void positiveNullMarkedOtherPackageAggressive() { aggressiveHelper .addSourceLines( "Foo.java", - "import org.jspecify.nullness.NullMarked;", + "import org.jspecify.annotations.NullMarked;", "@NullMarked", "class Foo {", " void consume(String s) {}", @@ -194,7 +194,7 @@ public void negativeNullMarkedNonComGoogleCommonPackageConservative() { conservativeHelper .addSourceLines( "Foo.java", - "import org.jspecify.nullness.NullMarked;", + "import org.jspecify.annotations.NullMarked;", "@NullMarked", "class Foo {", " void consume(String s) {}", diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java index 37daeb2cb79..fb85ffc548e 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java @@ -584,44 +584,6 @@ public void orElseNull() { .doTest(); } - @Test - public void emptyToNull() { - createCompilationTestHelper() - .addSourceLines( - "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java", - "package com.google.errorprone.bugpatterns.nullness;", - "import static com.google.common.base.Strings.emptyToNull;", - "class LiteralNullReturnTest {", - " public String getMessage(String s) {", - " // BUG: Diagnostic contains: @Nullable", - " return emptyToNull(s);", - " }", - "}") - .doTest(); - } - - @Test - public void implementsMap() { - createCompilationTestHelper() - .addSourceLines( - "NotMap.java", // - "interface NotMap {", - " Integer get(String o);", - "}") - .addSourceLines( - "MyMap.java", - "import java.util.Map;", - "interface MyMap extends Map, NotMap {", - " // BUG: Diagnostic contains: @Nullable", - " @Override V get(Object o);", - " // BUG: Diagnostic contains: @Nullable", - " @Override V replace(K k, V v);", - " @Override boolean replace(K k, V expect, V update);", - " @Override Integer get(String o);", - "}") - .doTest(); - } - @Test public void implementsMapButAlwaysThrows() { createCompilationTestHelper() @@ -878,7 +840,7 @@ public void removeSuppressWarnings_removeNullnessReturnWarning() { .addOutputLines( "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java", "package com.google.errorprone.bugpatterns.nullness;", - "import org.jspecify.nullness.Nullable;", + "import org.jspecify.annotations.Nullable;", "public class LiteralNullReturnTest {", "", " public @Nullable String getMessage(boolean b) {", @@ -1665,6 +1627,18 @@ public void negativeCases_suppressionAboveMethodLevel() { .doTest(); } + @Test + public void negativeCases_implementsMapButRunningInConservativeMode() { + createCompilationTestHelper() + .addSourceLines( + "MyMap.java", + "import java.util.Map;", + "interface MyMap extends Map {", + " @Override V get(Object o);", + "}") + .doTest(); + } + @Test public void returnSameSymbolDifferentObjectInsideIfNull() { createCompilationTestHelper() @@ -1715,8 +1689,8 @@ public void nonAnnotationNullable() { .addOutputLines( "out/Test.java", "class T {", - " @org.jspecify.nullness.Nullable private final Object method(boolean b) { return b ?" - + " null : 0; }", + " @org.jspecify.annotations.Nullable private final Object method(boolean b) { return b" + + " ? null : 0; }", " class Nullable {}", "}") .doTest(); @@ -1738,7 +1712,7 @@ public void multipleNullReturns() { "}") .addOutputLines( "out/Test.java", - "import org.jspecify.nullness.Nullable;", + "import org.jspecify.annotations.Nullable;", "class T {", " private final @Nullable Object method(boolean b) {", " if (b) {", @@ -1992,6 +1966,28 @@ public void negativeCases_doesNotRemoveNecessarySuppressWarnings() { .doTest(TEXT_MATCH); } + @Test + public void aggressive_implementsMap() { + createAggressiveCompilationTestHelper() + .addSourceLines( + "NotMap.java", // + "interface NotMap {", + " Integer get(String o);", + "}") + .addSourceLines( + "MyMap.java", + "import java.util.Map;", + "interface MyMap extends Map, NotMap {", + " // BUG: Diagnostic contains: @Nullable", + " @Override V get(Object o);", + " // BUG: Diagnostic contains: @Nullable", + " @Override V replace(K k, V v);", + " @Override boolean replace(K k, V expect, V update);", + " @Override Integer get(String o);", + "}") + .doTest(); + } + private CompilationTestHelper createCompilationTestHelper() { return CompilationTestHelper.newInstance(ReturnMissingNullable.class, getClass()); } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullableTest.java index 749c88f9bf6..8cf45b61b60 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullableTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullableTest.java @@ -334,7 +334,7 @@ public void positiveConservativeNullMarked() { .addSourceLines( "Test.java", "import javax.annotation.Nullable;", - "import org.jspecify.nullness.NullMarked;", + "import org.jspecify.annotations.NullMarked;", "@NullMarked", "class Test {", " // BUG: Diagnostic contains: @Nullable", diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/time/TimeUnitConversionCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/time/TimeUnitConversionCheckerTest.java index d928fd12a6d..3364c04bd29 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/time/TimeUnitConversionCheckerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/time/TimeUnitConversionCheckerTest.java @@ -168,4 +168,18 @@ public void largeUnitToSmallUnit_succeeds() { "}") .doTest(); } + + @Test + public void receiverExpression() { + helper + .addSourceLines( + "TestClass.java", + "import java.util.concurrent.TimeUnit;", + "public class TestClass {", + " long f(TimeUnit timeUnit, long time) {", + " return (timeUnit != null ? timeUnit : TimeUnit.MILLISECONDS).toMillis(time);", + " }", + "}") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/refaster/TemplateIntegrationTest.java b/core/src/test/java/com/google/errorprone/refaster/TemplateIntegrationTest.java index e9000acc4be..0fa97eb22e2 100644 --- a/core/src/test/java/com/google/errorprone/refaster/TemplateIntegrationTest.java +++ b/core/src/test/java/com/google/errorprone/refaster/TemplateIntegrationTest.java @@ -374,4 +374,9 @@ public void suppressWarnings() throws IOException { public void typeArgumentsMethodInvocation() throws IOException { runTest("TypeArgumentsMethodInvocationTemplate"); } + + @Test + public void memberSelectAndMethodParameterDisambiguation() throws IOException { + runTest("MemberSelectAndMethodParameterDisambiguationTemplate"); + } } diff --git a/check_api/src/main/java/com/google/errorprone/apply/DiffNotApplicableException.java b/core/src/test/java/com/google/errorprone/refaster/testdata/input/MemberSelectAndMethodParameterDisambiguationTemplateExample.java similarity index 54% rename from check_api/src/main/java/com/google/errorprone/apply/DiffNotApplicableException.java rename to core/src/test/java/com/google/errorprone/refaster/testdata/input/MemberSelectAndMethodParameterDisambiguationTemplateExample.java index b4ef8ad600c..0649db249f2 100644 --- a/check_api/src/main/java/com/google/errorprone/apply/DiffNotApplicableException.java +++ b/core/src/test/java/com/google/errorprone/refaster/testdata/input/MemberSelectAndMethodParameterDisambiguationTemplateExample.java @@ -1,5 +1,5 @@ /* - * Copyright 2011 The Error Prone Authors. + * Copyright 2021 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. @@ -14,19 +14,13 @@ * limitations under the License. */ -package com.google.errorprone.apply; +package com.google.errorprone.refaster.testdata; -/** Exception thrown if a {@link Diff} could not be applied by a {@link DiffApplier} */ -public class DiffNotApplicableException extends RuntimeException { - public DiffNotApplicableException(String msg) { - super(msg); - } - - public DiffNotApplicableException(String msg, Throwable cause) { - super(msg, cause); - } +import java.util.Objects; - public DiffNotApplicableException(Throwable cause) { - super(cause); +/** Test data for {@code MemberSelectAndMethodParameterDisambiguationTemplate}. */ +public class MemberSelectAndMethodParameterDisambiguationTemplateExample { + int example(int length) { + return Objects.hashCode(length); } } diff --git a/core/src/test/java/com/google/errorprone/refaster/testdata/output/MemberSelectAndMethodParameterDisambiguationTemplateExample.java b/core/src/test/java/com/google/errorprone/refaster/testdata/output/MemberSelectAndMethodParameterDisambiguationTemplateExample.java new file mode 100644 index 00000000000..0649db249f2 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/refaster/testdata/output/MemberSelectAndMethodParameterDisambiguationTemplateExample.java @@ -0,0 +1,26 @@ +/* + * Copyright 2021 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.refaster.testdata; + +import java.util.Objects; + +/** Test data for {@code MemberSelectAndMethodParameterDisambiguationTemplate}. */ +public class MemberSelectAndMethodParameterDisambiguationTemplateExample { + int example(int length) { + return Objects.hashCode(length); + } +} diff --git a/core/src/test/java/com/google/errorprone/refaster/testdata/template/MemberSelectAndMethodParameterDisambiguationTemplate.java b/core/src/test/java/com/google/errorprone/refaster/testdata/template/MemberSelectAndMethodParameterDisambiguationTemplate.java new file mode 100644 index 00000000000..808de3a73e9 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/refaster/testdata/template/MemberSelectAndMethodParameterDisambiguationTemplate.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 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.refaster.testdata.template; + +import com.google.errorprone.refaster.annotation.AfterTemplate; +import com.google.errorprone.refaster.annotation.BeforeTemplate; +import java.util.Objects; + +/** + * Example template that references a field ("length") with a name which also commonly occurs as a + * method parameter name. + */ +public class MemberSelectAndMethodParameterDisambiguationTemplate { + @BeforeTemplate + public int before(T[] array) { + return Objects.hashCode(array.length); + } + + @AfterTemplate + public int after(T[] array) { + return Objects.hashCode(array); + } +} diff --git a/docgen/pom.xml b/docgen/pom.xml index 8cddb3257e1..5ebac93ae78 100644 --- a/docgen/pom.xml +++ b/docgen/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.22.0 Documentation tool for generating Error Prone bugpattern documentation diff --git a/docgen_processor/pom.xml b/docgen_processor/pom.xml index 42915547130..08c0473068f 100644 --- a/docgen_processor/pom.xml +++ b/docgen_processor/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.22.0 JSR-269 annotation processor for @BugPattern annotation diff --git a/docs/bugpattern/ClosingStandardOutputStreams.md b/docs/bugpattern/ClosingStandardOutputStreams.md new file mode 100644 index 00000000000..46200e2d61a --- /dev/null +++ b/docs/bugpattern/ClosingStandardOutputStreams.md @@ -0,0 +1,52 @@ +Closing the standard output streams `System.out` or `System.err` will cause all +subsequent standard output to be dropped, including stack traces from exceptions +that propagate to the top level. + +Avoid using try-with-resources to manage `PrintWriter`s or `OutputStream`s that +wrap `System.out` or `System.err`, since the try-with-resource statemnet will +close the underlying streams. + +That is, prefer this: + +``` {.good} +PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.err)); +pw.println("hello"); +pw.flush(); +``` + +Instead of this: + +``` {.bad} +try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.err))) { + pw.println("hello"); +} +``` + +Consider the following example: + +``` +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class X { + public static void main(String[] args) { + System.err.println("one"); + try (PrintWriter err = new PrintWriter(new OutputStreamWriter(System.err, UTF_8))) { + err.print("two"); + } + // System.err has been closed, no more output will be printed! + System.err.println("three"); + throw new AssertionError(); + } +} +``` + +The program will print the following, and return with exit code 1. Note that the +last `println` doesn't produce any output, and the exception's stack trace is +not printed: + +``` +one +two +``` diff --git a/docs/bugpattern/ReturnAtTheEndOfVoidFunction.md b/docs/bugpattern/ReturnAtTheEndOfVoidFunction.md new file mode 100644 index 00000000000..2129e45b3cb --- /dev/null +++ b/docs/bugpattern/ReturnAtTheEndOfVoidFunction.md @@ -0,0 +1,19 @@ +Detects no-op `return` statements in `void` functions when they occur at the end +of the method. + +Instead of: + +```java +public void stuff() { + int x = 5; + return; +} +``` + +do: + +```java +public void stuff() { + int x = 5; +} +``` diff --git a/pom.xml b/pom.xml index 248c8b26c22..f417fff8d0a 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ Error Prone parent POM com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.22.0 pom Error Prone is a static analysis tool for Java that catches common programming mistakes at compile-time. @@ -45,7 +45,7 @@ 1.6.13 3.19.6 1.43.3 - 0.2.0 + 0.3.0 5.1.0 diff --git a/refaster/pom.xml b/refaster/pom.xml index d7951f0e492..3f1cb69165d 100644 --- a/refaster/pom.xml +++ b/refaster/pom.xml @@ -19,7 +19,7 @@ error_prone_parent com.google.errorprone - HEAD-SNAPSHOT + 2.22.0 4.0.0 diff --git a/test_helpers/pom.xml b/test_helpers/pom.xml index 702bb09c897..18b1530be4e 100644 --- a/test_helpers/pom.xml +++ b/test_helpers/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.22.0 error-prone test helpers diff --git a/test_helpers/src/main/java/com/google/errorprone/BugCheckerRefactoringTestHelper.java b/test_helpers/src/main/java/com/google/errorprone/BugCheckerRefactoringTestHelper.java index 4d3405c3286..d757146b70e 100644 --- a/test_helpers/src/main/java/com/google/errorprone/BugCheckerRefactoringTestHelper.java +++ b/test_helpers/src/main/java/com/google/errorprone/BugCheckerRefactoringTestHelper.java @@ -294,7 +294,7 @@ private JCCompilationUnit doCompile( new PrintWriter(out, true), FileManagers.testFileManager(), diagnosticsCollector, - ImmutableList.copyOf(errorProneOptions.getRemainingArgs()), + errorProneOptions.getRemainingArgs(), /* classes= */ null, files, context); diff --git a/type_annotations/pom.xml b/type_annotations/pom.xml index b2495a2167f..9a6fec0dcab 100644 --- a/type_annotations/pom.xml +++ b/type_annotations/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.22.0 error-prone type annotations