Compare commits

...

142 Commits

Author SHA1 Message Date
Stephan Schroevers
2356c61314 [maven-release-plugin] prepare release v0.7.0 2023-01-06 11:29:09 +01:00
Stephan Schroevers
9a9ef3c59d Have apply-error-prone-suggestions.sh download JitPack-hosted artifacts (#441)
While there, tweak the usage message of both `apply-error-prone-suggestions.sh`
and `run-mutation-tests.sh`.
2023-01-06 10:38:54 +01:00
Rick Ossendrijver
e9a1d54035 Add @OnlineDocumentation to TestNGToAssertJRules (#447) 2023-01-06 10:28:21 +01:00
Picnic-Bot
e9733f7426 Upgrade AssertJ Core 3.23.1 -> 3.24.0 (#448)
While there, use the new BOM.

See:
- https://assertj.github.io/doc/#assertj-core-release-notes
- https://github.com/joel-costigliola/assertj-core/compare/assertj-core-3.23.1...assertj-build-3.24.0
2023-01-06 09:12:29 +01:00
Picnic-Bot
534ebb62a1 Upgrade Checker Framework Annotations 3.28.0 -> 3.29.0 (#449)
See:
- https://github.com/typetools/checker-framework/releases/tag/checker-framework-3.29.0
- https://github.com/typetools/checker-framework/compare/checker-framework-3.28.0...checker-framework-3.29.0
2023-01-06 08:40:54 +01:00
Rick Ossendrijver
1ed1e6cd03 Update year to 2023 in footer_custom.html and LICENSE.md (#446) 2023-01-05 14:47:26 +01:00
Picnic-Bot
85e3db6f0a Upgrade pitest-maven-plugin 1.10.3 -> 1.10.4 (#445)
See:
- https://github.com/hcoles/pitest/releases/tag/1.10.4
- https://github.com/hcoles/pitest/compare/1.10.3...1.10.4
2023-01-05 09:58:06 +01:00
Picnic-Bot
6f4db8fc4d Upgrade NullAway 0.10.6 -> 0.10.7 (#444)
See:
- https://github.com/uber/NullAway/blob/master/CHANGELOG.md
- https://github.com/uber/NullAway/compare/v0.10.6...v0.10.7
2023-01-05 08:32:46 +01:00
chamil-prabodha
9d08e8fd4d Have RequestParamType ignore parameter types with custom deserialization support (#426)
While there, introduce and use a new `Flags` utility class; various checks'
list flags now better support empty lists.
2023-01-04 11:13:44 +01:00
Picnic-Bot
9992ff49ce Upgrade pitest-junit5-plugin 1.1.0 -> 1.1.1 (#440)
See https://github.com/pitest/pitest-junit5-plugin/compare/1.1.0...1.1.1
2023-01-04 10:21:44 +01:00
jarmilakaiser
190b47870b Show original Cody in README and on website home page (#438)
This reverts commit 0153c1495f.
2023-01-04 09:27:21 +01:00
Stephan Schroevers
becfcb5374 Upgrade Error Prone 2.16 -> 2.17.0 (#432)
See:
- https://github.com/google/error-prone/releases/tag/v2.17.0
- https://github.com/google/error-prone/compare/v2.16...v2.17.0
- https://github.com/PicnicSupermarket/error-prone/compare/v2.16-picnic-2...v2.17.0-picnic-1
2023-01-03 13:47:21 +01:00
Benedek Halasi
d45682143d Introduce/extend RequireNonNullElse{,Get} Refaster rules (#425)
Fixes #364.
2023-01-02 10:25:10 +01:00
Picnic-Bot
4237732c5b Upgrade errorprone-slf4j 0.1.16 -> 0.1.17 (#433)
See:
- https://github.com/KengoTODA/errorprone-slf4j/releases/tag/v0.1.17
- https://github.com/KengoTODA/errorprone-slf4j/compare/v0.1.16...v0.1.17
2023-01-02 10:04:51 +01:00
Picnic-Bot
d7c86c4854 Upgrade Checkstyle 10.5.0 -> 10.6.0 (#435)
See:
- https://checkstyle.sourceforge.io/releasenotes.html
- https://github.com/checkstyle/checkstyle/releases/tag/checkstyle-10.6.0
- https://github.com/checkstyle/checkstyle/compare/checkstyle-10.5.0...checkstyle-10.6.0
2023-01-02 09:33:21 +01:00
Christos Giallouros
e6e50717d3 Introduce JUnitToAssertJRules Refaster rule collection (#417) 2023-01-02 08:51:46 +01:00
Picnic-Bot
834f9ae49b Upgrade NullAway 0.10.5 -> 0.10.6 (#429)
See:
- https://github.com/uber/NullAway/blob/master/CHANGELOG.md
- https://github.com/uber/NullAway/compare/v0.10.5...v0.10.6
2022-12-30 12:22:12 +01:00
Pieter Dirk Soels
601fcf2648 Update website styling and add Google site verification (#408) 2022-12-30 08:46:27 +01:00
Picnic-Bot
27c6c48e68 Upgrade Mockito 4.10.0 -> 4.11.0 (#427)
See:
- https://github.com/mockito/mockito/releases/tag/v4.11.0
- https://github.com/mockito/mockito/compare/v4.10.0...v4.11.0
2022-12-29 08:02:16 +01:00
Picnic-Bot
b22078657a Upgrade AspectJ 1.9.9.1 -> 1.9.19 (#422)
See:
- https://github.com/eclipse/org.aspectj/releases/tag/V1_9_19
- https://github.com/eclipse/org.aspectj/compare/V1_9_9_1...V1_9_19
2022-12-27 08:51:16 +01:00
Picnic-Bot
165a003f6a Upgrade Spring Boot 2.7.6 -> 2.7.7 (#423)
See:
- https://github.com/spring-projects/spring-boot/releases/tag/v2.7.7
- https://github.com/spring-projects/spring-boot/compare/v2.7.6...v2.7.7
2022-12-27 08:34:41 +01:00
Picnic-Bot
ecb8820d80 Upgrade versions-maven-plugin 2.14.1 -> 2.14.2 (#424)
See:
- https://github.com/mojohaus/versions/releases/tag/2.14.2
- https://github.com/mojohaus/versions-maven-plugin/compare/2.14.1...2.14.2
2022-12-27 06:38:04 +01:00
Stephan Schroevers
6313bd56d8 Improve JUnitMethodDeclaration check (#406)
Implemented changes:
- Ignore method overrides even if not annotated with `@Override`.
- Don't rename methods to `true`, `false` or `null`.
- Don't rename methods to a name declared in a super type. This
  prevents e.g. renaming `testToString` to `toString`.
2022-12-22 08:34:11 +01:00
Stephan Schroevers
5665470fe4 Improve IdentityConversion check (#407)
If the result of an explicit boxing operation is immediately
dereferenced, then the explicit conversion operation is not redundant.
2022-12-21 09:44:30 +01:00
EvgheniiShipilov
d0a89da24d Have IdentityConversion flag com.google.errorprone.matchers.Matchers#{allOf,anyOf} (#420)
While there, sort and rename some (test) code.

Fixes #340.
2022-12-20 11:14:46 +01:00
Picnic-Bot
7c40fdc033 Upgrade Arcmutate 1.0.1 -> 1.0.2 (#418) 2022-12-19 21:02:39 +01:00
Picnic-Bot
8724701baf Upgrade Immutables Annotations 2.9.2 -> 2.9.3 (#413)
See:
- https://github.com/immutables/immutables/releases/tag/2.9.3
- https://github.com/immutables/immutables/compare/2.9.2...2.9.3
2022-12-19 14:12:40 +01:00
Picnic-Bot
e9ae238c2b Upgrade Mockito 4.9.0 -> 4.10.0 (#416)
See:
- https://github.com/mockito/mockito/releases/tag/v4.10.0
- https://github.com/mockito/mockito/compare/v4.9.0...v4.10.0
2022-12-19 13:52:49 +01:00
Picnic-Bot
ff2ed6f82c Upgrade JSpecify 0.2.0 -> 0.3.0 (#415)
See:
- https://github.com/jspecify/jspecify/releases/tag/v0.3.0-alpha-1
- https://github.com/jspecify/jspecify/releases/tag/v0.3.0-alpha-2
- https://github.com/jspecify/jspecify/releases/tag/v0.3.0-alpha-3
- https://github.com/jspecify/jspecify/releases/tag/v0.3.0
- https://github.com/jspecify/jspecify/compare/v0.2.0...v0.3.0
2022-12-19 13:14:50 +01:00
Picnic-Bot
17aeeb9ea9 Upgrade versions-maven-plugin 2.13.0 -> 2.14.1 (#414)
See:
- https://github.com/mojohaus/versions/releases/tag/2.14.0
- https://github.com/mojohaus/versions/releases/tag/2.14.1
- https://github.com/mojohaus/versions-maven-plugin/compare/2.13.0...2.14.1
2022-12-19 12:56:03 +01:00
Stephan Schroevers
fd2946a9c8 Disable failing JDK 20-ea build for now (#419)
The build fails due to
openjdk/jdk20@2cb64a7557; the upcoming
Error Prone release includes a workaround for this:
google/error-prone@df033f03cb.
2022-12-19 09:37:03 +01:00
Guillaume Toison
870d16a0b6 Prevent NestedOptionals from throwing an NPE (#412)
Previously, a `NullPointerException` was thrown if during compilation the
`java.util.Optional` class was not loaded at all.
2022-12-16 09:40:05 +01:00
Picnic-Bot
96114235c5 Upgrade Project Reactor 2022.0.0 -> 2022.0.1 (#411)
See:
- https://github.com/reactor/reactor/releases/tag/2022.0.1
- https://github.com/reactor/reactor/compare/2022.0.0...2022.0.1
2022-12-14 07:18:40 +01:00
Picnic-Bot
bfbf748d47 Upgrade SLF4J API 2.0.5 -> 2.0.6 (#409)
See:
- https://www.slf4j.org/news.html
- https://github.com/qos-ch/slf4j/compare/v_2.0.5...v_2.0.6
2022-12-13 17:11:06 +01:00
Stephan Schroevers
8d0f1d78e6 Upgrade Error Prone fork v2.16-picnic-1 -> v2.16-picnic-2 (#410)
See:
- https://github.com/PicnicSupermarket/error-prone/releases/tag/v2.16-picnic-2
- https://github.com/PicnicSupermarket/error-prone/compare/v2.16-picnic-1...v2.16-picnic-2
2022-12-13 12:51:50 +01:00
Stephan Schroevers
ec00a5522f [maven-release-plugin] prepare for next development iteration 2022-12-12 09:57:05 +01:00
Stephan Schroevers
465b16c471 [maven-release-plugin] prepare release v0.6.0 2022-12-12 09:57:03 +01:00
Paco van Beckhoven
2cbd48ec47 Introduce MonoIdentity and MonoThen Refaster rules (#405)
The `MonoIdentity` rule is a generalization of the existing
`MonoSwitchIfEmptyOfEmptyPublisher` rule.
2022-12-12 08:52:56 +01:00
jarmilakaiser
0153c1495f Show Christmas Cody in README and on website home page (#404) 2022-12-09 16:27:51 +01:00
Rick Ossendrijver
81450285be Fix suggestions emitted by the StringCaseLocaleUsage check (#400)
The suggested `Locale` arguments are now always located in the correct place.
2022-12-09 14:35:35 +01:00
Bastien Diederichs
096acfb14f Improve IsInstanceLambdaUsage check (#401)
Fixes #399.
2022-12-09 13:27:46 +01:00
Shang Xiang
17bcdb6faa Introduce Flux and Stream Refaster rules to suggest filtering before sorting (#393)
Fixes #386.
2022-12-09 13:07:31 +01:00
Rick Ossendrijver
3ee527fda2 Drop indentation in feature request issue template (#403)
While there, add "Improve performance" as a rewrite reason.
2022-12-09 08:55:12 +01:00
Stephan Schroevers
b1c815770b Prevent ReverseOrder Refaster rule from introducing a static import (#397)
This is a workaround for the issue resolved by google/error-prone#3584.

After application of this Refaster rule, any static imports of
`java.util.Collections.reverseOrder` are obsolete. These can be removed by
running Google Java Format or Error Prone's `RemoveUnusedImports` check.

Where possible, subsequent application of the `StaticImport` check will
statically import `java.util.Comparator.reverseOrder`.
2022-12-08 09:06:19 +01:00
Vincent Koeman
bc1f204877 Prefer BigDecimal.valueOf(double) over new BigDecimal(double) (#394)
See https://rules.sonarsource.com/java/RSPEC-2111
2022-12-07 18:58:54 +01:00
Picnic-Bot
cf995ece2b Upgrade actions/setup-java v3.6.0 -> v3.8.0 (#395)
See:
- https://github.com/actions/setup-java/releases/tag/v3.8.0
- https://github.com/actions/setup-java/compare/v3.6.0...v3.8.0
2022-12-07 11:48:55 +01:00
Stephan Schroevers
d427e298e2 Introduce additional Refaster rules to ComparatorRules (#388) 2022-12-07 11:38:25 +01:00
Picnic-Bot
ae327d8d64 Upgrade pitest-maven-plugin 1.9.11 -> 1.10.3 (#378)
See:
- https://github.com/hcoles/pitest/releases/tag/1.10.0
- https://github.com/hcoles/pitest/releases/tag/1.10.1
- https://github.com/hcoles/pitest/releases/tag/1.10.2
- https://github.com/hcoles/pitest/releases/tag/1.10.3
- https://github.com/hcoles/pitest/compare/1.9.11...1.10.3
2022-12-06 11:58:47 +01:00
Gonzalo Amestoy
a6f794de3d Introduce CollectionForEach Refaster rule (#390)
Fixes #387.
2022-12-06 09:28:24 +01:00
Picnic-Bot
1794d36053 Upgrade Pitest Git plugins 1.0.2 -> 1.0.3 (#391) 2022-12-06 09:04:37 +01:00
Phil Werli
ee62af4a86 Introduce MonoFromOptionalSwitchIfEmpty and OptionalMapMonoJust Refaster rules (#384) 2022-12-06 08:26:34 +01:00
Phil Werli
1afce12b52 Introduce Mono{Empty,Just,JustOrEmpty} Refaster rules (#385) 2022-12-06 08:15:39 +01:00
Rick Ossendrijver
f585306a1f Downgrade actions/setup-java v3.7.0 -> v3.6.0 (#392)
This reverts commit 5afa7e1878. Tag v3.7.0 was
deleted; see actions/setup-java#422 for details.
2022-12-06 08:06:58 +01:00
Luke Prananta
4f9aba83ec Introduce StringCaseLocaleUsage check (#376) 2022-12-05 13:49:20 +01:00
Stephan Schroevers
066591c379 Improve mutation testing setup (#383)
Summary of changes:
- Enable Pitest's built-in `STRONGER` mutator group.
- Enable Arcmutate's `EXTENDED` mutator group.
- Enable Arcmutate's JUnit 5 Accelerator Plugin.
- Modify `MoreTypesTest` such that it is impacted by mutations of the
  `MoreTypes` class.

See:
- https://pitest.org/quickstart/mutators/
- https://docs.arcmutate.com/docs/extended-operators.html
- https://docs.arcmutate.com/docs/accelerator.html
2022-12-05 09:11:51 +01:00
Picnic-Bot
789f8c86f2 Upgrade Pitest Git plugins 1.0.1 -> 1.0.2 (#380) 2022-12-05 09:01:20 +01:00
Picnic-Bot
0ccebcc9c4 Upgrade Checker Framework Annotations 3.27.0 -> 3.28.0 (#382)
See:
- https://github.com/typetools/checker-framework/releases/tag/checker-framework-3.28.0
- https://github.com/typetools/checker-framework/compare/checker-framework-3.27.0...checker-framework-3.28.0
2022-12-04 19:57:42 +01:00
Gijs de Jong
8803d23a8e Introduce JUnitClassModifiers check (#214) 2022-12-04 16:49:01 +01:00
Picnic-Bot
5afa7e1878 Upgrade actions/setup-java v3.6.0 -> v3.7.0 (#381)
See:
- https://github.com/actions/setup-java/releases/tag/v3.7.0
- https://github.com/actions/setup-java/compare/v3.6.0...v3.7.0
2022-12-02 09:49:13 +01:00
Picnic-Bot
8e3beb9d5c Upgrade ruby/setup-ruby v1.123.0 -> v1.126.0 (#379)
See:
- https://github.com/ruby/setup-ruby/releases/tag/v1.124.0
- https://github.com/ruby/setup-ruby/releases/tag/v1.125.0
- https://github.com/ruby/setup-ruby/releases/tag/v1.126.0
- https://github.com/ruby/setup-ruby/compare/v1.123.0...v1.126.0
2022-12-01 09:01:08 +01:00
Picnic-Bot
96ab66cdcf Upgrade maven-dependency-plugin 3.3.0 -> 3.4.0 (#377)
See:
- https://issues.apache.org/jira/issues/?jql=project%20%3D%20MDEP%20AND%20fixVersion%20%3E%203.3.0%20AND%20fixVersion%20%3C%3D%203.4.0%20AND%20statusCategory%20%3D%20Done%20
- https://github.com/apache/maven-dependency-plugin/compare/maven-dependency-plugin-3.3.0...maven-dependency-plugin-3.4.0
2022-11-30 16:44:09 +01:00
Stephan Schroevers
330328329f Report mutation test coverage of proposed changes (#346)
Thanks to a free Arcmutate OSS license, GitHub Actions now runs Pitest against
files changed in the context of a PR. 

While there, update Pitest's configuration to ignore Refaster rule collection
classes, as mutations of those classes will not impact the associated unit
tests.

See:
- https://www.arcmutate.com
- https://pitest.org
2022-11-30 09:14:58 +01:00
Phil Werli
f46859ae3a Introduce some Refaster rules that avoid nested Publishers (#374) 2022-11-28 16:24:55 +01:00
Christos Giallouros
6d15cfe7ff Introduce {Mono,Flux}DefaultIfEmpty Refaster rules (#370)
Resolves #363.
2022-11-28 13:15:25 +01:00
Picnic-Bot
fa1bb8aa94 Upgrade Spring Boot 2.7.5 -> 2.7.6 (#372)
See:
- https://github.com/spring-projects/spring-boot/releases/tag/v2.7.6
- https://github.com/spring-projects/spring-boot/compare/v2.7.5...v2.7.6
2022-11-28 13:01:20 +01:00
Picnic-Bot
415ae35906 Upgrade SLF4J API 2.0.4 -> 2.0.5 (#371)
See:
- https://www.slf4j.org/news.html
- https://github.com/qos-ch/slf4j/compare/v_2.0.4...v_2.0.5
2022-11-28 12:46:35 +01:00
Picnic-Bot
7cc8abc3de Upgrade Checkstyle 10.4 -> 10.5.0 (#375)
See:
- https://checkstyle.sourceforge.io/releasenotes.html
- https://github.com/checkstyle/checkstyle/releases/tag/checkstyle-10.5.0
- https://github.com/checkstyle/checkstyle/compare/checkstyle-10.4...checkstyle-10.5.0
2022-11-28 09:04:31 +01:00
Picnic-Bot
16c8bb0b27 Upgrade actions/configure-pages v2.1.2 -> v2.1.3 (#316)
See:
- https://github.com/actions/configure-pages/releases/tag/v2.1.3
- https://github.com/actions/configure-pages/compare/v2.1.2...v2.1.3
2022-11-24 08:37:02 +01:00
Rick Ossendrijver
609d80c9fa Drop unused Palantir {assertj,baseline}-error-prone dependencies (#367) 2022-11-23 16:20:04 +01:00
Picnic-Bot
793bda50d2 Upgrade Jackson 2.14.0 -> 2.14.1 (#366)
See:
- https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.14.1
- https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.14.0...jackson-bom-2.14.1
2022-11-23 08:14:27 +01:00
Picnic-Bot
5d86a66791 Upgrade actions/deploy-pages v1.2.2 -> v1.2.3 (#365)
See:
- https://github.com/actions/deploy-pages/releases/tag/v1.2.3
- https://github.com/actions/deploy-pages/compare/v1.2.2...v1.2.3
2022-11-22 14:27:40 +01:00
Eric Staffas
84d425e4ca Introduce More{ASTHelpers,JUnitMatchers,Matchers} utility classes (#335) 2022-11-22 13:35:03 +01:00
Picnic-Bot
4fead24e73 Upgrade maven-install-plugin 3.0.1 -> 3.1.0 (#362)
See:
- https://github.com/apache/maven-install-plugin/releases/tag/maven-install-plugin-3.1.0
- https://github.com/apache/maven-install-plugin/compare/maven-install-plugin-3.0.1...maven-install-plugin-3.1.0
2022-11-21 22:23:25 +01:00
Picnic-Bot
92df829801 Upgrade SLF4J API 2.0.3 -> 2.0.4 (#360)
See:
- https://www.slf4j.org/news.html
- https://github.com/qos-ch/slf4j/compare/v_2.0.3...v_2.0.4
2022-11-21 15:44:14 +01:00
Stephan Schroevers
98185b92ae Improve Tree deletion suggestions (#347)
When suggesting to remove a method or method annotation, also remove any
trailing whitespace. This avoids the possible introduction of an empty
line right at the start of a code block.
2022-11-21 13:01:32 +01:00
Picnic-Bot
1b6356a876 Upgrade NullAway 0.10.4 -> 0.10.5 (#359)
See:
- https://github.com/uber/NullAway/blob/master/CHANGELOG.md
- https://github.com/uber/NullAway/compare/v0.10.4...v0.10.5
2022-11-19 13:31:29 +01:00
Picnic-Bot
919a7c7ebe Upgrade AutoValue 1.10 -> 1.10.1 (#361)
See:
- https://github.com/google/auto/releases/tag/auto-value-1.10.1
- https://github.com/google/auto/compare/auto-value-1.10...auto-value-1.10.1
2022-11-19 13:16:30 +01:00
Phil Werli
79b0123f41 Extend MonoFlux Refaster rule (#358) 2022-11-19 11:56:44 +01:00
Picnic-Bot
3967542edf Upgrade Spring 5.3.23 -> 5.3.24 (#355)
See:
- https://github.com/spring-projects/spring-framework/releases/tag/v5.3.24
- https://github.com/spring-projects/spring-framework/compare/v5.3.23...v5.3.24
2022-11-19 10:30:34 +01:00
Rick Ossendrijver
0f05d15dd2 Have Renovate file ruby/setup-ruby upgrade PRs at most once a month (#357) 2022-11-18 14:16:20 +01:00
Picnic-Bot
5bdb90634a Upgrade New Relic Java Agent 7.11.0 -> 7.11.1 (#350)
See:
- https://github.com/newrelic/newrelic-java-agent/releases/tag/v7.11.1
- https://github.com/newrelic/newrelic-java-agent/compare/v7.11.0...v7.11.1
2022-11-18 10:44:45 +01:00
Picnic-Bot
1808164c0d Upgrade swagger-annotations 1.6.8 -> 1.6.9 (#352)
See:
- https://github.com/swagger-api/swagger-core/releases/tag/v1.6.9
- https://github.com/swagger-api/swagger-core/compare/v1.6.8...v1.6.9
2022-11-18 10:21:53 +01:00
Picnic-Bot
f451b4cb73 Upgrade ruby/setup-ruby v1.121.0 -> v1.123.0 (#345)
See:
- https://github.com/ruby/setup-ruby/releases/tag/v1.122.0
- https://github.com/ruby/setup-ruby/releases/tag/v1.123.0
- https://github.com/ruby/setup-ruby/compare/v1.121.0...v1.123.0
2022-11-18 09:39:42 +01:00
Picnic-Bot
d4c95299eb Upgrade pitest-maven-plugin 1.9.10 -> 1.9.11 (#354)
See:
- https://github.com/hcoles/pitest/releases/tag/1.9.11
- https://github.com/hcoles/pitest/compare/1.9.10...1.9.11
2022-11-18 09:16:08 +01:00
Picnic-Bot
1b93843d62 Upgrade swagger-annotations 2.2.6 -> 2.2.7 (#351)
See:
- https://github.com/swagger-api/swagger-core/releases/tag/v2.2.7
- https://github.com/swagger-api/swagger-core/compare/v2.2.6...v2.2.7
2022-11-17 16:37:39 +01:00
Picnic-Bot
3929b39e62 Upgrade actions/upload-pages-artifact v1.0.4 -> v1.0.5 (#353)
See:
- https://github.com/actions/upload-pages-artifact/releases/tag/v1.0.5
- https://github.com/actions/upload-pages-artifact/compare/v1.0.4...v1.0.5
2022-11-17 14:22:02 +01:00
Picnic-Bot
81f701fd9e Upgrade Mockito 4.8.1 -> 4.9.0 (#349)
See:
- https://github.com/mockito/mockito/releases/tag/v4.9.0
- https://github.com/mockito/mockito/compare/v4.8.1...v4.9.0
2022-11-15 09:19:39 +01:00
Picnic-Bot
b040648400 Upgrade modernizer-maven-plugin 2.4.0 -> 2.5.0 (#348)
See:
- https://github.com/gaul/modernizer-maven-plugin/releases/tag/modernizer-maven-plugin-2.5.0
- https://github.com/gaul/modernizer-maven-plugin/compare/modernizer-maven-plugin-2.4.0...modernizer-maven-plugin-2.5.0
2022-11-14 09:41:07 +01:00
Picnic-Bot
79ca98d20f Upgrade pitest-maven-plugin 1.9.9 -> 1.9.10 (#344)
See:
- https://github.com/hcoles/pitest/releases/tag/1.9.10
- https://github.com/hcoles/pitest/compare/1.9.9...1.9.10
2022-11-12 16:08:30 +01:00
Picnic-Bot
a6122270a9 Upgrade Project Reactor 2020.0.24 -> 2022.0.0 (#342)
See:
- https://github.com/reactor/reactor/releases/tag/2022.0.0
- https://github.com/reactor/reactor/compare/2020.0.24...2022.0.0
2022-11-12 15:50:15 +01:00
Picnic-Bot
4c0f2c966c Upgrade ruby/setup-ruby v1.120.1 -> v1.121.0 (#343)
See:
- https://github.com/ruby/setup-ruby/releases/tag/v1.121.0
- https://github.com/ruby/setup-ruby/compare/v1.120.1...v1.121.0
2022-11-12 13:38:33 +01:00
Picnic-Bot
778a6e7043 Upgrade ruby/setup-ruby v1.120.0 -> v1.120.1 (#341)
See:
- https://github.com/ruby/setup-ruby/releases/tag/v1.120.1
- https://github.com/ruby/setup-ruby/compare/v1.120.0...v1.120.1
2022-11-10 07:45:10 +01:00
Luke Prananta
b6146efaf6 Introduce MapIsEmpty Refaster rule (#339) 2022-11-09 10:45:53 +01:00
Stephan Schroevers
fcaa1f7068 Improve and extend Refaster Map rules (#337)
Summary of changes:
- Move relevant rules from `AssertJRules` to `AssertJMapRules` and introduce 
  associated tests.
- Extract relevant rules from `AssortedRules` to the new `MapRules` Refaster 
  rule collection.
- Add a few new rules to both `AssertJMapRules` and `MapRules`.
2022-11-09 08:54:43 +01:00
Picnic-Bot
ece33b0061 Upgrade Jackson 2.13.4.20221013 -> 2.14.0 (#338)
See:
- https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.14
- https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.13.4.20221013...jackson-bom-2.14.0
2022-11-07 10:34:44 +01:00
Phil Werli
48772a044e Introduce Optional{Filter,Map} Refaster rules (#327) 2022-11-04 19:41:23 +01:00
Shang Xiang
281534aeca Introduce Refaster rules to streamline java.time type creation (#322) 2022-11-04 19:27:20 +01:00
Bastien Diederichs
42e632e5db Introduce IsInstanceLambdaUsage check (#323) 2022-11-04 11:47:29 +01:00
Hervé Boutemy
7febccb7ff Add Reproducible Builds badge to README (#333) 2022-11-03 20:27:45 +01:00
Guillaume Toison
d5c1c858d5 Configure documentation URL for StringJoin check (#331) 2022-11-03 16:15:59 +01:00
Picnic-Bot
55d2622380 Upgrade Checker Framework Annotations 3.26.0 -> 3.27.0 (#330)
See:
- https://github.com/typetools/checker-framework/releases/tag/checker-framework-3.27.0
- https://github.com/typetools/checker-framework/compare/checker-framework-3.26.0...checker-framework-3.27.0
2022-11-03 08:56:18 +01:00
Picnic-Bot
f10f2c9209 Upgrade NullAway 0.10.3 -> 0.10.4 (#328)
See:
- https://github.com/uber/NullAway/blob/master/CHANGELOG.md
- https://github.com/uber/NullAway/compare/v0.10.3...v0.10.4
2022-11-03 08:18:25 +01:00
Picnic-Bot
99f85614fd Upgrade swagger-annotations 2.2.4 -> 2.2.6 (#329)
See:
- https://github.com/swagger-api/swagger-core/releases/tag/v2.2.5
- https://github.com/swagger-api/swagger-core/releases/tag/v2.2.6
- https://github.com/swagger-api/swagger-core/compare/v2.2.4...v2.2.6
2022-11-03 08:00:58 +01:00
Stephan Schroevers
be24edadae [maven-release-plugin] prepare for next development iteration 2022-11-01 08:56:43 +01:00
Stephan Schroevers
068c03708b [maven-release-plugin] prepare release v0.5.0 2022-11-01 08:56:40 +01:00
Picnic-Bot
9c330981ea Upgrade Checkstyle 10.3.4 -> 10.4 (#325)
See:
- https://checkstyle.sourceforge.io/releasenotes.html
- https://github.com/checkstyle/checkstyle/releases/tag/checkstyle-10.4
- https://github.com/checkstyle/checkstyle/compare/checkstyle-10.3.4...checkstyle-10.4
2022-10-31 08:15:38 +01:00
Bastien Diederichs
b780c05dc0 Introduce assorted Reactor error handling Refaster rules (#318) 2022-10-30 16:45:40 +01:00
Picnic-Bot
16955a9cfa Upgrade NullAway 0.10.2 -> 0.10.3 (#324)
See:
- https://github.com/uber/NullAway/blob/master/CHANGELOG.md
- https://github.com/uber/NullAway/compare/v0.10.2...v0.10.3
2022-10-30 16:34:31 +01:00
Stephan Schroevers
8fa3ff3702 By default, prevent BugCheckers from introducing new dependencies (#308) 2022-10-29 13:42:51 +02:00
Picnic-Bot
022a3d293e Upgrade New Relic Java Agent 7.10.0 -> 7.11.0 (#320)
See:
- https://github.com/newrelic/newrelic-java-agent/releases/tag/v7.11.0
- https://github.com/newrelic/newrelic-java-agent/compare/v7.10.0...v7.11.0
2022-10-28 08:56:32 +02:00
Picnic-Bot
81227cdd94 Upgrade tidy-maven-plugin 1.1.0 -> 1.2.0 (#271)
See:
- https://github.com/mojohaus/tidy-maven-plugin/releases/tag/tidy-maven-plugin-1.2.0
- https://github.com/mojohaus/tidy-maven-plugin/compare/tidy-maven-plugin-1.1.0...tidy-maven-plugin-1.2.0
2022-10-27 16:28:43 +02:00
Stephan Schroevers
04d886c031 Improve build and deployment concurrency handling (#284)
Builds for the same branch are now serialized. Among other things this prevents
concurrent deployments.

While there, reduce the permission assigned to each of the two jobs.
2022-10-27 10:31:06 +02:00
Cernat Catalin Stefan
afb2a28dcf Introduce {Mono,Flux}Map{,NotNull} Refaster rules (#142) 2022-10-26 17:11:16 +02:00
Phil Werli
dc0f90e981 Introduce {Mono,Flux}#zipWith{,Iterable} Refaster rules (#293) 2022-10-26 10:53:27 +02:00
Elena Liashenko
2196bbd8f9 Have FluxFlatMapUsage better handle nested Publishers (#224) 2022-10-26 10:40:09 +02:00
Picnic-Bot
f3b81304b9 Upgrade pitest-maven-plugin 1.9.8 -> 1.9.9 (#136)
See https://github.com/hcoles/pitest/compare/1.9.8...1.9.9
2022-10-26 10:00:34 +02:00
Picnic-Bot
b0d374040a Upgrade ruby/setup-ruby v1.118.0 -> v1.120.0 (#317)
See:
- https://github.com/ruby/setup-ruby/releases/tag/v1.119.0
- https://github.com/ruby/setup-ruby/releases/tag/v1.120.0
- https://github.com/ruby/setup-ruby/compare/v1.118.0...v1.120.0
2022-10-26 09:19:33 +02:00
Eric Staffas
45dfc53d40 Prefer Flux#take(long, boolean) over Flux#take(long) to limit upstream generation (#314) 2022-10-26 08:28:35 +02:00
Rick Ossendrijver
6cb10ffe2f Build and test on additional platforms and against additional JDKs (#301) 2022-10-25 17:51:24 +02:00
Stephan Schroevers
92f2b0ab0f Introduce MoreTypes utility class (#234)
The static methods of this class allow one to construct complex types,
against which expression types can subsequently be matched.
2022-10-25 17:13:43 +02:00
chamil-prabodha
21388273c5 Have TimeZoneUsage check flag {OffsetDate,Offset,ZonedDate}Time#now (#311) 2022-10-25 16:54:28 +02:00
Stephan Schroevers
b2e15607c1 Migrate from JSR 305 to JSpecify (#181)
JSpecify's annotations have more well-defined semantics. Its `@Nullable`
annotation is also a type-use annotation recognized by Google Java
Format, so the formatter places it after any field or method modifiers.

See https://jspecify.dev
2022-10-25 10:18:22 +02:00
Stephan Schroevers
50e730fb40 Have LexicographicalAnnotationListing sort TYPE_USE annotations last (#182)
This ensures compatibility with Error Prone's `AnnotationPosition`
check.
2022-10-25 10:01:23 +02:00
Picnic-Bot
a844b9e532 Upgrade actions/configure-pages v2.1.1 -> v2.1.2 (#312)
See:
- https://github.com/actions/configure-pages/releases/tag/v2.1.2
- https://github.com/actions/configure-pages/compare/v2.1.1...v2.1.2
2022-10-25 09:24:43 +02:00
Stephan Schroevers
671ee1eedb Don't update project.build.outputTimestamp on mvn versions:set (#310)
As we rely on the value of `git.commit.time` instead.
2022-10-24 13:31:35 +02:00
Picnic-Bot
7fe61c226a Upgrade versions-maven-plugin 2.12.0 -> 2.13.0 (#309)
See:
- https://github.com/mojohaus/versions-maven-plugin/releases/tag/2.13.0
- https://github.com/mojohaus/versions-maven-plugin/compare/versions-maven-plugin-2.12.0...2.13.0
2022-10-24 13:21:55 +02:00
Picnic-Bot
8fcc91febf Upgrade Spring Boot 2.7.4 -> 2.7.5 (#307)
See:
- https://github.com/spring-projects/spring-boot/releases/tag/v2.7.5
- https://github.com/spring-projects/spring-boot/compare/v2.7.4...v2.7.5
2022-10-24 10:14:17 +02:00
Rick Ossendrijver
e00aba12c3 Make the build JDK 18+ compatible (#304) 2022-10-23 18:15:38 +02:00
Phil Werli
0118cc6c10 Introduce Reactor ContextEmpty Refaster rule (#306) 2022-10-23 17:30:01 +02:00
Picnic-Bot
91e009cab0 Upgrade actions/setup-java v3.5.1 -> v3.6.0 (#305)
See:
- https://github.com/actions/setup-java/releases/tag/v3.6.0
- https://github.com/actions/setup-java/compare/v3.5.1...v3.6.0
2022-10-20 09:13:00 +02:00
Phil Werli
68dca3204e Introduce Guava Preconditions Refaster rules (#292) 2022-10-20 08:55:49 +02:00
Picnic-Bot
e6ccb523fd Upgrade Mockito 4.8.0 -> 4.8.1 (#303)
See:
- https://github.com/mockito/mockito/releases/tag/v4.8.1
- https://github.com/mockito/mockito/compare/v4.8.0...v4.8.1
2022-10-20 08:46:03 +02:00
Picnic-Bot
d0533f7190 Upgrade actions/deploy-pages v1.2.1 -> v1.2.2 (#302)
See:
- https://github.com/actions/deploy-pages/releases/tag/v1.2.2
- https://github.com/actions/deploy-pages/compare/v1.2.1...v1.2.2
2022-10-20 08:12:58 +02:00
Picnic-Bot
759e7ea2ff Upgrade swagger-annotations 2.2.3 -> 2.2.4 (#299)
See:
- https://github.com/swagger-api/swagger-core/releases/tag/v2.2.4
- https://github.com/swagger-api/swagger-core/compare/v2.2.3...v2.2.4
2022-10-17 14:54:27 +02:00
Picnic-Bot
2578857a37 Upgrade errorprone-slf4j 0.1.15 -> 0.1.16 (#296)
See:
- https://github.com/KengoTODA/errorprone-slf4j/releases/tag/v0.1.16
- https://github.com/KengoTODA/errorprone-slf4j/compare/v0.1.15...v0.1.16
2022-10-17 11:10:20 +02:00
Picnic-Bot
1d28512f09 Upgrade swagger-annotations 1.6.7 -> 1.6.8 (#300)
See:
- https://github.com/swagger-api/swagger-core/releases/tag/v1.6.8
- https://github.com/swagger-api/swagger-core/compare/v1.6.7...v1.6.8
2022-10-17 10:44:11 +02:00
Picnic-Bot
4efd79bfa6 Upgrade Project Reactor 2020.0.23 -> 2020.0.24 (#295)
See:
- https://github.com/reactor/reactor/releases/tag/2020.0.24
- https://github.com/reactor/reactor/compare/2020.0.23...2020.0.24
2022-10-17 10:25:01 +02:00
Picnic-Bot
d6b28733f6 Upgrade Jackson 2.13.4 -> 2.13.4.20221013 (#294)
See:
- https://github.com/FasterXML/jackson-databind/compare/jackson-databind-2.13.4...jackson-databind-2.13.4.2
- https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.13.4...jackson-bom-2.13.4.20221013
2022-10-15 17:01:00 +02:00
Picnic-Bot
b9884fa6d6 Upgrade Error Prone 2.15.0 -> 2.16 (#291)
See:
- https://github.com/google/error-prone/releases/tag/v2.16
- https://github.com/google/error-prone/compare/v2.15.0...v2.16
- https://github.com/PicnicSupermarket/error-prone/compare/v2.15.0-picnic-3...v2.16-picnic-1
2022-10-15 13:22:02 +02:00
Picnic-Bot
4fd8f57430 Upgrade ruby/setup-ruby v1.117.0 -> v1.118.0 (#298)
See:
- https://github.com/ruby/setup-ruby/releases/tag/v1.118.0
- https://github.com/ruby/setup-ruby/compare/v1.117.0...v1.118.0
2022-10-15 11:03:17 +02:00
Jelmer Borst
a2559bddda Fix and simplify documented example compiler output (#297) 2022-10-13 10:12:37 +02:00
Stephan Schroevers
7de9bede19 [maven-release-plugin] prepare for next development iteration 2022-10-13 09:20:46 +02:00
164 changed files with 6788 additions and 802 deletions

View File

@@ -17,23 +17,24 @@ you'd like to be solved through Error Prone Support. -->
- [ ] Support a stylistic preference.
- [ ] Avoid a common gotcha, or potential problem.
- [ ] Improve performance.
<!--
Here, provide a clear and concise description of the desired change.
If possible, provide a simple and minimal example using the following format:
I would like to rewrite the following code:
```java
// XXX: Write the code to match here.
```
to:
```java
// XXX: Write the desired code here.
```
-->
I would like to rewrite the following code:
```java
// XXX: Write the code to match here.
```
to:
```java
// XXX: Write the desired code here.
```
### Considerations
<!--

View File

@@ -7,10 +7,27 @@ permissions:
contents: read
jobs:
build:
runs-on: ubuntu-22.04
strategy:
matrix:
jdk: [ 11.0.16, 17.0.4 ]
os: [ ubuntu-22.04 ]
jdk: [ 11.0.16, 17.0.4, 19 ]
distribution: [ temurin ]
experimental: [ false ]
include:
- os: macos-12
jdk: 17.0.4
distribution: temurin
experimental: false
- os: windows-2022
jdk: 17.0.4
distribution: temurin
experimental: false
- os: ubuntu-22.04
jdk: 20-ea
distribution: zulu
experimental: true
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
steps:
# We run the build twice for each supported JDK: once against the
# original Error Prone release, using only Error Prone checks available
@@ -20,10 +37,10 @@ jobs:
- name: Check out code
uses: actions/checkout@v3.1.0
- name: Set up JDK
uses: actions/setup-java@v3.5.1
uses: actions/setup-java@v3.8.0
with:
java-version: ${{ matrix.jdk }}
distribution: temurin
distribution: ${{ matrix.distribution }}
cache: maven
- name: Display build environment details
run: mvn --version

View File

@@ -3,25 +3,22 @@ on:
pull_request:
push:
branches: [ master, website ]
permissions:
contents: read
id-token: write
pages: write
concurrency:
group: pages
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
build:
permissions:
contents: read
runs-on: ubuntu-22.04
steps:
- name: Check out code
uses: actions/checkout@v3.1.0
- uses: ruby/setup-ruby@v1.117.0
- uses: ruby/setup-ruby@v1.126.0
with:
working-directory: ./website
bundler-cache: true
- name: Configure Github Pages
uses: actions/configure-pages@v2.1.1
uses: actions/configure-pages@v2.1.3
- name: Generate documentation
run: ./generate-docs.sh
- name: Build website with Jekyll
@@ -33,12 +30,15 @@ jobs:
# "Refaster rules" terminology on our website and in the code.
run: bundle exec htmlproofer --disable_external true --check-external-hash false ./_site
- name: Upload website as artifact
uses: actions/upload-pages-artifact@v1.0.4
uses: actions/upload-pages-artifact@v1.0.5
with:
path: ./website/_site
deploy:
if: github.ref == 'refs/heads/website'
needs: build
permissions:
id-token: write
pages: write
runs-on: ubuntu-22.04
environment:
name: github-pages
@@ -46,4 +46,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1.2.1
uses: actions/deploy-pages@v1.2.3

37
.github/workflows/pitest-analyze-pr.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
# Performs mutation testing analysis on the files changed by a pull request and
# uploads the results. The associated PR is subsequently updated by the
# `pitest-update-pr.yml` workflow. See https://blog.pitest.org/oss-pitest-pr/
# for details.
name: "Mutation testing"
on:
pull_request:
permissions:
contents: read
jobs:
analyze-pr:
runs-on: ubuntu-22.04
steps:
- name: Check out code
uses: actions/checkout@v3.1.0
with:
fetch-depth: 2
- name: Set up JDK
uses: actions/setup-java@v3.8.0
with:
java-version: 17.0.4
distribution: temurin
cache: maven
- name: Run Pitest
# By running with features `+GIT(from[HEAD~1]), +gitci`, Pitest only
# analyzes lines changed in the associated pull request, as GitHub
# exposes the changes unique to the PR as a single commit on top of the
# target branch. See https://blog.pitest.org/pitest-pr-setup for
# details.
run: mvn test pitest:mutationCoverage -DargLine.xmx=2048m -Dverification.skip -Dfeatures="+GIT(from[HEAD~1]), +gitci"
- name: Aggregate Pitest reports
run: mvn pitest-git:aggregate -DkilledEmoji=":tada:" -DmutantEmoji=":zombie:" -DtrailingText="Mutation testing report by [Pitest](https://pitest.org/). Review any surviving mutants by inspecting the line comments under [_Files changed_](${{ github.event.number }}/files)."
- name: Upload Pitest reports as artifact
uses: actions/upload-artifact@v3.1.1
with:
name: pitest-reports
path: ./target/pit-reports-ci

35
.github/workflows/pitest-update-pr.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
# Updates a pull request based on the corresponding mutation testing analysis
# performed by the `pitest-analyze-pr.yml` workflow. See
# https://blog.pitest.org/oss-pitest-pr/ for details.
name: "Mutation testing: post results"
on:
workflow_run:
workflows: ["Mutation testing"]
types:
- completed
permissions:
actions: read
checks: write
contents: read
pull-requests: write
jobs:
update-pr:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-22.04
steps:
- name: Check out code
uses: actions/checkout@v3.1.0
- name: Set up JDK
uses: actions/setup-java@v3.8.0
with:
java-version: 17.0.4
distribution: temurin
cache: maven
- name: Download Pitest analysis artifact
uses: dawidd6/action-download-artifact@v2.24.2
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
name: pitest-reports
path: ./target/pit-reports-ci
- name: Update PR
run: mvn -DrepoToken="${{ secrets.GITHUB_TOKEN }}" pitest-github:updatePR

View File

@@ -10,7 +10,7 @@
},
{
"matchPackagePatterns": [
"^com\\.palantir\\.baseline:baseline-error-prone$"
"^ruby\\/setup-ruby$"
],
"schedule": "* * 1 * *"
}

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017-2022 Picnic Technologies BV
Copyright (c) 2017-2023 Picnic Technologies BV
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -20,6 +20,7 @@ loves Error Prone: producing high-quality and consistent Java
code_][picnic-blog-ep-post].
[![Maven Central][maven-central-badge]][maven-central-search]
[![Reproducible Builds][reproducible-builds-badge]][reproducible-builds-report]
[![GitHub Actions][github-actions-build-badge]][github-actions-build-master]
[![License][license-badge]][license]
[![PRs Welcome][pr-badge]][contributing]
@@ -119,16 +120,13 @@ code with Maven should yield two compiler warnings:
```sh
$ mvn clean install
...
[INFO] -------------------------------------------------------------
[WARNING] COMPILATION WARNING :
[INFO] -------------------------------------------------------------
[WARNING] Example.java:[9,34] [tech.picnic.errorprone.refasterrules.BigDecimalRules.BigDecimalZero]
[INFO] Example.java:[9,34] [Refaster Rule] BigDecimalRules.BigDecimalZero: Refactoring opportunity
(see https://error-prone.picnic.tech/refasterrules/BigDecimalRules#BigDecimalZero)
Did you mean 'return BigDecimal.ZERO;'?
...
[WARNING] Example.java:[13,35] [IdentityConversion] This method invocation appears redundant; remove it or suppress this warning and add a comment explaining its purpose
(see https://error-prone.picnic.tech/bugpatterns/IdentityConversion)
Did you mean 'return set;' or '@SuppressWarnings("IdentityConversion") public ImmutableSet<Integer> getSet() {'?
[INFO] 2 warnings
[INFO] -------------------------------------------------------------
...
```
@@ -168,7 +166,7 @@ Some other commands one may find relevant:
- `mvn fmt:format` formats the code using
[`google-java-format`][google-java-format].
- `./run-mutation-tests.sh` runs mutation tests using [PIT][pitest]. The
- `./run-mutation-tests.sh` runs mutation tests using [Pitest][pitest]. The
results can be reviewed by opening the respective
`target/pit-reports/index.html` files. For more information check the [PIT
Maven plugin][pitest-maven].
@@ -229,3 +227,5 @@ guidelines][contributing].
[refaster]: https://errorprone.info/docs/refaster
[refaster-rules-bigdecimal]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/BigDecimalRules.java
[refaster-rules]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/
[reproducible-builds-badge]: https://img.shields.io/badge/Reproducible_Builds-ok-success?labelColor=1e5b96
[reproducible-builds-report]: https://github.com/jvm-repo-rebuild/reproducible-central/blob/master/content/tech/picnic/error-prone-support/error-prone-support/README.md

View File

@@ -9,13 +9,14 @@
set -e -u -o pipefail
if [ "${#}" -gt 1 ]; then
echo "Usage: ./$(basename "${0}") [PatchChecks]"
echo "Usage: ${0} [PatchChecks]"
exit 1
fi
patchChecks=${1:-}
mvn clean test-compile fmt:format \
-s "$(dirname "${0}")/settings.xml" \
-T 1.0C \
-Perror-prone \
-Perror-prone-fork \

7
cdg-pitest-licence.txt Normal file
View File

@@ -0,0 +1,7 @@
# Arcmutate license for Error Prone Support, requested by sending an email to
# support@arcmutate.com.
expires=07/11/2023
keyVersion=1
signature=MhZxMbnO6UovNfllM0JuVWkZyvRT3/G5o/uT0Mm36c7200VpZNVu03gTAGivnl9W5RzvZhfpIHccuQ5ctjQkrqhsFSrl4fyqPqu3y5V2fsHIdFXP/G72EGj6Kay9ndLpaEHalqE0bEwxdnHMzEYq5y3O9vUPv8MhUl57xk+rvBo\=
packages=tech.picnic.errorprone.*
type=OSSS

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>tech.picnic.error-prone-support</groupId>
<artifactId>error-prone-support</artifactId>
<version>0.4.0</version>
<version>0.7.0</version>
</parent>
<artifactId>error-prone-contrib</artifactId>
@@ -64,11 +64,6 @@
<artifactId>auto-service-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.googlejavaformat</groupId>
<artifactId>google-java-format</artifactId>
@@ -98,6 +93,11 @@
<artifactId>reactor-adapter</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.projectreactor.addons</groupId>
<artifactId>reactor-extra</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava2</groupId>
<artifactId>rxjava</artifactId>
@@ -146,7 +146,7 @@
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
@@ -173,6 +173,11 @@
<artifactId>slf4j-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>

View File

@@ -36,6 +36,9 @@ public final class AmbiguousJsonCreator extends BugChecker implements Annotation
private static final Matcher<AnnotationTree> IS_JSON_CREATOR_ANNOTATION =
isType("com.fasterxml.jackson.annotation.JsonCreator");
/** Instantiates a new {@link AmbiguousJsonCreator} instance. */
public AmbiguousJsonCreator() {}
@Override
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
if (!IS_JSON_CREATOR_ANNOTATION.matches(tree, state)) {

View File

@@ -44,6 +44,9 @@ public final class AssertJIsNull extends BugChecker implements MethodInvocationT
argumentCount(1),
argument(0, nullLiteral()));
/** Instantiates a new {@link AssertJIsNull} instance. */
public AssertJIsNull() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!ASSERT_IS_EQUAL_TO_NULL.matches(tree, state)) {

View File

@@ -15,7 +15,6 @@ import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.MultiMatcher;
import com.google.errorprone.util.ASTHelpers;
@@ -24,6 +23,7 @@ import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import java.util.List;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/** A {@link BugChecker} that flags redundant {@code @Autowired} constructor annotations. */
@AutoService(BugChecker.class)
@@ -38,6 +38,9 @@ public final class AutowiredConstructor extends BugChecker implements ClassTreeM
private static final MultiMatcher<Tree, AnnotationTree> AUTOWIRED_ANNOTATION =
annotations(AT_LEAST_ONE, isType("org.springframework.beans.factory.annotation.Autowired"));
/** Instantiates a new {@link AutowiredConstructor} instance. */
public AutowiredConstructor() {}
@Override
public Description matchClass(ClassTree tree, VisitorState state) {
List<MethodTree> constructors = ASTHelpers.getConstructors(tree);
@@ -59,6 +62,6 @@ public final class AutowiredConstructor extends BugChecker implements ClassTreeM
* leave flagging the unused import to Error Prone's `RemoveUnusedImports` check.
*/
AnnotationTree annotation = Iterables.getOnlyElement(annotations);
return describeMatch(annotation, SuggestedFix.delete(annotation));
return describeMatch(annotation, SourceCode.deleteWithTrailingWhitespace(annotation, state));
}
}

View File

@@ -46,6 +46,9 @@ public final class CanonicalAnnotationSyntax extends BugChecker implements Annot
CanonicalAnnotationSyntax::dropRedundantValueAttribute,
CanonicalAnnotationSyntax::dropRedundantCurlies);
/** Instantiates a new {@link CanonicalAnnotationSyntax} instance. */
public CanonicalAnnotationSyntax() {}
@Override
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
return FIX_FACTORIES.stream()

View File

@@ -18,6 +18,7 @@ import com.google.errorprone.matchers.Matcher;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import java.util.stream.Collector;
import tech.picnic.errorprone.bugpatterns.util.ThirdPartyLibrary;
/**
* A {@link BugChecker} that flags {@link Collector Collectors} that don't clearly express
@@ -45,9 +46,13 @@ public final class CollectorMutability extends BugChecker implements MethodInvoc
private static final Matcher<ExpressionTree> SET_COLLECTOR =
staticMethod().anyClass().named("toSet");
/** Instantiates a new {@link CollectorMutability} instance. */
public CollectorMutability() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!COLLECTOR_METHOD.matches(tree, state)) {
if (!ThirdPartyLibrary.GUAVA.isIntroductionAllowed(state)
|| !COLLECTOR_METHOD.matches(tree, state)) {
return Description.NO_MATCH;
}

View File

@@ -14,7 +14,6 @@ import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
@@ -22,6 +21,7 @@ import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import java.util.Optional;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/** A {@link BugChecker} that flags empty methods that seemingly can simply be deleted. */
@AutoService(BugChecker.class)
@@ -38,6 +38,9 @@ public final class EmptyMethod extends BugChecker implements MethodTreeMatcher {
AT_LEAST_ONE,
anyOf(isType("java.lang.Override"), isType("org.aspectj.lang.annotation.Pointcut")));
/** Instantiates a new {@link EmptyMethod} instance. */
public EmptyMethod() {}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (tree.getBody() == null
@@ -52,7 +55,7 @@ public final class EmptyMethod extends BugChecker implements MethodTreeMatcher {
return Description.NO_MATCH;
}
return describeMatch(tree, SuggestedFix.delete(tree));
return describeMatch(tree, SourceCode.deleteWithTrailingWhitespace(tree, state));
}
private static boolean isInPossibleTestHelperClass(VisitorState state) {

View File

@@ -72,6 +72,9 @@ public final class ErrorProneTestHelperSourceFormat extends BugChecker
.onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper.ExpectOutput")
.named("addOutputLines");
/** Instantiates a new {@link ErrorProneTestHelperSourceFormat} instance. */
public ErrorProneTestHelperSourceFormat() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
boolean isOutputSource = OUTPUT_SOURCE_ACCEPTING_METHOD.matches(tree, state);

View File

@@ -46,6 +46,9 @@ public final class ExplicitEnumOrdering extends BugChecker implements MethodInvo
private static final Matcher<ExpressionTree> EXPLICIT_ORDERING =
staticMethod().onClass(Ordering.class.getName()).named("explicit");
/** Instantiates a new {@link ExplicitEnumOrdering} instance. */
public ExplicitEnumOrdering() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!EXPLICIT_ORDERING.matches(tree, state)) {

View File

@@ -5,6 +5,10 @@ import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.generic;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.subOf;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.type;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.unbound;
import com.google.auto.service.AutoService;
import com.google.common.collect.Iterables;
@@ -17,11 +21,14 @@ import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.suppliers.Supplier;
import com.google.errorprone.suppliers.Suppliers;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MemberReferenceTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.tools.javac.code.Type;
import java.util.function.Function;
import java.util.function.Supplier;
import reactor.core.publisher.Flux;
/**
@@ -33,11 +40,12 @@ import reactor.core.publisher.Flux;
* former interleaves values as they are emitted, yielding nondeterministic results. In most cases
* {@link Flux#concatMap(Function)} should be preferred, as it produces consistent results and
* avoids potentially saturating the thread pool on which subscription happens. If {@code
* concatMap}'s single-subscription semantics are undesirable one should invoke a {@code flatMap} or
* {@code flatMapSequential} overload with an explicit concurrency level.
* concatMap}'s sequential-subscription semantics are undesirable one should invoke a {@code
* flatMap} or {@code flatMapSequential} overload with an explicit concurrency level.
*
* <p>NB: The rarely-used overload {@link Flux#flatMap(Function, Function, Supplier)} is not flagged
* by this check because there is no clear alternative to point to.
* <p>NB: The rarely-used overload {@link Flux#flatMap(Function, Function,
* java.util.function.Supplier)} is not flagged by this check because there is no clear alternative
* to point to.
*/
@AutoService(BugChecker.class)
@BugPattern(
@@ -52,11 +60,19 @@ public final class FluxFlatMapUsage extends BugChecker
implements MethodInvocationTreeMatcher, MemberReferenceTreeMatcher {
private static final long serialVersionUID = 1L;
private static final String MAX_CONCURRENCY_ARG_NAME = "MAX_CONCURRENCY";
private static final Supplier<Type> FLUX =
Suppliers.typeFromString("reactor.core.publisher.Flux");
private static final Matcher<ExpressionTree> FLUX_FLATMAP =
instanceMethod()
.onDescendantOf("reactor.core.publisher.Flux")
.onDescendantOf(FLUX)
.namedAnyOf("flatMap", "flatMapSequential")
.withParameters(Function.class.getName());
private static final Supplier<Type> FLUX_OF_PUBLISHERS =
VisitorState.memoize(
generic(FLUX, subOf(generic(type("org.reactivestreams.Publisher"), unbound()))));
/** Instantiates a new {@link FluxFlatMapUsage} instance. */
public FluxFlatMapUsage() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
@@ -64,14 +80,27 @@ public final class FluxFlatMapUsage extends BugChecker
return Description.NO_MATCH;
}
return buildDescription(tree)
.addFix(SuggestedFixes.renameMethodInvocation(tree, "concatMap", state))
.addFix(
SuggestedFix.builder()
.postfixWith(
Iterables.getOnlyElement(tree.getArguments()), ", " + MAX_CONCURRENCY_ARG_NAME)
.build())
.build();
SuggestedFix serializationFix = SuggestedFixes.renameMethodInvocation(tree, "concatMap", state);
SuggestedFix concurrencyCapFix =
SuggestedFix.builder()
.postfixWith(
Iterables.getOnlyElement(tree.getArguments()), ", " + MAX_CONCURRENCY_ARG_NAME)
.build();
Description.Builder description = buildDescription(tree);
if (state.getTypes().isSubtype(ASTHelpers.getType(tree), FLUX_OF_PUBLISHERS.get(state))) {
/*
* Nested publishers may need to be subscribed to eagerly in order to avoid a deadlock, e.g.
* if they are produced by `Flux#groupBy`. In this case we suggest specifying an explicit
* concurrently bound, in favour of sequential subscriptions using `Flux#concatMap`.
*/
description.addFix(concurrencyCapFix).addFix(serializationFix);
} else {
description.addFix(serializationFix).addFix(concurrencyCapFix);
}
return description.build();
}
@Override

View File

@@ -32,7 +32,7 @@ import com.sun.source.util.SimpleTreeVisitor;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
@@ -129,6 +129,9 @@ public final class FormatStringConcatenation extends BugChecker
.onDescendantOf("org.slf4j.Logger")
.namedAnyOf("debug", "error", "info", "trace", "warn");
/** Instantiates a new {@link FormatStringConcatenation} instance. */
public FormatStringConcatenation() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (hasNonConstantStringConcatenationArgument(tree, 0, state)) {
@@ -204,7 +207,7 @@ public final class FormatStringConcatenation extends BugChecker
}
private static class ReplacementArgumentsConstructor
extends SimpleTreeVisitor<Void, VisitorState> {
extends SimpleTreeVisitor<@Nullable Void, VisitorState> {
private final StringBuilder formatString = new StringBuilder();
private final List<Tree> formatArguments = new ArrayList<>();
private final String formatSpecifier;
@@ -213,9 +216,8 @@ public final class FormatStringConcatenation extends BugChecker
this.formatSpecifier = formatSpecifier;
}
@Nullable
@Override
public Void visitBinary(BinaryTree tree, VisitorState state) {
public @Nullable Void visitBinary(BinaryTree tree, VisitorState state) {
if (tree.getKind() == Kind.PLUS && isStringTyped(tree, state)) {
tree.getLeftOperand().accept(this, state);
tree.getRightOperand().accept(this, state);
@@ -226,15 +228,13 @@ public final class FormatStringConcatenation extends BugChecker
return null;
}
@Nullable
@Override
public Void visitParenthesized(ParenthesizedTree tree, VisitorState state) {
public @Nullable Void visitParenthesized(ParenthesizedTree tree, VisitorState state) {
return tree.getExpression().accept(this, state);
}
@Nullable
@Override
protected Void defaultAction(Tree tree, VisitorState state) {
protected @Nullable Void defaultAction(Tree tree, VisitorState state) {
appendExpression(tree);
return null;
}

View File

@@ -23,6 +23,7 @@ import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.google.errorprone.util.ASTHelpers.TargetType;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
@@ -32,7 +33,7 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/** A {@link BugChecker} that flags redundant identity conversions. */
// XXX: Consider detecting cases where a flagged expression is passed to a method, and where removal
// of the identify conversion would cause a different method overload to be selected. Depending on
// of the identity conversion would cause a different method overload to be selected. Depending on
// the target method such a modification may change the code's semantics or performance.
@AutoService(BugChecker.class)
@BugPattern(
@@ -45,6 +46,13 @@ public final class IdentityConversion extends BugChecker implements MethodInvoca
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> IS_CONVERSION_METHOD =
anyOf(
staticMethod()
.onClassAny(
Primitives.allWrapperTypes().stream()
.map(Class::getName)
.collect(toImmutableSet()))
.named("valueOf"),
staticMethod().onClass(String.class.getName()).named("valueOf"),
staticMethod()
.onClassAny(
"com.google.common.collect.ImmutableBiMap",
@@ -60,18 +68,17 @@ public final class IdentityConversion extends BugChecker implements MethodInvoca
"com.google.common.collect.ImmutableTable")
.named("copyOf"),
staticMethod()
.onClassAny(
Primitives.allWrapperTypes().stream()
.map(Class::getName)
.collect(toImmutableSet()))
.named("valueOf"),
staticMethod().onClass(String.class.getName()).named("valueOf"),
.onClass("com.google.errorprone.matchers.Matchers")
.namedAnyOf("allOf", "anyOf"),
staticMethod().onClass("reactor.adapter.rxjava.RxJava2Adapter"),
staticMethod()
.onClass("reactor.core.publisher.Flux")
.namedAnyOf("concat", "firstWithSignal", "from", "merge"),
staticMethod().onClass("reactor.core.publisher.Mono").namedAnyOf("from", "fromDirect"));
/** Instantiates a new {@link IdentityConversion} instance. */
public IdentityConversion() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
List<? extends ExpressionTree> arguments = tree.getArguments();
@@ -92,6 +99,15 @@ public final class IdentityConversion extends BugChecker implements MethodInvoca
return Description.NO_MATCH;
}
if (sourceType.isPrimitive()
&& state.getPath().getParentPath().getLeaf() instanceof MemberSelectTree) {
/*
* The result of the conversion method is dereferenced, while the source type is a primitive:
* dropping the conversion would yield uncompilable code.
*/
return Description.NO_MATCH;
}
return buildDescription(tree)
.setMessage(
"This method invocation appears redundant; remove it or suppress this warning and "

View File

@@ -67,6 +67,9 @@ public final class ImmutablesSortedSetComparator extends BugChecker implements M
hasAnnotation("org.immutables.value.Value.NaturalOrder"),
hasAnnotation("org.immutables.value.Value.ReverseOrder"))));
/** Instantiates a new {@link ImmutablesSortedSetComparator} instance. */
public ImmutablesSortedSetComparator() {}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (!METHOD_LACKS_ANNOTATION.matches(tree, state)) {

View File

@@ -0,0 +1,60 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.LambdaExpressionTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.InstanceOfTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.VariableTree;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags lambda expressions that can be replaced with a method reference
* of the form {@code T.class::isInstance}.
*
* @see MethodReferenceUsage
*/
// XXX: Consider folding this logic into the `MethodReferenceUsage` check.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Prefer `Class::isInstance` method reference over equivalent lambda expression",
link = BUG_PATTERNS_BASE_URL + "IsInstanceLambdaUsage",
linkType = CUSTOM,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class IsInstanceLambdaUsage extends BugChecker implements LambdaExpressionTreeMatcher {
private static final long serialVersionUID = 1L;
/** Instantiates a new {@link IsInstanceLambdaUsage} instance. */
public IsInstanceLambdaUsage() {}
@Override
public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) {
if (tree.getParameters().size() != 1 || tree.getBody().getKind() != Kind.INSTANCE_OF) {
return Description.NO_MATCH;
}
VariableTree param = Iterables.getOnlyElement(tree.getParameters());
InstanceOfTree instanceOf = (InstanceOfTree) tree.getBody();
if (!ASTHelpers.getSymbol(param).equals(ASTHelpers.getSymbol(instanceOf.getExpression()))) {
return Description.NO_MATCH;
}
return describeMatch(
tree,
SuggestedFix.replace(
tree, SourceCode.treeToString(instanceOf.getType(), state) + ".class::isInstance"));
}
}

View File

@@ -0,0 +1,82 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.annotations;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.hasMethod;
import static com.google.errorprone.matchers.Matchers.hasModifier;
import static com.google.errorprone.matchers.Matchers.isType;
import static com.google.errorprone.matchers.Matchers.not;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.TEST_METHOD;
import static tech.picnic.errorprone.bugpatterns.util.MoreMatchers.hasMetaAnnotation;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.sun.source.tree.ClassTree;
import javax.lang.model.element.Modifier;
/**
* A {@link BugChecker} that flags non-final and non package-private JUnit test class declarations,
* unless abstract.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Non-abstract JUnit test classes should be declared package-private and final",
linkType = CUSTOM,
link = BUG_PATTERNS_BASE_URL + "JUnitClassModifiers",
severity = SUGGESTION,
tags = STYLE)
public final class JUnitClassModifiers extends BugChecker implements ClassTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ClassTree> HAS_SPRING_CONFIGURATION_ANNOTATION =
annotations(
AT_LEAST_ONE,
anyOf(
isType("org.springframework.context.annotation.Configuration"),
hasMetaAnnotation("org.springframework.context.annotation.Configuration")));
private static final Matcher<ClassTree> TEST_CLASS_WITH_INCORRECT_MODIFIERS =
allOf(
hasMethod(TEST_METHOD),
not(hasModifier(Modifier.ABSTRACT)),
anyOf(
hasModifier(Modifier.PRIVATE),
hasModifier(Modifier.PROTECTED),
hasModifier(Modifier.PUBLIC),
allOf(not(hasModifier(Modifier.FINAL)), not(HAS_SPRING_CONFIGURATION_ANNOTATION))));
/** Instantiates a new {@link JUnitClassModifiers} instance. */
public JUnitClassModifiers() {}
@Override
public Description matchClass(ClassTree tree, VisitorState state) {
if (!TEST_CLASS_WITH_INCORRECT_MODIFIERS.matches(tree, state)) {
return Description.NO_MATCH;
}
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
SuggestedFixes.removeModifiers(
tree.getModifiers(),
state,
ImmutableSet.of(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC))
.ifPresent(fixBuilder::merge);
if (!HAS_SPRING_CONFIGURATION_ANNOTATION.matches(tree, state)) {
SuggestedFixes.addModifiers(tree, state, Modifier.FINAL).ifPresent(fixBuilder::merge);
}
return describeMatch(tree, fixBuilder.build());
}
}

View File

@@ -3,19 +3,19 @@ package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.annotations;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.enclosingClass;
import static com.google.errorprone.matchers.Matchers.hasModifier;
import static com.google.errorprone.matchers.Matchers.isType;
import static com.google.errorprone.matchers.Matchers.not;
import static java.util.function.Predicate.not;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import static tech.picnic.errorprone.bugpatterns.util.JavaKeywords.isReservedKeyword;
import static tech.picnic.errorprone.bugpatterns.util.JavaKeywords.isValidIdentifier;
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.SETUP_OR_TEARDOWN_METHOD;
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.TEST_METHOD;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
@@ -24,19 +24,15 @@ import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
import com.google.errorprone.matchers.MultiMatcher;
import com.google.errorprone.predicates.TypePredicate;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Type;
import java.util.Optional;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/** A {@link BugChecker} that flags non-canonical JUnit method declarations. */
@@ -56,32 +52,19 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
private static final long serialVersionUID = 1L;
private static final String TEST_PREFIX = "test";
private static final ImmutableSet<Modifier> ILLEGAL_MODIFIERS =
ImmutableSet.of(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC);
private static final Matcher<MethodTree> HAS_UNMODIFIABLE_SIGNATURE =
anyOf(
annotations(AT_LEAST_ONE, isType("java.lang.Override")),
allOf(
Matchers.not(hasModifier(Modifier.FINAL)),
Matchers.not(hasModifier(Modifier.PRIVATE)),
enclosingClass(hasModifier(Modifier.ABSTRACT))));
private static final MultiMatcher<MethodTree, AnnotationTree> TEST_METHOD =
annotations(
AT_LEAST_ONE,
anyOf(
isType("org.junit.jupiter.api.Test"),
hasMetaAnnotation("org.junit.jupiter.api.TestTemplate")));
private static final MultiMatcher<MethodTree, AnnotationTree> SETUP_OR_TEARDOWN_METHOD =
annotations(
AT_LEAST_ONE,
anyOf(
isType("org.junit.jupiter.api.AfterAll"),
isType("org.junit.jupiter.api.AfterEach"),
isType("org.junit.jupiter.api.BeforeAll"),
isType("org.junit.jupiter.api.BeforeEach")));
Sets.immutableEnumSet(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC);
private static final Matcher<MethodTree> IS_LIKELY_OVERRIDDEN =
allOf(
not(hasModifier(Modifier.FINAL)),
not(hasModifier(Modifier.PRIVATE)),
enclosingClass(hasModifier(Modifier.ABSTRACT)));
/** Instantiates a new {@link JUnitMethodDeclaration} instance. */
public JUnitMethodDeclaration() {}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (HAS_UNMODIFIABLE_SIGNATURE.matches(tree, state)) {
if (IS_LIKELY_OVERRIDDEN.matches(tree, state) || isOverride(tree, state)) {
return Description.NO_MATCH;
}
@@ -103,10 +86,11 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
private void suggestTestMethodRenameIfApplicable(
MethodTree tree, SuggestedFix.Builder fixBuilder, VisitorState state) {
tryCanonicalizeMethodName(tree)
MethodSymbol symbol = ASTHelpers.getSymbol(tree);
tryCanonicalizeMethodName(symbol)
.ifPresent(
newName ->
findMethodRenameBlocker(newName, state)
findMethodRenameBlocker(symbol, newName, state)
.ifPresentOrElse(
blocker -> reportMethodRenameBlocker(tree, blocker, state),
() -> fixBuilder.merge(SuggestedFixes.renameMethod(tree, newName, state))));
@@ -138,30 +122,29 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
* consideration cannot be referenced directly.)
* </ul>
*/
private static Optional<String> findMethodRenameBlocker(String methodName, VisitorState state) {
if (isMethodInEnclosingClass(methodName, state)) {
private static Optional<String> findMethodRenameBlocker(
MethodSymbol method, String newName, VisitorState state) {
if (isExistingMethodName(method.owner.type, newName, state)) {
return Optional.of(
String.format("a method named `%s` already exists in this class", methodName));
String.format(
"a method named `%s` is already defined in this class or a supertype", newName));
}
if (isSimpleNameStaticallyImported(methodName, state)) {
return Optional.of(String.format("`%s` is already statically imported", methodName));
if (isSimpleNameStaticallyImported(newName, state)) {
return Optional.of(String.format("`%s` is already statically imported", newName));
}
if (isReservedKeyword(methodName)) {
return Optional.of(String.format("`%s` is a reserved keyword", methodName));
if (!isValidIdentifier(newName)) {
return Optional.of(String.format("`%s` is not a valid identifier", newName));
}
return Optional.empty();
}
private static boolean isMethodInEnclosingClass(String methodName, VisitorState state) {
return state.findEnclosing(ClassTree.class).getMembers().stream()
.filter(MethodTree.class::isInstance)
.map(MethodTree.class::cast)
.map(MethodTree::getName)
.map(Name::toString)
.anyMatch(methodName::equals);
private static boolean isExistingMethodName(Type clazz, String name, VisitorState state) {
return ASTHelpers.matchingMethods(state.getName(name), method -> true, clazz, state.getTypes())
.findAny()
.isPresent();
}
private static boolean isSimpleNameStaticallyImported(String simpleName, VisitorState state) {
@@ -177,8 +160,8 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
return source.subSequence(source.lastIndexOf('.') + 1, source.length());
}
private static Optional<String> tryCanonicalizeMethodName(MethodTree tree) {
return Optional.of(ASTHelpers.getSymbol(tree).getQualifiedName().toString())
private static Optional<String> tryCanonicalizeMethodName(Symbol symbol) {
return Optional.of(symbol.getQualifiedName().toString())
.filter(name -> name.startsWith(TEST_PREFIX))
.map(name -> name.substring(TEST_PREFIX.length()))
.filter(not(String::isEmpty))
@@ -186,17 +169,9 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
.filter(name -> !Character.isDigit(name.charAt(0)));
}
// XXX: Move to a `MoreMatchers` utility class.
private static Matcher<AnnotationTree> hasMetaAnnotation(String annotationClassName) {
TypePredicate typePredicate = hasAnnotation(annotationClassName);
return (tree, state) -> {
Symbol sym = ASTHelpers.getSymbol(tree);
return sym != null && typePredicate.apply(sym.type, state);
};
}
// XXX: Move to a `MoreTypePredicates` utility class.
private static TypePredicate hasAnnotation(String annotationClassName) {
return (type, state) -> ASTHelpers.hasAnnotation(type.tsym, annotationClassName, state);
private static boolean isOverride(MethodTree tree, VisitorState state) {
return ASTHelpers.streamSuperMethods(ASTHelpers.getSymbol(tree), state.getTypes())
.findAny()
.isPresent();
}
}

View File

@@ -40,8 +40,9 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.bugpatterns.util.AnnotationAttributeMatcher;
import tech.picnic.errorprone.bugpatterns.util.Flags;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
@@ -82,7 +83,7 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
private final AnnotationAttributeMatcher matcher;
/** Instantiates the default {@link LexicographicalAnnotationAttributeListing}. */
/** Instantiates a default {@link LexicographicalAnnotationAttributeListing} instance. */
public LexicographicalAnnotationAttributeListing() {
this(ErrorProneFlags.empty());
}
@@ -184,17 +185,15 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
private static ImmutableList<ImmutableList<String>> getStructure(ExpressionTree array) {
ImmutableList.Builder<ImmutableList<String>> nodes = ImmutableList.builder();
new TreeScanner<Void, Void>() {
@Nullable
new TreeScanner<@Nullable Void, @Nullable Void>() {
@Override
public Void visitIdentifier(IdentifierTree node, @Nullable Void unused) {
public @Nullable Void visitIdentifier(IdentifierTree node, @Nullable Void unused) {
nodes.add(ImmutableList.of(node.getName().toString()));
return super.visitIdentifier(node, unused);
}
@Nullable
@Override
public Void visitLiteral(LiteralTree node, @Nullable Void unused) {
public @Nullable Void visitLiteral(LiteralTree node, @Nullable Void unused) {
Object value = ASTHelpers.constValue(node);
nodes.add(
value instanceof String
@@ -204,9 +203,8 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
return super.visitLiteral(node, unused);
}
@Nullable
@Override
public Void visitPrimitiveType(PrimitiveTypeTree node, @Nullable Void unused) {
public @Nullable Void visitPrimitiveType(PrimitiveTypeTree node, @Nullable Void unused) {
nodes.add(ImmutableList.of(node.getPrimitiveTypeKind().toString()));
return super.visitPrimitiveType(node, unused);
}
@@ -223,7 +221,7 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
private static ImmutableList<String> excludedAnnotations(ErrorProneFlags flags) {
Set<String> exclusions = new HashSet<>();
flags.getList(EXCLUDED_ANNOTATIONS_FLAG).ifPresent(exclusions::addAll);
exclusions.addAll(Flags.getList(flags, EXCLUDED_ANNOTATIONS_FLAG));
exclusions.addAll(BLACKLISTED_ANNOTATIONS);
return ImmutableList.copyOf(exclusions);
}

View File

@@ -4,10 +4,13 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
import static com.sun.tools.javac.code.TypeAnnotations.AnnotationType.DECLARATION;
import static com.sun.tools.javac.code.TypeAnnotations.AnnotationType.TYPE;
import static java.util.Comparator.comparing;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import com.google.errorprone.BugPattern;
@@ -17,10 +20,14 @@ import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.MethodTree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.TypeAnnotations.AnnotationType;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
@@ -39,6 +46,12 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
public final class LexicographicalAnnotationListing extends BugChecker
implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Comparator<@Nullable AnnotationType> BY_ANNOTATION_TYPE =
(a, b) ->
(a == null || a == DECLARATION) && b == TYPE ? -1 : a == TYPE && b == DECLARATION ? 1 : 0;
/** Instantiates a new {@link LexicographicalAnnotationListing} instance. */
public LexicographicalAnnotationListing() {}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
@@ -47,26 +60,29 @@ public final class LexicographicalAnnotationListing extends BugChecker
return Description.NO_MATCH;
}
ImmutableList<? extends AnnotationTree> sortedAnnotations = sort(originalOrdering, state);
ImmutableList<? extends AnnotationTree> sortedAnnotations =
sort(originalOrdering, ASTHelpers.getSymbol(tree), state);
if (originalOrdering.equals(sortedAnnotations)) {
return Description.NO_MATCH;
}
Optional<Fix> fix = tryFixOrdering(originalOrdering, sortedAnnotations, state);
Description.Builder description = buildDescription(originalOrdering.get(0));
fix.ifPresent(description::addFix);
return description.build();
return describeMatch(
originalOrdering.get(0), fixOrdering(originalOrdering, sortedAnnotations, state));
}
private static ImmutableList<? extends AnnotationTree> sort(
List<? extends AnnotationTree> annotations, VisitorState state) {
List<? extends AnnotationTree> annotations, Symbol symbol, VisitorState state) {
return annotations.stream()
.sorted(comparing(annotation -> SourceCode.treeToString(annotation, state)))
.sorted(
comparing(
(AnnotationTree annotation) ->
ASTHelpers.getAnnotationType(annotation, symbol, state),
BY_ANNOTATION_TYPE)
.thenComparing(annotation -> SourceCode.treeToString(annotation, state)))
.collect(toImmutableList());
}
private static Optional<Fix> tryFixOrdering(
private static Fix fixOrdering(
List<? extends AnnotationTree> originalAnnotations,
ImmutableList<? extends AnnotationTree> sortedAnnotations,
VisitorState state) {
@@ -77,6 +93,7 @@ public final class LexicographicalAnnotationListing extends BugChecker
SuggestedFix.builder()
.replace(original, SourceCode.treeToString(replacement, state)))
.reduce(SuggestedFix.Builder::merge)
.map(SuggestedFix.Builder::build);
.map(SuggestedFix.Builder::build)
.orElseThrow(() -> new VerifyException("No annotations were provided"));
}
}

View File

@@ -37,6 +37,8 @@ import javax.lang.model.element.Name;
/**
* A {@link BugChecker} that flags lambda expressions that can be replaced with method references.
*
* @see IsInstanceLambdaUsage
*/
// XXX: Other custom expressions we could rewrite:
// - `a -> "str" + a` to `"str"::concat`. But only if `str` is provably non-null.
@@ -50,6 +52,9 @@ import javax.lang.model.element.Name;
// `not(some::reference)`.
// XXX: This check is extremely inefficient due to its reliance on `SuggestedFixes.compilesWithFix`.
// Palantir's `LambdaMethodReference` check seems to suffer a similar issue at this time.
// XXX: Expressions of the form `i -> SomeType.class.isInstance(i)` are not replaced; fix that using
// a suitable generalization.
// XXX: Consider folding the `IsInstanceLambdaUsage` check into this class.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Prefer method references over lambda expressions",
@@ -60,6 +65,9 @@ import javax.lang.model.element.Name;
public final class MethodReferenceUsage extends BugChecker implements LambdaExpressionTreeMatcher {
private static final long serialVersionUID = 1L;
/** Instantiates a new {@link MethodReferenceUsage} instance. */
public MethodReferenceUsage() {}
@Override
public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) {
/*
@@ -122,7 +130,7 @@ public final class MethodReferenceUsage extends BugChecker implements LambdaExpr
return Optional.empty();
}
Symbol sym = ASTHelpers.getSymbol(methodSelect);
if (!sym.isStatic()) {
if (!ASTHelpers.isStatic(sym)) {
return constructFix(lambdaExpr, "this", methodSelect);
}
return constructFix(lambdaExpr, sym.owner, methodSelect);
@@ -192,7 +200,7 @@ public final class MethodReferenceUsage extends BugChecker implements LambdaExpr
Name sName = target.getSimpleName();
Optional<SuggestedFix.Builder> fix = constructFix(lambdaExpr, sName, methodName);
if (!"java.lang".equals(target.packge().toString())) {
if (!"java.lang".equals(ASTHelpers.enclosingPackage(target).toString())) {
Name fqName = target.getQualifiedName();
if (!sName.equals(fqName)) {
return fix.map(b -> b.addImport(fqName.toString()));

View File

@@ -40,6 +40,9 @@ public final class MissingRefasterAnnotation extends BugChecker implements Class
isType("com.google.errorprone.refaster.annotation.BeforeTemplate"),
isType("com.google.errorprone.refaster.annotation.AfterTemplate")));
/** Instantiates a new {@link MissingRefasterAnnotation} instance. */
public MissingRefasterAnnotation() {}
@Override
public Description matchClass(ClassTree tree, VisitorState state) {
long methodTypes =

View File

@@ -36,6 +36,9 @@ public final class MockitoStubbing extends BugChecker implements MethodInvocatio
private static final Matcher<ExpressionTree> MOCKITO_EQ_METHOD =
staticMethod().onClass("org.mockito.ArgumentMatchers").named("eq");
/** Instantiates a new {@link MockitoStubbing} instance. */
public MockitoStubbing() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
List<? extends ExpressionTree> arguments = tree.getArguments();

View File

@@ -4,9 +4,11 @@ import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.generic;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.raw;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.subOf;
import com.google.auto.service.AutoService;
import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
@@ -16,9 +18,7 @@ import com.google.errorprone.suppliers.Supplier;
import com.google.errorprone.suppliers.Suppliers;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.util.List;
import java.util.Optional;
/** A {@link BugChecker} that flags nesting of {@link Optional Optionals}. */
@@ -33,21 +33,19 @@ import java.util.Optional;
public final class NestedOptionals extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Supplier<Type> OPTIONAL = Suppliers.typeFromClass(Optional.class);
private static final Supplier<Type> OPTIONAL_OF_OPTIONAL =
VisitorState.memoize(generic(OPTIONAL, subOf(raw(OPTIONAL))));
/** Instantiates a new {@link NestedOptionals} instance. */
public NestedOptionals() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
return isOptionalOfOptional(tree, state) ? describeMatch(tree) : Description.NO_MATCH;
}
private static boolean isOptionalOfOptional(Tree tree, VisitorState state) {
Type optionalType = OPTIONAL.get(state);
Type type = ASTHelpers.getType(tree);
if (!ASTHelpers.isSubtype(type, optionalType, state)) {
return false;
Type type = OPTIONAL_OF_OPTIONAL.get(state);
if (type == null || !state.getTypes().isSubtype(ASTHelpers.getType(tree), type)) {
return Description.NO_MATCH;
}
List<Type> typeArguments = type.getTypeArguments();
return !typeArguments.isEmpty()
&& ASTHelpers.isSubtype(Iterables.getOnlyElement(typeArguments), optionalType, state);
return describeMatch(tree);
}
}

View File

@@ -76,6 +76,9 @@ public final class NonEmptyMono extends BugChecker implements MethodInvocationTr
.onDescendantOf("reactor.core.publisher.Mono")
.namedAnyOf("defaultIfEmpty", "hasElement", "single"));
/** Instantiates a new {@link NonEmptyMono} instance. */
public NonEmptyMono() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!MONO_SIZE_CHECK.matches(tree, state)) {

View File

@@ -70,6 +70,9 @@ public final class PrimitiveComparison extends BugChecker implements MethodInvoc
.named("thenComparing")
.withParameters(Function.class.getName()));
/** Instantiates a new {@link PrimitiveComparison} instance. */
public PrimitiveComparison() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
boolean isStatic = STATIC_COMPARISON_METHOD.matches(tree, state);

View File

@@ -49,6 +49,7 @@ import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import tech.picnic.errorprone.bugpatterns.util.Flags;
import tech.picnic.errorprone.bugpatterns.util.MethodMatcherFactory;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
@@ -63,9 +64,8 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
public final class RedundantStringConversion extends BugChecker
implements BinaryTreeMatcher, CompoundAssignmentTreeMatcher, MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final String FLAG_PREFIX = "RedundantStringConversion:";
private static final String EXTRA_STRING_CONVERSION_METHODS_FLAG =
FLAG_PREFIX + "ExtraConversionMethods";
"RedundantStringConversion:ExtraConversionMethods";
@SuppressWarnings("UnnecessaryLambda")
private static final Matcher<ExpressionTree> ANY_EXPR = (t, s) -> true;
@@ -141,7 +141,7 @@ public final class RedundantStringConversion extends BugChecker
private final Matcher<MethodInvocationTree> conversionMethodMatcher;
/** Instantiates the default {@link RedundantStringConversion}. */
/** Instantiates a default {@link RedundantStringConversion} instance. */
public RedundantStringConversion() {
this(ErrorProneFlags.empty());
}
@@ -374,10 +374,9 @@ public final class RedundantStringConversion extends BugChecker
ErrorProneFlags flags) {
// XXX: ErrorProneFlags#getList splits by comma, but method signatures may also contain commas.
// For this class methods accepting more than one argument are not valid, but still: not nice.
return flags
.getList(EXTRA_STRING_CONVERSION_METHODS_FLAG)
.map(new MethodMatcherFactory()::create)
.map(m -> anyOf(WELL_KNOWN_STRING_CONVERSION_METHODS, m))
.orElse(WELL_KNOWN_STRING_CONVERSION_METHODS);
return anyOf(
WELL_KNOWN_STRING_CONVERSION_METHODS,
new MethodMatcherFactory()
.create(Flags.getList(flags, EXTRA_STRING_CONVERSION_METHODS_FLAG)));
}
}

View File

@@ -37,6 +37,9 @@ public final class RefasterAnyOfUsage extends BugChecker implements MethodInvoca
private static final Matcher<ExpressionTree> REFASTER_ANY_OF =
staticMethod().onClass(Refaster.class.getName()).named("anyOf");
/** Instantiates a new {@link RefasterAnyOfUsage} instance. */
public RefasterAnyOfUsage() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (REFASTER_ANY_OF.matches(tree, state)) {

View File

@@ -29,7 +29,7 @@ import java.util.Set;
import javax.lang.model.element.Modifier;
/**
* A {@link BugChecker} which suggests a canonical set of modifiers for Refaster class and method
* A {@link BugChecker} that suggests a canonical set of modifiers for Refaster class and method
* definitions.
*/
@AutoService(BugChecker.class)
@@ -48,6 +48,9 @@ public final class RefasterRuleModifiers extends BugChecker
private static final Matcher<Tree> REFASTER_METHOD =
anyOf(BEFORE_TEMPLATE_METHOD, AFTER_TEMPLATE_METHOD, PLACEHOLDER_METHOD);
/** Instantiates a new {@link RefasterRuleModifiers} instance. */
public RefasterRuleModifiers() {}
@Override
public Description matchClass(ClassTree tree, VisitorState state) {
if (!hasMatchingMember(tree, BEFORE_TEMPLATE_METHOD, state)) {

View File

@@ -83,6 +83,9 @@ public final class RequestMappingAnnotation extends BugChecker implements Method
isSameType("org.springframework.web.util.UriBuilder"),
isSameType("org.springframework.web.util.UriComponentsBuilder"))));
/** Instantiates a new {@link RequestMappingAnnotation} instance. */
public RequestMappingAnnotation() {}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
// XXX: Auto-add `@RequestParam` where applicable.

View File

@@ -1,5 +1,6 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
@@ -9,38 +10,72 @@ import static com.google.errorprone.matchers.Matchers.annotations;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
import static com.google.errorprone.matchers.Matchers.isType;
import static com.google.errorprone.matchers.Matchers.not;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.BugPattern;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.suppliers.Suppliers;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import tech.picnic.errorprone.bugpatterns.util.Flags;
/** A {@link BugChecker} that flags {@code @RequestParam} parameters with an unsupported type. */
@AutoService(BugChecker.class)
@BugPattern(
summary = "`@RequestParam` does not support `ImmutableCollection` and `ImmutableMap` subtypes",
summary =
"By default, `@RequestParam` does not support `ImmutableCollection` and `ImmutableMap` subtypes",
link = BUG_PATTERNS_BASE_URL + "RequestParamType",
linkType = CUSTOM,
severity = ERROR,
tags = LIKELY_ERROR)
public final class RequestParamType extends BugChecker implements VariableTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<VariableTree> HAS_UNSUPPORTED_REQUEST_PARAM =
allOf(
annotations(AT_LEAST_ONE, isType("org.springframework.web.bind.annotation.RequestParam")),
anyOf(isSubtypeOf(ImmutableCollection.class), isSubtypeOf(ImmutableMap.class)));
private static final String SUPPORTED_CUSTOM_TYPES_FLAG = "RequestParamType:SupportedCustomTypes";
private final Matcher<VariableTree> hasUnsupportedRequestParamType;
/** Instantiates a default {@link RequestParamType} instance. */
public RequestParamType() {
this(ErrorProneFlags.empty());
}
/**
* Instantiates a customized {@link RequestParamType} instance.
*
* @param flags Any provided command line flags.
*/
public RequestParamType(ErrorProneFlags flags) {
hasUnsupportedRequestParamType = hasUnsupportedRequestParamType(flags);
}
@Override
public Description matchVariable(VariableTree tree, VisitorState state) {
return HAS_UNSUPPORTED_REQUEST_PARAM.matches(tree, state)
return hasUnsupportedRequestParamType.matches(tree, state)
? describeMatch(tree)
: Description.NO_MATCH;
}
private static Matcher<VariableTree> hasUnsupportedRequestParamType(ErrorProneFlags flags) {
return allOf(
annotations(AT_LEAST_ONE, isType("org.springframework.web.bind.annotation.RequestParam")),
anyOf(isSubtypeOf(ImmutableCollection.class), isSubtypeOf(ImmutableMap.class)),
not(isSubtypeOfAny(Flags.getList(flags, SUPPORTED_CUSTOM_TYPES_FLAG))));
}
private static Matcher<Tree> isSubtypeOfAny(ImmutableList<String> inclusions) {
return anyOf(
inclusions.stream()
.map(inclusion -> isSubtypeOf(Suppliers.typeFromString(inclusion)))
.collect(toImmutableList()));
}
}

View File

@@ -26,6 +26,7 @@ import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import tech.picnic.errorprone.bugpatterns.util.ThirdPartyLibrary;
/**
* A {@link BugChecker} that flags methods with Spring's {@code @Scheduled} annotation that lack New
@@ -46,9 +47,13 @@ public final class ScheduledTransactionTrace extends BugChecker implements Metho
private static final MultiMatcher<Tree, AnnotationTree> TRACE_ANNOTATION =
annotations(AT_LEAST_ONE, isType(TRACE_ANNOTATION_FQCN));
/** Instantiates a new {@link ScheduledTransactionTrace} instance. */
public ScheduledTransactionTrace() {}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (!IS_SCHEDULED.matches(tree, state)) {
if (!ThirdPartyLibrary.NEW_RELIC_AGENT_API.isIntroductionAllowed(state)
|| !IS_SCHEDULED.matches(tree, state)) {
return Description.NO_MATCH;
}

View File

@@ -48,6 +48,9 @@ public final class Slf4jLogStatement extends BugChecker implements MethodInvocat
.onDescendantOf("org.slf4j.Logger")
.namedAnyOf("trace", "debug", "info", "warn", "error");
/** Instantiates a new {@link Slf4jLogStatement} instance. */
public Slf4jLogStatement() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!SLF4J_LOGGER_INVOCATION.matches(tree, state)) {

View File

@@ -57,6 +57,9 @@ public final class SpringMvcAnnotation extends BugChecker implements AnnotationT
.put("PUT", "PutMapping")
.build();
/** Instantiates a new {@link SpringMvcAnnotation} instance. */
public SpringMvcAnnotation() {}
@Override
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
// XXX: We could remove the `@RequestMapping` import if not other usages remain.

View File

@@ -101,7 +101,8 @@ public final class StaticImport extends BugChecker implements MemberSelectTreeMa
"org.springframework.http.HttpMethod",
"org.springframework.http.MediaType",
"org.testng.Assert",
"reactor.function.TupleUtils");
"reactor.function.TupleUtils",
"tech.picnic.errorprone.bugpatterns.util.MoreTypes");
/** Type members that should be statically imported. */
@VisibleForTesting
@@ -190,6 +191,9 @@ public final class StaticImport extends BugChecker implements MemberSelectTreeMa
"of",
"valueOf");
/** Instantiates a new {@link StaticImport} instance. */
public StaticImport() {}
@Override
public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) {
if (!isCandidateContext(state) || !isCandidate(tree)) {

View File

@@ -0,0 +1,89 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static com.sun.tools.javac.parser.Tokens.TokenKind.RPAREN;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.Streams;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.google.errorprone.util.ErrorProneTokens;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.tools.javac.util.Position;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags calls to {@link String#toLowerCase()} and {@link
* String#toUpperCase()}, as these methods implicitly rely on the environment's default locale.
*/
// XXX: Also flag `String::toLowerCase` and `String::toUpperCase` method references. For these cases
// the suggested fix should introduce a lambda expression with a parameter of which the name does
// not coincide with the name of an existing variable name. Such functionality should likely be
// introduced in a utility class.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Specify a `Locale` when calling `String#to{Lower,Upper}Case`",
link = BUG_PATTERNS_BASE_URL + "StringCaseLocaleUsage",
linkType = CUSTOM,
severity = WARNING,
tags = FRAGILE_CODE)
public final class StringCaseLocaleUsage extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> DEFAULT_LOCALE_CASE_CONVERSION =
instanceMethod()
.onExactClass(String.class.getName())
.namedAnyOf("toLowerCase", "toUpperCase")
.withNoParameters();
/** Instantiates a new {@link StringCaseLocaleUsage} instance. */
public StringCaseLocaleUsage() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!DEFAULT_LOCALE_CASE_CONVERSION.matches(tree, state)) {
return Description.NO_MATCH;
}
int closingParenPosition = getClosingParenPosition(tree, state);
if (closingParenPosition == Position.NOPOS) {
return describeMatch(tree);
}
return buildDescription(tree)
.addFix(suggestLocale(closingParenPosition, "Locale.ROOT"))
.addFix(suggestLocale(closingParenPosition, "Locale.getDefault()"))
.build();
}
private static Fix suggestLocale(int insertPosition, String locale) {
return SuggestedFix.builder()
.addImport("java.util.Locale")
.replace(insertPosition, insertPosition, locale)
.build();
}
private static int getClosingParenPosition(MethodInvocationTree tree, VisitorState state) {
int startPosition = ASTHelpers.getStartPosition(tree);
if (startPosition == Position.NOPOS) {
return Position.NOPOS;
}
return Streams.findLast(
ErrorProneTokens.getTokens(SourceCode.treeToString(tree, state), state.context).stream()
.filter(t -> t.kind() == RPAREN))
.map(token -> startPosition + token.pos())
.orElse(Position.NOPOS);
}
}

View File

@@ -1,9 +1,10 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.Splitter;
@@ -26,12 +27,12 @@ import com.sun.tools.javac.util.Convert;
import java.util.Formattable;
import java.util.Iterator;
import java.util.List;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} which flags {@link String#format(String, Object...)} invocations which can
* be replaced with a {@link String#join(CharSequence, CharSequence...)} or even a {@link
* A {@link BugChecker} that flags {@link String#format(String, Object...)} invocations which can be
* replaced with a {@link String#join(CharSequence, CharSequence...)} or even a {@link
* String#valueOf} invocation.
*/
// XXX: What about `v1 + "sep" + v2` and similar expressions? Do we want to rewrite those to
@@ -40,7 +41,8 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
@AutoService(BugChecker.class)
@BugPattern(
summary = "Prefer `String#join` over `String#format`",
linkType = NONE,
link = BUG_PATTERNS_BASE_URL + "StringJoin",
linkType = CUSTOM,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class StringJoin extends BugChecker implements MethodInvocationTreeMatcher {
@@ -52,6 +54,9 @@ public final class StringJoin extends BugChecker implements MethodInvocationTree
Suppliers.typeFromClass(CharSequence.class);
private static final Supplier<Type> FORMATTABLE_TYPE = Suppliers.typeFromClass(Formattable.class);
/** Instantiates a new {@link StringJoin} instance. */
public StringJoin() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!STRING_FORMAT_INVOCATION.matches(tree, state)) {

View File

@@ -26,6 +26,9 @@ import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
/** A {@link BugChecker} that flags illegal time-zone related operations. */
@AutoService(BugChecker.class)
@@ -58,10 +61,16 @@ public final class TimeZoneUsage extends BugChecker implements MethodInvocationT
.onClassAny(
LocalDate.class.getName(),
LocalDateTime.class.getName(),
LocalTime.class.getName())
LocalTime.class.getName(),
OffsetDateTime.class.getName(),
OffsetTime.class.getName(),
ZonedDateTime.class.getName())
.named("now"),
staticMethod().onClassAny(Instant.class.getName()).named("now").withNoParameters());
/** Instantiates a new {@link TimeZoneUsage} instance. */
public TimeZoneUsage() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
return BANNED_TIME_METHOD.matches(tree, state)

View File

@@ -1,4 +1,4 @@
/** Picnic Error Prone Contrib checks. */
@com.google.errorprone.annotations.CheckReturnValue
@javax.annotation.ParametersAreNonnullByDefault
@org.jspecify.annotations.NullMarked
package tech.picnic.errorprone.bugpatterns;

View File

@@ -0,0 +1,25 @@
package tech.picnic.errorprone.bugpatterns.util;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.ErrorProneFlags;
/** Helper methods for working with {@link ErrorProneFlags}. */
public final class Flags {
private Flags() {}
/**
* Returns the list of (comma-separated) arguments passed using the given Error Prone flag.
*
* @param errorProneFlags The full set of flags provided.
* @param name The name of the flag of interest.
* @return A non-{@code null} list of provided arguments; this list is empty if the flag was not
* provided, or if the flag's value is the empty string.
*/
public static ImmutableList<String> getList(ErrorProneFlags errorProneFlags, String name) {
return errorProneFlags
.getList(name)
.map(ImmutableList::copyOf)
.filter(flags -> !flags.equals(ImmutableList.of("")))
.orElseGet(ImmutableList::of);
}
}

View File

@@ -4,7 +4,19 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
/** Utility class that can be used to identify reserved keywords of the Java language. */
// XXX: This class is no longer only about keywords. Consider changing its name and class-level
// documentation.
public final class JavaKeywords {
/**
* Enumeration of boolean and null literals.
*
* @see <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-3.html#jls-3.10.3">JDK 17
* JLS section 3.10.3: Boolean Literals</a>
* @see <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-3.html#jls-3.10.8">JDK 17
* JLS section 3.10.8: The Null Literal</a>
*/
private static final ImmutableSet<String> BOOLEAN_AND_NULL_LITERALS =
ImmutableSet.of("true", "false", "null");
/**
* List of all reserved keywords in the Java language.
*
@@ -64,7 +76,6 @@ public final class JavaKeywords {
"void",
"volatile",
"while");
/**
* List of all contextual keywords in the Java language.
*
@@ -89,13 +100,28 @@ public final class JavaKeywords {
"var",
"with",
"yield");
/** List of all keywords in the Java language. */
private static final ImmutableSet<String> ALL_KEYWORDS =
Sets.union(RESERVED_KEYWORDS, CONTEXTUAL_KEYWORDS).immutableCopy();
private JavaKeywords() {}
/**
* Tells whether the given string is a valid identifier in the Java language.
*
* @param str The string of interest.
* @return {@code true} if the given string is a valid identifier in the Java language.
* @see <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-3.html#jls-3.8">JDK 17 JLS
* section 3.8: Identifiers</a>
*/
public static boolean isValidIdentifier(String str) {
return !str.isEmpty()
&& !isReservedKeyword(str)
&& !BOOLEAN_AND_NULL_LITERALS.contains(str)
&& Character.isJavaIdentifierStart(str.codePointAt(0))
&& str.codePoints().skip(1).allMatch(Character::isUnicodeIdentifierPart);
}
/**
* Tells whether the given string is a reserved keyword in the Java language.
*

View File

@@ -21,6 +21,9 @@ public final class MethodMatcherFactory {
private static final Pattern METHOD_SIGNATURE =
Pattern.compile("([^\\s#(,)]+)#([^\\s#(,)]+)\\(((?:[^\\s#(,)]+(?:,[^\\s#(,)]+)*)?)\\)");
/** Instantiates a new {@link MethodMatcherFactory} instance. */
public MethodMatcherFactory() {}
/**
* Creates a {@link Matcher} of methods with any of the given signatures.
*

View File

@@ -0,0 +1,49 @@
package tech.picnic.errorprone.bugpatterns.util;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
/**
* A collection of helper methods for working with the AST.
*
* <p>These methods are additions to the ones found in {@link
* com.google.errorprone.util.ASTHelpers}.
*/
public final class MoreASTHelpers {
private MoreASTHelpers() {}
/**
* Finds methods with the specified name in given the {@link VisitorState}'s current enclosing
* class.
*
* @param methodName The method name to search for.
* @param state The {@link VisitorState} from which to derive the enclosing class of interest.
* @return The {@link MethodTree}s of the methods with the given name in the enclosing class.
*/
public static ImmutableList<MethodTree> findMethods(CharSequence methodName, VisitorState state) {
ClassTree clazz = state.findEnclosing(ClassTree.class);
checkArgument(clazz != null, "Visited node is not enclosed by a class");
return clazz.getMembers().stream()
.filter(MethodTree.class::isInstance)
.map(MethodTree.class::cast)
.filter(method -> method.getName().contentEquals(methodName))
.collect(toImmutableList());
}
/**
* Determines whether there are any methods with the specified name in given the {@link
* VisitorState}'s current enclosing class.
*
* @param methodName The method name to search for.
* @param state The {@link VisitorState} from which to derive the enclosing class of interest.
* @return Whether there are any methods with the given name in the enclosing class.
*/
public static boolean methodExistsInEnclosingClass(CharSequence methodName, VisitorState state) {
return !findMethods(methodName, state).isEmpty();
}
}

View File

@@ -0,0 +1,81 @@
package tech.picnic.errorprone.bugpatterns.util;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
import static com.google.errorprone.matchers.Matchers.annotations;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.isType;
import static java.util.Objects.requireNonNullElse;
import static tech.picnic.errorprone.bugpatterns.util.MoreMatchers.hasMetaAnnotation;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.matchers.AnnotationMatcherUtils;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.MultiMatcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewArrayTree;
import org.jspecify.annotations.Nullable;
/**
* A collection of JUnit-specific helper methods and {@link Matcher}s.
*
* <p>These constants and methods are additions to the ones found in {@link
* com.google.errorprone.matchers.JUnitMatchers}.
*/
public final class MoreJUnitMatchers {
/** Matches JUnit Jupiter test methods. */
public static final MultiMatcher<MethodTree, AnnotationTree> TEST_METHOD =
annotations(
AT_LEAST_ONE,
anyOf(
isType("org.junit.jupiter.api.Test"),
hasMetaAnnotation("org.junit.jupiter.api.TestTemplate")));
/** Matches JUnit Jupiter setup and teardown methods. */
public static final MultiMatcher<MethodTree, AnnotationTree> SETUP_OR_TEARDOWN_METHOD =
annotations(
AT_LEAST_ONE,
anyOf(
isType("org.junit.jupiter.api.AfterAll"),
isType("org.junit.jupiter.api.AfterEach"),
isType("org.junit.jupiter.api.BeforeAll"),
isType("org.junit.jupiter.api.BeforeEach")));
/**
* Matches methods that have a {@link org.junit.jupiter.params.provider.MethodSource} annotation.
*/
public static final MultiMatcher<MethodTree, AnnotationTree> HAS_METHOD_SOURCE =
annotations(AT_LEAST_ONE, isType("org.junit.jupiter.params.provider.MethodSource"));
private MoreJUnitMatchers() {}
/**
* Returns the names of the JUnit value factory methods specified by the given {@link
* org.junit.jupiter.params.provider.MethodSource} annotation.
*
* @param methodSourceAnnotation The annotation from which to extract value factory method names.
* @return One or more value factory names.
*/
static ImmutableSet<String> getMethodSourceFactoryNames(
AnnotationTree methodSourceAnnotation, MethodTree method) {
String methodName = method.getName().toString();
ExpressionTree value = AnnotationMatcherUtils.getArgument(methodSourceAnnotation, "value");
if (!(value instanceof NewArrayTree)) {
return ImmutableSet.of(toMethodSourceFactoryName(value, methodName));
}
return ((NewArrayTree) value)
.getInitializers().stream()
.map(name -> toMethodSourceFactoryName(name, methodName))
.collect(toImmutableSet());
}
private static String toMethodSourceFactoryName(
@Nullable ExpressionTree tree, String annotatedMethodName) {
return requireNonNullElse(
Strings.emptyToNull(ASTHelpers.constValue(tree, String.class)), annotatedMethodName);
}
}

View File

@@ -0,0 +1,39 @@
package tech.picnic.errorprone.bugpatterns.util;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
import com.google.errorprone.predicates.TypePredicate;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.tools.javac.code.Symbol;
/**
* A collection of general-purpose {@link Matcher}s.
*
* <p>These methods are additions to the ones found in {@link Matchers}.
*/
public final class MoreMatchers {
private MoreMatchers() {}
/**
* Returns a {@link Matcher} that determines whether a given {@link AnnotationTree} has a
* meta-annotation of the specified type.
*
* @param <T> The type of tree to match against.
* @param annotationType The binary type name of the annotation (e.g.
* "org.jspecify.annotations.Nullable", or "some.package.OuterClassName$InnerClassName")
* @return A {@link Matcher} that matches trees with the specified meta-annotation.
*/
public static <T extends AnnotationTree> Matcher<T> hasMetaAnnotation(String annotationType) {
TypePredicate typePredicate = hasAnnotation(annotationType);
return (tree, state) -> {
Symbol sym = ASTHelpers.getSymbol(tree);
return sym != null && typePredicate.apply(sym.type, state);
};
}
// XXX: Consider moving to a `MoreTypePredicates` utility class.
private static TypePredicate hasAnnotation(String annotationClassName) {
return (type, state) -> ASTHelpers.hasAnnotation(type.tsym, annotationClassName, state);
}
}

View File

@@ -0,0 +1,132 @@
package tech.picnic.errorprone.bugpatterns.util;
import static java.util.stream.Collectors.toCollection;
import com.google.errorprone.VisitorState;
import com.google.errorprone.suppliers.Supplier;
import com.google.errorprone.suppliers.Suppliers;
import com.sun.tools.javac.code.BoundKind;
import com.sun.tools.javac.code.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
/**
* A set of helper methods which together define a DSL for defining {@link Type types}.
*
* <p>These methods are meant to be statically imported. Example usage:
*
* <pre>{@code
* Supplier<Type> type =
* VisitorState.memoize(
* generic(
* type("reactor.core.publisher.Flux"),
* subOf(generic(type("org.reactivestreams.Publisher"), unbound()))));
* }</pre>
*
* This statement produces a memoized supplier of the type {@code Flux<? extends Publisher<?>>}.
*/
public final class MoreTypes {
private MoreTypes() {}
/**
* Creates a supplier of the type with the given fully qualified name.
*
* <p>This method should only be used when building more complex types in combination with other
* {@link MoreTypes} methods. In other cases prefer directly calling {@link
* Suppliers#typeFromString(String)}.
*
* @param typeName The type of interest.
* @return A supplier which returns the described type if available in the given state, and {@code
* null} otherwise.
*/
public static Supplier<Type> type(String typeName) {
return Suppliers.typeFromString(typeName);
}
/**
* Creates a supplier of the described generic type.
*
* @param type The base type of interest.
* @param typeArgs The desired type arguments.
* @return A supplier which returns the described type if available in the given state, and {@code
* null} otherwise.
*/
// XXX: The given `type` should be a generic type, so perhaps `withParams` would be a better
// method name. But the DSL wouldn't look as nice that way.
@SafeVarargs
@SuppressWarnings("varargs")
public static Supplier<Type> generic(Supplier<Type> type, Supplier<Type>... typeArgs) {
return propagateNull(
type,
(state, baseType) -> {
List<Type> params =
Arrays.stream(typeArgs).map(s -> s.get(state)).collect(toCollection(ArrayList::new));
if (params.stream().anyMatch(Objects::isNull)) {
return null;
}
return state.getType(baseType, /* isArray= */ false, params);
});
}
/**
* Creates a raw (erased, non-generic) variant of the given type.
*
* @param type The base type of interest.
* @return A supplier which returns the described type if available in the given state, and {@code
* null} otherwise.
*/
public static Supplier<Type> raw(Supplier<Type> type) {
return propagateNull(type, (state, baseType) -> baseType.tsym.erasure(state.getTypes()));
}
/**
* Creates a {@code ? super T} wildcard type, with {@code T} bound to the given type.
*
* @param type The base type of interest.
* @return A supplier which returns the described type if available in the given state, and {@code
* null} otherwise.
*/
public static Supplier<Type> superOf(Supplier<Type> type) {
return propagateNull(
type,
(state, baseType) ->
new Type.WildcardType(baseType, BoundKind.SUPER, state.getSymtab().boundClass));
}
/**
* Creates a {@code ? extends T} wildcard type, with {@code T} bound to the given type.
*
* @param type The base type of interest.
* @return A supplier which returns the described type if available in the given state, and {@code
* null} otherwise.
*/
public static Supplier<Type> subOf(Supplier<Type> type) {
return propagateNull(
type,
(state, baseType) ->
new Type.WildcardType(
type.get(state), BoundKind.EXTENDS, state.getSymtab().boundClass));
}
/**
* Creates an unbound wildcard type ({@code ?}).
*
* @return A supplier which returns the described type.
*/
public static Supplier<Type> unbound() {
return state ->
new Type.WildcardType(
state.getSymtab().objectType, BoundKind.UNBOUND, state.getSymtab().boundClass);
}
private static Supplier<Type> propagateNull(
Supplier<Type> type, BiFunction<VisitorState, Type, Type> transformer) {
return state ->
Optional.ofNullable(type.get(state)).map(t -> transformer.apply(state, t)).orElse(null);
}
}

View File

@@ -1,14 +1,21 @@
package tech.picnic.errorprone.bugpatterns.util;
import static com.sun.tools.javac.util.Position.NOPOS;
import com.google.common.base.CharMatcher;
import com.google.errorprone.VisitorState;
import com.google.errorprone.fixes.SuggestedFix;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
/**
* A collection of Error Prone utility methods for dealing with the source code representation of
* AST nodes.
*/
// XXX: Can we locate this code in a better place? Maybe contribute it upstream?
public final class SourceCode {
/** The complement of {@link CharMatcher#whitespace()}. */
private static final CharMatcher NON_WHITESPACE_MATCHER = CharMatcher.whitespace().negate();
private SourceCode() {}
/**
@@ -24,4 +31,32 @@ public final class SourceCode {
String src = state.getSourceForNode(tree);
return src != null ? src : tree.toString();
}
/**
* Creates a {@link SuggestedFix} for the deletion of the given {@link Tree}, including any
* whitespace that follows it.
*
* <p>Removing trailing whitespace may prevent the introduction of an empty line at the start of a
* code block; such empty lines are not removed when formatting the code using Google Java Format.
*
* @param tree The AST node of interest.
* @param state A {@link VisitorState} describing the context in which the given {@link Tree} is
* found.
* @return A non-{@code null} {@link SuggestedFix} similar to one produced by {@link
* SuggestedFix#delete(Tree)}.
*/
public static SuggestedFix deleteWithTrailingWhitespace(Tree tree, VisitorState state) {
CharSequence sourceCode = state.getSourceCode();
int endPos = state.getEndPosition(tree);
if (sourceCode == null || endPos == NOPOS) {
/* We can't identify the trailing whitespace; delete just the tree. */
return SuggestedFix.delete(tree);
}
int whitespaceEndPos = NON_WHITESPACE_MATCHER.indexIn(sourceCode, endPos);
return SuggestedFix.replace(
((DiagnosticPosition) tree).getStartPosition(),
whitespaceEndPos == -1 ? sourceCode.length() : whitespaceEndPos,
"");
}
}

View File

@@ -0,0 +1,105 @@
package tech.picnic.errorprone.bugpatterns.util;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.suppliers.Supplier;
import com.sun.tools.javac.code.ClassFinder;
import com.sun.tools.javac.code.Symbol.CompletionFailure;
import com.sun.tools.javac.util.Name;
/**
* Utility class that helps decide whether it is appropriate to introduce references to (well-known)
* third-party libraries.
*
* <p>This class should be used by {@link BugChecker}s that may otherwise suggest the introduction
* of code that depends on possibly-not-present third-party libraries.
*/
// XXX: Consider giving users more fine-grained control. This would be beneficial in cases where a
// dependency is on the classpath, but new usages are undesirable.
public enum ThirdPartyLibrary {
/**
* AssertJ.
*
* @see <a href="https://assertj.github.io/doc">AssertJ documentation</a>
*/
ASSERTJ("org.assertj.core.api.Assertions"),
/**
* Google's Guava.
*
* @see <a href="https://github.com/google/guava">Guava on GitHub</a>
*/
GUAVA("com.google.common.collect.ImmutableList"),
/**
* New Relic's Java agent API.
*
* @see <a href="https://github.com/newrelic/newrelic-java-agent/tree/main/newrelic-api">New Relic
* Java agent API on GitHub</a>
*/
NEW_RELIC_AGENT_API("com.newrelic.api.agent.Agent"),
/**
* VMWare's Project Reactor.
*
* @see <a href="https://projectreactor.io">Home page</a>
*/
REACTOR("reactor.core.publisher.Flux");
private static final String IGNORE_CLASSPATH_COMPAT_FLAG =
"ErrorProneSupport:IgnoreClasspathCompat";
@SuppressWarnings("ImmutableEnumChecker" /* Supplier is deterministic. */)
private final Supplier<Boolean> canUse;
/**
* Instantiates a {@link ThirdPartyLibrary} enum value.
*
* @param witnessFqcn The fully-qualified class name of a type that is expected to be on the
* classpath iff the associated third-party library is on the classpath.
*/
ThirdPartyLibrary(String witnessFqcn) {
this.canUse = VisitorState.memoize(state -> canIntroduceUsage(witnessFqcn, state));
}
/**
* Tells whether it is okay to introduce a dependency on this well-known third party library in
* the given context.
*
* @param state The context under consideration.
* @return {@code true} iff it is okay to assume or create a dependency on this library.
*/
public boolean isIntroductionAllowed(VisitorState state) {
return canUse.get(state);
}
private static boolean canIntroduceUsage(String className, VisitorState state) {
return shouldIgnoreClasspath(state) || isKnownClass(className, state);
}
/**
* Attempts to determine whether a class with the given FQCN is on the classpath.
*
* <p>The {@link VisitorState}'s symbol table is consulted first. If the type has not yet been
* loaded, then an attempt is made to do so.
*/
private static boolean isKnownClass(String className, VisitorState state) {
return state.getTypeFromString(className) != null || canLoadClass(className, state);
}
private static boolean canLoadClass(String className, VisitorState state) {
ClassFinder classFinder = ClassFinder.instance(state.context);
Name binaryName = state.binaryNameFromClassname(className);
try {
classFinder.loadClass(state.getSymtab().unnamedModule, binaryName);
return true;
} catch (CompletionFailure e) {
return false;
}
}
private static boolean shouldIgnoreClasspath(VisitorState state) {
return state
.errorProneOptions()
.getFlags()
.getBoolean(IGNORE_CLASSPATH_COMPAT_FLAG)
.orElse(Boolean.FALSE);
}
}

View File

@@ -1,4 +1,4 @@
/** Auxiliary utilities for use by Error Prone checks. */
@com.google.errorprone.annotations.CheckReturnValue
@javax.annotation.ParametersAreNonnullByDefault
@org.jspecify.annotations.NullMarked
package tech.picnic.errorprone.bugpatterns.util;

View File

@@ -1,16 +1,148 @@
package tech.picnic.errorprone.refasterrules;
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedMultiset;
import com.google.common.collect.ImmutableSortedSet;
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.AbstractBooleanAssert;
import org.assertj.core.api.AbstractMapAssert;
import org.assertj.core.api.MapAssert;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
@OnlineDocumentation
final class AssertJMapRules {
private AssertJMapRules() {}
// XXX: Reduce boilerplate using a `Matcher` that identifies "empty" instances.
static final class AbstractMapAssertIsEmpty<K, V> {
@BeforeTemplate
@SuppressWarnings("unchecked")
void before(AbstractMapAssert<?, ?, K, V> mapAssert) {
Refaster.anyOf(
mapAssert.containsExactlyEntriesOf(
Refaster.anyOf(
ImmutableMap.of(),
ImmutableBiMap.of(),
ImmutableSortedMap.of(),
new HashMap<>(),
new LinkedHashMap<>(),
new TreeMap<>())),
mapAssert.hasSameSizeAs(
Refaster.anyOf(
ImmutableMap.of(),
ImmutableBiMap.of(),
ImmutableSortedMap.of(),
new HashMap<>(),
new LinkedHashMap<>(),
new TreeMap<>())),
mapAssert.isEqualTo(
Refaster.anyOf(
ImmutableMap.of(),
ImmutableBiMap.of(),
ImmutableSortedMap.of(),
new HashMap<>(),
new LinkedHashMap<>(),
new TreeMap<>())),
mapAssert.containsOnlyKeys(
Refaster.anyOf(
ImmutableList.of(),
new ArrayList<>(),
ImmutableSet.of(),
new HashSet<>(),
new LinkedHashSet<>(),
ImmutableSortedSet.of(),
new TreeSet<>(),
ImmutableMultiset.of(),
ImmutableSortedMultiset.of())),
mapAssert.containsExactly(),
mapAssert.containsOnly(),
mapAssert.containsOnlyKeys());
}
@AfterTemplate
void after(AbstractMapAssert<?, ?, K, V> mapAssert) {
mapAssert.isEmpty();
}
}
static final class AssertThatMapIsEmpty<K, V> {
@BeforeTemplate
void before(Map<K, V> map) {
Refaster.anyOf(
assertThat(map).hasSize(0),
assertThat(map.isEmpty()).isTrue(),
assertThat(map.size()).isEqualTo(0L),
assertThat(map.size()).isNotPositive());
}
@BeforeTemplate
void before2(Map<K, V> map) {
assertThat(Refaster.anyOf(map.keySet(), map.values(), map.entrySet())).isEmpty();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Map<K, V> map) {
assertThat(map).isEmpty();
}
}
static final class AbstractMapAssertIsNotEmpty<K, V> {
@BeforeTemplate
AbstractMapAssert<?, ?, K, V> before(AbstractMapAssert<?, ?, K, V> mapAssert) {
return mapAssert.isNotEqualTo(
Refaster.anyOf(
ImmutableMap.of(),
ImmutableBiMap.of(),
ImmutableSortedMap.of(),
new HashMap<>(),
new LinkedHashMap<>(),
new TreeMap<>()));
}
@AfterTemplate
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert) {
return mapAssert.isNotEmpty();
}
}
static final class AssertThatMapIsNotEmpty<K, V> {
@BeforeTemplate
AbstractAssert<?, ?> before(Map<K, V> map) {
return Refaster.anyOf(
assertThat(map.isEmpty()).isFalse(),
assertThat(map.size()).isNotEqualTo(0),
assertThat(map.size()).isPositive(),
assertThat(Refaster.anyOf(map.keySet(), map.values(), map.entrySet())).isNotEmpty());
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map) {
return assertThat(map).isNotEmpty();
}
}
static final class AbstractMapAssertContainsExactlyInAnyOrderEntriesOf<K, V> {
@BeforeTemplate
AbstractMapAssert<?, ?, K, V> before(AbstractMapAssert<?, ?, K, V> mapAssert, Map<K, V> map) {
@@ -34,4 +166,83 @@ final class AssertJMapRules {
return mapAssert.containsExactlyEntriesOf(ImmutableMap.of(key, value));
}
}
static final class AssertThatMapHasSize<K, V> {
@BeforeTemplate
AbstractAssert<?, ?> before(Map<K, V> map, int length) {
return Refaster.anyOf(
assertThat(map.size()).isEqualTo(length),
assertThat(Refaster.anyOf(map.keySet(), map.values(), map.entrySet())).hasSize(length));
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, int length) {
return assertThat(map).hasSize(length);
}
}
static final class AbstractMapAssertHasSameSizeAs<K, V> {
@BeforeTemplate
AbstractMapAssert<?, ?, K, V> before(AbstractMapAssert<?, ?, K, V> mapAssert, Map<K, V> map) {
return mapAssert.hasSize(map.size());
}
@AfterTemplate
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert, Map<K, V> map) {
return mapAssert.hasSameSizeAs(map);
}
}
static final class AssertThatMapContainsKey<K, V> {
@BeforeTemplate
AbstractBooleanAssert<?> before(Map<K, V> map, K key) {
return assertThat(map.containsKey(key)).isTrue();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, K key) {
return assertThat(map).containsKey(key);
}
}
static final class AssertThatMapDoesNotContainKey<K, V> {
@BeforeTemplate
AbstractBooleanAssert<?> before(Map<K, V> map, K key) {
return assertThat(map.containsKey(key)).isFalse();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, K key) {
return assertThat(map).doesNotContainKey(key);
}
}
static final class AssertThatMapContainsValue<K, V> {
@BeforeTemplate
AbstractBooleanAssert<?> before(Map<K, V> map, V value) {
return assertThat(map.containsValue(value)).isTrue();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, V value) {
return assertThat(map).containsValue(value);
}
}
static final class AssertThatMapDoesNotContainValue<K, V> {
@BeforeTemplate
AbstractBooleanAssert<?> before(Map<K, V> map, V value) {
return assertThat(map.containsValue(value)).isFalse();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, V value) {
return assertThat(map).doesNotContainValue(value);
}
}
}

View File

@@ -22,7 +22,6 @@ final class AssertJOptionalRules {
static final class AssertThatOptional<T> {
@BeforeTemplate
@SuppressWarnings("NullAway")
ObjectAssert<T> before(Optional<T> optional) {
return assertThat(optional.orElseThrow());
}

View File

@@ -3,12 +3,9 @@ package tech.picnic.errorprone.refasterrules;
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedMultiset;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
@@ -22,9 +19,7 @@ import com.google.errorprone.refaster.annotation.UseImportPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -32,19 +27,16 @@ import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Stream;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.AbstractBooleanAssert;
import org.assertj.core.api.AbstractCollectionAssert;
import org.assertj.core.api.AbstractComparableAssert;
import org.assertj.core.api.AbstractDoubleAssert;
import org.assertj.core.api.AbstractIntegerAssert;
import org.assertj.core.api.AbstractLongAssert;
import org.assertj.core.api.AbstractMapAssert;
import org.assertj.core.api.IterableAssert;
import org.assertj.core.api.ListAssert;
import org.assertj.core.api.MapAssert;
@@ -566,173 +558,8 @@ final class AssertJRules {
// Map
//
static final class AssertThatMapIsEmpty<K, V> {
@BeforeTemplate
@SuppressWarnings("unchecked")
void before(AbstractMapAssert<?, ?, K, V> mapAssert) {
Refaster.anyOf(
mapAssert.containsExactlyEntriesOf(
Refaster.anyOf(
ImmutableMap.of(),
ImmutableBiMap.of(),
ImmutableSortedMap.of(),
new HashMap<>(),
new LinkedHashMap<>(),
new TreeMap<>())),
mapAssert.hasSameSizeAs(
Refaster.anyOf(
ImmutableMap.of(),
ImmutableBiMap.of(),
ImmutableSortedMap.of(),
new HashMap<>(),
new LinkedHashMap<>(),
new TreeMap<>())),
mapAssert.isEqualTo(
Refaster.anyOf(
ImmutableMap.of(),
ImmutableBiMap.of(),
ImmutableSortedMap.of(),
new HashMap<>(),
new LinkedHashMap<>(),
new TreeMap<>())),
mapAssert.containsOnlyKeys(
Refaster.anyOf(
ImmutableList.of(),
new ArrayList<>(),
ImmutableSet.of(),
new HashSet<>(),
new LinkedHashSet<>(),
ImmutableSortedSet.of(),
new TreeSet<>(),
ImmutableMultiset.of(),
ImmutableSortedMultiset.of())),
mapAssert.containsExactly(),
mapAssert.containsOnly(),
mapAssert.containsOnlyKeys());
}
@AfterTemplate
void after(AbstractMapAssert<?, ?, K, V> mapAssert) {
mapAssert.isEmpty();
}
}
// XXX: Find a better name.
static final class AssertThatMapIsEmpty2<K, V> {
@BeforeTemplate
void before(Map<K, V> map) {
Refaster.anyOf(
assertThat(map).hasSize(0),
assertThat(map.isEmpty()).isTrue(),
assertThat(map.size()).isEqualTo(0L),
assertThat(map.size()).isNotPositive());
}
@BeforeTemplate
void before2(Map<K, V> map) {
assertThat(Refaster.anyOf(map.keySet(), map.values())).isEmpty();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Map<K, V> map) {
assertThat(map).isEmpty();
}
}
static final class AssertThatMapIsNotEmpty<K, V> {
@BeforeTemplate
AbstractMapAssert<?, ?, K, V> before(AbstractMapAssert<?, ?, K, V> mapAssert) {
return mapAssert.isNotEqualTo(
Refaster.anyOf(
ImmutableMap.of(),
ImmutableBiMap.of(),
ImmutableSortedMap.of(),
new HashMap<>(),
new LinkedHashMap<>(),
new TreeMap<>()));
}
@AfterTemplate
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert) {
return mapAssert.isNotEmpty();
}
}
// XXX: Find a better name.
static final class AssertThatMapIsNotEmpty2<K, V> {
@BeforeTemplate
AbstractAssert<?, ?> before(Map<K, V> map) {
return Refaster.anyOf(
assertThat(map.isEmpty()).isFalse(),
assertThat(map.size()).isNotEqualTo(0),
assertThat(map.size()).isPositive(),
assertThat(Refaster.anyOf(map.keySet(), map.values())).isNotEmpty());
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map) {
return assertThat(map).isNotEmpty();
}
}
static final class AssertThatMapHasSize<K, V> {
@BeforeTemplate
AbstractAssert<?, ?> before(Map<K, V> map, int length) {
return Refaster.anyOf(
assertThat(map.size()).isEqualTo(length),
assertThat(Refaster.anyOf(map.keySet(), map.values())).hasSize(length));
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, int length) {
return assertThat(map).hasSize(length);
}
}
static final class AssertThatMapsHaveSameSize<K, V> {
@BeforeTemplate
AbstractAssert<?, ?> before(Map<K, V> map1, Map<K, V> map2) {
return assertThat(map1)
.hasSize(Refaster.anyOf(map2.size(), map2.keySet().size(), map2.values().size()));
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map1, Map<K, V> map2) {
return assertThat(map1).hasSameSizeAs(map2);
}
}
// XXX: Should also add a rule (elsewhere) to simplify `map.keySet().contains(key)`.
static final class AssertThatMapContainsKey<K, V> {
@BeforeTemplate
AbstractBooleanAssert<?> before(Map<K, V> map, K key) {
return assertThat(map.containsKey(key)).isTrue();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, K key) {
return assertThat(map).containsKey(key);
}
}
static final class AssertThatMapDoesNotContainKey<K, V> {
@BeforeTemplate
AbstractBooleanAssert<?> before(Map<K, V> map, K key) {
return assertThat(map.containsKey(key)).isFalse();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, K key) {
return assertThat(map).doesNotContainKey(key);
}
}
// XXX: To match in all cases there'll need to be a `@BeforeTemplate` variant for each
// `assertThat` overload. Consider defining a `BugChecker` instead.
static final class AssertThatMapContainsEntry<K, V> {
@BeforeTemplate
ObjectAssert<?> before(Map<K, V> map, K key, V value) {

View File

@@ -19,15 +19,12 @@ import com.google.errorprone.refaster.annotation.BeforeTemplate;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/**
@@ -46,36 +43,30 @@ final class AssortedRules {
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
int after(int index, int size) {
return checkIndex(index, size);
}
}
// XXX: We could add a rule for `new EnumMap(Map<K, ? extends V> m)`, but that constructor does
// not allow an empty non-EnumMap to be provided.
static final class CreateEnumMap<K extends Enum<K>, V> {
/**
* Prefer {@link Objects#checkIndex(int, int)} over less descriptive or more verbose alternatives.
*
* <p>If a custom error message is desired, consider using Guava's {@link
* com.google.common.base.Preconditions#checkElementIndex(int, int, String)}.
*/
static final class CheckIndexConditional {
@BeforeTemplate
Map<K, V> before() {
return new HashMap<>();
void before(int index, int size) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
}
@AfterTemplate
Map<K, V> after() {
return new EnumMap<>(Refaster.<K>clazz());
}
}
static final class MapGetOrNull<K, V, L> {
@BeforeTemplate
@Nullable
V before(Map<K, V> map, L key) {
return map.getOrDefault(key, null);
}
@AfterTemplate
@Nullable
V after(Map<K, V> map, L key) {
return map.get(key);
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(int index, int size) {
checkIndex(index, size);
}
}
@@ -205,32 +196,6 @@ final class AssortedRules {
}
}
/** Don't unnecessarily use {@link Map#entrySet()}. */
static final class MapKeyStream<K, V> {
@BeforeTemplate
Stream<K> before(Map<K, V> map) {
return map.entrySet().stream().map(Map.Entry::getKey);
}
@AfterTemplate
Stream<K> after(Map<K, V> map) {
return map.keySet().stream();
}
}
/** Don't unnecessarily use {@link Map#entrySet()}. */
static final class MapValueStream<K, V> {
@BeforeTemplate
Stream<V> before(Map<K, V> map) {
return map.entrySet().stream().map(Map.Entry::getValue);
}
@AfterTemplate
Stream<V> after(Map<K, V> map) {
return map.values().stream();
}
}
/** Prefer {@link Splitter#splitToStream(CharSequence)} over less efficient alternatives. */
static final class SplitToStream {
@BeforeTemplate

View File

@@ -50,17 +50,18 @@ final class BigDecimalRules {
}
}
/** Prefer {@link BigDecimal#valueOf(long)} over the associated constructor. */
// XXX: Ideally we'd also rewrite `BigDecimal.valueOf("<some-integer-value>")`, but it doesn't
// appear that's currently possible with Error Prone.
static final class BigDecimalFactoryMethod {
/** Prefer {@link BigDecimal#valueOf(double)} over the associated constructor. */
// XXX: Ideally we also rewrite `new BigDecimal("<some-integer-value>")` in cases where the
// specified number can be represented as an `int` or `long`, but that requires a custom
// `BugChecker`.
static final class BigDecimalValueOf {
@BeforeTemplate
BigDecimal before(long value) {
BigDecimal before(double value) {
return new BigDecimal(value);
}
@AfterTemplate
BigDecimal after(long value) {
BigDecimal after(double value) {
return BigDecimal.valueOf(value);
}
}

View File

@@ -17,6 +17,7 @@ import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.SortedSet;
import java.util.function.Consumer;
import java.util.function.IntFunction;
import java.util.stream.Stream;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
@@ -404,6 +405,19 @@ final class CollectionRules {
}
}
/** Prefer {@link Collection#forEach(Consumer)} over more contrived alternatives. */
static final class CollectionForEach<T> {
@BeforeTemplate
void before(Collection<T> collection, Consumer<? super T> consumer) {
collection.stream().forEach(consumer);
}
@AfterTemplate
void after(Collection<T> collection, Consumer<? super T> consumer) {
collection.forEach(consumer);
}
}
// XXX: collection.stream().noneMatch(e -> e.equals(other))
// ^ This is !collection.contains(other). Do we already rewrite variations on this?
}

View File

@@ -15,6 +15,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import com.google.errorprone.refaster.annotation.Repeated;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
import java.util.Arrays;
import java.util.Collections;
@@ -24,6 +25,7 @@ import java.util.function.Function;
import java.util.function.ToDoubleFunction;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;
import java.util.stream.Stream;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/** Refaster rules related to expressions dealing with {@link Comparator}s. */
@@ -37,7 +39,10 @@ final class ComparatorRules {
@BeforeTemplate
Comparator<T> before() {
return Refaster.anyOf(
comparing(Refaster.anyOf(identity(), v -> v)), Comparator.<T>reverseOrder().reversed());
T::compareTo,
comparing(Refaster.anyOf(identity(), v -> v)),
Collections.<T>reverseOrder(reverseOrder()),
Comparator.<T>reverseOrder().reversed());
}
@AfterTemplate
@@ -51,11 +56,15 @@ final class ComparatorRules {
static final class ReverseOrder<T extends Comparable<? super T>> {
@BeforeTemplate
Comparator<T> before() {
return Comparator.<T>naturalOrder().reversed();
return Refaster.anyOf(
Collections.reverseOrder(),
Collections.<T>reverseOrder(naturalOrder()),
Comparator.<T>naturalOrder().reversed());
}
// XXX: Add `@UseImportPolicy(STATIC_IMPORT_ALWAYS)` if/when
// https://github.com/google/error-prone/pull/3584 is merged and released.
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
Comparator<T> after() {
return reverseOrder();
}
@@ -189,15 +198,54 @@ final class ComparatorRules {
}
}
/** Prefer {@link Comparable#compareTo(Object)}} over more verbose alternatives. */
static final class CompareTo<T extends Comparable<? super T>> {
@BeforeTemplate
int before(T value1, T value2) {
return Refaster.anyOf(
Comparator.<T>naturalOrder().compare(value1, value2),
Comparator.<T>reverseOrder().compare(value2, value1));
}
@AfterTemplate
int after(T value1, T value2) {
return value1.compareTo(value2);
}
}
/**
* Avoid unnecessary creation of a {@link Stream} to determine the minimum of a known collection
* of values.
*/
static final class MinOfVarargs<T> {
@BeforeTemplate
@SuppressWarnings("StreamOfArray" /* In practice individual values are provided. */)
T before(@Repeated T value, Comparator<T> cmp) {
return Stream.of(Refaster.asVarargs(value)).min(cmp).orElseThrow();
}
@AfterTemplate
T after(@Repeated T value, Comparator<T> cmp) {
return Collections.min(Arrays.asList(value), cmp);
}
}
/** Prefer {@link Comparators#min(Comparable, Comparable)}} over more verbose alternatives. */
static final class MinOfPairNaturalOrder<T extends Comparable<? super T>> {
@BeforeTemplate
T before(T value1, T value2) {
return Collections.min(
Refaster.anyOf(
Arrays.asList(value1, value2),
ImmutableList.of(value1, value2),
ImmutableSet.of(value1, value2)));
return Refaster.anyOf(
value1.compareTo(value2) <= 0 ? value1 : value2,
value1.compareTo(value2) > 0 ? value2 : value1,
value2.compareTo(value1) < 0 ? value2 : value1,
value2.compareTo(value1) >= 0 ? value1 : value2,
Comparators.min(value1, value2, naturalOrder()),
Comparators.max(value1, value2, reverseOrder()),
Collections.min(
Refaster.anyOf(
Arrays.asList(value1, value2),
ImmutableList.of(value1, value2),
ImmutableSet.of(value1, value2))));
}
@AfterTemplate
@@ -212,12 +260,17 @@ final class ComparatorRules {
static final class MinOfPairCustomOrder<T> {
@BeforeTemplate
T before(T value1, T value2, Comparator<T> cmp) {
return Collections.min(
Refaster.anyOf(
Arrays.asList(value1, value2),
ImmutableList.of(value1, value2),
ImmutableSet.of(value1, value2)),
cmp);
return Refaster.anyOf(
cmp.compare(value1, value2) <= 0 ? value1 : value2,
cmp.compare(value1, value2) > 0 ? value2 : value1,
cmp.compare(value2, value1) < 0 ? value2 : value1,
cmp.compare(value2, value1) >= 0 ? value1 : value2,
Collections.min(
Refaster.anyOf(
Arrays.asList(value1, value2),
ImmutableList.of(value1, value2),
ImmutableSet.of(value1, value2)),
cmp));
}
@AfterTemplate
@@ -226,15 +279,39 @@ final class ComparatorRules {
}
}
/**
* Avoid unnecessary creation of a {@link Stream} to determine the maximum of a known collection
* of values.
*/
static final class MaxOfVarargs<T> {
@BeforeTemplate
@SuppressWarnings("StreamOfArray" /* In practice individual values are provided. */)
T before(@Repeated T value, Comparator<T> cmp) {
return Stream.of(Refaster.asVarargs(value)).max(cmp).orElseThrow();
}
@AfterTemplate
T after(@Repeated T value, Comparator<T> cmp) {
return Collections.max(Arrays.asList(value), cmp);
}
}
/** Prefer {@link Comparators#max(Comparable, Comparable)}} over more verbose alternatives. */
static final class MaxOfPairNaturalOrder<T extends Comparable<? super T>> {
@BeforeTemplate
T before(T value1, T value2) {
return Collections.max(
Refaster.anyOf(
Arrays.asList(value1, value2),
ImmutableList.of(value1, value2),
ImmutableSet.of(value1, value2)));
return Refaster.anyOf(
value1.compareTo(value2) >= 0 ? value1 : value2,
value1.compareTo(value2) < 0 ? value2 : value1,
value2.compareTo(value1) > 0 ? value2 : value1,
value2.compareTo(value1) <= 0 ? value1 : value2,
Comparators.max(value1, value2, naturalOrder()),
Comparators.min(value1, value2, reverseOrder()),
Collections.max(
Refaster.anyOf(
Arrays.asList(value1, value2),
ImmutableList.of(value1, value2),
ImmutableSet.of(value1, value2))));
}
@AfterTemplate
@@ -249,12 +326,17 @@ final class ComparatorRules {
static final class MaxOfPairCustomOrder<T> {
@BeforeTemplate
T before(T value1, T value2, Comparator<T> cmp) {
return Collections.max(
Refaster.anyOf(
Arrays.asList(value1, value2),
ImmutableList.of(value1, value2),
ImmutableSet.of(value1, value2)),
cmp);
return Refaster.anyOf(
cmp.compare(value1, value2) >= 0 ? value1 : value2,
cmp.compare(value1, value2) < 0 ? value2 : value1,
cmp.compare(value2, value1) > 0 ? value2 : value1,
cmp.compare(value2, value1) <= 0 ? value1 : value2,
Collections.max(
Refaster.anyOf(
Arrays.asList(value1, value2),
ImmutableList.of(value1, value2),
ImmutableSet.of(value1, value2)),
cmp));
}
@AfterTemplate

View File

@@ -141,6 +141,22 @@ final class DoubleStreamRules {
}
}
/**
* Apply {@link DoubleStream#filter(DoublePredicate)} before {@link DoubleStream#sorted()} to
* reduce the number of elements to sort.
*/
static final class DoubleStreamFilterSorted {
@BeforeTemplate
DoubleStream before(DoubleStream stream, DoublePredicate predicate) {
return stream.sorted().filter(predicate);
}
@AfterTemplate
DoubleStream after(DoubleStream stream, DoublePredicate predicate) {
return stream.filter(predicate).sorted();
}
}
/** In order to test whether a stream has any element, simply try to find one. */
static final class DoubleStreamIsEmpty {
@BeforeTemplate

View File

@@ -154,6 +154,22 @@ final class IntStreamRules {
}
}
/**
* Apply {@link IntStream#filter(IntPredicate)} before {@link IntStream#sorted()} to reduce the
* number of elements to sort.
*/
static final class IntStreamFilterSorted {
@BeforeTemplate
IntStream before(IntStream stream, IntPredicate predicate) {
return stream.sorted().filter(predicate);
}
@AfterTemplate
IntStream after(IntStream stream, IntPredicate predicate) {
return stream.filter(predicate).sorted();
}
}
/** In order to test whether a stream has any element, simply try to find one. */
static final class IntStreamIsEmpty {
@BeforeTemplate

View File

@@ -0,0 +1,521 @@
package tech.picnic.errorprone.refasterrules;
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.DoNotCall;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
import java.util.function.Supplier;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.api.function.ThrowingSupplier;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/**
* Refaster rules to replace JUnit assertions with AssertJ equivalents.
*
* <p>Note that, while both libraries throw an {@link AssertionError} in case of an assertion
* failure, the exact subtype used generally differs.
*/
// XXX: Not all `org.assertj.core.api.Assertions` methods have an associated Refaster rule yet;
// expand this class.
// XXX: Introduce a `@Matcher` on `Executable` and `ThrowingSupplier` expressions, such that they
// are only matched if they are also compatible with the `ThrowingCallable` functional interface.
// When implementing such a matcher, note that expressions with a non-void return type such as
// `() -> toString()` match both `ThrowingSupplier` and `ThrowingCallable`, but `() -> "constant"`
// is only compatible with the former.
@OnlineDocumentation
final class JUnitToAssertJRules {
private JUnitToAssertJRules() {}
public ImmutableSet<?> elidedTypesAndStaticImports() {
return ImmutableSet.of(
Assertions.class,
assertDoesNotThrow(() -> null),
assertInstanceOf(null, null),
assertThrows(null, null),
assertThrowsExactly(null, null),
(Runnable) () -> assertFalse(true),
(Runnable) () -> assertNotNull(null),
(Runnable) () -> assertNotSame(null, null),
(Runnable) () -> assertNull(null),
(Runnable) () -> assertSame(null, null),
(Runnable) () -> assertTrue(true));
}
static final class ThrowNewAssertionError {
@BeforeTemplate
void before() {
Assertions.fail();
}
@AfterTemplate
@DoNotCall
void after() {
throw new AssertionError();
}
}
static final class FailWithMessage<T> {
@BeforeTemplate
T before(String message) {
return Assertions.fail(message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
T after(String message) {
return fail(message);
}
}
static final class FailWithMessageAndThrowable<T> {
@BeforeTemplate
T before(String message, Throwable throwable) {
return Assertions.fail(message, throwable);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
T after(String message, Throwable throwable) {
return fail(message, throwable);
}
}
static final class FailWithThrowable {
@BeforeTemplate
void before(Throwable throwable) {
Assertions.fail(throwable);
}
@AfterTemplate
@DoNotCall
void after(Throwable throwable) {
throw new AssertionError(throwable);
}
}
static final class AssertThatIsTrue {
@BeforeTemplate
void before(boolean actual) {
assertTrue(actual);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(boolean actual) {
assertThat(actual).isTrue();
}
}
static final class AssertThatWithFailMessageStringIsTrue {
@BeforeTemplate
void before(boolean actual, String message) {
assertTrue(actual, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(boolean actual, String message) {
assertThat(actual).withFailMessage(message).isTrue();
}
}
static final class AssertThatWithFailMessageSupplierIsTrue {
@BeforeTemplate
void before(boolean actual, Supplier<String> supplier) {
assertTrue(actual, supplier);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(boolean actual, Supplier<String> supplier) {
assertThat(actual).withFailMessage(supplier).isTrue();
}
}
static final class AssertThatIsFalse {
@BeforeTemplate
void before(boolean actual) {
assertFalse(actual);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(boolean actual) {
assertThat(actual).isFalse();
}
}
static final class AssertThatWithFailMessageStringIsFalse {
@BeforeTemplate
void before(boolean actual, String message) {
assertFalse(actual, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(boolean actual, String message) {
assertThat(actual).withFailMessage(message).isFalse();
}
}
static final class AssertThatWithFailMessageSupplierIsFalse {
@BeforeTemplate
void before(boolean actual, Supplier<String> supplier) {
assertFalse(actual, supplier);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(boolean actual, Supplier<String> supplier) {
assertThat(actual).withFailMessage(supplier).isFalse();
}
}
static final class AssertThatIsNull {
@BeforeTemplate
void before(Object actual) {
assertNull(actual);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual) {
assertThat(actual).isNull();
}
}
static final class AssertThatWithFailMessageStringIsNull {
@BeforeTemplate
void before(Object actual, String message) {
assertNull(actual, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual, String message) {
assertThat(actual).withFailMessage(message).isNull();
}
}
static final class AssertThatWithFailMessageSupplierIsNull {
@BeforeTemplate
void before(Object actual, Supplier<String> supplier) {
assertNull(actual, supplier);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual, Supplier<String> supplier) {
assertThat(actual).withFailMessage(supplier).isNull();
}
}
static final class AssertThatIsNotNull {
@BeforeTemplate
void before(Object actual) {
assertNotNull(actual);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual) {
assertThat(actual).isNotNull();
}
}
static final class AssertThatWithFailMessageStringIsNotNull {
@BeforeTemplate
void before(Object actual, String message) {
assertNotNull(actual, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual, String message) {
assertThat(actual).withFailMessage(message).isNotNull();
}
}
static final class AssertThatWithFailMessageSupplierIsNotNull {
@BeforeTemplate
void before(Object actual, Supplier<String> supplier) {
assertNotNull(actual, supplier);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual, Supplier<String> supplier) {
assertThat(actual).withFailMessage(supplier).isNotNull();
}
}
static final class AssertThatIsSameAs {
@BeforeTemplate
void before(Object actual, Object expected) {
assertSame(expected, actual);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual, Object expected) {
assertThat(actual).isSameAs(expected);
}
}
static final class AssertThatWithFailMessageStringIsSameAs {
@BeforeTemplate
void before(Object actual, Object expected, String message) {
assertSame(expected, actual, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual, Object expected, String message) {
assertThat(actual).withFailMessage(message).isSameAs(expected);
}
}
static final class AssertThatWithFailMessageSupplierIsSameAs {
@BeforeTemplate
void before(Object actual, Object expected, Supplier<String> supplier) {
assertSame(expected, actual, supplier);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual, Object expected, Supplier<String> supplier) {
assertThat(actual).withFailMessage(supplier).isSameAs(expected);
}
}
static final class AssertThatIsNotSameAs {
@BeforeTemplate
void before(Object actual, Object expected) {
assertNotSame(expected, actual);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual, Object expected) {
assertThat(actual).isNotSameAs(expected);
}
}
static final class AssertThatWithFailMessageStringIsNotSameAs {
@BeforeTemplate
void before(Object actual, Object expected, String message) {
assertNotSame(expected, actual, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual, Object expected, String message) {
assertThat(actual).withFailMessage(message).isNotSameAs(expected);
}
}
static final class AssertThatWithFailMessageSupplierIsNotSameAs {
@BeforeTemplate
void before(Object actual, Object expected, Supplier<String> supplier) {
assertNotSame(expected, actual, supplier);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual, Object expected, Supplier<String> supplier) {
assertThat(actual).withFailMessage(supplier).isNotSameAs(expected);
}
}
static final class AssertThatThrownByIsExactlyInstanceOf<T extends Throwable> {
@BeforeTemplate
void before(Executable throwingCallable, Class<T> clazz) {
assertThrowsExactly(clazz, throwingCallable);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(ThrowingCallable throwingCallable, Class<T> clazz) {
assertThatThrownBy(throwingCallable).isExactlyInstanceOf(clazz);
}
}
static final class AssertThatThrownByWithFailMessageStringIsExactlyInstanceOf<
T extends Throwable> {
@BeforeTemplate
void before(Executable throwingCallable, Class<T> clazz, String message) {
assertThrowsExactly(clazz, throwingCallable, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(ThrowingCallable throwingCallable, Class<T> clazz, String message) {
assertThatThrownBy(throwingCallable).withFailMessage(message).isExactlyInstanceOf(clazz);
}
}
static final class AssertThatThrownByWithFailMessageSupplierIsExactlyInstanceOf<
T extends Throwable> {
@BeforeTemplate
void before(Executable throwingCallable, Class<T> clazz, Supplier<String> supplier) {
assertThrowsExactly(clazz, throwingCallable, supplier);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(ThrowingCallable throwingCallable, Class<T> clazz, Supplier<String> supplier) {
assertThatThrownBy(throwingCallable).withFailMessage(supplier).isExactlyInstanceOf(clazz);
}
}
static final class AssertThatThrownByIsInstanceOf<T extends Throwable> {
@BeforeTemplate
void before(Executable throwingCallable, Class<T> clazz) {
assertThrows(clazz, throwingCallable);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(ThrowingCallable throwingCallable, Class<T> clazz) {
assertThatThrownBy(throwingCallable).isInstanceOf(clazz);
}
}
static final class AssertThatThrownByWithFailMessageStringIsInstanceOf<T extends Throwable> {
@BeforeTemplate
void before(Executable throwingCallable, Class<T> clazz, String message) {
assertThrows(clazz, throwingCallable, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(ThrowingCallable throwingCallable, Class<T> clazz, String message) {
assertThatThrownBy(throwingCallable).withFailMessage(message).isInstanceOf(clazz);
}
}
static final class AssertThatThrownByWithFailMessageSupplierIsInstanceOf<T extends Throwable> {
@BeforeTemplate
void before(Executable throwingCallable, Class<T> clazz, Supplier<String> supplier) {
assertThrows(clazz, throwingCallable, supplier);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(ThrowingCallable throwingCallable, Class<T> clazz, Supplier<String> supplier) {
assertThatThrownBy(throwingCallable).withFailMessage(supplier).isInstanceOf(clazz);
}
}
static final class AssertThatCodeDoesNotThrowAnyException {
@BeforeTemplate
void before(Executable throwingCallable) {
assertDoesNotThrow(throwingCallable);
}
@BeforeTemplate
void before(ThrowingSupplier<?> throwingCallable) {
assertDoesNotThrow(throwingCallable);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(ThrowingCallable throwingCallable) {
assertThatCode(throwingCallable).doesNotThrowAnyException();
}
}
static final class AssertThatCodeWithFailMessageStringDoesNotThrowAnyException {
@BeforeTemplate
void before(Executable throwingCallable, String message) {
assertDoesNotThrow(throwingCallable, message);
}
@BeforeTemplate
void before(ThrowingSupplier<?> throwingCallable, String message) {
assertDoesNotThrow(throwingCallable, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(ThrowingCallable throwingCallable, String message) {
assertThatCode(throwingCallable).withFailMessage(message).doesNotThrowAnyException();
}
}
static final class AssertThatCodeWithFailMessageSupplierDoesNotThrowAnyException {
@BeforeTemplate
void before(Executable throwingCallable, Supplier<String> supplier) {
assertDoesNotThrow(throwingCallable, supplier);
}
@BeforeTemplate
void before(ThrowingSupplier<?> throwingCallable, Supplier<String> supplier) {
assertDoesNotThrow(throwingCallable, supplier);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(ThrowingCallable throwingCallable, Supplier<String> supplier) {
assertThatCode(throwingCallable).withFailMessage(supplier).doesNotThrowAnyException();
}
}
static final class AssertThatIsInstanceOf<T> {
@BeforeTemplate
void before(Object actual, Class<T> clazz) {
assertInstanceOf(clazz, actual);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual, Class<T> clazz) {
assertThat(actual).isInstanceOf(clazz);
}
}
static final class AssertThatWithFailMessageStringIsInstanceOf<T> {
@BeforeTemplate
void before(Object actual, Class<T> clazz, String message) {
assertInstanceOf(clazz, actual, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual, Class<T> clazz, String message) {
assertThat(actual).withFailMessage(message).isInstanceOf(clazz);
}
}
static final class AssertThatWithFailMessageSupplierIsInstanceOf<T> {
@BeforeTemplate
void before(Object actual, Class<T> clazz, Supplier<String> supplier) {
assertInstanceOf(clazz, actual, supplier);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object actual, Class<T> clazz, Supplier<String> supplier) {
assertThat(actual).withFailMessage(supplier).isInstanceOf(clazz);
}
}
}

View File

@@ -154,6 +154,22 @@ final class LongStreamRules {
}
}
/**
* Apply {@link LongStream#filter(LongPredicate)} before {@link LongStream#sorted()} to reduce the
* number of elements to sort.
*/
static final class LongStreamFilterSorted {
@BeforeTemplate
LongStream before(LongStream stream, LongPredicate predicate) {
return stream.sorted().filter(predicate);
}
@AfterTemplate
LongStream after(LongStream stream, LongPredicate predicate) {
return stream.filter(predicate).sorted();
}
}
/** In order to test whether a stream has any element, simply try to find one. */
static final class LongStreamIsEmpty {
@BeforeTemplate

View File

@@ -0,0 +1,123 @@
package tech.picnic.errorprone.refasterrules;
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/** Refaster rules related to expressions dealing with {@link Map} instances. */
@OnlineDocumentation
final class MapRules {
private MapRules() {}
// XXX: We could add a rule for `new EnumMap(Map<K, ? extends V> m)`, but that constructor does
// not allow an empty non-EnumMap to be provided.
static final class CreateEnumMap<K extends Enum<K>, V> {
@BeforeTemplate
Map<K, V> before() {
return new HashMap<>();
}
@AfterTemplate
Map<K, V> after() {
return new EnumMap<>(Refaster.<K>clazz());
}
}
static final class MapGetOrNull<K, V, T> {
@BeforeTemplate
@Nullable
V before(Map<K, V> map, T key) {
return map.getOrDefault(key, null);
}
@AfterTemplate
@Nullable
V after(Map<K, V> map, T key) {
return map.get(key);
}
}
/** Prefer {@link Map#isEmpty()} over more contrived alternatives. */
static final class MapIsEmpty<K, V> {
@BeforeTemplate
boolean before(Map<K, V> map) {
return Refaster.anyOf(map.keySet(), map.values(), map.entrySet()).isEmpty();
}
@AfterTemplate
boolean after(Map<K, V> map) {
return map.isEmpty();
}
}
/** Prefer {@link Map#size()} over more contrived alternatives. */
static final class MapSize<K, V> {
@BeforeTemplate
int before(Map<K, V> map) {
return Refaster.anyOf(map.keySet(), map.values(), map.entrySet()).size();
}
@AfterTemplate
int after(Map<K, V> map) {
return map.size();
}
}
/** Prefer {@link Map#containsKey(Object)} over more contrived alternatives. */
static final class MapContainsKey<K, V, T> {
@BeforeTemplate
boolean before(Map<K, V> map, T key) {
return map.keySet().contains(key);
}
@AfterTemplate
boolean after(Map<K, V> map, T key) {
return map.containsKey(key);
}
}
/** Prefer {@link Map#containsValue(Object)} over more contrived alternatives. */
static final class MapContainsValue<K, V, T> {
@BeforeTemplate
boolean before(Map<K, V> map, T value) {
return map.values().contains(value);
}
@AfterTemplate
boolean after(Map<K, V> map, T value) {
return map.containsValue(value);
}
}
/** Don't unnecessarily use {@link Map#entrySet()}. */
static final class MapKeyStream<K, V> {
@BeforeTemplate
Stream<K> before(Map<K, V> map) {
return map.entrySet().stream().map(Map.Entry::getKey);
}
@AfterTemplate
Stream<K> after(Map<K, V> map) {
return map.keySet().stream();
}
}
/** Don't unnecessarily use {@link Map#entrySet()}. */
static final class MapValueStream<K, V> {
@BeforeTemplate
Stream<V> before(Map<K, V> map) {
return map.entrySet().stream().map(Map.Entry::getValue);
}
@AfterTemplate
Stream<V> after(Map<K, V> map) {
return map.values().stream();
}
}
}

View File

@@ -7,7 +7,7 @@ import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import java.util.Collection;
import java.util.Set;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/** Refaster rules related to expressions dealing with {@link Multimap}s. */

View File

@@ -2,14 +2,18 @@ package tech.picnic.errorprone.refasterrules;
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
import static java.util.Objects.requireNonNullElse;
import static java.util.Objects.requireNonNullElseGet;
import com.google.common.base.MoreObjects;
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import javax.annotation.Nullable;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/** Refaster rules related to expressions dealing with (possibly) null values. */
@@ -43,13 +47,18 @@ final class NullRules {
}
}
/** Prefer {@link Objects#requireNonNullElse(Object, Object)} over the Guava alternative. */
// XXX: This rule is not valid in case `second` is `@Nullable`: in that case the Guava variant
// will return `null`, while the JDK variant will throw an NPE.
/**
* Prefer {@link Objects#requireNonNullElse(Object, Object)} over non-JDK or more contrived
* alternatives.
*/
// XXX: This rule is not valid in case `second` is `@Nullable`: in that case the Guava and
// `Optional` variants will return `null`, where the `requireNonNullElse` alternative will throw
// an NPE.
static final class RequireNonNullElse<T> {
@BeforeTemplate
T before(T first, T second) {
return MoreObjects.firstNonNull(first, second);
return Refaster.anyOf(
MoreObjects.firstNonNull(first, second), Optional.ofNullable(first).orElse(second));
}
@AfterTemplate
@@ -59,6 +68,26 @@ final class NullRules {
}
}
/**
* Prefer {@link Objects#requireNonNullElseGet(Object, Supplier)} over more contrived
* alternatives.
*/
// XXX: This rule is not valid in case `supplier` yields `@Nullable` values: in that case the
// `Optional` variant will return `null`, where the `requireNonNullElseGet` alternative will throw
// an NPE.
static final class RequireNonNullElseGet<T, S extends T> {
@BeforeTemplate
T before(T object, Supplier<S> supplier) {
return Optional.ofNullable(object).orElseGet(supplier);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
T after(T object, Supplier<S> supplier) {
return requireNonNullElseGet(object, supplier);
}
}
/** Prefer {@link Objects#isNull(Object)} over the equivalent lambda function. */
static final class IsNullFunction<T> {
@BeforeTemplate

View File

@@ -16,7 +16,7 @@ import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/** Refaster rules related to expressions dealing with {@link Optional}s. */
@@ -355,6 +355,42 @@ final class OptionalRules {
}
}
/**
* Avoid unnecessary {@link Optional} to {@link Stream} conversion when filtering a value of the
* former type.
*/
static final class OptionalFilter<T> {
@BeforeTemplate
Optional<T> before(Optional<T> optional, Predicate<? super T> predicate) {
return Refaster.anyOf(
optional.stream().filter(predicate).findFirst(),
optional.stream().filter(predicate).findAny());
}
@AfterTemplate
Optional<T> after(Optional<T> optional, Predicate<? super T> predicate) {
return optional.filter(predicate);
}
}
/**
* Avoid unnecessary {@link Optional} to {@link Stream} conversion when mapping a value of the
* former type.
*/
// XXX: If `StreamMapFirst` also simplifies `.findAny()` expressions, then this rule can be
// dropped in favour of `StreamMapFirst` and `OptionalIdentity`.
static final class OptionalMap<S, T> {
@BeforeTemplate
Optional<? extends T> before(Optional<S> optional, Function<? super S, ? extends T> function) {
return optional.stream().map(function).findAny();
}
@AfterTemplate
Optional<? extends T> after(Optional<S> optional, Function<? super S, ? extends T> function) {
return optional.map(function);
}
}
// XXX: Add a rule for:
// `optional.flatMap(x -> pred(x) ? Optional.empty() : Optional.of(x))` and variants.
// (Maybe canonicalize the inner expression. Maybe we rewrite already.)

View File

@@ -0,0 +1,176 @@
package tech.picnic.errorprone.refasterrules;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkElementIndex;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkPositionIndex;
import static com.google.common.base.Preconditions.checkState;
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
import com.google.common.base.Preconditions;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/** Refaster templates related to statements dealing with {@link Preconditions}. */
@OnlineDocumentation
final class PreconditionsRules {
private PreconditionsRules() {}
/** Prefer {@link Preconditions#checkArgument(boolean)} over more verbose alternatives. */
static final class CheckArgument {
@BeforeTemplate
void before(boolean condition) {
if (condition) {
throw new IllegalArgumentException();
}
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(boolean condition) {
checkArgument(!condition);
}
}
/** Prefer {@link Preconditions#checkArgument(boolean, Object)} over more verbose alternatives. */
static final class CheckArgumentWithMessage {
@BeforeTemplate
void before(boolean condition, String message) {
if (condition) {
throw new IllegalArgumentException(message);
}
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(boolean condition, String message) {
checkArgument(!condition, message);
}
}
/**
* Prefer {@link Preconditions#checkElementIndex(int, int, String)} over less descriptive or more
* verbose alternatives.
*
* <p>Note that the two-argument {@link Preconditions#checkElementIndex(int, int)} is better
* replaced with {@link java.util.Objects#checkIndex(int, int)}.
*/
static final class CheckElementIndexWithMessage {
@BeforeTemplate
void before(int index, int size, String message) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException(message);
}
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(int index, int size, String message) {
checkElementIndex(index, size, message);
}
}
/** Prefer {@link Preconditions#checkNotNull(Object)} over more verbose alternatives. */
static final class CheckNotNull<T> {
@BeforeTemplate
void before(T object) {
if (object == null) {
throw new NullPointerException();
}
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(T object) {
checkNotNull(object);
}
}
/** Prefer {@link Preconditions#checkNotNull(Object, Object)} over more verbose alternatives. */
static final class CheckNotNullWithMessage<T> {
@BeforeTemplate
void before(T object, String message) {
if (object == null) {
throw new NullPointerException(message);
}
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(T object, String message) {
checkNotNull(object, message);
}
}
/**
* Prefer {@link Preconditions#checkPositionIndex(int, int)} over less descriptive or more verbose
* alternatives.
*/
static final class CheckPositionIndex {
@BeforeTemplate
void before(int index, int size) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException();
}
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(int index, int size) {
checkPositionIndex(index, size);
}
}
/**
* Prefer {@link Preconditions#checkPositionIndex(int, int, String)} over less descriptive or more
* verbose alternatives.
*/
static final class CheckPositionIndexWithMessage {
@BeforeTemplate
void before(int index, int size, String message) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException(message);
}
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(int index, int size, String message) {
checkPositionIndex(index, size, message);
}
}
/** Prefer {@link Preconditions#checkState(boolean)} over more verbose alternatives. */
static final class CheckState {
@BeforeTemplate
void before(boolean condition) {
if (condition) {
throw new IllegalStateException();
}
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(boolean condition) {
checkState(!condition);
}
}
/** Prefer {@link Preconditions#checkState(boolean, Object)} over more verbose alternatives. */
static final class CheckStateWithMessage {
@BeforeTemplate
void before(boolean condition, String message) {
if (condition) {
throw new IllegalStateException(message);
}
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(boolean condition, String message) {
checkState(!condition, message);
}
}
}

View File

@@ -1,10 +1,13 @@
package tech.picnic.errorprone.refasterrules;
import static com.google.common.collect.MoreCollectors.toOptional;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
import static java.util.function.Function.identity;
import static org.assertj.core.api.Assertions.assertThat;
import static reactor.function.TupleUtils.function;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.MoreCollectors;
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
@@ -14,18 +17,27 @@ import com.google.errorprone.refaster.annotation.NotMatches;
import com.google.errorprone.refaster.annotation.Placeholder;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
import java.time.Duration;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.test.publisher.PublisherProbe;
import reactor.util.context.Context;
import reactor.util.function.Tuple2;
import tech.picnic.errorprone.refaster.annotation.Description;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
import tech.picnic.errorprone.refaster.annotation.Severity;
import tech.picnic.errorprone.refaster.matchers.ThrowsCheckedException;
/** Refaster rules related to Reactor expressions and statements. */
@@ -49,6 +61,45 @@ final class ReactorRules {
}
}
/** Prefer {@link Mono#empty()} over more contrived alternatives. */
static final class MonoEmpty<T> {
@BeforeTemplate
Mono<T> before() {
return Refaster.anyOf(Mono.justOrEmpty(null), Mono.justOrEmpty(Optional.empty()));
}
@AfterTemplate
Mono<T> after() {
return Mono.empty();
}
}
/** Prefer {@link Mono#just(Object)} over more contrived alternatives. */
static final class MonoJust<T> {
@BeforeTemplate
Mono<T> before(T value) {
return Mono.justOrEmpty(Optional.of(value));
}
@AfterTemplate
Mono<T> after(T value) {
return Mono.just(value);
}
}
/** Prefer {@link Mono#justOrEmpty(Object)} over more contrived alternatives. */
static final class MonoJustOrEmpty<@Nullable T> {
@BeforeTemplate
Mono<T> before(T value) {
return Mono.justOrEmpty(Optional.ofNullable(value));
}
@AfterTemplate
Mono<T> after(T value) {
return Mono.justOrEmpty(value);
}
}
/** Prefer {@link Mono#justOrEmpty(Optional)} over more verbose alternatives. */
// XXX: If `optional` is a constant and effectively-final expression then the `Mono.defer` can be
// dropped. Should look into Refaster support for identifying this.
@@ -68,6 +119,125 @@ final class ReactorRules {
}
}
/**
* Try to avoid expressions of type {@code Optional<Mono<T>>}, but if you must map an {@link
* Optional} to this type, prefer using {@link Mono#just(Object)}.
*/
static final class OptionalMapMonoJust<T> {
@BeforeTemplate
Optional<Mono<T>> before(Optional<T> optional) {
return optional.map(Mono::justOrEmpty);
}
@AfterTemplate
Optional<Mono<T>> after(Optional<T> optional) {
return optional.map(Mono::just);
}
}
/**
* Prefer a {@link Mono#justOrEmpty(Optional)} and {@link Mono#switchIfEmpty(Mono)} chain over
* more contrived alternatives.
*
* <p>In particular, avoid mixing of the {@link Optional} and {@link Mono} APIs.
*/
static final class MonoFromOptionalSwitchIfEmpty<T> {
@BeforeTemplate
Mono<T> before(Optional<T> optional, Mono<T> mono) {
return optional.map(Mono::just).orElse(mono);
}
@AfterTemplate
Mono<T> after(Optional<T> optional, Mono<T> mono) {
return Mono.justOrEmpty(optional).switchIfEmpty(mono);
}
}
/**
* Prefer {@link Mono#zip(Mono, Mono)} over a chained {@link Mono#zipWith(Mono)}, as the former
* better conveys that the {@link Mono}s may be subscribed to concurrently, and generalizes to
* combining three or more reactive streams.
*/
static final class MonoZip<T, S> {
@BeforeTemplate
Mono<Tuple2<T, S>> before(Mono<T> mono, Mono<S> other) {
return mono.zipWith(other);
}
@AfterTemplate
Mono<Tuple2<T, S>> after(Mono<T> mono, Mono<S> other) {
return Mono.zip(mono, other);
}
}
/**
* Prefer {@link Mono#zip(Mono, Mono)} with a chained combinator over a chained {@link
* Mono#zipWith(Mono, BiFunction)}, as the former better conveys that the {@link Mono}s may be
* subscribed to concurrently, and generalizes to combining three or more reactive streams.
*/
static final class MonoZipWithCombinator<T, S, R> {
@BeforeTemplate
Mono<R> before(Mono<T> mono, Mono<S> other, BiFunction<T, S, R> combinator) {
return mono.zipWith(other, combinator);
}
@AfterTemplate
Mono<R> after(Mono<T> mono, Mono<S> other, BiFunction<T, S, R> combinator) {
return Mono.zip(mono, other).map(function(combinator));
}
}
/**
* Prefer {@link Flux#zip(Publisher, Publisher)} over a chained {@link Flux#zipWith(Publisher)},
* as the former better conveys that the {@link Publisher}s may be subscribed to concurrently, and
* generalizes to combining three or more reactive streams.
*/
static final class FluxZip<T, S> {
@BeforeTemplate
Flux<Tuple2<T, S>> before(Flux<T> flux, Publisher<S> other) {
return flux.zipWith(other);
}
@AfterTemplate
Flux<Tuple2<T, S>> after(Flux<T> flux, Publisher<S> other) {
return Flux.zip(flux, other);
}
}
/**
* Prefer {@link Flux#zip(Publisher, Publisher)} with a chained combinator over a chained {@link
* Flux#zipWith(Publisher, BiFunction)}, as the former better conveys that the {@link Publisher}s
* may be subscribed to concurrently, and generalizes to combining three or more reactive streams.
*/
static final class FluxZipWithCombinator<T, S, R> {
@BeforeTemplate
Flux<R> before(Flux<T> flux, Publisher<S> other, BiFunction<T, S, R> combinator) {
return flux.zipWith(other, combinator);
}
@AfterTemplate
Flux<R> after(Flux<T> flux, Publisher<S> other, BiFunction<T, S, R> combinator) {
return Flux.zip(flux, other).map(function(combinator));
}
}
/**
* Prefer {@link Flux#zipWithIterable(Iterable)} with a chained combinator over {@link
* Flux#zipWithIterable(Iterable, BiFunction)}, as the former generally yields more readable code.
*/
static final class FluxZipWithIterable<T, S, R> {
@BeforeTemplate
Flux<R> before(Flux<T> flux, Iterable<S> iterable, BiFunction<T, S, R> combinator) {
return flux.zipWithIterable(iterable, combinator);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
Flux<R> after(Flux<T> flux, Iterable<S> iterable, BiFunction<T, S, R> combinator) {
return flux.zipWithIterable(iterable).map(function(combinator));
}
}
/** Don't unnecessarily defer {@link Mono#error(Throwable)}. */
static final class MonoDeferredError<T> {
@BeforeTemplate
@@ -141,13 +311,72 @@ final class ReactorRules {
}
}
/** Don't unnecessarily pass an empty publisher to {@link Mono#switchIfEmpty(Mono)}. */
static final class MonoSwitchIfEmptyOfEmptyPublisher<T> {
/**
* Prefer {@link Flux#take(long, boolean)} over {@link Flux#take(long)}.
*
* <p>In Reactor versions prior to 3.5.0, {@code Flux#take(long)} makes an unbounded request
* upstream, and is equivalent to {@code Flux#take(long, false)}. In 3.5.0, the behavior of {@code
* Flux#take(long)} will change to that of {@code Flux#take(long, true)}.
*
* <p>The intent with this Refaster rule is to get the new behavior before upgrading to Reactor
* 3.5.0.
*/
// XXX: Drop this rule some time after upgrading to Reactor 3.6.0, or introduce a way to apply
// this rule only when an older version of Reactor is on the classpath.
// XXX: Once Reactor 3.6.0 is out, introduce a rule that rewrites code in the opposite direction.
@Description(
"Prior to Reactor 3.5.0, `take(n)` requests and unbounded number of elements upstream.")
@Severity(WARNING)
static final class FluxTake<T> {
@BeforeTemplate
Flux<T> before(Flux<T> flux, long n) {
return flux.take(n);
}
@AfterTemplate
Flux<T> after(Flux<T> flux, long n) {
return flux.take(n, /* limitRequest= */ true);
}
}
/** Prefer {@link Mono#defaultIfEmpty(Object)} over more contrived alternatives. */
static final class MonoDefaultIfEmpty<T> {
@BeforeTemplate
Mono<T> before(Mono<T> mono, T object) {
return mono.switchIfEmpty(Mono.just(object));
}
@AfterTemplate
Mono<T> after(Mono<T> mono, T object) {
return mono.defaultIfEmpty(object);
}
}
/** Prefer {@link Flux#defaultIfEmpty(Object)} over more contrived alternatives. */
static final class FluxDefaultIfEmpty<T> {
@BeforeTemplate
Flux<T> before(Flux<T> flux, T object) {
return flux.switchIfEmpty(Refaster.anyOf(Mono.just(object), Flux.just(object)));
}
@AfterTemplate
Flux<T> after(Flux<T> flux, T object) {
return flux.defaultIfEmpty(object);
}
}
/** Don't unnecessarily transform a {@link Mono} to an equivalent instance. */
static final class MonoIdentity<T> {
@BeforeTemplate
Mono<T> before(Mono<T> mono) {
return mono.switchIfEmpty(Mono.empty());
}
@BeforeTemplate
Mono<@Nullable Void> before2(Mono<@Nullable Void> mono) {
return mono.then();
}
@AfterTemplate
Mono<T> after(Mono<T> mono) {
return mono;
@@ -171,7 +400,10 @@ final class ReactorRules {
static final class FluxConcatMap<T, S> {
@BeforeTemplate
Flux<S> before(Flux<T> flux, Function<? super T, ? extends Publisher<? extends S>> function) {
return Refaster.anyOf(flux.flatMap(function, 1), flux.flatMapSequential(function, 1));
return Refaster.anyOf(
flux.flatMap(function, 1),
flux.flatMapSequential(function, 1),
flux.map(function).concatMap(identity()));
}
@AfterTemplate
@@ -188,7 +420,9 @@ final class ReactorRules {
Function<? super T, ? extends Publisher<? extends S>> function,
int prefetch) {
return Refaster.anyOf(
flux.flatMap(function, 1, prefetch), flux.flatMapSequential(function, 1, prefetch));
flux.flatMap(function, 1, prefetch),
flux.flatMapSequential(function, 1, prefetch),
flux.map(function).concatMap(identity(), prefetch));
}
@AfterTemplate
@@ -240,16 +474,195 @@ final class ReactorRules {
*/
abstract static class MonoFlatMapToFlux<T, S> {
@Placeholder(allowsIdentity = true)
abstract Mono<S> valueTransformation(@MayOptionallyUse T value);
abstract Mono<S> transformation(@MayOptionallyUse T value);
@BeforeTemplate
Flux<S> before(Mono<T> mono) {
return mono.flatMapMany(v -> valueTransformation(v));
return mono.flatMapMany(v -> transformation(v));
}
@AfterTemplate
Flux<S> after(Mono<T> mono) {
return mono.flatMap(v -> valueTransformation(v)).flux();
return mono.flatMap(v -> transformation(v)).flux();
}
}
/**
* Prefer {@link Mono#map(Function)} over alternatives that unnecessarily require an inner
* subscription.
*/
abstract static class MonoMap<T, S> {
@Placeholder(allowsIdentity = true)
abstract S transformation(@MayOptionallyUse T value);
@BeforeTemplate
Mono<S> before(Mono<T> mono) {
return mono.flatMap(x -> Mono.just(transformation(x)));
}
@AfterTemplate
Mono<S> after(Mono<T> mono) {
return mono.map(x -> transformation(x));
}
}
/**
* Prefer {@link Flux#map(Function)} over alternatives that unnecessarily require an inner
* subscription.
*/
abstract static class FluxMap<T, S> {
@Placeholder(allowsIdentity = true)
abstract S transformation(@MayOptionallyUse T value);
@BeforeTemplate
Flux<S> before(Flux<T> flux, boolean delayUntilEnd, int maxConcurrency, int prefetch) {
return Refaster.anyOf(
flux.concatMap(x -> Mono.just(transformation(x))),
flux.concatMap(x -> Flux.just(transformation(x))),
flux.concatMap(x -> Mono.just(transformation(x)), prefetch),
flux.concatMap(x -> Flux.just(transformation(x)), prefetch),
flux.concatMapDelayError(x -> Mono.just(transformation(x))),
flux.concatMapDelayError(x -> Flux.just(transformation(x))),
flux.concatMapDelayError(x -> Mono.just(transformation(x)), prefetch),
flux.concatMapDelayError(x -> Flux.just(transformation(x)), prefetch),
flux.concatMapDelayError(x -> Mono.just(transformation(x)), delayUntilEnd, prefetch),
flux.concatMapDelayError(x -> Flux.just(transformation(x)), delayUntilEnd, prefetch),
flux.flatMap(x -> Mono.just(transformation(x)), maxConcurrency),
flux.flatMap(x -> Flux.just(transformation(x)), maxConcurrency),
flux.flatMap(x -> Mono.just(transformation(x)), maxConcurrency, prefetch),
flux.flatMap(x -> Flux.just(transformation(x)), maxConcurrency, prefetch),
flux.flatMapDelayError(x -> Mono.just(transformation(x)), maxConcurrency, prefetch),
flux.flatMapDelayError(x -> Flux.just(transformation(x)), maxConcurrency, prefetch),
flux.flatMapSequential(x -> Mono.just(transformation(x)), maxConcurrency),
flux.flatMapSequential(x -> Flux.just(transformation(x)), maxConcurrency),
flux.flatMapSequential(x -> Mono.just(transformation(x)), maxConcurrency, prefetch),
flux.flatMapSequential(x -> Flux.just(transformation(x)), maxConcurrency, prefetch),
flux.flatMapSequentialDelayError(
x -> Mono.just(transformation(x)), maxConcurrency, prefetch),
flux.flatMapSequentialDelayError(
x -> Flux.just(transformation(x)), maxConcurrency, prefetch),
flux.switchMap(x -> Mono.just(transformation(x))),
flux.switchMap(x -> Flux.just(transformation(x))));
}
@AfterTemplate
Flux<S> after(Flux<T> flux) {
return flux.map(x -> transformation(x));
}
}
/**
* Prefer {@link Mono#mapNotNull(Function)} over alternatives that unnecessarily require an inner
* subscription.
*/
abstract static class MonoMapNotNull<T, S> {
@Placeholder(allowsIdentity = true)
abstract S transformation(@MayOptionallyUse T value);
@BeforeTemplate
Mono<S> before(Mono<T> mono) {
return mono.flatMap(
x ->
Refaster.anyOf(
Mono.justOrEmpty(transformation(x)), Mono.fromSupplier(() -> transformation(x))));
}
@AfterTemplate
Mono<S> after(Mono<T> mono) {
return mono.mapNotNull(x -> transformation(x));
}
}
/**
* Prefer {@link Flux#mapNotNull(Function)} over alternatives that unnecessarily require an inner
* subscription.
*/
abstract static class FluxMapNotNull<T, S> {
@Placeholder(allowsIdentity = true)
abstract S transformation(@MayOptionallyUse T value);
@BeforeTemplate
Publisher<S> before(Flux<T> flux, boolean delayUntilEnd, int maxConcurrency, int prefetch) {
return Refaster.anyOf(
flux.concatMap(
x ->
Refaster.anyOf(
Mono.justOrEmpty(transformation(x)),
Mono.fromSupplier(() -> transformation(x)))),
flux.concatMap(
x ->
Refaster.anyOf(
Mono.justOrEmpty(transformation(x)),
Mono.fromSupplier(() -> transformation(x))),
prefetch),
flux.concatMapDelayError(
x ->
Refaster.anyOf(
Mono.justOrEmpty(transformation(x)),
Mono.fromSupplier(() -> transformation(x)))),
flux.concatMapDelayError(
x ->
Refaster.anyOf(
Mono.justOrEmpty(transformation(x)),
Mono.fromSupplier(() -> transformation(x))),
prefetch),
flux.concatMapDelayError(
x ->
Refaster.anyOf(
Mono.justOrEmpty(transformation(x)),
Mono.fromSupplier(() -> transformation(x))),
delayUntilEnd,
prefetch),
flux.flatMap(
x ->
Refaster.anyOf(
Mono.justOrEmpty(transformation(x)),
Mono.fromSupplier(() -> transformation(x))),
maxConcurrency),
flux.flatMap(
x ->
Refaster.anyOf(
Mono.justOrEmpty(transformation(x)),
Mono.fromSupplier(() -> transformation(x))),
maxConcurrency,
prefetch),
flux.flatMapDelayError(
x ->
Refaster.anyOf(
Mono.justOrEmpty(transformation(x)),
Mono.fromSupplier(() -> transformation(x))),
maxConcurrency,
prefetch),
flux.flatMapSequential(
x ->
Refaster.anyOf(
Mono.justOrEmpty(transformation(x)),
Mono.fromSupplier(() -> transformation(x))),
maxConcurrency),
flux.flatMapSequential(
x ->
Refaster.anyOf(
Mono.justOrEmpty(transformation(x)),
Mono.fromSupplier(() -> transformation(x))),
maxConcurrency,
prefetch),
flux.flatMapSequentialDelayError(
x ->
Refaster.anyOf(
Mono.justOrEmpty(transformation(x)),
Mono.fromSupplier(() -> transformation(x))),
maxConcurrency,
prefetch),
flux.switchMap(
x ->
Refaster.anyOf(
Mono.justOrEmpty(transformation(x)),
Mono.fromSupplier(() -> transformation(x)))));
}
@AfterTemplate
Flux<S> after(Flux<T> flux) {
return flux.mapNotNull(x -> transformation(x));
}
}
@@ -257,7 +670,8 @@ final class ReactorRules {
static final class MonoFlux<T> {
@BeforeTemplate
Flux<T> before(Mono<T> mono) {
return Flux.concat(mono);
return Refaster.anyOf(
mono.flatMapMany(Mono::just), mono.flatMapMany(Flux::just), Flux.concat(mono));
}
@AfterTemplate
@@ -266,6 +680,19 @@ final class ReactorRules {
}
}
/** Prefer direct invocation of {@link Mono#then()}} over more contrived alternatives. */
static final class MonoThen<T> {
@BeforeTemplate
Mono<@Nullable Void> before(Mono<T> mono) {
return mono.flux().then();
}
@AfterTemplate
Mono<@Nullable Void> after(Mono<T> mono) {
return mono.then();
}
}
/**
* Prefer a collection using {@link MoreCollectors#toOptional()} over more contrived alternatives.
*/
@@ -274,9 +701,7 @@ final class ReactorRules {
static final class MonoCollectToOptional<T> {
@BeforeTemplate
Mono<Optional<T>> before(Mono<T> mono) {
return Refaster.anyOf(
mono.map(Optional::of).defaultIfEmpty(Optional.empty()),
mono.map(Optional::of).switchIfEmpty(Mono.just(Optional.empty())));
return mono.map(Optional::of).defaultIfEmpty(Optional.empty());
}
@AfterTemplate
@@ -312,6 +737,32 @@ final class ReactorRules {
}
}
/** Prefer {@link Mono#flatMap(Function)} over more contrived alternatives. */
static final class MonoFlatMap<S, T> {
@BeforeTemplate
Mono<T> before(Mono<S> mono, Function<? super S, ? extends Mono<? extends T>> function) {
return mono.map(function).flatMap(identity());
}
@AfterTemplate
Mono<T> after(Mono<S> mono, Function<? super S, ? extends Mono<? extends T>> function) {
return mono.flatMap(function);
}
}
/** Prefer {@link Mono#flatMapMany(Function)} over more contrived alternatives. */
static final class MonoFlatMapMany<S, T> {
@BeforeTemplate
Flux<T> before(Mono<S> mono, Function<? super S, ? extends Publisher<? extends T>> function) {
return mono.map(function).flatMapMany(identity());
}
@AfterTemplate
Flux<T> after(Mono<S> mono, Function<? super S, ? extends Publisher<? extends T>> function) {
return mono.flatMapMany(function);
}
}
/**
* Prefer {@link Flux#concatMapIterable(Function)} over alternatives that require an additional
* subscription.
@@ -349,6 +800,42 @@ final class ReactorRules {
}
}
/**
* Prefer {@link Mono#doOnError(Class, Consumer)} over {@link Mono#doOnError(Predicate, Consumer)}
* where possible.
*/
static final class MonoDoOnError<T> {
@BeforeTemplate
Mono<T> before(
Mono<T> mono, Class<? extends Throwable> clazz, Consumer<? super Throwable> onError) {
return mono.doOnError(clazz::isInstance, onError);
}
@AfterTemplate
Mono<T> after(
Mono<T> mono, Class<? extends Throwable> clazz, Consumer<? super Throwable> onError) {
return mono.doOnError(clazz, onError);
}
}
/**
* Prefer {@link Flux#doOnError(Class, Consumer)} over {@link Flux#doOnError(Predicate, Consumer)}
* where possible.
*/
static final class FluxDoOnError<T> {
@BeforeTemplate
Flux<T> before(
Flux<T> flux, Class<? extends Throwable> clazz, Consumer<? super Throwable> onError) {
return flux.doOnError(clazz::isInstance, onError);
}
@AfterTemplate
Flux<T> after(
Flux<T> flux, Class<? extends Throwable> clazz, Consumer<? super Throwable> onError) {
return flux.doOnError(clazz, onError);
}
}
/** Prefer {@link Mono#onErrorComplete()} over more contrived alternatives. */
static final class MonoOnErrorComplete<T> {
@BeforeTemplate
@@ -375,6 +862,272 @@ final class ReactorRules {
}
}
/** Prefer {@link Mono#onErrorComplete(Class)}} over more contrived alternatives. */
static final class MonoOnErrorCompleteClass<T> {
@BeforeTemplate
Mono<T> before(Mono<T> mono, Class<? extends Throwable> clazz) {
return Refaster.anyOf(
mono.onErrorComplete(clazz::isInstance), mono.onErrorResume(clazz, e -> Mono.empty()));
}
@AfterTemplate
Mono<T> after(Mono<T> mono, Class<? extends Throwable> clazz) {
return mono.onErrorComplete(clazz);
}
}
/** Prefer {@link Flux#onErrorComplete(Class)}} over more contrived alternatives. */
static final class FluxOnErrorCompleteClass<T> {
@BeforeTemplate
Flux<T> before(Flux<T> flux, Class<? extends Throwable> clazz) {
return Refaster.anyOf(
flux.onErrorComplete(clazz::isInstance),
flux.onErrorResume(clazz, e -> Refaster.anyOf(Mono.empty(), Flux.empty())));
}
@AfterTemplate
Flux<T> after(Flux<T> flux, Class<? extends Throwable> clazz) {
return flux.onErrorComplete(clazz);
}
}
/** Prefer {@link Mono#onErrorComplete(Predicate)}} over more contrived alternatives. */
static final class MonoOnErrorCompletePredicate<T> {
@BeforeTemplate
Mono<T> before(Mono<T> mono, Predicate<? super Throwable> predicate) {
return mono.onErrorResume(predicate, e -> Mono.empty());
}
@AfterTemplate
Mono<T> after(Mono<T> mono, Predicate<? super Throwable> predicate) {
return mono.onErrorComplete(predicate);
}
}
/** Prefer {@link Flux#onErrorComplete(Predicate)}} over more contrived alternatives. */
static final class FluxOnErrorCompletePredicate<T> {
@BeforeTemplate
Flux<T> before(Flux<T> flux, Predicate<? super Throwable> predicate) {
return flux.onErrorResume(predicate, e -> Refaster.anyOf(Mono.empty(), Flux.empty()));
}
@AfterTemplate
Flux<T> after(Flux<T> flux, Predicate<? super Throwable> predicate) {
return flux.onErrorComplete(predicate);
}
}
/**
* Prefer {@link Mono#onErrorContinue(Class, BiConsumer)} over {@link
* Mono#onErrorContinue(Predicate, BiConsumer)} where possible.
*/
static final class MonoOnErrorContinue<T> {
@BeforeTemplate
Mono<T> before(
Mono<T> mono,
Class<? extends Throwable> clazz,
BiConsumer<Throwable, Object> errorConsumer) {
return mono.onErrorContinue(clazz::isInstance, errorConsumer);
}
@AfterTemplate
Mono<T> after(
Mono<T> mono,
Class<? extends Throwable> clazz,
BiConsumer<Throwable, Object> errorConsumer) {
return mono.onErrorContinue(clazz, errorConsumer);
}
}
/**
* Prefer {@link Flux#onErrorContinue(Class, BiConsumer)} over {@link
* Flux#onErrorContinue(Predicate, BiConsumer)} where possible.
*/
static final class FluxOnErrorContinue<T> {
@BeforeTemplate
Flux<T> before(
Flux<T> flux,
Class<? extends Throwable> clazz,
BiConsumer<Throwable, Object> errorConsumer) {
return flux.onErrorContinue(clazz::isInstance, errorConsumer);
}
@AfterTemplate
Flux<T> after(
Flux<T> flux,
Class<? extends Throwable> clazz,
BiConsumer<Throwable, Object> errorConsumer) {
return flux.onErrorContinue(clazz, errorConsumer);
}
}
/**
* Prefer {@link Mono#onErrorMap(Class, Function)} over {@link Mono#onErrorMap(Predicate,
* Function)} where possible.
*/
static final class MonoOnErrorMap<T> {
@BeforeTemplate
Mono<T> before(
Mono<T> mono,
Class<? extends Throwable> clazz,
Function<? super Throwable, ? extends Throwable> mapper) {
return mono.onErrorMap(clazz::isInstance, mapper);
}
@AfterTemplate
Mono<T> after(
Mono<T> mono,
Class<? extends Throwable> clazz,
Function<? super Throwable, ? extends Throwable> mapper) {
return mono.onErrorMap(clazz, mapper);
}
}
/**
* Prefer {@link Flux#onErrorMap(Class, Function)} over {@link Flux#onErrorMap(Predicate,
* Function)} where possible.
*/
static final class FluxOnErrorMap<T> {
@BeforeTemplate
Flux<T> before(
Flux<T> flux,
Class<? extends Throwable> clazz,
Function<? super Throwable, ? extends Throwable> mapper) {
return flux.onErrorMap(clazz::isInstance, mapper);
}
@AfterTemplate
Flux<T> after(
Flux<T> flux,
Class<? extends Throwable> clazz,
Function<? super Throwable, ? extends Throwable> mapper) {
return flux.onErrorMap(clazz, mapper);
}
}
/**
* Prefer {@link Mono#onErrorResume(Class, Function)} over {@link Mono#onErrorResume(Predicate,
* Function)} where possible.
*/
static final class MonoOnErrorResume<T> {
@BeforeTemplate
Mono<T> before(
Mono<T> mono,
Class<? extends Throwable> clazz,
Function<? super Throwable, ? extends Mono<? extends T>> fallback) {
return mono.onErrorResume(clazz::isInstance, fallback);
}
@AfterTemplate
Mono<T> after(
Mono<T> mono,
Class<? extends Throwable> clazz,
Function<? super Throwable, ? extends Mono<? extends T>> fallback) {
return mono.onErrorResume(clazz, fallback);
}
}
/**
* Prefer {@link Flux#onErrorResume(Class, Function)} over {@link Flux#onErrorResume(Predicate,
* Function)} where possible.
*/
static final class FluxOnErrorResume<T> {
@BeforeTemplate
Flux<T> before(
Flux<T> flux,
Class<? extends Throwable> clazz,
Function<? super Throwable, ? extends Publisher<? extends T>> fallback) {
return flux.onErrorResume(clazz::isInstance, fallback);
}
@AfterTemplate
Flux<T> after(
Flux<T> flux,
Class<? extends Throwable> clazz,
Function<? super Throwable, ? extends Publisher<? extends T>> fallback) {
return flux.onErrorResume(clazz, fallback);
}
}
/**
* Prefer {@link Mono#onErrorReturn(Class, Object)} over {@link Mono#onErrorReturn(Predicate,
* Object)} where possible.
*/
static final class MonoOnErrorReturn<T> {
@BeforeTemplate
Mono<T> before(Mono<T> mono, Class<? extends Throwable> clazz, T fallbackValue) {
return mono.onErrorReturn(clazz::isInstance, fallbackValue);
}
@AfterTemplate
Mono<T> after(Mono<T> mono, Class<? extends Throwable> clazz, T fallbackValue) {
return mono.onErrorReturn(clazz, fallbackValue);
}
}
/**
* Prefer {@link Flux#onErrorReturn(Class, Object)} over {@link Flux#onErrorReturn(Predicate,
* Object)} where possible.
*/
static final class FluxOnErrorReturn<T> {
@BeforeTemplate
Flux<T> before(Flux<T> flux, Class<? extends Throwable> clazz, T fallbackValue) {
return flux.onErrorReturn(clazz::isInstance, fallbackValue);
}
@AfterTemplate
Flux<T> after(Flux<T> flux, Class<? extends Throwable> clazz, T fallbackValue) {
return flux.onErrorReturn(clazz, fallbackValue);
}
}
/**
* Apply {@link Flux#filter(Predicate)} before {@link Flux#sort()} to reduce the number of
* elements to sort.
*/
static final class FluxFilterSort<T> {
@BeforeTemplate
Flux<T> before(Flux<T> flux, Predicate<? super T> predicate) {
return flux.sort().filter(predicate);
}
@AfterTemplate
Flux<T> after(Flux<T> flux, Predicate<? super T> predicate) {
return flux.filter(predicate).sort();
}
}
/**
* Apply {@link Flux#filter(Predicate)} before {@link Flux#sort(Comparator)} to reduce the number
* of elements to sort.
*/
static final class FluxFilterSortWithComparator<T> {
@BeforeTemplate
Flux<T> before(Flux<T> flux, Predicate<? super T> predicate, Comparator<? super T> comparator) {
return flux.sort(comparator).filter(predicate);
}
@AfterTemplate
Flux<T> after(Flux<T> flux, Predicate<? super T> predicate, Comparator<? super T> comparator) {
return flux.filter(predicate).sort(comparator);
}
}
/** Prefer {@link reactor.util.context.Context#empty()}} over more verbose alternatives. */
// XXX: Consider introducing an `IsEmpty` matcher that identifies a wide range of guaranteed-empty
// `Collection` and `Map` expressions.
static final class ContextEmpty {
@BeforeTemplate
Context before() {
return Context.of(Refaster.anyOf(new HashMap<>(), ImmutableMap.of()));
}
@AfterTemplate
Context after() {
return Context.empty();
}
}
/** Prefer {@link PublisherProbe#empty()}} over more verbose alternatives. */
static final class PublisherProbeEmpty<T> {
@BeforeTemplate

View File

@@ -9,7 +9,7 @@ import io.reactivex.Flowable;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Single;
import org.jspecify.nullness.Nullable;
import org.jspecify.annotations.Nullable;
import reactor.adapter.rxjava.RxJava2Adapter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

View File

@@ -77,6 +77,9 @@ final class StreamRules {
* Prefer {@link Arrays#stream(Object[])} over {@link Stream#of(Object[])}, as the former is
* clearer.
*/
// XXX: Introduce a `Matcher` that identifies `Refaster.asVarargs(...)` invocations and annotate
// the `array` parameter as `@NotMatches(IsRefasterAsVarargs.class)`. Then elsewhere
// `@SuppressWarnings("StreamOfArray")` annotations can be dropped.
static final class StreamOfArray<T> {
@BeforeTemplate
Stream<T> before(T[] array) {
@@ -164,12 +167,47 @@ final class StreamRules {
}
}
/**
* Apply {@link Stream#filter(Predicate)} before {@link Stream#sorted()} to reduce the number of
* elements to sort.
*/
static final class StreamFilterSorted<T> {
@BeforeTemplate
Stream<T> before(Stream<T> stream, Predicate<? super T> predicate) {
return stream.sorted().filter(predicate);
}
@AfterTemplate
Stream<T> after(Stream<T> stream, Predicate<? super T> predicate) {
return stream.filter(predicate).sorted();
}
}
/**
* Apply {@link Stream#filter(Predicate)} before {@link Stream#sorted(Comparator)} to reduce the
* number of elements to sort.
*/
static final class StreamFilterSortedWithComparator<T> {
@BeforeTemplate
Stream<T> before(
Stream<T> stream, Predicate<? super T> predicate, Comparator<? super T> comparator) {
return stream.sorted(comparator).filter(predicate);
}
@AfterTemplate
Stream<T> after(
Stream<T> stream, Predicate<? super T> predicate, Comparator<? super T> comparator) {
return stream.filter(predicate).sorted(comparator);
}
}
/**
* Where possible, clarify that a mapping operation will be applied only to a single stream
* element.
*/
// XXX: Consider whether to have a similar rule for `.findAny()`. For parallel streams it
// wouldn't be quite the same....
// XXX: Implement a similar rule for `.findAny()`. For parallel streams this wouldn't be quite the
// same, so such a rule requires a `Matcher` that heuristically identifies `Stream` expressions
// with deterministic order.
// XXX: This change is not equivalent for `null`-returning functions, as the original code throws
// an NPE if the first element is `null`, while the latter yields an empty `Optional`.
static final class StreamMapFirst<T, S> {

View File

@@ -16,7 +16,7 @@ import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/** Refaster rules related to expressions dealing with {@link String}s. */

View File

@@ -28,6 +28,7 @@ import java.util.Set;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.testng.Assert;
import org.testng.Assert.ThrowingRunnable;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/**
* Refaster rules that replace TestNG assertions with equivalent AssertJ assertions.
@@ -72,6 +73,7 @@ import org.testng.Assert.ThrowingRunnable;
// XXX: As-is these rules do not result in a complete migration:
// - Expressions containing comments are skipped due to a limitation of Refaster.
// - Assertions inside lambda expressions are also skipped. Unclear why.
@OnlineDocumentation
final class TestNGToAssertJRules {
private TestNGToAssertJRules() {}

View File

@@ -13,9 +13,11 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.ChronoLocalDateTime;
import java.time.chrono.ChronoZonedDateTime;
@@ -64,7 +66,83 @@ final class TimeRules {
}
}
/** Prefer {@link Instant#atOffset(ZoneOffset)} over the more verbose alternative. */
/** Prefer {@link LocalDate#ofInstant(Instant, ZoneId)} over more indirect alternatives. */
static final class LocalDateOfInstant {
@BeforeTemplate
LocalDate before(Instant instant, ZoneId zoneId) {
return Refaster.anyOf(
instant.atZone(zoneId).toLocalDate(),
LocalDateTime.ofInstant(instant, zoneId).toLocalDate(),
OffsetDateTime.ofInstant(instant, zoneId).toLocalDate());
}
@BeforeTemplate
LocalDate before(Instant instant, ZoneOffset zoneId) {
return instant.atOffset(zoneId).toLocalDate();
}
@AfterTemplate
LocalDate after(Instant instant, ZoneId zoneId) {
return LocalDate.ofInstant(instant, zoneId);
}
}
/** Prefer {@link LocalDateTime#ofInstant(Instant, ZoneId)} over more indirect alternatives. */
static final class LocalDateTimeOfInstant {
@BeforeTemplate
LocalDateTime before(Instant instant, ZoneId zoneId) {
return Refaster.anyOf(
instant.atZone(zoneId).toLocalDateTime(),
OffsetDateTime.ofInstant(instant, zoneId).toLocalDateTime());
}
@BeforeTemplate
LocalDateTime before(Instant instant, ZoneOffset zoneId) {
return instant.atOffset(zoneId).toLocalDateTime();
}
@AfterTemplate
LocalDateTime after(Instant instant, ZoneId zoneId) {
return LocalDateTime.ofInstant(instant, zoneId);
}
}
/** Prefer {@link LocalTime#ofInstant(Instant, ZoneId)} over more indirect alternatives. */
static final class LocalTimeOfInstant {
@BeforeTemplate
LocalTime before(Instant instant, ZoneId zoneId) {
return Refaster.anyOf(
instant.atZone(zoneId).toLocalTime(),
LocalDateTime.ofInstant(instant, zoneId).toLocalTime(),
OffsetDateTime.ofInstant(instant, zoneId).toLocalTime(),
OffsetTime.ofInstant(instant, zoneId).toLocalTime());
}
@BeforeTemplate
LocalTime before(Instant instant, ZoneOffset zoneId) {
return instant.atOffset(zoneId).toLocalTime();
}
@AfterTemplate
LocalTime after(Instant instant, ZoneId zoneId) {
return LocalTime.ofInstant(instant, zoneId);
}
}
/** Prefer {@link OffsetDateTime#ofInstant(Instant, ZoneId)} over more indirect alternatives. */
static final class OffsetDateTimeOfInstant {
@BeforeTemplate
OffsetDateTime before(Instant instant, ZoneId zoneId) {
return instant.atZone(zoneId).toOffsetDateTime();
}
@AfterTemplate
OffsetDateTime after(Instant instant, ZoneId zoneId) {
return OffsetDateTime.ofInstant(instant, zoneId);
}
}
/** Prefer {@link Instant#atOffset(ZoneOffset)} over more verbose alternatives. */
static final class InstantAtOffset {
@BeforeTemplate
OffsetDateTime before(Instant instant, ZoneOffset zoneOffset) {
@@ -77,6 +155,37 @@ final class TimeRules {
}
}
/** Prefer {@link OffsetTime#ofInstant(Instant, ZoneId)} over more indirect alternatives. */
static final class OffsetTimeOfInstant {
@BeforeTemplate
OffsetTime before(Instant instant, ZoneId zoneId) {
return OffsetDateTime.ofInstant(instant, zoneId).toOffsetTime();
}
@BeforeTemplate
OffsetTime before(Instant instant, ZoneOffset zoneId) {
return instant.atOffset(zoneId).toOffsetTime();
}
@AfterTemplate
OffsetTime after(Instant instant, ZoneId zoneId) {
return OffsetTime.ofInstant(instant, zoneId);
}
}
/** Prefer {@link Instant#atZone(ZoneId)} over more verbose alternatives. */
static final class InstantAtZone {
@BeforeTemplate
ZonedDateTime before(Instant instant, ZoneId zoneId) {
return ZonedDateTime.ofInstant(instant, zoneId);
}
@AfterTemplate
ZonedDateTime after(Instant instant, ZoneId zoneId) {
return instant.atZone(zoneId);
}
}
/** Use {@link Clock#systemUTC()} when possible. */
static final class UtcClock {
@BeforeTemplate

View File

@@ -1,4 +1,4 @@
/** Picnic Refaster rules. */
@com.google.errorprone.annotations.CheckReturnValue
@javax.annotation.ParametersAreNonnullByDefault
@org.jspecify.annotations.NullMarked
package tech.picnic.errorprone.refasterrules;

View File

@@ -94,13 +94,11 @@ final class AutowiredConstructorTest {
"",
"interface Container {",
" class A {",
"",
" @Deprecated",
" A() {}",
" }",
"",
" class B {",
"",
" B(String x) {}",
" }",
"}")

View File

@@ -65,6 +65,23 @@ final class CollectorMutabilityTest {
.doTest();
}
@Test
void identificationWithoutGuavaOnClasspath() {
compilationTestHelper
.withClasspath()
.addSourceLines(
"A.java",
"import java.util.stream.Collectors;",
"import java.util.stream.Stream;",
"",
"class A {",
" void m() {",
" Stream.empty().collect(Collectors.toList());",
" }",
"}")
.doTest();
}
@Test
void replacementFirstSuggestedFix() {
refactoringTestHelper

View File

@@ -76,8 +76,18 @@ final class EmptyMethodTest {
" void instanceMethod() {}",
"",
" static void staticMethod() {}",
"",
" static void staticMethodWithComment() {",
" /* Foo. */",
" }",
"}")
.addOutputLines(
"A.java",
"final class A {",
" static void staticMethodWithComment() {",
" /* Foo. */",
" }",
"}")
.addOutputLines("A.java", "final class A {}")
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -4,6 +4,7 @@ import static com.google.errorprone.BugCheckerRefactoringTestHelper.newInstance;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
@@ -33,6 +34,14 @@ final class FluxFlatMapUsageTest {
" Flux.just(1).flatMapSequential(Flux::just);",
" // BUG: Diagnostic contains:",
" Flux.just(1).<String>flatMapSequential(i -> Flux.just(String.valueOf(i)));",
" // BUG: Diagnostic contains:",
" Flux.just(1, 2).groupBy(i -> i).flatMap(Flux::just);",
" // BUG: Diagnostic contains:",
" Flux.just(1, 2).groupBy(i -> i).<String>flatMap(i -> Flux.just(String.valueOf(i)));",
" // BUG: Diagnostic contains:",
" Flux.just(1, 2).groupBy(i -> i).flatMapSequential(Flux::just);",
" // BUG: Diagnostic contains:",
" Flux.just(1, 2).groupBy(i -> i).<String>flatMapSequential(i -> Flux.just(String.valueOf(i)));",
"",
" Mono.just(1).flatMap(Mono::just);",
" Flux.just(1).concatMap(Flux::just);",
@@ -71,9 +80,13 @@ final class FluxFlatMapUsageTest {
"import reactor.core.publisher.Flux;",
"",
"class A {",
" private static final int MAX_CONCURRENCY = 8;",
"",
" void m() {",
" Flux.just(1).flatMap(Flux::just);",
" Flux.just(1).flatMapSequential(Flux::just);",
" Flux.just(1, 2).groupBy(i -> i).flatMap(Flux::just);",
" Flux.just(1, 2).groupBy(i -> i).flatMapSequential(Flux::just);",
" }",
"}")
.addOutputLines(
@@ -81,12 +94,16 @@ final class FluxFlatMapUsageTest {
"import reactor.core.publisher.Flux;",
"",
"class A {",
" private static final int MAX_CONCURRENCY = 8;",
"",
" void m() {",
" Flux.just(1).concatMap(Flux::just);",
" Flux.just(1).concatMap(Flux::just);",
" Flux.just(1, 2).groupBy(i -> i).flatMap(Flux::just, MAX_CONCURRENCY);",
" Flux.just(1, 2).groupBy(i -> i).flatMapSequential(Flux::just, MAX_CONCURRENCY);",
" }",
"}")
.doTest();
.doTest(TestMode.TEXT_MATCH);
}
@Test
@@ -103,6 +120,8 @@ final class FluxFlatMapUsageTest {
" void m() {",
" Flux.just(1).flatMap(Flux::just);",
" Flux.just(1).flatMapSequential(Flux::just);",
" Flux.just(1, 2).groupBy(i -> i).flatMap(Flux::just);",
" Flux.just(1, 2).groupBy(i -> i).flatMapSequential(Flux::just);",
" }",
"}")
.addOutputLines(
@@ -115,8 +134,10 @@ final class FluxFlatMapUsageTest {
" void m() {",
" Flux.just(1).flatMap(Flux::just, MAX_CONCURRENCY);",
" Flux.just(1).flatMapSequential(Flux::just, MAX_CONCURRENCY);",
" Flux.just(1, 2).groupBy(i -> i).concatMap(Flux::just);",
" Flux.just(1, 2).groupBy(i -> i).concatMap(Flux::just);",
" }",
"}")
.doTest();
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -16,7 +16,10 @@ final class IdentityConversionTest {
void identification() {
compilationTestHelper
.addSourceLines(
"Foo.java",
"A.java",
"import static com.google.errorprone.matchers.Matchers.instanceMethod;",
"import static com.google.errorprone.matchers.Matchers.staticMethod;",
"",
"import com.google.common.collect.ImmutableBiMap;",
"import com.google.common.collect.ImmutableList;",
"import com.google.common.collect.ImmutableListMultimap;",
@@ -28,12 +31,14 @@ final class IdentityConversionTest {
"import com.google.common.collect.ImmutableSet;",
"import com.google.common.collect.ImmutableSetMultimap;",
"import com.google.common.collect.ImmutableTable;",
"import com.google.errorprone.matchers.Matcher;",
"import com.google.errorprone.matchers.Matchers;",
"import reactor.adapter.rxjava.RxJava2Adapter;",
"import reactor.core.publisher.Flux;",
"import reactor.core.publisher.Mono;",
"",
"public final class Foo {",
" public void foo() {",
"public final class A {",
" public void m() {",
" // BUG: Diagnostic contains:",
" Boolean b1 = Boolean.valueOf(Boolean.FALSE);",
" // BUG: Diagnostic contains:",
@@ -113,6 +118,13 @@ final class IdentityConversionTest {
" // BUG: Diagnostic contains:",
" short s4 = Short.valueOf(Short.MIN_VALUE);",
"",
" // BUG: Diagnostic contains:",
" String boolStr = Boolean.valueOf(Boolean.FALSE).toString();",
" int boolHash = Boolean.valueOf(false).hashCode();",
" // BUG: Diagnostic contains:",
" int byteHash = Byte.valueOf((Byte) Byte.MIN_VALUE).hashCode();",
" String byteStr = Byte.valueOf(Byte.MIN_VALUE).toString();",
"",
" String str1 = String.valueOf(0);",
" // BUG: Diagnostic contains:",
" String str2 = String.valueOf(\"1\");",
@@ -143,7 +155,15 @@ final class IdentityConversionTest {
" ImmutableTable<Object, Object, Object> o11 = ImmutableTable.copyOf(ImmutableTable.of());",
"",
" // BUG: Diagnostic contains:",
" Matcher allOf1 = Matchers.allOf(instanceMethod());",
" Matcher allOf2 = Matchers.allOf(instanceMethod(), staticMethod());",
" // BUG: Diagnostic contains:",
" Matcher anyOf1 = Matchers.anyOf(staticMethod());",
" Matcher anyOf2 = Matchers.anyOf(instanceMethod(), staticMethod());",
"",
" // BUG: Diagnostic contains:",
" Flux<Integer> flux1 = Flux.just(1).flatMap(e -> RxJava2Adapter.fluxToFlowable(Flux.just(2)));",
"",
" // BUG: Diagnostic contains:",
" Flux<Integer> flux2 = Flux.concat(Flux.just(1));",
" // BUG: Diagnostic contains:",
@@ -154,9 +174,9 @@ final class IdentityConversionTest {
" Flux<Integer> flux5 = Flux.merge(Flux.just(1));",
"",
" // BUG: Diagnostic contains:",
" Mono<Integer> m1 = Mono.from(Mono.just(1));",
" Mono<Integer> mono1 = Mono.from(Mono.just(1));",
" // BUG: Diagnostic contains:",
" Mono<Integer> m2 = Mono.fromDirect(Mono.just(1));",
" Mono<Integer> mono2 = Mono.fromDirect(Mono.just(1));",
" }",
"}")
.doTest();
@@ -167,12 +187,15 @@ final class IdentityConversionTest {
refactoringTestHelper
.setFixChooser(FixChoosers.FIRST)
.addInputLines(
"Foo.java",
"A.java",
"import static com.google.errorprone.matchers.Matchers.staticMethod;",
"import static org.mockito.Mockito.when;",
"",
"import com.google.common.collect.ImmutableCollection;",
"import com.google.common.collect.ImmutableList;",
"import com.google.common.collect.ImmutableSet;",
"import com.google.errorprone.matchers.Matcher;",
"import com.google.errorprone.matchers.Matchers;",
"import java.util.ArrayList;",
"import java.util.Collection;",
"import org.reactivestreams.Publisher;",
@@ -180,8 +203,8 @@ final class IdentityConversionTest {
"import reactor.core.publisher.Flux;",
"import reactor.core.publisher.Mono;",
"",
"public final class Foo {",
" public void foo() {",
"public final class A {",
" public void m() {",
" ImmutableSet<Object> set1 = ImmutableSet.copyOf(ImmutableSet.of());",
" ImmutableSet<Object> set2 = ImmutableSet.copyOf(ImmutableList.of());",
"",
@@ -206,18 +229,23 @@ final class IdentityConversionTest {
" Object o1 = ImmutableSet.copyOf(ImmutableList.of());",
" Object o2 = ImmutableSet.copyOf(ImmutableSet.of());",
"",
" Matcher matcher = Matchers.allOf(staticMethod());",
"",
" when(\"foo\".contains(\"f\")).thenAnswer(inv -> ImmutableSet.copyOf(ImmutableList.of(1)));",
" }",
"",
" void bar(Publisher<Integer> publisher) {}",
"}")
.addOutputLines(
"Foo.java",
"A.java",
"import static com.google.errorprone.matchers.Matchers.staticMethod;",
"import static org.mockito.Mockito.when;",
"",
"import com.google.common.collect.ImmutableCollection;",
"import com.google.common.collect.ImmutableList;",
"import com.google.common.collect.ImmutableSet;",
"import com.google.errorprone.matchers.Matcher;",
"import com.google.errorprone.matchers.Matchers;",
"import java.util.ArrayList;",
"import java.util.Collection;",
"import org.reactivestreams.Publisher;",
@@ -225,8 +253,8 @@ final class IdentityConversionTest {
"import reactor.core.publisher.Flux;",
"import reactor.core.publisher.Mono;",
"",
"public final class Foo {",
" public void foo() {",
"public final class A {",
" public void m() {",
" ImmutableSet<Object> set1 = ImmutableSet.of();",
" ImmutableSet<Object> set2 = ImmutableSet.copyOf(ImmutableList.of());",
"",
@@ -251,6 +279,8 @@ final class IdentityConversionTest {
" Object o1 = ImmutableSet.copyOf(ImmutableList.of());",
" Object o2 = ImmutableSet.of();",
"",
" Matcher matcher = staticMethod();",
"",
" when(\"foo\".contains(\"f\")).thenAnswer(inv -> ImmutableSet.copyOf(ImmutableList.of(1)));",
" }",
"",
@@ -264,14 +294,14 @@ final class IdentityConversionTest {
refactoringTestHelper
.setFixChooser(FixChoosers.SECOND)
.addInputLines(
"Foo.java",
"A.java",
"import com.google.common.collect.ImmutableCollection;",
"import com.google.common.collect.ImmutableList;",
"import com.google.common.collect.ImmutableSet;",
"import java.util.ArrayList;",
"",
"public final class Foo {",
" public void foo() {",
"public final class A {",
" public void m() {",
" ImmutableSet<Object> set1 = ImmutableSet.copyOf(ImmutableSet.of());",
" ImmutableSet<Object> set2 = ImmutableSet.copyOf(ImmutableList.of());",
"",
@@ -280,14 +310,14 @@ final class IdentityConversionTest {
" }",
"}")
.addOutputLines(
"Foo.java",
"A.java",
"import com.google.common.collect.ImmutableCollection;",
"import com.google.common.collect.ImmutableList;",
"import com.google.common.collect.ImmutableSet;",
"import java.util.ArrayList;",
"",
"public final class Foo {",
" public void foo() {",
"public final class A {",
" public void m() {",
" @SuppressWarnings(\"IdentityConversion\")",
" ImmutableSet<Object> set1 = ImmutableSet.copyOf(ImmutableSet.of());",
" ImmutableSet<Object> set2 = ImmutableSet.copyOf(ImmutableList.of());",

View File

@@ -0,0 +1,70 @@
package tech.picnic.errorprone.bugpatterns;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class IsInstanceLambdaUsageTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(IsInstanceLambdaUsage.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(IsInstanceLambdaUsage.class, getClass());
@Test
void identification() {
compilationTestHelper
.addSourceLines(
"A.java",
"import java.util.stream.Stream;",
"import reactor.core.publisher.Flux;",
"",
"class A {",
" void m() {",
" Integer localVariable = 0;",
"",
" Stream.of(0).map(i -> i + 1);",
" Stream.of(1).filter(Integer.class::isInstance);",
" Stream.of(2).filter(i -> i.getClass() instanceof Class);",
" Stream.of(3).filter(i -> localVariable instanceof Integer);",
" // XXX: Ideally this case is also flagged. Pick this up in the context of merging the",
" // `IsInstanceLambdaUsage` and `MethodReferenceUsage` checks, or introduce a separate check that",
" // simplifies unnecessary block lambda expressions.",
" Stream.of(4)",
" .filter(",
" i -> {",
" return localVariable instanceof Integer;",
" });",
" Flux.just(5, \"foo\").distinctUntilChanged(v -> v, (a, b) -> a instanceof Integer);",
"",
" // BUG: Diagnostic contains:",
" Stream.of(6).filter(i -> i instanceof Integer);",
" }",
"}")
.doTest();
}
@Test
void replacement() {
refactoringTestHelper
.addInputLines(
"A.java",
"import java.util.stream.Stream;",
"",
"class A {",
" void m() {",
" Stream.of(1).filter(i -> i instanceof Integer);",
" }",
"}")
.addOutputLines(
"A.java",
"import java.util.stream.Stream;",
"",
"class A {",
" void m() {",
" Stream.of(1).filter(Integer.class::isInstance);",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -0,0 +1,134 @@
package tech.picnic.errorprone.bugpatterns;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class JUnitClassModifiersTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(JUnitClassModifiers.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(JUnitClassModifiers.class, getClass());
@Test
void identification() {
compilationTestHelper
.addSourceLines(
"Container.java",
"import org.junit.jupiter.api.Test;",
"import org.junit.jupiter.params.ParameterizedTest;",
"import org.springframework.boot.test.context.TestConfiguration;",
"import org.springframework.context.annotation.Configuration;",
"",
"class Container {",
" final class FinalAndPackagePrivate {",
" @Test",
" void foo() {}",
" }",
"",
" final class FinalAndPackagePrivateWithCustomTestMethod {",
" @ParameterizedTest",
" void foo() {}",
" }",
"",
" public abstract class Abstract {",
" @Test",
" void foo() {}",
" }",
"",
" @Configuration",
" class WithConfigurationAnnotation {",
" @Test",
" void foo() {}",
" }",
"",
" @TestConfiguration",
" class WithConfigurationMetaAnnotation {",
" @Test",
" void foo() {}",
" }",
"",
" // BUG: Diagnostic contains:",
" private final class Private {",
" @Test",
" void foo() {}",
" }",
"",
" // BUG: Diagnostic contains:",
" protected final class Protected {",
" @Test",
" void foo() {}",
" }",
"",
" // BUG: Diagnostic contains:",
" public final class Public {",
" @Test",
" void foo() {}",
" }",
"",
" // BUG: Diagnostic contains:",
" class NonFinal {",
" @Test",
" void foo() {}",
" }",
"",
" // BUG: Diagnostic contains:",
" class NonFinalWithCustomTestMethod {",
" @ParameterizedTest",
" void foo() {}",
" }",
"",
" @Configuration",
" // BUG: Diagnostic contains:",
" public class PublicWithConfigurationAnnotation {",
" @Test",
" void foo() {}",
" }",
"",
" @TestConfiguration",
" // BUG: Diagnostic contains:",
" protected class ProtectedWithConfigurationMetaAnnotation {",
" @Test",
" void foo() {}",
" }",
"}")
.doTest();
}
@Test
void replacement() {
refactoringTestHelper
.addInputLines(
"A.java",
"import org.junit.jupiter.api.Test;",
"import org.springframework.context.annotation.Configuration;",
"",
"public class A {",
" @Test",
" void foo() {}",
"",
" @Configuration",
" private static class B {",
" @Test",
" void bar() {}",
" }",
"}")
.addOutputLines(
"A.java",
"import org.junit.jupiter.api.Test;",
"import org.springframework.context.annotation.Configuration;",
"",
"final class A {",
" @Test",
" void foo() {}",
"",
" @Configuration",
" static class B {",
" @Test",
" void bar() {}",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -91,6 +91,9 @@ final class JUnitMethodDeclarationTest {
" private void tearDown8() {}",
"",
" @Test",
" void test() {}",
"",
" @Test",
" void method1() {}",
"",
" @Test",
@@ -144,8 +147,13 @@ final class JUnitMethodDeclarationTest {
" void test5() {}",
"",
" @Test",
" // BUG: Diagnostic contains: (but note that a method named `overload` already exists in this",
" // class)",
" // BUG: Diagnostic contains: (but note that a method named `toString` is already defined in this",
" // class or a supertype)",
" void testToString() {}",
"",
" @Test",
" // BUG: Diagnostic contains: (but note that a method named `overload` is already defined in this",
" // class or a supertype)",
" void testOverload() {}",
"",
" void overload() {}",
@@ -155,8 +163,20 @@ final class JUnitMethodDeclarationTest {
" void testArguments() {}",
"",
" @Test",
" // BUG: Diagnostic contains: (but note that `public` is a reserved keyword)",
" // BUG: Diagnostic contains: (but note that `public` is not a valid identifier)",
" void testPublic() {}",
"",
" @Test",
" // BUG: Diagnostic contains: (but note that `null` is not a valid identifier)",
" void testNull() {}",
"",
" @Test",
" // BUG: Diagnostic contains:",
" void testRecord() {}",
"",
" @Test",
" // BUG: Diagnostic contains:",
" void testMethodThatIsOverriddenWithoutOverrideAnnotation() {}",
"}")
.addSourceLines(
"B.java",
@@ -218,6 +238,10 @@ final class JUnitMethodDeclarationTest {
"",
" @Override",
" @Test",
" void test() {}",
"",
" @Override",
" @Test",
" void method1() {}",
"",
" @Override",
@@ -267,6 +291,10 @@ final class JUnitMethodDeclarationTest {
"",
" @Override",
" @Test",
" void testToString() {}",
"",
" @Override",
" @Test",
" void testOverload() {}",
"",
" @Override",
@@ -279,6 +307,17 @@ final class JUnitMethodDeclarationTest {
" @Override",
" @Test",
" void testPublic() {}",
"",
" @Override",
" @Test",
" void testNull() {}",
"",
" @Override",
" @Test",
" void testRecord() {}",
"",
" @Test",
" void testMethodThatIsOverriddenWithoutOverrideAnnotation() {}",
"}")
.addSourceLines(
"C.java",
@@ -352,6 +391,9 @@ final class JUnitMethodDeclarationTest {
" protected void quux() {}",
"",
" @Test",
" public void testToString() {}",
"",
" @Test",
" public void testOverload() {}",
"",
" void overload() {}",
@@ -361,6 +403,9 @@ final class JUnitMethodDeclarationTest {
"",
" @Test",
" private void testClass() {}",
"",
" @Test",
" private void testTrue() {}",
"}")
.addOutputLines(
"A.java",
@@ -407,6 +452,9 @@ final class JUnitMethodDeclarationTest {
" void quux() {}",
"",
" @Test",
" void testToString() {}",
"",
" @Test",
" void testOverload() {}",
"",
" void overload() {}",
@@ -416,6 +464,9 @@ final class JUnitMethodDeclarationTest {
"",
" @Test",
" void testClass() {}",
"",
" @Test",
" void testTrue() {}",
"}")
.doTest(TestMode.TEXT_MATCH);
}

View File

@@ -1,7 +1,5 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.common.base.Predicates.containsPattern;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
@@ -9,9 +7,7 @@ import org.junit.jupiter.api.Test;
final class LexicographicalAnnotationListingTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(LexicographicalAnnotationListing.class, getClass())
.expectErrorMessage(
"X", containsPattern("Sort annotations lexicographically where possible"));
CompilationTestHelper.newInstance(LexicographicalAnnotationListing.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(
LexicographicalAnnotationListing.class, getClass());
@@ -21,7 +17,9 @@ final class LexicographicalAnnotationListingTest {
compilationTestHelper
.addSourceLines(
"A.java",
"import java.lang.annotation.ElementType;",
"import java.lang.annotation.Repeatable;",
"import java.lang.annotation.Target;",
"",
"interface A {",
" @Repeatable(Foos.class)",
@@ -33,6 +31,7 @@ final class LexicographicalAnnotationListingTest {
" Bar[] anns() default {};",
" }",
"",
" @Target(ElementType.METHOD)",
" @interface Bar {",
" String[] value() default {};",
" }",
@@ -45,11 +44,21 @@ final class LexicographicalAnnotationListingTest {
" Foo[] value();",
" }",
"",
" // BUG: Diagnostic matches: X",
" @Target(ElementType.TYPE_USE)",
" @interface FooTypeUse {",
" String[] value() default {};",
" }",
"",
" @Target(ElementType.TYPE_USE)",
" @interface BarTypeUse {",
" String[] value() default {};",
" }",
"",
" // BUG: Diagnostic contains:",
" @Foo",
" @Bar",
" A unsortedSimpleCase();",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" @Foo()",
" @Bar()",
" A unsortedWithParens();",
@@ -61,12 +70,12 @@ final class LexicographicalAnnotationListingTest {
" @Foo()",
" A sortedAnnotationsOneWithParens();",
"",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" @Foo",
" @Baz",
" @Bar",
" A threeUnsortedAnnotationsSameInitialLetter();",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" @Bar",
" @Foo()",
" @Baz",
@@ -77,16 +86,16 @@ final class LexicographicalAnnotationListingTest {
" @Foo()",
" A threeSortedAnnotations();",
"",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" @Foo({\"b\"})",
" @Bar({\"a\"})",
" A unsortedWithStringAttributes();",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" @Baz(str = {\"a\", \"b\"})",
" @Foo(ints = {1, 0})",
" @Bar",
" A unsortedWithAttributes();",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" @Bar",
" @Foo(anns = {@Bar(\"b\"), @Bar(\"a\")})",
" @Baz",
@@ -101,11 +110,23 @@ final class LexicographicalAnnotationListingTest {
" @Foo(ints = {1, 2})",
" @Foo({\"b\"})",
" A sortedRepeatableAnnotation();",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" @Foo(anns = {@Bar(\"b\"), @Bar(\"a\")})",
" @Bar",
" @Foo(ints = {1, 2})",
" A unsortedRepeatableAnnotation();",
"",
" // BUG: Diagnostic contains:",
" default @FooTypeUse @BarTypeUse A unsortedTypeAnnotations() {",
" return null;",
" }",
"",
" // BUG: Diagnostic contains:",
" @Baz",
" @Bar",
" default @FooTypeUse @BarTypeUse A unsortedTypeUseAndOtherAnnotations() {",
" return null;",
" }",
"}")
.doTest();
}
@@ -115,7 +136,9 @@ final class LexicographicalAnnotationListingTest {
refactoringTestHelper
.addInputLines(
"A.java",
"import java.lang.annotation.ElementType;",
"import java.lang.annotation.Repeatable;",
"import java.lang.annotation.Target;",
"",
"interface A {",
" @Repeatable(Foos.class)",
@@ -127,6 +150,7 @@ final class LexicographicalAnnotationListingTest {
" Bar[] anns() default {};",
" }",
"",
" @Target(ElementType.METHOD)",
" @interface Bar {",
" String[] value() default {};",
" }",
@@ -139,6 +163,16 @@ final class LexicographicalAnnotationListingTest {
" Foo[] value();",
" }",
"",
" @Target(ElementType.TYPE_USE)",
" @interface FooTypeUse {",
" String[] value() default {};",
" }",
"",
" @Target(ElementType.TYPE_USE)",
" @interface BarTypeUse {",
" String[] value() default {};",
" }",
"",
" @Bar",
" A singleAnnotation();",
"",
@@ -174,10 +208,18 @@ final class LexicographicalAnnotationListingTest {
" @Bar",
" @Foo(ints = {1, 2})",
" A unsortedRepeatableAnnotation();",
"",
" @Baz",
" @Bar",
" default @FooTypeUse @BarTypeUse A unsortedWithTypeUseAnnotations() {",
" return null;",
" }",
"}")
.addOutputLines(
"A.java",
"import java.lang.annotation.ElementType;",
"import java.lang.annotation.Repeatable;",
"import java.lang.annotation.Target;",
"",
"interface A {",
" @Repeatable(Foos.class)",
@@ -189,6 +231,7 @@ final class LexicographicalAnnotationListingTest {
" Bar[] anns() default {};",
" }",
"",
" @Target(ElementType.METHOD)",
" @interface Bar {",
" String[] value() default {};",
" }",
@@ -201,6 +244,16 @@ final class LexicographicalAnnotationListingTest {
" Foo[] value();",
" }",
"",
" @Target(ElementType.TYPE_USE)",
" @interface FooTypeUse {",
" String[] value() default {};",
" }",
"",
" @Target(ElementType.TYPE_USE)",
" @interface BarTypeUse {",
" String[] value() default {};",
" }",
"",
" @Bar",
" A singleAnnotation();",
"",
@@ -236,6 +289,12 @@ final class LexicographicalAnnotationListingTest {
" @Foo(anns = {@Bar(\"b\"), @Bar(\"a\")})",
" @Foo(ints = {1, 2})",
" A unsortedRepeatableAnnotation();",
"",
" @Bar",
" @Baz",
" default @BarTypeUse @FooTypeUse A unsortedWithTypeUseAnnotations() {",
" return null;",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}

View File

@@ -39,4 +39,19 @@ final class NestedOptionalsTest {
"}")
.doTest();
}
@Test
void identificationOptionalTypeNotLoaded() {
compilationTestHelper
.addSourceLines(
"A.java",
"import java.time.Duration;",
"",
"class A {",
" void m() {",
" Duration.ofSeconds(1);",
" }",
"}")
.doTest();
}
}

View File

@@ -6,6 +6,10 @@ import org.junit.jupiter.api.Test;
final class RequestParamTypeTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(RequestParamType.class, getClass());
private final CompilationTestHelper restrictedCompilationTestHelper =
CompilationTestHelper.newInstance(RequestParamType.class, getClass())
.setArgs(
"-XepOpt:RequestParamType:SupportedCustomTypes=com.google.common.collect.ImmutableSet,com.google.common.collect.ImmutableSortedMultiset");
@Test
void identification() {
@@ -19,7 +23,7 @@ final class RequestParamTypeTest {
"import java.util.List;",
"import java.util.Map;",
"import java.util.Set;",
"import javax.annotation.Nullable;",
"import org.jspecify.annotations.Nullable;",
"import org.springframework.web.bind.annotation.DeleteMapping;",
"import org.springframework.web.bind.annotation.GetMapping;",
"import org.springframework.web.bind.annotation.PostMapping;",
@@ -63,4 +67,53 @@ final class RequestParamTypeTest {
"}")
.doTest();
}
@Test
void identificationRestricted() {
restrictedCompilationTestHelper
.addSourceLines(
"A.java",
"import com.google.common.collect.ImmutableBiMap;",
"import com.google.common.collect.ImmutableCollection;",
"import com.google.common.collect.ImmutableList;",
"import com.google.common.collect.ImmutableMap;",
"import com.google.common.collect.ImmutableMultiset;",
"import com.google.common.collect.ImmutableSet;",
"import com.google.common.collect.ImmutableSortedMultiset;",
"import com.google.common.collect.ImmutableSortedSet;",
"import org.springframework.web.bind.annotation.GetMapping;",
"import org.springframework.web.bind.annotation.RequestParam;",
"",
"interface A {",
" @GetMapping",
" // BUG: Diagnostic contains:",
" A immutableCollection(@RequestParam ImmutableCollection<String> param);",
"",
" @GetMapping",
" // BUG: Diagnostic contains:",
" A immutableList(@RequestParam ImmutableList<String> param);",
"",
" @GetMapping",
" A immutableSet(@RequestParam ImmutableSet<String> param);",
"",
" @GetMapping",
" A immutableSortedSet(@RequestParam ImmutableSortedSet<String> param);",
"",
" @GetMapping",
" // BUG: Diagnostic contains:",
" A immutableMultiset(@RequestParam ImmutableMultiset<String> param);",
"",
" @GetMapping",
" A immutableSortedMultiset(@RequestParam ImmutableSortedMultiset<String> param);",
"",
" @GetMapping",
" // BUG: Diagnostic contains:",
" A immutableMap(@RequestParam ImmutableMap<String, String> param);",
"",
" @GetMapping",
" // BUG: Diagnostic contains:",
" A immutableBiMap(@RequestParam ImmutableBiMap<String, String> param);",
"}")
.doTest();
}
}

View File

@@ -4,6 +4,7 @@ import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
import org.springframework.scheduling.annotation.Scheduled;
final class ScheduledTransactionTraceTest {
private final CompilationTestHelper compilationTestHelper =
@@ -43,6 +44,21 @@ final class ScheduledTransactionTraceTest {
.doTest();
}
@Test
void identificationWithoutNewRelicAgentApiOnClasspath() {
compilationTestHelper
.withClasspath(Scheduled.class)
.addSourceLines(
"A.java",
"import org.springframework.scheduling.annotation.Scheduled;",
"",
"class A {",
" @Scheduled(fixedDelay = 1)",
" void scheduledButNotTraced() {}",
"}")
.doTest();
}
@Test
void replacement() {
refactoringTestHelper

View File

@@ -0,0 +1,132 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugCheckerRefactoringTestHelper.newInstance;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class StringCaseLocaleUsageTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(StringCaseLocaleUsage.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
newInstance(StringCaseLocaleUsage.class, getClass());
@Test
void identification() {
compilationTestHelper
.addSourceLines(
"A.java",
"import static java.util.Locale.ROOT;",
"",
"import java.util.Locale;",
"",
"class A {",
" void m() {",
" \"a\".toLowerCase(Locale.ROOT);",
" \"a\".toUpperCase(Locale.ROOT);",
" \"b\".toLowerCase(ROOT);",
" \"b\".toUpperCase(ROOT);",
" \"c\".toLowerCase(Locale.getDefault());",
" \"c\".toUpperCase(Locale.getDefault());",
" \"d\".toLowerCase(Locale.ENGLISH);",
" \"d\".toUpperCase(Locale.ENGLISH);",
" \"e\".toLowerCase(new Locale(\"foo\"));",
" \"e\".toUpperCase(new Locale(\"foo\"));",
"",
" // BUG: Diagnostic contains:",
" \"f\".toLowerCase();",
" // BUG: Diagnostic contains:",
" \"g\".toUpperCase();",
"",
" String h = \"h\";",
" // BUG: Diagnostic contains:",
" h.toLowerCase();",
" String i = \"i\";",
" // BUG: Diagnostic contains:",
" i.toUpperCase();",
" }",
"}")
.doTest();
}
@Test
void replacementFirstSuggestedFix() {
refactoringTestHelper
.setFixChooser(FixChoosers.FIRST)
.addInputLines(
"A.java",
"class A {",
" void m() {",
" \"a\".toLowerCase(/* Comment with parens: (). */ );",
" \"b\".toUpperCase();",
" \"c\".toLowerCase().toString();",
"",
" toString().toLowerCase();",
" toString().toUpperCase /* Comment with parens: (). */();",
"",
" this.toString().toLowerCase() /* Comment with parens: (). */;",
" this.toString().toUpperCase();",
" }",
"}")
.addOutputLines(
"A.java",
"import java.util.Locale;",
"",
"class A {",
" void m() {",
" \"a\".toLowerCase(/* Comment with parens: (). */ Locale.ROOT);",
" \"b\".toUpperCase(Locale.ROOT);",
" \"c\".toLowerCase(Locale.ROOT).toString();",
"",
" toString().toLowerCase(Locale.ROOT);",
" toString().toUpperCase /* Comment with parens: (). */(Locale.ROOT);",
"",
" this.toString().toLowerCase(Locale.ROOT) /* Comment with parens: (). */;",
" this.toString().toUpperCase(Locale.ROOT);",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
@Test
void replacementSecondSuggestedFix() {
refactoringTestHelper
.setFixChooser(FixChoosers.SECOND)
.addInputLines(
"A.java",
"class A {",
" void m() {",
" \"a\".toLowerCase();",
" \"b\".toUpperCase(/* Comment with parens: (). */ );",
" \"c\".toLowerCase().toString();",
"",
" toString().toLowerCase();",
" toString().toUpperCase /* Comment with parens: (). */();",
"",
" this.toString().toLowerCase() /* Comment with parens: (). */;",
" this.toString().toUpperCase();",
" }",
"}")
.addOutputLines(
"A.java",
"import java.util.Locale;",
"",
"class A {",
" void m() {",
" \"a\".toLowerCase(Locale.getDefault());",
" \"b\".toUpperCase(/* Comment with parens: (). */ Locale.getDefault());",
" \"c\".toLowerCase(Locale.getDefault()).toString();",
"",
" toString().toLowerCase(Locale.getDefault());",
" toString().toUpperCase /* Comment with parens: (). */(Locale.getDefault());",
"",
" this.toString().toLowerCase(Locale.getDefault()) /* Comment with parens: (). */;",
" this.toString().toUpperCase(Locale.getDefault());",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -1,17 +1,11 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.common.base.Predicates.containsPattern;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class TimeZoneUsageTest {
private final CompilationTestHelper compilationHelper =
CompilationTestHelper.newInstance(TimeZoneUsage.class, getClass())
.expectErrorMessage(
"X",
containsPattern(
"Derive the current time from an existing `Clock` Spring bean, and don't rely on a `Clock`'s time zone"));
CompilationTestHelper.newInstance(TimeZoneUsage.class, getClass());
@Test
void identification() {
@@ -26,7 +20,10 @@ final class TimeZoneUsageTest {
"import java.time.LocalDate;",
"import java.time.LocalDateTime;",
"import java.time.LocalTime;",
"import java.time.OffsetDateTime;",
"import java.time.OffsetTime;",
"import java.time.ZoneId;",
"import java.time.ZonedDateTime;",
"",
"class A {",
" void m() {",
@@ -36,48 +33,69 @@ final class TimeZoneUsageTest {
" Clock.offset(clock, Duration.ZERO);",
" Clock.tick(clock, Duration.ZERO);",
"",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" Clock.systemUTC();",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" Clock.systemDefaultZone();",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" Clock.system(UTC);",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" Clock.tickMillis(UTC);",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" Clock.tickMinutes(UTC);",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" Clock.tickSeconds(UTC);",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" clock.getZone();",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" clock.withZone(UTC);",
"",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" Instant.now();",
" // This is equivalent to `clock.instant()`, which is fine.",
" Instant.now(clock);",
"",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" LocalDate.now();",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" LocalDate.now(clock);",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" LocalDate.now(UTC);",
"",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" LocalDateTime.now();",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" LocalDateTime.now(clock);",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" LocalDateTime.now(UTC);",
"",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" LocalTime.now();",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" LocalTime.now(clock);",
" // BUG: Diagnostic matches: X",
" // BUG: Diagnostic contains:",
" LocalTime.now(UTC);",
"",
" // BUG: Diagnostic contains:",
" OffsetDateTime.now();",
" // BUG: Diagnostic contains:",
" OffsetDateTime.now(clock);",
" // BUG: Diagnostic contains:",
" OffsetDateTime.now(UTC);",
"",
" // BUG: Diagnostic contains:",
" OffsetTime.now();",
" // BUG: Diagnostic contains:",
" OffsetTime.now(clock);",
" // BUG: Diagnostic contains:",
" OffsetTime.now(UTC);",
"",
" // BUG: Diagnostic contains:",
" ZonedDateTime.now();",
" // BUG: Diagnostic contains:",
" ZonedDateTime.now(clock);",
" // BUG: Diagnostic contains:",
" ZonedDateTime.now(UTC);",
" }",
"",
" abstract class ForwardingClock extends Clock {",

View File

@@ -0,0 +1,31 @@
package tech.picnic.errorprone.bugpatterns.util;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.ErrorProneOptions;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
final class FlagsTest {
private static Stream<Arguments> getListTestCases() {
/* { args, flag, expected } */
return Stream.of(
arguments(ImmutableList.of(), "Foo", ImmutableList.of()),
arguments(ImmutableList.of("-XepOpt:Foo=bar,baz"), "Qux", ImmutableList.of()),
arguments(ImmutableList.of("-XepOpt:Foo="), "Foo", ImmutableList.of()),
arguments(ImmutableList.of("-XepOpt:Foo=bar"), "Foo", ImmutableList.of("bar")),
arguments(ImmutableList.of("-XepOpt:Foo=bar,baz"), "Foo", ImmutableList.of("bar", "baz")),
arguments(ImmutableList.of("-XepOpt:Foo=,"), "Foo", ImmutableList.of("", "")));
}
@MethodSource("getListTestCases")
@ParameterizedTest
void getList(ImmutableList<String> args, String flag, ImmutableList<String> expected) {
assertThat(Flags.getList(ErrorProneOptions.processArgs(args).getFlags(), flag))
.containsExactlyElementsOf(expected);
}
}

View File

@@ -0,0 +1,34 @@
package tech.picnic.errorprone.bugpatterns.util;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
final class JavaKeywordsTest {
private static Stream<Arguments> isValidIdentifierTestCases() {
/* { str, expected } */
return Stream.of(
arguments("", false),
arguments("public", false),
arguments("true", false),
arguments("false", false),
arguments("null", false),
arguments("0", false),
arguments("\0", false),
arguments("a%\0", false),
arguments("a", true),
arguments("a0", true),
arguments("_a0", true),
arguments("test", true));
}
@MethodSource("isValidIdentifierTestCases")
@ParameterizedTest
void isValidIdentifier(String str, boolean expected) {
assertThat(JavaKeywords.isValidIdentifier(str)).isEqualTo(expected);
}
}

View File

@@ -0,0 +1,110 @@
package tech.picnic.errorprone.bugpatterns.util;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.errorprone.BugPattern;
import com.google.errorprone.CompilationTestHelper;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.MethodTree;
import java.util.function.BiFunction;
import org.junit.jupiter.api.Test;
final class MoreASTHelpersTest {
@Test
void findMethods() {
CompilationTestHelper.newInstance(FindMethodsTestChecker.class, getClass())
.addSourceLines(
"A.java",
"class A {",
" // BUG: Diagnostic contains: {foo=1, bar=2, baz=0}",
" void foo() {}",
"",
" // BUG: Diagnostic contains: {foo=1, bar=2, baz=0}",
" void bar() {}",
"",
" // BUG: Diagnostic contains: {foo=1, bar=2, baz=0}",
" void bar(int i) {}",
"",
" static class B {",
" // BUG: Diagnostic contains: {foo=0, bar=1, baz=1}",
" void bar() {}",
"",
" // BUG: Diagnostic contains: {foo=0, bar=1, baz=1}",
" void baz() {}",
" }",
"}")
.doTest();
}
@Test
void methodExistsInEnclosingClass() {
CompilationTestHelper.newInstance(MethodExistsTestChecker.class, getClass())
.addSourceLines(
"A.java",
"class A {",
" // BUG: Diagnostic contains: {foo=true, bar=true, baz=false}",
" void foo() {}",
"",
" // BUG: Diagnostic contains: {foo=true, bar=true, baz=false}",
" void bar() {}",
"",
" // BUG: Diagnostic contains: {foo=true, bar=true, baz=false}",
" void bar(int i) {}",
"",
" static class B {",
" // BUG: Diagnostic contains: {foo=false, bar=true, baz=true}",
" void bar() {}",
"",
" // BUG: Diagnostic contains: {foo=false, bar=true, baz=true}",
" void baz() {}",
" }",
"}")
.doTest();
}
private static String createDiagnosticsMessage(
BiFunction<String, VisitorState, Object> valueFunction, VisitorState state) {
return Maps.toMap(ImmutableSet.of("foo", "bar", "baz"), key -> valueFunction.apply(key, state))
.toString();
}
/**
* A {@link BugChecker} that delegates to {@link MoreASTHelpers#findMethods(CharSequence,
* VisitorState)}.
*/
@BugPattern(summary = "Interacts with `MoreASTHelpers` for testing purposes", severity = ERROR)
public static final class FindMethodsTestChecker extends BugChecker implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
return buildDescription(tree)
.setMessage(
createDiagnosticsMessage(
(methodName, s) -> MoreASTHelpers.findMethods(methodName, s).size(), state))
.build();
}
}
/**
* A {@link BugChecker} that delegates to {@link
* MoreASTHelpers#methodExistsInEnclosingClass(CharSequence, VisitorState)}.
*/
@BugPattern(summary = "Interacts with `MoreASTHelpers` for testing purposes", severity = ERROR)
public static final class MethodExistsTestChecker extends BugChecker
implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
return buildDescription(tree)
.setMessage(createDiagnosticsMessage(MoreASTHelpers::methodExistsInEnclosingClass, state))
.build();
}
}
}

View File

@@ -0,0 +1,173 @@
package tech.picnic.errorprone.bugpatterns.util;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.HAS_METHOD_SOURCE;
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.SETUP_OR_TEARDOWN_METHOD;
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.TEST_METHOD;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
import com.google.errorprone.CompilationTestHelper;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.MethodTree;
import java.util.Map;
import org.junit.jupiter.api.Test;
final class MoreJUnitMatchersTest {
@Test
void methodMatchers() {
CompilationTestHelper.newInstance(MethodMatchersTestChecker.class, getClass())
.addSourceLines(
"A.java",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import java.util.stream.Stream;",
"import org.junit.jupiter.api.AfterAll;",
"import org.junit.jupiter.api.AfterEach;",
"import org.junit.jupiter.api.BeforeAll;",
"import org.junit.jupiter.api.BeforeEach;",
"import org.junit.jupiter.api.RepeatedTest;",
"import org.junit.jupiter.api.Test;",
"import org.junit.jupiter.params.ParameterizedTest;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.junit.jupiter.params.provider.MethodSource;",
"",
"class A {",
" @BeforeAll",
" // BUG: Diagnostic contains: SETUP_OR_TEARDOWN_METHOD",
" public void beforeAll() {}",
"",
" @BeforeEach",
" @Test",
" // BUG: Diagnostic contains: TEST_METHOD, SETUP_OR_TEARDOWN_METHOD",
" protected void beforeEachAndTest() {}",
"",
" @AfterEach",
" // BUG: Diagnostic contains: SETUP_OR_TEARDOWN_METHOD",
" private void afterEach() {}",
"",
" @AfterAll",
" // BUG: Diagnostic contains: SETUP_OR_TEARDOWN_METHOD",
" private void afterAll() {}",
"",
" @Test",
" // BUG: Diagnostic contains: TEST_METHOD",
" void test() {}",
"",
" private static Stream<Arguments> booleanArgs() {",
" return Stream.of(arguments(false), arguments(true));",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"booleanArgs\")",
" // BUG: Diagnostic contains: TEST_METHOD, HAS_METHOD_SOURCE",
" void parameterizedTest(boolean b) {}",
"",
" @RepeatedTest(2)",
" // BUG: Diagnostic contains: TEST_METHOD",
" private void repeatedTest() {}",
"",
" private void unannotatedMethod() {}",
"}")
.doTest();
}
@Test
void getMethodSourceFactoryNames() {
CompilationTestHelper.newInstance(MethodSourceFactoryNamesTestChecker.class, getClass())
.addSourceLines(
"A.java",
"import org.junit.jupiter.params.provider.MethodSource;",
"",
"class A {",
" @MethodSource",
" // BUG: Diagnostic contains: [matchingMethodSource]",
" void matchingMethodSource(boolean b) {}",
"",
" @MethodSource()",
" // BUG: Diagnostic contains: [matchingMethodSourceWithParens]",
" void matchingMethodSourceWithParens(boolean b) {}",
"",
" @MethodSource(\"\")",
" // BUG: Diagnostic contains: [matchingMethodSourceMadeExplicit]",
" void matchingMethodSourceMadeExplicit(boolean b) {}",
"",
" @MethodSource({\"\"})",
" // BUG: Diagnostic contains: [matchingMethodSourceMadeExplicitWithParens]",
" void matchingMethodSourceMadeExplicitWithParens(boolean b) {}",
"",
" @MethodSource({})",
" // BUG: Diagnostic contains: []",
" void noMethodSources(boolean b) {}",
"",
" @MethodSource(\"myValueFactory\")",
" // BUG: Diagnostic contains: [myValueFactory]",
" void singleCustomMethodSource(boolean b) {}",
"",
" @MethodSource({\"firstValueFactory\", \"secondValueFactory\"})",
" // BUG: Diagnostic contains: [firstValueFactory, secondValueFactory]",
" void twoCustomMethodSources(boolean b) {}",
"",
" @MethodSource({\"myValueFactory\", \"\"})",
" // BUG: Diagnostic contains: [myValueFactory, customAndMatchingMethodSources]",
" void customAndMatchingMethodSources(boolean b) {}",
"}")
.doTest();
}
/**
* A {@link BugChecker} that flags methods matched by {@link Matcher}s of {@link MethodTree}s
* exposed by {@link MoreJUnitMatchers}.
*/
@BugPattern(summary = "Interacts with `MoreJUnitMatchers` for testing purposes", severity = ERROR)
public static final class MethodMatchersTestChecker extends BugChecker
implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
private static final ImmutableMap<String, Matcher<MethodTree>> METHOD_MATCHERS =
ImmutableMap.of(
"TEST_METHOD", TEST_METHOD,
"HAS_METHOD_SOURCE", HAS_METHOD_SOURCE,
"SETUP_OR_TEARDOWN_METHOD", SETUP_OR_TEARDOWN_METHOD);
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
ImmutableSet<String> matches =
METHOD_MATCHERS.entrySet().stream()
.filter(e -> e.getValue().matches(tree, state))
.map(Map.Entry::getKey)
.collect(toImmutableSet());
return matches.isEmpty()
? Description.NO_MATCH
: buildDescription(tree).setMessage(String.join(", ", matches)).build();
}
}
/**
* A {@link BugChecker} that flags methods with a JUnit {@code @MethodSource} annotation by
* enumerating the associated value factory method names.
*/
@BugPattern(summary = "Interacts with `MoreJUnitMatchers` for testing purposes", severity = ERROR)
public static final class MethodSourceFactoryNamesTestChecker extends BugChecker
implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
AnnotationTree annotation =
Iterables.getOnlyElement(HAS_METHOD_SOURCE.multiMatchResult(tree, state).matchingNodes());
return buildDescription(tree)
.setMessage(MoreJUnitMatchers.getMethodSourceFactoryNames(annotation, tree).toString())
.build();
}
}
}

View File

@@ -0,0 +1,62 @@
package tech.picnic.errorprone.bugpatterns.util;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import com.google.errorprone.BugPattern;
import com.google.errorprone.CompilationTestHelper;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.AnnotationTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.sun.source.tree.AnnotationTree;
import org.junit.jupiter.api.Test;
final class MoreMatchersTest {
@Test
void hasMetaAnnotation() {
CompilationTestHelper.newInstance(TestMatcher.class, getClass())
.addSourceLines(
"A.java",
"import org.junit.jupiter.api.AfterAll;",
"import org.junit.jupiter.api.RepeatedTest;",
"import org.junit.jupiter.api.Test;",
"import org.junit.jupiter.api.TestTemplate;",
"import org.junit.jupiter.params.ParameterizedTest;",
"",
"class A {",
" void negative1() {}",
"",
" @Test",
" void negative2() {}",
"",
" @AfterAll",
" void negative3() {}",
"",
" @TestTemplate",
" void negative4() {}",
"",
" // BUG: Diagnostic contains:",
" @ParameterizedTest",
" void positive1() {}",
"",
" // BUG: Diagnostic contains:",
" @RepeatedTest(2)",
" void positive2() {}",
"}")
.doTest();
}
/** A {@link BugChecker} that delegates to {@link MoreMatchers#hasMetaAnnotation(String)} . */
@BugPattern(summary = "Interacts with `MoreMatchers` for testing purposes", severity = ERROR)
public static final class TestMatcher extends BugChecker implements AnnotationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<AnnotationTree> DELEGATE =
MoreMatchers.hasMetaAnnotation("org.junit.jupiter.api.TestTemplate");
@Override
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
return DELEGATE.matches(tree, state) ? describeMatch(tree) : Description.NO_MATCH;
}
}
}

View File

@@ -0,0 +1,195 @@
package tech.picnic.errorprone.bugpatterns.util;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.generic;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.raw;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.subOf;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.superOf;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.type;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.unbound;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.BugPattern;
import com.google.errorprone.CompilationTestHelper;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.suppliers.Supplier;
import com.google.errorprone.util.ASTHelpers;
import com.google.errorprone.util.Signatures;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.tools.javac.code.Type;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
final class MoreTypesTest {
@Test
void matcher() {
CompilationTestHelper.newInstance(SubtypeFlagger.class, getClass())
.addSourceLines(
"/A.java",
"import java.util.Collection;",
"import java.util.List;",
"import java.util.Map;",
"import java.util.Optional;",
"import java.util.Set;",
"",
"class A<S, T> {",
" void m() {",
" Object object = factory();",
" A a = factory();",
"",
" // BUG: Diagnostic contains: [Number, ? super Number, Integer, ? super Integer]",
" int integer = factory();",
"",
" // BUG: Diagnostic contains: [String]",
" String string = factory();",
"",
" // BUG: Diagnostic contains: [Optional]",
" Optional rawOptional = factory();",
" // BUG: Diagnostic contains: [Optional, Optional<?>]",
" Optional<S> optionalOfS = factory();",
" // BUG: Diagnostic contains: [Optional, Optional<?>]",
" Optional<T> optionalOfT = factory();",
" // BUG: Diagnostic contains: [Optional, Optional<?>, Optional<Number>]",
" Optional<Number> optionalOfNumber = factory();",
" // BUG: Diagnostic contains: [Optional, Optional<?>]",
" Optional<Integer> optionalOfInteger = factory();",
"",
" // BUG: Diagnostic contains: [Collection]",
" Collection rawCollection = factory();",
" // BUG: Diagnostic contains: [Collection, Collection<?>, Collection<Number>, Collection<? super",
" // Number>, Collection<? extends Number>, Collection<? super Integer>]",
" Collection<Number> collectionOfNumber = factory();",
" // BUG: Diagnostic contains: [Collection, Collection<?>, Collection<? extends Number>,",
" // Collection<Integer>, Collection<? super Integer>, Collection<? extends Integer>]",
" Collection<Integer> collectionOfInteger = factory();",
" // BUG: Diagnostic contains: [Collection, Collection<?>, Collection<? extends Number>]",
" Collection<Short> collectionOfShort = factory();",
"",
" // BUG: Diagnostic contains: [Collection, List]",
" List rawList = factory();",
" // BUG: Diagnostic contains: [Collection, Collection<?>, Collection<Number>, Collection<? super",
" // Number>, Collection<? extends Number>, Collection<? super Integer>, List, List<?>,",
" // List<Number>, List<? super Number>, List<? extends Number>, List<? super Integer>]",
" List<Number> listOfNumber = factory();",
" // BUG: Diagnostic contains: [Collection, Collection<?>, Collection<? extends Number>,",
" // Collection<Integer>, Collection<? super Integer>, Collection<? extends Integer>, List,",
" // List<?>, List<? extends Number>, List<Integer>, List<? super Integer>, List<? extends",
" // Integer>]",
" List<Integer> listOfInteger = factory();",
" // BUG: Diagnostic contains: [Collection, Collection<?>, Collection<? extends Number>, List,",
" // List<?>, List<? extends Number>]",
" List<Short> listOfShort = factory();",
"",
" // BUG: Diagnostic contains: [Collection]",
" Set rawSet = factory();",
" // BUG: Diagnostic contains: [Collection, Collection<?>, Collection<Number>, Collection<? super",
" // Number>, Collection<? extends Number>, Collection<? super Integer>]",
" Set<Number> setOfNumber = factory();",
" // BUG: Diagnostic contains: [Collection, Collection<?>, Collection<? extends Number>,",
" // Collection<Integer>, Collection<? super Integer>, Collection<? extends Integer>]",
" Set<Integer> setOfInteger = factory();",
" // BUG: Diagnostic contains: [Collection, Collection<?>, Collection<? extends Number>]",
" Set<Short> setOfShort = factory();",
"",
" Map rawMap = factory();",
" Map<Number, Collection<Number>> mapFromNumberToCollectionOfNumber = factory();",
" Map<Number, Collection<Short>> mapFromNumberToCollectionOfShort = factory();",
" Map<Number, Collection<Integer>> mapFromNumberToCollectionOfInteger = factory();",
" // BUG: Diagnostic contains: [Map<String, ? extends Collection<? super Short>>]",
" Map<String, Collection<Number>> mapFromStringToCollectionOfNumber = factory();",
" // BUG: Diagnostic contains: [Map<String, ? extends Collection<? super Short>>]",
" Map<String, Collection<Short>> mapFromStringToCollectionOfShort = factory();",
" Map<String, Collection<Integer>> mapFromStringToCollectionOfInteger = factory();",
" // BUG: Diagnostic contains: [Map<String, ? extends Collection<? super Short>>]",
" Map<String, List<Number>> mapFromStringToListOfNumber = factory();",
" // BUG: Diagnostic contains: [Map<String, ? extends Collection<? super Short>>]",
" Map<String, List<Short>> mapFromStringToListOfShort = factory();",
" Map<String, List<Integer>> mapFromStringToListOfInteger = factory();",
" }",
"",
" private <T> T factory() {",
" return null;",
" }",
"}")
.doTest();
}
/**
* A {@link BugChecker} that flags method invocations that are a subtype of any type defined by
* {@link #getTestTypes()}.
*/
@BugPattern(summary = "Flags invocations of methods with select return types", severity = ERROR)
public static final class SubtypeFlagger extends BugChecker
implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
Type treeType = ASTHelpers.getType(tree);
List<String> matches = new ArrayList<>();
for (Supplier<Type> type : getTestTypes()) {
Type testType = type.get(state);
if (testType != null && state.getTypes().isSubtype(treeType, testType)) {
matches.add(Signatures.prettyType(testType));
}
}
return matches.isEmpty()
? Description.NO_MATCH
: buildDescription(tree).setMessage(matches.toString()).build();
}
/**
* Returns the type suppliers under test.
*
* @implNote The return value of this method should not be assigned to a field, as that would
* prevent mutations introduced by Pitest from being killed.
*/
private static ImmutableSet<Supplier<Type>> getTestTypes() {
return ImmutableSet.of(
// Invalid types.
type("java.lang.Nonexistent"),
generic(type("java.util.Integer"), unbound()),
// Valid types.
type("java.lang.String"),
type("java.lang.Number"),
superOf(type("java.lang.Number")),
subOf(type("java.lang.Number")),
type("java.lang.Integer"),
superOf(type("java.lang.Integer")),
subOf(type("java.lang.Integer")),
type("java.util.Optional"),
raw(type("java.util.Optional")),
generic(type("java.util.Optional"), unbound()),
generic(type("java.util.Optional"), type("java.lang.Number")),
type("java.util.Collection"),
raw(type("java.util.Collection")),
generic(type("java.util.Collection"), unbound()),
generic(type("java.util.Collection"), type("java.lang.Number")),
generic(type("java.util.Collection"), superOf(type("java.lang.Number"))),
generic(type("java.util.Collection"), subOf(type("java.lang.Number"))),
generic(type("java.util.Collection"), type("java.lang.Integer")),
generic(type("java.util.Collection"), superOf(type("java.lang.Integer"))),
generic(type("java.util.Collection"), subOf(type("java.lang.Integer"))),
type("java.util.List"),
raw(type("java.util.List")),
generic(type("java.util.List"), unbound()),
generic(type("java.util.List"), type("java.lang.Number")),
generic(type("java.util.List"), superOf(type("java.lang.Number"))),
generic(type("java.util.List"), subOf(type("java.lang.Number"))),
generic(type("java.util.List"), type("java.lang.Integer")),
generic(type("java.util.List"), superOf(type("java.lang.Integer"))),
generic(type("java.util.List"), subOf(type("java.lang.Integer"))),
generic(
type("java.util.Map"),
type("java.lang.String"),
subOf(generic(type("java.util.Collection"), superOf(type("java.lang.Short"))))));
}
}
}

Some files were not shown because too many files have changed in this diff Show More