Compare commits

...

245 Commits

Author SHA1 Message Date
Stephan Schroevers
8c6bd1b6e7 [maven-release-plugin] prepare release v0.9.0 2023-03-31 09:30:59 +02:00
Picnic-Bot
0c1817c589 Upgrade Pitest Git plugins 1.0.6 -> 1.0.7 (#556) 2023-03-31 08:50:04 +02:00
Stephan Schroevers
73cf28e7ff Introduce DirectReturn check (#513) 2023-03-30 20:51:04 +02:00
Picnic-Bot
8a0abf5957 Upgrade Byte Buddy 1.14.2 -> 1.14.3 (#555)
See:
- https://github.com/raphw/byte-buddy/releases/tag/byte-buddy-1.14.3
- https://github.com/raphw/byte-buddy/compare/byte-buddy-1.14.2...byte-buddy-1.14.3
2023-03-30 20:39:49 +02:00
Picnic-Bot
5fb4aed3ad Upgrade extra-enforcer-rules 1.6.1 -> 1.6.2 (#554)
See:
- https://github.com/mojohaus/extra-enforcer-rules/releases/tag/1.6.2
- https://github.com/mojohaus/extra-enforcer-rules/compare/extra-enforcer-rules-1.6.1...1.6.2
2023-03-30 09:34:58 +02:00
Picnic-Bot
aef9c5da7a Upgrade Forbidden APIs plugin 3.4 -> 3.5 (#553)
See:
- https://github.com/policeman-tools/forbidden-apis/wiki/Changes
- https://github.com/policeman-tools/forbidden-apis/compare/3.4...3.5
2023-03-28 08:09:28 +02:00
Picnic-Bot
7069e7a6d8 Upgrade pitest-maven-plugin 1.11.6 -> 1.11.7 (#552)
See:
- https://github.com/hcoles/pitest/releases/tag/1.11.7
- https://github.com/hcoles/pitest/compare/1.11.6...1.11.7
2023-03-28 07:32:21 +02:00
Bastien Diederichs
334c374ca1 Extend null check Refaster rules (#523)
Summary of changes:
- Replace `CheckNotNull` with `RequireNonNull{,WithMessage}{,Statement}`.
- Extend `Is{,Not}Null`.

Fixes #437.
2023-03-27 22:08:34 +02:00
Mohamed Sameh
57cd084f82 Extend StepVerifierStepIdentity Refaster rule (#541)
By flagging expressions of the form `step.expectNextCount(0)`.
2023-03-27 10:30:25 +02:00
Picnic-Bot
0b3be1b75b Upgrade Spring Boot 2.7.9 -> 2.7.10 (#549)
See:
- https://github.com/spring-projects/spring-boot/releases/tag/v2.7.10
- https://github.com/spring-projects/spring-boot/compare/v2.7.9...v2.7.10
2023-03-27 10:09:28 +02:00
Picnic-Bot
902538fd4a Upgrade maven-deploy-plugin 3.1.0 -> 3.1.1 (#546)
See:
- https://issues.apache.org/jira/issues/?jql=project%20%3D%20MDEPLOY%20AND%20fixVersion%20%3E%203.1.0%20AND%20fixVersion%20%3C%3D%203.1.1
- https://github.com/apache/maven-deploy-plugin/releases/tag/maven-deploy-plugin-3.1.1
- https://github.com/apache/maven-deploy-plugin/compare/maven-deploy-plugin-3.1.0...maven-deploy-plugin-3.1.1
2023-03-27 09:47:22 +02:00
Picnic-Bot
50f6b770e4 Upgrade maven-install-plugin 3.1.0 -> 3.1.1 (#547)
See:
- https://issues.apache.org/jira/issues/?jql=project%20%3D%20MINSTALL%20AND%20fixVersion%20%3E%203.1.0%20AND%20fixVersion%20%3C%3D%203.1.1
- https://github.com/apache/maven-install-plugin/releases/tag/maven-install-plugin-3.1.1
- https://github.com/apache/maven-install-plugin/compare/maven-install-plugin-3.1.0...maven-install-plugin-3.1.1
2023-03-27 09:30:46 +02:00
Picnic-Bot
47e0a779bd Upgrade maven-resources-plugin 3.3.0 -> 3.3.1 (#548)
See:
- https://issues.apache.org/jira/issues/?jql=project%20%3D%20MRESOURCES%20AND%20fixVersion%20%3E%203.3.0%20AND%20fixVersion%20%3C%3D%203.3.1
- https://github.com/apache/maven-resources-plugin/releases/tag/maven-resources-plugin-3.3.1
- https://github.com/apache/maven-resources-plugin/compare/maven-resources-plugin-3.3.0...maven-resources-plugin-3.3.1
2023-03-27 09:09:20 +02:00
Picnic-Bot
973d3c3cd9 Upgrade maven-release-plugin 2.5.3 -> 3.0.0 (#540)
See:
- https://issues.apache.org/jira/issues/?jql=project%20%3D%20MRELEASE%20AND%20fixVersion%20%3E%202.5.3%20AND%20fixVersion%20%3C%3D%203.0.0
- https://github.com/apache/maven-release/releases/tag/maven-release-3.0.0-M1
- https://github.com/apache/maven-release/releases/tag/maven-release-3.0.0-M2
- https://github.com/apache/maven-release/releases/tag/maven-release-3.0.0-M3
- https://github.com/apache/maven-release/releases/tag/maven-release-3.0.0-M4
- https://github.com/apache/maven-release/releases/tag/maven-release-3.0.0-M5
- https://github.com/apache/maven-release/releases/tag/maven-release-3.0.0-M6
- https://github.com/apache/maven-release/releases/tag/maven-release-3.0.0-M7
- https://github.com/apache/maven-release/releases/tag/maven-release-3.0.0
- https://github.com/apache/maven-release/compare/maven-release-2.5.3...maven-release-3.0.0
2023-03-27 08:44:43 +02:00
Picnic-Bot
edb7290e2e Upgrade Checkstyle 10.9.2 -> 10.9.3 (#551)
See:
- https://checkstyle.sourceforge.io/releasenotes.html
- https://github.com/checkstyle/checkstyle/releases/tag/checkstyle-10.9.3
- https://github.com/checkstyle/checkstyle/compare/checkstyle-10.9.2...checkstyle-10.9.3
2023-03-27 08:32:43 +02:00
Picnic-Bot
d5c45e003f Upgrade modernizer-maven-plugin 2.5.0 -> 2.6.0 (#550)
See:
- https://github.com/gaul/modernizer-maven-plugin/releases/tag/modernizer-maven-plugin-2.6.0
- https://github.com/gaul/modernizer-maven-plugin/compare/modernizer-maven-plugin-2.5.0...modernizer-maven-plugin-2.6.0
2023-03-26 19:22:16 +02:00
Picnic-Bot
f784c64150 Upgrade pitest-maven-plugin 1.11.5 -> 1.11.6 (#544)
See:
- https://github.com/hcoles/pitest/releases/tag/1.11.6
- https://github.com/hcoles/pitest/compare/1.11.5...1.11.6
2023-03-25 19:57:48 +01:00
Guillaume Toison
978c90db9d Extend set of parameter types recognized by RequestMappingAnnotation (#543)
Additional types recognized:
- `jakarta.servlet.http.HttpServletRequest`
- `jakarta.servlet.http.HttpServletResponse`
- `org.springframework.ui.Model`
- `org.springframework.validation.BindingResult`
2023-03-25 19:48:00 +01:00
Picnic-Bot
ae89a37934 Upgrade swagger-annotations 1.6.9 -> 1.6.10 (#542)
See:
- https://github.com/swagger-api/swagger-core/releases/tag/v1.6.10
- https://github.com/swagger-api/swagger-core/compare/v1.6.9...v1.6.10
2023-03-23 09:22:22 +01:00
Bastien Diederichs
8f1d1df747 Introduce BugCheckerRules Refaster rule collection (#526) 2023-03-23 09:01:18 +01:00
Picnic-Bot
04368e9243 Upgrade SLF4J API 2.0.6 -> 2.0.7 (#536)
See:
- https://www.slf4j.org/news.html
- https://github.com/qos-ch/slf4j/compare/v_2.0.6...v_2.0.7
2023-03-22 18:52:40 +01:00
Picnic-Bot
156df71616 Upgrade Spring 5.3.25 -> 5.3.26 (#539)
See:
- https://github.com/spring-projects/spring-framework/releases/tag/v5.3.26
- https://github.com/spring-projects/spring-framework/compare/v5.3.25...v5.3.26
2023-03-21 17:28:18 +01:00
Picnic-Bot
64b1c7eea4 Upgrade swagger-annotations 2.2.8 -> 2.2.9 (#538)
See:
- https://github.com/swagger-api/swagger-core/releases/tag/v2.2.9
- https://github.com/swagger-api/swagger-core/compare/v2.2.8...v2.2.9
2023-03-21 17:15:44 +01:00
Picnic-Bot
80d0d85826 Upgrade Checkstyle 10.9.1 -> 10.9.2 (#537)
See:
- https://checkstyle.sourceforge.io/releasenotes.html
- https://github.com/checkstyle/checkstyle/releases/tag/checkstyle-10.9.2
- https://github.com/checkstyle/checkstyle/compare/checkstyle-10.9.1...checkstyle-10.9.2
2023-03-21 11:44:37 +01:00
Stephan Schroevers
d30c99a28f Introduce AssertThatPathContent{,Utf8} Refaster rules (#530) 2023-03-20 13:48:41 +01:00
Picnic-Bot
29c23542da Upgrade pitest-maven-plugin 1.11.4 -> 1.11.5 (#534)
While there, fix `run-mutation-tests.sh` for compatibility with Surefire 3.0.0.

See:
- https://github.com/hcoles/pitest/releases/tag/1.11.5
- https://github.com/hcoles/pitest/compare/1.11.4...1.11.5
2023-03-18 15:10:16 +01:00
Picnic-Bot
62c1c277ae Upgrade Checkstyle 10.8.1 -> 10.9.1 (#535)
See:
- https://checkstyle.sourceforge.io/releasenotes.html
- https://github.com/checkstyle/checkstyle/releases/tag/checkstyle-10.9.0
- https://github.com/checkstyle/checkstyle/releases/tag/checkstyle-10.9.1
- https://github.com/checkstyle/checkstyle/compare/checkstyle-10.8.1...checkstyle-10.9.1
2023-03-18 14:23:16 +01:00
Picnic-Bot
8580e89008 Upgrade Project Reactor 2022.0.4 -> 2022.0.5 (#533)
See:
- https://github.com/reactor/reactor/releases/tag/2022.0.5
- https://github.com/reactor/reactor/compare/2022.0.4...2022.0.5
2023-03-16 15:48:01 +01:00
Stephan Schroevers
06c8b164e9 Upgrade JDKs used by GitHub Actions builds (#521)
Additionally:
- Update the example version numbers mentioned in the issue template.
- Drop some redundant whitespace from `SourceCodeTest` test code.
- Sort some compiler arguments.

See:
- https://www.oracle.com/java/technologies/javase/11-0-17-relnotes.html
- https://www.oracle.com/java/technologies/javase/11-0-18-relnotes.html
- https://www.oracle.com/java/technologies/javase/17-0-5-relnotes.html
- https://www.oracle.com/java/technologies/javase/17-0-6-relnotes.html
- https://www.oracle.com/java/technologies/javase/19-0-1-relnotes.html
- https://www.oracle.com/java/technologies/javase/19-0-2-relnotes.html
2023-03-15 13:26:40 +01:00
Picnic-Bot
fd9d3157bc Upgrade Surefire 2.22.2 -> 3.0.0 (#532)
While there, drop an unnecessary JUnit configuration parameter.

See:
- https://issues.apache.org/jira/issues/?jql=project%20%3D%20SUREFIRE%20AND%20fixVersion%20%3E%202.22.2%20AND%20fixVersion%20%3C%3D%203.0.0
- https://github.com/apache/maven-surefire/releases/tag/surefire-3.0.0-M1
- https://github.com/apache/maven-surefire/releases/tag/surefire-3.0.0-M2
- https://github.com/apache/maven-surefire/releases/tag/surefire-3.0.0-M3
- https://github.com/apache/maven-surefire/releases/tag/surefire-3.0.0-M4
- https://github.com/apache/maven-surefire/releases/tag/surefire-3.0.0-M5
- https://github.com/apache/maven-surefire/releases/tag/surefire-3.0.0-M6
- https://github.com/apache/maven-surefire/releases/tag/surefire-3.0.0-M7
- https://github.com/apache/maven-surefire/releases/tag/surefire-3.0.0-M8
- https://github.com/apache/maven-surefire/releases/tag/surefire-3.0.0-M9
- https://github.com/apache/maven-surefire/releases/tag/surefire-3.0.0
- https://github.com/apache/maven-surefire/compare/surefire-2.22.2..surefire-3.0.0
2023-03-15 13:01:20 +01:00
Picnic-Bot
a623f73c1c Upgrade Byte Buddy 1.14.1 -> 1.14.2 (#531)
See:
- https://github.com/raphw/byte-buddy/releases/tag/byte-buddy-1.14.2
- https://github.com/raphw/byte-buddy/compare/byte-buddy-1.14.1...byte-buddy-1.14.2
2023-03-15 10:39:02 +01:00
Picnic-Bot
f9d0cd99d6 Upgrade Mockito 5.1.1 -> 5.2.0 (#529)
See:
- https://github.com/mockito/mockito/releases/tag/v5.2.0
- https://github.com/mockito/mockito/compare/v5.1.1...v5.2.0
2023-03-13 15:15:29 +01:00
Picnic-Bot
9bec3de372 Upgrade Pitest Git plugins 1.0.5 -> 1.0.6 (#522) 2023-03-13 10:33:22 +01:00
Picnic-Bot
4164514c5b Upgrade Checkstyle 10.8.0 -> 10.8.1 (#528)
See:
- https://checkstyle.sourceforge.io/releasenotes.html
- https://github.com/checkstyle/checkstyle/releases/tag/checkstyle-10.8.1
- https://github.com/checkstyle/checkstyle/compare/checkstyle-10.8.0...checkstyle-10.8.1
2023-03-13 09:58:38 +01:00
Picnic-Bot
c3cd535b16 Upgrade NullAway 0.10.9 -> 0.10.10 (#524)
See:
- https://github.com/uber/NullAway/blob/master/CHANGELOG.md
- https://github.com/uber/NullAway/compare/v0.10.9...v0.10.10
2023-03-09 10:04:48 +01:00
Picnic-Bot
64195279cc Upgrade Byte Buddy 1.14.0 -> 1.14.1 (#525)
See:
- https://github.com/raphw/byte-buddy/releases/tag/byte-buddy-1.14.1
- https://github.com/raphw/byte-buddy/compare/byte-buddy-1.14.0...byte-buddy-1.14.1
2023-03-09 08:36:25 +01:00
Pieter Dirk Soels
61c9f67f66 Introduce MockitoMockClassReference check (#454)
Flags Mockito mock and spy creation expressions that explicitly specify the
type of mock or spy to create, while this information can also be inferred from
context.
2023-03-06 09:54:26 +01:00
Picnic-Bot
4bb14b01ec Upgrade pitest-maven-plugin 1.11.3 -> 1.11.4 (#520)
See:
- https://github.com/hcoles/pitest/releases/tag/1.11.4
- https://github.com/hcoles/pitest/compare/1.11.3...1.11.4
2023-03-04 14:19:29 +01:00
Bastien Diederichs
b267b4dba8 Introduce ImmutableMapCopyOfMapsFilter{Keys,Values} Refaster rules (#517) 2023-03-03 13:09:44 +01:00
Picnic-Bot
03f0e0493b Upgrade Checker Framework Annotations 3.31.0 -> 3.32.0 (#519)
See:
- https://github.com/typetools/checker-framework/releases/tag/checker-framework-3.32.0
- https://github.com/typetools/checker-framework/compare/checker-framework-3.31.0...checker-framework-3.32.0
2023-03-03 10:17:22 +01:00
Picnic-Bot
2111c81784 Upgrade pitest-maven-plugin 1.11.1 -> 1.11.3 (#514)
See:
- https://github.com/hcoles/pitest/releases/tag/1.11.2
- https://github.com/hcoles/pitest/releases/tag/1.11.3
- https://github.com/hcoles/pitest/compare/1.11.1...1.11.3
2023-03-03 08:53:01 +01:00
Picnic-Bot
43d50f2ef9 Upgrade Project Reactor 2022.0.3 -> 2022.0.4 (#518)
See:
- https://github.com/reactor/reactor/releases/tag/2022.0.4
- https://github.com/reactor/reactor/compare/2022.0.3...2022.0.4
2023-03-03 08:35:36 +01:00
Gijs de Jong
2d972fd975 Introduce JUnitValueSource check (#188)
This new check replaces JUnit `@MethodSource` usages with an equivalent
`@ValueSource` annotation where possible.
2023-03-02 10:45:35 +01:00
Bastien Diederichs
ee265a87ae Introduce FluxCountMapMathToIntExact Refaster rule (#516) 2023-03-02 08:48:33 +01:00
Picnic-Bot
6b4fba62da Upgrade pitest-maven-plugin 1.11.0 -> 1.11.1 (#509)
See:
- https://github.com/hcoles/pitest/releases/tag/1.11.1
- https://github.com/hcoles/pitest/compare/1.11.0...1.11.1
2023-02-27 16:47:31 +01:00
Picnic-Bot
e883e28e34 Upgrade Checkstyle 10.7.0 -> 10.8.0 (#512)
See:
- https://checkstyle.sourceforge.io/releasenotes.html
- https://github.com/checkstyle/checkstyle/releases/tag/checkstyle-10.8.0
- https://github.com/checkstyle/checkstyle/compare/checkstyle-10.7.0...checkstyle-10.8.0
2023-02-27 07:46:00 +01:00
Picnic-Bot
d84de6efba Upgrade Google Java Format 1.15.0 -> 1.16.0 (#511)
See:
- https://github.com/google/google-java-format/releases/tag/v1.16.0
- https://github.com/google/google-java-format/compare/v1.15.0...v1.16.0
2023-02-26 14:40:42 +01:00
Picnic-Bot
4dca61a144 Upgrade New Relic Java Agent 8.0.0 -> 8.0.1 (#508)
See:
- https://github.com/newrelic/newrelic-java-agent/releases/tag/v8.0.1
- https://github.com/newrelic/newrelic-java-agent/compare/v8.0.0...v8.0.1
2023-02-25 12:23:02 +01:00
Picnic-Bot
dc9597a603 Upgrade Spring Boot 2.7.8 -> 2.7.9 (#510)
See:
- https://github.com/spring-projects/spring-boot/releases/tag/v2.7.9
- https://github.com/spring-projects/spring-boot/compare/v2.7.8...v2.7.9
2023-02-24 17:52:45 +01:00
Picnic-Bot
ec9853ac88 Upgrade versions-maven-plugin 2.14.2 -> 2.15.0 (#506)
See:
- https://github.com/mojohaus/versions/releases/tag/2.15.0
- https://github.com/mojohaus/versions-maven-plugin/compare/2.14.2...2.15.0
2023-02-22 08:55:17 +01:00
Giovanni Zotta
5bb1dd1a10 Introduce StreamMapTo{Double,Int,Long}Sum Refaster rules (#497)
As well as a new `IsLambdaExpressionOrMethodReference` matcher.
2023-02-21 16:35:29 +01:00
Picnic-Bot
fd6a45ebd8 Upgrade Project Reactor 2022.0.2 -> 2022.0.3 (#499)
See:
- https://github.com/reactor/reactor/releases/tag/2022.0.3
- https://github.com/reactor/reactor/compare/2022.0.2...2022.0.3
2023-02-20 18:58:23 +01:00
Benedek Halasi
82d4677509 Introduce FluxImplicitBlock check (#472) 2023-02-20 10:17:17 +01:00
Picnic-Bot
1fdf1016b7 Upgrade Byte Buddy 1.13.0 -> 1.14.0 (#505)
See:
- https://github.com/raphw/byte-buddy/releases/tag/byte-buddy-1.14.0
- https://github.com/raphw/byte-buddy/compare/byte-buddy-1.13.0...byte-buddy-1.14.0
2023-02-20 08:46:28 +01:00
Picnic-Bot
80e537fce2 Upgrade Pitest Git plugins 1.0.4 -> 1.0.5 (#504) 2023-02-19 12:51:22 +01:00
Picnic-Bot
d85897ea62 Upgrade Checker Framework Annotations 3.30.0 -> 3.31.0 (#502)
See:
- https://github.com/typetools/checker-framework/releases/tag/checker-framework-3.31.0
- https://github.com/typetools/checker-framework/compare/checker-framework-3.30.0...checker-framework-3.31.0
2023-02-18 14:38:57 +01:00
Picnic-Bot
c5bde3999d Upgrade maven-javadoc-plugin 3.4.1 -> 3.5.0 (#500)
See:
- https://github.com/apache/maven-javadoc-plugin/releases/tag/maven-javadoc-plugin-3.5.0
- https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.4.1...maven-javadoc-plugin-3.5.0
2023-02-17 13:28:27 +01:00
Rick Ossendrijver
575d494303 Upgrade Maven API 3.8.6 -> 3.8.7 (#498)
See:
- https://maven.apache.org/docs/3.8.7/release-notes.html
- https://github.com/apache/maven/releases/tag/maven-3.8.7
- https://github.com/apache/maven/compare/maven-3.8.6...maven-3.8.7
2023-02-16 13:44:25 +01:00
Picnic-Bot
844ef84d55 Upgrade maven-deploy-plugin 3.0.0 -> 3.1.0 (#495)
See:
- https://issues.apache.org/jira/issues/?jql=project%20%3D%20MDEPLOY%20AND%20fixVersion%20%3E%203.0.0%20AND%20fixVersion%20%3C%3D%203.1.0
- https://github.com/apache/maven-deploy-plugin/releases/tag/maven-deploy-plugin-3.1.0
- https://github.com/apache/maven-deploy-plugin/compare/maven-deploy-plugin-3.0.0...maven-deploy-plugin-3.1.0
2023-02-14 11:19:11 +01:00
Eric Staffas
29469cbbfd Introduce ConflictDetection utility class (#478) 2023-02-13 12:43:17 +01:00
Picnic-Bot
da9a6dd270 Upgrade Byte Buddy 1.12.23 -> 1.13.0 (#496)
See:
- https://github.com/raphw/byte-buddy/releases/tag/byte-buddy-1.13.0
- https://github.com/raphw/byte-buddy/compare/byte-buddy-1.12.23...byte-buddy-1.13.0
2023-02-13 11:35:27 +01:00
Gökhun Çelik
0cb03aa132 Add Gradle installation instructions to README (#430) 2023-02-13 11:18:36 +01:00
Rick Ossendrijver
0f15070883 Introduce documentation-support module (#428)
This new module provides the initial version of a framework for the extraction 
of data from bug checkers and Refaster rules, to be used as input for website
generation.
2023-02-13 09:27:08 +01:00
Stephan Schroevers
14b5fa1feb Update .mvn/maven.config for compatibility with Maven 3.9.0 (#493)
See https://issues.apache.org/jira/browse/MNG-7684
2023-02-08 09:08:09 +01:00
Stephan Schroevers
d1f513373f Enable additional maven-enforcer-plugin rules (#489) 2023-02-06 14:16:05 +01:00
Picnic-Bot
cd1593009b Upgrade Pitest Git plugins 1.0.3 -> 1.0.4 (#490) 2023-02-06 11:49:37 +01:00
Picnic-Bot
0d52414c04 Upgrade Byte Buddy 1.12.22 -> 1.12.23 (#492)
See:
- https://github.com/raphw/byte-buddy/releases/tag/byte-buddy-1.12.23
- https://github.com/raphw/byte-buddy/compare/byte-buddy-1.12.22...byte-buddy-1.12.23
2023-02-06 08:17:38 +01:00
Picnic-Bot
a55ed9cea9 Upgrade maven-enforcer-plugin 3.1.0 -> 3.2.1 (#487)
See:
- https://issues.apache.org/jira/issues/?jql=project%20%3D%20MENFORCER%20AND%20fixVersion%20%3E%203.1.0%20AND%20fixVersion%20%3C%3D%203.2.1%20
- https://github.com/apache/maven-enforcer/releases/tag/enforcer-3.2.1
- https://github.com/apache/maven-enforcer/compare/enforcer-3.1.0...enforcer-3.2.1
2023-02-04 10:33:11 +01:00
Picnic-Bot
9b191f46aa Upgrade Checker Framework Annotations 3.29.0 -> 3.30.0 (#488)
See:
- https://github.com/typetools/checker-framework/releases/tag/checker-framework-3.30.0
- https://github.com/typetools/checker-framework/compare/checker-framework-3.29.0...checker-framework-3.30.0
2023-02-03 09:37:00 +01:00
Picnic-Bot
6ea756f3ce Upgrade sortpom-maven-plugin 3.2.0 -> 3.2.1 (#481)
See:
- https://github.com/Ekryd/sortpom/wiki/Versions
- https://github.com/Ekryd/sortpom/releases/tag/sortpom-parent-3.2.1
- https://github.com/Ekryd/sortpom/compare/sortpom-parent-3.2.0...sortpom-parent-3.2.1
2023-02-03 09:06:17 +01:00
Picnic-Bot
adbcc4a94f Upgrade Checkstyle 10.6.0 -> 10.7.0 (#486)
See:
- https://checkstyle.sourceforge.io/releasenotes.html
- https://github.com/checkstyle/checkstyle/releases/tag/checkstyle-10.7.0
- https://github.com/checkstyle/checkstyle/compare/checkstyle-10.6.0...checkstyle-10.7.0
2023-02-02 10:19:11 +01:00
Picnic-Bot
0ed2788dbd Upgrade NullAway 0.10.8 -> 0.10.9 (#485)
See:
- https://github.com/uber/NullAway/blob/master/CHANGELOG.md
- https://github.com/uber/NullAway/compare/v0.10.8...v0.10.9
2023-02-02 09:22:00 +01:00
Picnic-Bot
04749ffcf5 Upgrade pitest-maven-plugin 1.10.4 -> 1.11.0 (#483)
See https://github.com/hcoles/pitest/compare/1.10.4...1.11.0
2023-01-31 08:49:11 +01:00
Picnic-Bot
37077bd03c Upgrade Mockito 5.1.0 -> 5.1.1 (#482)
See:
- https://github.com/mockito/mockito/releases/tag/v5.1.1
- https://github.com/mockito/mockito/compare/v5.1.0...v5.1.1
2023-01-31 08:30:51 +01:00
Picnic-Bot
4798f7cf5f Upgrade Jackson 2.14.1 -> 2.14.2 (#479)
See:
- https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.14.2
- https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.14.1...jackson-bom-2.14.2
2023-01-30 09:36:53 +01:00
Picnic-Bot
ac285f0c50 Upgrade Mockito 5.0.0 -> 5.1.0 (#480)
See:
- https://github.com/mockito/mockito/releases/tag/v5.1.0
- https://github.com/mockito/mockito/compare/v5.0.0...v5.1.0
2023-01-30 09:25:08 +01:00
Picnic-Bot
1f3fb08082 Upgrade New Relic Java Agent 7.11.1 -> 8.0.0 (#477)
See:
- https://github.com/newrelic/newrelic-java-agent/releases/tag/v8.0.0
- https://github.com/newrelic/newrelic-java-agent/compare/v7.11.1...v8.0.0
2023-01-28 10:08:14 +01:00
Stephan Schroevers
9a397aa047 [maven-release-plugin] prepare for next development iteration 2023-01-27 09:20:57 +01:00
Stephan Schroevers
60e74332de [maven-release-plugin] prepare release v0.8.0 2023-01-27 09:20:54 +01:00
Stephan Schroevers
3a94aad3b0 Document MonoFlatMapToFlux Refaster rule limitation (#473) 2023-01-24 15:38:52 +01:00
Picnic-Bot
a5b5f43974 Upgrade TestNG 7.4.0 -> 7.7.1 (#469)
See:
- https://github.com/cbeust/testng/blob/master/CHANGES.txt
- https://github.com/cbeust/testng/releases/tag/7.6.0
- https://github.com/cbeust/testng/releases/tag/7.6.1
- https://github.com/cbeust/testng/releases/tag/7.7.0
- https://github.com/cbeust/testng/releases/tag/7.7.1
- https://github.com/cbeust/testng/compare/7.4.0...7.7.1
2023-01-24 09:45:13 +01:00
Rick Ossendrijver
c212b9a171 Enable Checkstyle's JavadocStyle module (#451)
See: 
- https://checkstyle.org/config_javadoc.html#JavadocStyle
- https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/checks/javadoc/JavadocStyleCheck.html
2023-01-23 11:16:12 +01:00
Picnic-Bot
499f922328 Upgrade Spring 5.3.24 -> 5.3.25 (#460)
See:
- https://github.com/spring-projects/spring-framework/releases/tag/v5.3.25
- https://github.com/spring-projects/spring-framework/compare/v5.3.24...v5.3.25
2023-01-20 09:59:49 +01:00
Picnic-Bot
5720732b48 Upgrade Spring Boot 2.7.7 -> 2.7.8 (#471)
See:
- https://github.com/spring-projects/spring-boot/releases/tag/v2.7.8
- https://github.com/spring-projects/spring-boot/compare/v2.7.7...v2.7.8
2023-01-20 08:47:55 +01:00
Phil Werli
81ffd04fe4 Extend MonoIdentity Refaster rule (#470)
By flagging expressions of the form `mono.flux().singleOrEmpty()`.
2023-01-19 13:49:57 +01:00
Picnic-Bot
b2f514f0a5 Upgrade Mockito 4.11.0 -> 5.0.0 (#463)
See:
- https://github.com/mockito/mockito/releases/tag/v5.0.0
- https://github.com/mockito/mockito/compare/v4.11.0...v5.0.0
2023-01-19 10:59:24 +01:00
Picnic-Bot
dad92b5fa6 Upgrade maven-dependency-plugin 3.4.0 -> 3.5.0 (#461)
See:
- https://issues.apache.org/jira/issues/?jql=project%20%3D%20MDEP%20AND%20fixVersion%20%3E%203.4.0%20AND%20fixVersion%20%3C%3D%203.5.0%20AND%20statusCategory%20%3D%20Done%20
- https://github.com/apache/maven-dependency-plugin/compare/maven-dependency-plugin-3.4.0...maven-dependency-plugin-3.5.0
2023-01-18 17:13:44 +01:00
Picnic-Bot
6d699f75ad Upgrade maven-checkstyle-plugin 3.2.0 -> 3.2.1 (#459)
See:
- https://issues.apache.org/jira/issues/?jql=project%20%3D%20MCHECKSTYLE%20AND%20fixVersion%20%3E%203.2.0%20AND%20fixVersion%20%3C%3D%203.2.1
- https://github.com/apache/maven-checkstyle-plugin/compare/maven-checkstyle-plugin-3.2.0...maven-checkstyle-plugin-3.2.1
2023-01-18 16:38:54 +01:00
Picnic-Bot
cad0c74dc0 Upgrade nohttp-checkstyle 0.0.10 -> 0.0.11 (#458)
See https://github.com/spring-io/nohttp/compare/0.0.10...0.0.11
2023-01-17 09:15:00 +01:00
Picnic-Bot
a3a1f495f1 Upgrade errorprone-slf4j 0.1.17 -> 0.1.18 (#466)
See:
- https://github.com/KengoTODA/errorprone-slf4j/releases/tag/v0.1.18
- https://github.com/KengoTODA/errorprone-slf4j/compare/v0.1.17...v0.1.18
2023-01-17 08:48:31 +01:00
Picnic-Bot
b2a646fc21 Upgrade AssertJ 3.24.1 -> 3.24.2 (#467)
See:
- https://assertj.github.io/doc/#assertj-core-release-notes
- https://assertj.github.io/doc/#assertj-guava-release-notes
- https://github.com/assertj/assertj/compare/assertj-build-3.24.1...assertj-build-3.24.2
2023-01-17 08:15:38 +01:00
Picnic-Bot
1194b0c83c Upgrade NullAway 0.10.7 -> 0.10.8 (#464)
See:
- https://github.com/uber/NullAway/blob/master/CHANGELOG.md
- https://github.com/uber/NullAway/compare/v0.10.7...v0.10.8
2023-01-17 08:01:48 +01:00
Phil Werli
82a07fde25 Extend MonoIdentity Refaster rule (#465)
By flagging expressions of the form `mono.flux().next()`.
2023-01-17 07:33:56 +01:00
Picnic-Bot
dec3220b5b Upgrade pitest-junit5-plugin 1.1.1 -> 1.1.2 (#462)
See https://github.com/pitest/pitest-junit5-plugin/compare/1.1.1...1.1.2
2023-01-16 08:42:14 +01:00
Stephan Schroevers
79356ac553 Upgrade Error Prone 2.17.0 -> 2.18.0 (#455)
See:
- https://github.com/google/error-prone/releases/tag/v2.18.0
- https://github.com/google/error-prone/compare/v2.17.0...v2.18.0
- https://github.com/PicnicSupermarket/error-prone/compare/v2.17.0-picnic-1...v2.18.0-picnic-1
2023-01-11 19:12:39 +01:00
Picnic-Bot
f079c53914 Upgrade Project Reactor 2022.0.1 -> 2022.0.2 (#456)
See:
- https://github.com/reactor/reactor/releases/tag/2022.0.2
- https://github.com/reactor/reactor/compare/2022.0.1...2022.0.2
2023-01-11 13:58:41 +01:00
Picnic-Bot
8e24da907d Upgrade JUnit Jupiter 5.9.1 -> 5.9.2 (#457)
See:
- https://junit.org/junit5/docs/current/release-notes/index.html
- https://github.com/junit-team/junit5/releases/tag/r5.9.2
- https://github.com/junit-team/junit5/compare/r5.9.1...r5.9.2
2023-01-11 11:16:10 +01:00
Picnic-Bot
3c89a1c80d Upgrade AssertJ 3.24.0 -> 3.24.1 (#453)
See:
- https://assertj.github.io/doc/#assertj-core-release-notes
- https://github.com/assertj/assertj/compare/assertj-build-3.24.0...assertj-build-3.24.1
2023-01-09 09:05:59 +01:00
Rick Ossendrijver
9bd4b16001 Inline most {BugCheckerRefactoring,Compilation}TestHelper fields (#442) 2023-01-08 12:40:20 +01:00
Picnic-Bot
6370452803 Upgrade swagger-annotations 2.2.7 -> 2.2.8 (#452)
See:
- https://github.com/swagger-api/swagger-core/releases/tag/v2.2.8
- https://github.com/swagger-api/swagger-core/compare/v2.2.7...v2.2.8
2023-01-08 12:16:52 +01:00
Benedek Halasi
feb9abfa91 Introduce MapGetOrDefault Refaster rule (#439)
Fixes #431.
2023-01-06 14:57:12 +01:00
Stephan Schroevers
560f52bad0 [maven-release-plugin] prepare for next development iteration 2023-01-06 11:29:12 +01:00
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
Stephan Schroevers
85cb997f1b [maven-release-plugin] prepare release v0.4.0 2022-10-13 09:20:46 +02:00
227 changed files with 10458 additions and 1196 deletions

View File

@@ -42,9 +42,9 @@ Please replace this sentence with log output, if applicable.
<!-- Please complete the following information: -->
- Operating system (e.g. MacOS Monterey).
- Java version (i.e. `java --version`, e.g. `17.0.3`).
- Error Prone version (e.g. `2.15.0`).
- Error Prone Support version (e.g. `0.3.0`).
- Java version (i.e. `java --version`, e.g. `17.0.6`).
- Error Prone version (e.g. `2.18.0`).
- Error Prone Support version (e.g. `0.8.0`).
### Additional context

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.18, 17.0.6, 19.0.2 ]
distribution: [ temurin ]
experimental: [ false ]
include:
- os: macos-12
jdk: 17.0.6
distribution: temurin
experimental: false
- os: windows-2022
jdk: 17.0.6
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.6
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.6
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

@@ -1 +1,3 @@
--batch-mode --errors --strict-checksums
--batch-mode
--errors
--strict-checksums

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]
@@ -35,7 +36,9 @@ code_][picnic-blog-ep-post].
### Installation
This library is built on top of [Error Prone][error-prone-orig-repo]. To use
it:
it, read the installation guide for Maven or Gradle below.
#### Maven
1. First, follow Error Prone's [installation
guide][error-prone-installation-guide].
@@ -93,6 +96,31 @@ it:
Prone Support. Alternatively reference this project's `self-check` profile
definition. -->
#### Gradle
1. First, follow the [installation guide]
[error-prone-gradle-installation-guide] of the `gradle-errorprone-plugin`.
2. Next, edit your `build.gradle` file to add one or more Error Prone Support
modules:
```groovy
dependencies {
// Error Prone itself.
errorprone("com.google.errorprone:error_prone_core:${errorProneVersion}")
// Error Prone Support's additional bug checkers.
errorprone("tech.picnic.error-prone-support:error-prone-contrib:${errorProneSupportVersion}")
// Error Prone Support's Refaster rules.
errorprone("tech.picnic.error-prone-support:refaster-runner:${errorProneSupportVersion}")
}
tasks.withType(JavaCompile).configureEach {
options.errorprone.disableWarningsInGeneratedCode = true
// Add other Error Prone flags here. See:
// - https://github.com/tbroyer/gradle-errorprone-plugin#configuration
// - https://errorprone.info/docs/flags
}
```
### Seeing it in action
Consider the following example code:
@@ -119,16 +147,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 +193,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].
@@ -209,6 +234,7 @@ guidelines][contributing].
[error-prone-bugchecker]: https://github.com/google/error-prone/blob/master/check_api/src/main/java/com/google/errorprone/bugpatterns/BugChecker.java
[error-prone-fork-jitpack]: https://jitpack.io/#PicnicSupermarket/error-prone
[error-prone-fork-repo]: https://github.com/PicnicSupermarket/error-prone
[error-prone-gradle-installation-guide]: https://github.com/tbroyer/gradle-errorprone-plugin
[error-prone-installation-guide]: https://errorprone.info/docs/installation#maven
[error-prone-orig-repo]: https://github.com/google/error-prone
[error-prone-pull-3301]: https://github.com/google/error-prone/pull/3301
@@ -229,3 +255,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

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>tech.picnic.error-prone-support</groupId>
<artifactId>error-prone-support</artifactId>
<version>0.9.0</version>
</parent>
<artifactId>documentation-support</artifactId>
<name>Picnic :: Error Prone Support :: Documentation Support</name>
<description>Data extraction support for the purpose of documentation generation.</description>
<dependencies>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_annotation</artifactId>
</dependency>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_check_api</artifactId>
</dependency>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_test_helpers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.auto</groupId>
<artifactId>auto-common</artifactId>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,103 @@
package tech.picnic.errorprone.documentation;
import static com.google.common.base.Verify.verify;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.Objects.requireNonNull;
import com.google.auto.common.AnnotationMirrors;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.util.Context;
import javax.lang.model.element.AnnotationValue;
import tech.picnic.errorprone.documentation.BugPatternExtractor.BugPatternDocumentation;
/**
* An {@link Extractor} that describes how to extract data from a {@code @BugPattern} annotation.
*/
@Immutable
final class BugPatternExtractor implements Extractor<BugPatternDocumentation> {
@Override
public BugPatternDocumentation extract(ClassTree tree, Context context) {
ClassSymbol symbol = ASTHelpers.getSymbol(tree);
BugPattern annotation = symbol.getAnnotation(BugPattern.class);
requireNonNull(annotation, "BugPattern annotation must be present");
return new AutoValue_BugPatternExtractor_BugPatternDocumentation(
symbol.getQualifiedName().toString(),
annotation.name().isEmpty() ? tree.getSimpleName().toString() : annotation.name(),
ImmutableList.copyOf(annotation.altNames()),
annotation.link(),
ImmutableList.copyOf(annotation.tags()),
annotation.summary(),
annotation.explanation(),
annotation.severity(),
annotation.disableable(),
annotation.documentSuppression() ? getSuppressionAnnotations(tree) : ImmutableList.of());
}
@Override
public boolean canExtract(ClassTree tree) {
return ASTHelpers.hasDirectAnnotationWithSimpleName(tree, BugPattern.class.getSimpleName());
}
/**
* Returns the fully-qualified class names of suppression annotations specified by the {@link
* BugPattern} annotation located on the given tree.
*
* @implNote This method cannot simply invoke {@link BugPattern#suppressionAnnotations()}, as that
* will yield an "Attempt to access Class objects for TypeMirrors" exception.
*/
private static ImmutableList<String> getSuppressionAnnotations(ClassTree tree) {
AnnotationTree annotationTree =
ASTHelpers.getAnnotationWithSimpleName(
ASTHelpers.getAnnotations(tree), BugPattern.class.getSimpleName());
requireNonNull(annotationTree, "BugPattern annotation must be present");
Attribute.Array types =
doCast(
AnnotationMirrors.getAnnotationValue(
ASTHelpers.getAnnotationMirror(annotationTree), "suppressionAnnotations"),
Attribute.Array.class);
return types.getValue().stream()
.map(v -> doCast(v, Attribute.Class.class).classType.toString())
.collect(toImmutableList());
}
@SuppressWarnings("unchecked")
private static <T extends AnnotationValue> T doCast(AnnotationValue value, Class<T> target) {
verify(target.isInstance(value), "Value '%s' is not of type '%s'", value, target);
return (T) value;
}
@AutoValue
abstract static class BugPatternDocumentation {
abstract String fullyQualifiedName();
abstract String name();
abstract ImmutableList<String> altNames();
abstract String link();
abstract ImmutableList<String> tags();
abstract String summary();
abstract String explanation();
abstract SeverityLevel severityLevel();
abstract boolean canDisable();
abstract ImmutableList<String> suppressionAnnotations();
}
}

View File

@@ -0,0 +1,56 @@
package tech.picnic.errorprone.documentation;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.auto.service.AutoService;
import com.google.common.annotations.VisibleForTesting;
import com.sun.source.util.JavacTask;
import com.sun.source.util.Plugin;
import com.sun.tools.javac.api.BasicJavacTask;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A compiler {@link Plugin} that analyzes and extracts relevant information for documentation
* purposes from processed files.
*/
// XXX: Find a better name for this class; it doesn't generate documentation per se.
@AutoService(Plugin.class)
public final class DocumentationGenerator implements Plugin {
@VisibleForTesting static final String OUTPUT_DIRECTORY_FLAG = "-XoutputDirectory";
private static final Pattern OUTPUT_DIRECTORY_FLAG_PATTERN =
Pattern.compile(Pattern.quote(OUTPUT_DIRECTORY_FLAG) + "=(.*)");
/** Instantiates a new {@link DocumentationGenerator} instance. */
public DocumentationGenerator() {}
@Override
public String getName() {
return getClass().getSimpleName();
}
@Override
public void init(JavacTask javacTask, String... args) {
checkArgument(args.length == 1, "Precisely one path must be provided");
javacTask.addTaskListener(
new DocumentationGeneratorTaskListener(
((BasicJavacTask) javacTask).getContext(), getOutputPath(args[0])));
}
@VisibleForTesting
static Path getOutputPath(String pathArg) {
Matcher matcher = OUTPUT_DIRECTORY_FLAG_PATTERN.matcher(pathArg);
checkArgument(
matcher.matches(), "'%s' must be of the form '%s=<value>'", pathArg, OUTPUT_DIRECTORY_FLAG);
String path = matcher.group(1);
try {
return Path.of(path);
} catch (InvalidPathException e) {
throw new IllegalArgumentException(String.format("Invalid path '%s'", path), e);
}
}
}

View File

@@ -0,0 +1,91 @@
package tech.picnic.errorprone.documentation;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.source.tree.ClassTree;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskEvent.Kind;
import com.sun.source.util.TaskListener;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.util.Context;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.tools.JavaFileObject;
/**
* A {@link TaskListener} that identifies and extracts relevant content for documentation generation
* and writes it to disk.
*/
// XXX: Find a better name for this class; it doesn't generate documentation per se.
final class DocumentationGeneratorTaskListener implements TaskListener {
private static final ObjectMapper OBJECT_MAPPER =
new ObjectMapper().setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
private final Context context;
private final Path docsPath;
DocumentationGeneratorTaskListener(Context context, Path path) {
this.context = context;
this.docsPath = path;
}
@Override
public void started(TaskEvent taskEvent) {
if (taskEvent.getKind() == Kind.ANALYZE) {
createDocsDirectory();
}
}
@Override
public void finished(TaskEvent taskEvent) {
if (taskEvent.getKind() != Kind.ANALYZE) {
return;
}
ClassTree classTree = JavacTrees.instance(context).getTree(taskEvent.getTypeElement());
JavaFileObject sourceFile = taskEvent.getSourceFile();
if (classTree == null || sourceFile == null) {
return;
}
ExtractorType.findMatchingType(classTree)
.ifPresent(
extractorType ->
writeToFile(
extractorType.getIdentifier(),
getSimpleClassName(sourceFile.toUri()),
extractorType.getExtractor().extract(classTree, context)));
}
private void createDocsDirectory() {
try {
Files.createDirectories(docsPath);
} catch (IOException e) {
throw new IllegalStateException(
String.format("Error while creating directory with path '%s'", docsPath), e);
}
}
private <T> void writeToFile(String identifier, String className, T data) {
File file = docsPath.resolve(String.format("%s-%s.json", identifier, className)).toFile();
try (FileWriter fileWriter = new FileWriter(file, UTF_8)) {
OBJECT_MAPPER.writeValue(fileWriter, data);
} catch (IOException e) {
throw new UncheckedIOException(String.format("Cannot write to file '%s'", file.getPath()), e);
}
}
private static String getSimpleClassName(URI path) {
return Paths.get(path).getFileName().toString().replace(".java", "");
}
}

View File

@@ -0,0 +1,33 @@
package tech.picnic.errorprone.documentation;
import com.google.errorprone.annotations.Immutable;
import com.sun.source.tree.ClassTree;
import com.sun.tools.javac.util.Context;
/**
* Interface implemented by classes that define how to extract data of some type {@link T} from a
* given {@link ClassTree}.
*
* @param <T> The type of data that is extracted.
*/
@Immutable
interface Extractor<T> {
/**
* Extracts and returns an instance of {@link T} using the provided arguments.
*
* @param tree The {@link ClassTree} to analyze and from which to extract instances of {@link T}.
* @param context The {@link Context} in which the current compilation takes place.
* @return A non-null instance of {@link T}.
*/
// XXX: Drop `Context` parameter unless used.
T extract(ClassTree tree, Context context);
/**
* Tells whether this {@link Extractor} can extract documentation content from the given {@link
* ClassTree}.
*
* @param tree The {@link ClassTree} of interest.
* @return {@code true} iff data extraction is supported.
*/
boolean canExtract(ClassTree tree);
}

View File

@@ -0,0 +1,35 @@
package tech.picnic.errorprone.documentation;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.sun.source.tree.ClassTree;
import java.util.EnumSet;
import java.util.Optional;
/** An enumeration of {@link Extractor} types. */
enum ExtractorType {
BUG_PATTERN("bugpattern", new BugPatternExtractor());
private static final ImmutableSet<ExtractorType> TYPES =
Sets.immutableEnumSet(EnumSet.allOf(ExtractorType.class));
private final String identifier;
private final Extractor<?> extractor;
ExtractorType(String identifier, Extractor<?> extractor) {
this.identifier = identifier;
this.extractor = extractor;
}
String getIdentifier() {
return identifier;
}
Extractor<?> getExtractor() {
return extractor;
}
static Optional<ExtractorType> findMatchingType(ClassTree tree) {
return TYPES.stream().filter(type -> type.getExtractor().canExtract(tree)).findFirst();
}
}

View File

@@ -0,0 +1,7 @@
/**
* A Java compiler plugin that extracts data from compiled classes, in support of the Error Prone
* Support documentation.
*/
@com.google.errorprone.annotations.CheckReturnValue
@org.jspecify.annotations.NullMarked
package tech.picnic.errorprone.documentation;

View File

@@ -0,0 +1,153 @@
package tech.picnic.errorprone.documentation;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import com.google.common.io.Resources;
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.ClassTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.ClassTree;
import java.io.IOException;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
final class BugPatternExtractorTest {
@Test
void noBugPattern(@TempDir Path outputDirectory) {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"TestCheckerWithoutAnnotation.java",
"import com.google.errorprone.bugpatterns.BugChecker;",
"",
"public final class TestCheckerWithoutAnnotation extends BugChecker {}");
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
}
@Test
void minimalBugPattern(@TempDir Path outputDirectory) throws IOException {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"MinimalBugChecker.java",
"package pkg;",
"",
"import com.google.errorprone.BugPattern;",
"import com.google.errorprone.BugPattern.SeverityLevel;",
"import com.google.errorprone.bugpatterns.BugChecker;",
"",
"@BugPattern(summary = \"MinimalBugChecker summary\", severity = SeverityLevel.ERROR)",
"public final class MinimalBugChecker extends BugChecker {}");
verifyFileMatchesResource(
outputDirectory,
"bugpattern-MinimalBugChecker.json",
"bugpattern-documentation-minimal.json");
}
@Test
void completeBugPattern(@TempDir Path outputDirectory) throws IOException {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"CompleteBugChecker.java",
"package pkg;",
"",
"import com.google.errorprone.BugPattern;",
"import com.google.errorprone.BugPattern.SeverityLevel;",
"import com.google.errorprone.bugpatterns.BugChecker;",
"import org.junit.jupiter.api.Test;",
"",
"@BugPattern(",
" name = \"OtherName\",",
" summary = \"CompleteBugChecker summary\",",
" linkType = BugPattern.LinkType.CUSTOM,",
" link = \"https://error-prone.picnic.tech\",",
" explanation = \"Example explanation\",",
" severity = SeverityLevel.SUGGESTION,",
" altNames = \"Check\",",
" tags = BugPattern.StandardTags.SIMPLIFICATION,",
" disableable = false,",
" suppressionAnnotations = {BugPattern.class, Test.class})",
"public final class CompleteBugChecker extends BugChecker {}");
verifyFileMatchesResource(
outputDirectory,
"bugpattern-CompleteBugChecker.json",
"bugpattern-documentation-complete.json");
}
@Test
void undocumentedSuppressionBugPattern(@TempDir Path outputDirectory) throws IOException {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"UndocumentedSuppressionBugPattern.java",
"package pkg;",
"",
"import com.google.errorprone.BugPattern;",
"import com.google.errorprone.BugPattern.SeverityLevel;",
"import com.google.errorprone.bugpatterns.BugChecker;",
"",
"@BugPattern(",
" summary = \"UndocumentedSuppressionBugPattern summary\",",
" severity = SeverityLevel.WARNING,",
" documentSuppression = false)",
"public final class UndocumentedSuppressionBugPattern extends BugChecker {}");
verifyFileMatchesResource(
outputDirectory,
"bugpattern-UndocumentedSuppressionBugPattern.json",
"bugpattern-documentation-undocumented-suppression.json");
}
@Test
void bugPatternAnnotationIsAbsent() {
CompilationTestHelper.newInstance(TestChecker.class, getClass())
.addSourceLines(
"TestChecker.java",
"import com.google.errorprone.bugpatterns.BugChecker;",
"",
"// BUG: Diagnostic contains: Can extract: false",
"public final class TestChecker extends BugChecker {}")
.doTest();
}
private static void verifyFileMatchesResource(
Path outputDirectory, String fileName, String resourceName) throws IOException {
assertThat(outputDirectory.resolve(fileName))
.content(UTF_8)
.isEqualToIgnoringWhitespace(getResource(resourceName));
}
// XXX: Once we support only JDK 15+, drop this method in favour of including the resources as
// text blocks in this class. (This also requires renaming the `verifyFileMatchesResource`
// method.)
private static String getResource(String resourceName) throws IOException {
return Resources.toString(
Resources.getResource(BugPatternExtractorTest.class, resourceName), UTF_8);
}
/** A {@link BugChecker} that validates the {@link BugPatternExtractor}. */
@BugPattern(summary = "Validates `BugPatternExtractor` extraction", severity = ERROR)
public static final class TestChecker extends BugChecker implements ClassTreeMatcher {
private static final long serialVersionUID = 1L;
@Override
public Description matchClass(ClassTree tree, VisitorState state) {
BugPatternExtractor extractor = new BugPatternExtractor();
assertThatThrownBy(() -> extractor.extract(tree, state.context))
.isInstanceOf(NullPointerException.class)
.hasMessage("BugPattern annotation must be present");
return buildDescription(tree)
.setMessage(String.format("Can extract: %s", extractor.canExtract(tree)))
.build();
}
}
}

View File

@@ -0,0 +1,45 @@
package tech.picnic.errorprone.documentation;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.FileManagers;
import com.google.errorprone.FileObjects;
import com.sun.tools.javac.api.JavacTaskImpl;
import com.sun.tools.javac.api.JavacTool;
import com.sun.tools.javac.file.JavacFileManager;
import java.nio.file.Path;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
// XXX: Generalize and move this class so that it can also be used by `refaster-compiler`.
// XXX: Add support for this class to the `ErrorProneTestHelperSourceFormat` check.
public final class Compilation {
private Compilation() {}
public static void compileWithDocumentationGenerator(
Path outputDirectory, String fileName, String... lines) {
compileWithDocumentationGenerator(outputDirectory.toAbsolutePath().toString(), fileName, lines);
}
public static void compileWithDocumentationGenerator(
String outputDirectory, String fileName, String... lines) {
compile(
ImmutableList.of("-Xplugin:DocumentationGenerator -XoutputDirectory=" + outputDirectory),
FileObjects.forSourceLines(fileName, lines));
}
private static void compile(ImmutableList<String> options, JavaFileObject javaFileObject) {
JavacFileManager javacFileManager = FileManagers.testFileManager();
JavaCompiler compiler = JavacTool.create();
JavacTaskImpl task =
(JavacTaskImpl)
compiler.getTask(
null,
javacFileManager,
null,
options,
ImmutableList.of(),
ImmutableList.of(javaFileObject));
task.call();
}
}

View File

@@ -0,0 +1,77 @@
package tech.picnic.errorprone.documentation;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.nio.file.attribute.AclEntryPermission.ADD_SUBDIRECTORY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.condition.OS.WINDOWS;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.io.IOException;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.AclEntry;
import java.nio.file.attribute.AclFileAttributeView;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.io.TempDir;
final class DocumentationGeneratorTaskListenerTest {
@EnabledOnOs(WINDOWS)
@Test
void readOnlyFileSystemWindows(@TempDir Path outputDirectory) throws IOException {
AclFileAttributeView view =
Files.getFileAttributeView(outputDirectory, AclFileAttributeView.class);
view.setAcl(
view.getAcl().stream()
.map(
entry ->
AclEntry.newBuilder(entry)
.setPermissions(
Sets.difference(entry.permissions(), ImmutableSet.of(ADD_SUBDIRECTORY)))
.build())
.collect(toImmutableList()));
readOnlyFileSystemFailsToWrite(outputDirectory.resolve("nonexistent"));
}
@DisabledOnOs(WINDOWS)
@Test
void readOnlyFileSystemNonWindows(@TempDir Path outputDirectory) {
assertThat(outputDirectory.toFile().setWritable(false))
.describedAs("Failed to make test directory unwritable")
.isTrue();
readOnlyFileSystemFailsToWrite(outputDirectory.resolve("nonexistent"));
}
private static void readOnlyFileSystemFailsToWrite(Path outputDirectory) {
assertThatThrownBy(
() ->
Compilation.compileWithDocumentationGenerator(
outputDirectory, "A.java", "class A {}"))
.hasRootCauseInstanceOf(FileSystemException.class)
.hasCauseInstanceOf(IllegalStateException.class)
.hasMessageEndingWith("Error while creating directory with path '%s'", outputDirectory);
}
@Test
void noClassNoOutput(@TempDir Path outputDirectory) {
Compilation.compileWithDocumentationGenerator(outputDirectory, "A.java", "package pkg;");
assertThat(outputDirectory).isEmptyDirectory();
}
@Test
void excessArguments(@TempDir Path outputDirectory) {
assertThatThrownBy(
() ->
Compilation.compileWithDocumentationGenerator(
outputDirectory.toAbsolutePath() + " extra-arg", "A.java", "package pkg;"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Precisely one path must be provided");
}
}

View File

@@ -0,0 +1,38 @@
package tech.picnic.errorprone.documentation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static tech.picnic.errorprone.documentation.DocumentationGenerator.OUTPUT_DIRECTORY_FLAG;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
final class DocumentationGeneratorTest {
@ParameterizedTest
@ValueSource(strings = {"bar", "foo"})
void getOutputPath(String path) {
assertThat(DocumentationGenerator.getOutputPath(OUTPUT_DIRECTORY_FLAG + '=' + path))
.isEqualTo(Path.of(path));
}
@ParameterizedTest
@ValueSource(strings = {"", "-XoutputDirectory", "invalidOption=Test", "nothing"})
void getOutputPathWithInvalidArgument(String pathArg) {
assertThatThrownBy(() -> DocumentationGenerator.getOutputPath(pathArg))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("'%s' must be of the form '%s=<value>'", pathArg, OUTPUT_DIRECTORY_FLAG);
}
@Test
void getOutputPathWithInvalidPath() {
String basePath = "path-with-null-char-\0";
assertThatThrownBy(
() -> DocumentationGenerator.getOutputPath(OUTPUT_DIRECTORY_FLAG + '=' + basePath))
.isInstanceOf(IllegalArgumentException.class)
.hasCauseInstanceOf(InvalidPathException.class)
.hasMessageEndingWith("Invalid path '%s'", basePath);
}
}

View File

@@ -0,0 +1,19 @@
{
"fullyQualifiedName": "pkg.CompleteBugChecker",
"name": "OtherName",
"altNames": [
"Check"
],
"link": "https://error-prone.picnic.tech",
"tags": [
"Simplification"
],
"summary": "CompleteBugChecker summary",
"explanation": "Example explanation",
"severityLevel": "SUGGESTION",
"canDisable": false,
"suppressionAnnotations": [
"com.google.errorprone.BugPattern",
"org.junit.jupiter.api.Test"
]
}

View File

@@ -0,0 +1,14 @@
{
"fullyQualifiedName": "pkg.MinimalBugChecker",
"name": "MinimalBugChecker",
"altNames": [],
"link": "",
"tags": [],
"summary": "MinimalBugChecker summary",
"explanation": "",
"severityLevel": "ERROR",
"canDisable": true,
"suppressionAnnotations": [
"java.lang.SuppressWarnings"
]
}

View File

@@ -0,0 +1,12 @@
{
"fullyQualifiedName": "pkg.UndocumentedSuppressionBugPattern",
"name": "UndocumentedSuppressionBugPattern",
"altNames": [],
"link": "",
"tags": [],
"summary": "UndocumentedSuppressionBugPattern summary",
"explanation": "",
"severityLevel": "WARNING",
"canDisable": true,
"suppressionAnnotations": []
}

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>tech.picnic.error-prone-support</groupId>
<artifactId>error-prone-support</artifactId>
<version>0.3.1-SNAPSHOT</version>
<version>0.9.0</version>
</parent>
<artifactId>error-prone-contrib</artifactId>
@@ -37,7 +37,15 @@
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_test_helpers</artifactId>
<scope>test</scope>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>documentation-support</artifactId>
<!-- This dependency is declared only as a hint to Maven that
compilation depends on it; see the `maven-compiler-plugin`'s
`annotationProcessorPaths` configuration below. -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
@@ -64,11 +72,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 +101,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 +154,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 +181,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>
@@ -208,6 +221,11 @@
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths combine.children="append">
<path>
<groupId>${project.groupId}</groupId>
<artifactId>documentation-support</artifactId>
<version>${project.version}</version>
</path>
<path>
<groupId>${project.groupId}</groupId>
<artifactId>refaster-compiler</artifactId>
@@ -221,6 +239,7 @@
</annotationProcessorPaths>
<compilerArgs combine.children="append">
<arg>-Xplugin:RefasterRuleCompiler</arg>
<arg>-Xplugin:DocumentationGenerator -XoutputDirectory=${project.build.directory}/docs</arg>
</compilerArgs>
</configuration>
</plugin>

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

@@ -0,0 +1,131 @@
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.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.argument;
import static com.google.errorprone.matchers.Matchers.isSameType;
import static com.google.errorprone.matchers.Matchers.isVariable;
import static com.google.errorprone.matchers.Matchers.not;
import static com.google.errorprone.matchers.Matchers.returnStatement;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import static com.google.errorprone.matchers.Matchers.toType;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.BlockTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Symbol;
import java.util.List;
import java.util.Optional;
import tech.picnic.errorprone.bugpatterns.util.MoreASTHelpers;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags unnecessary local variable assignments preceding a return
* statement.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Variable assignment is redundant; value can be returned directly",
link = BUG_PATTERNS_BASE_URL + "DirectReturn",
linkType = CUSTOM,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class DirectReturn extends BugChecker implements BlockTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<StatementTree> VARIABLE_RETURN = returnStatement(isVariable());
private static final Matcher<ExpressionTree> MOCKITO_MOCK_OR_SPY_WITH_IMPLICIT_TYPE =
allOf(
not(toType(MethodInvocationTree.class, argument(0, isSameType(Class.class.getName())))),
staticMethod().onClass("org.mockito.Mockito").namedAnyOf("mock", "spy"));
/** Instantiates a new {@link DirectReturn} instance. */
public DirectReturn() {}
@Override
public Description matchBlock(BlockTree tree, VisitorState state) {
List<? extends StatementTree> statements = tree.getStatements();
if (statements.size() < 2) {
return Description.NO_MATCH;
}
StatementTree finalStatement = statements.get(statements.size() - 1);
if (!VARIABLE_RETURN.matches(finalStatement, state)) {
return Description.NO_MATCH;
}
Symbol variableSymbol = ASTHelpers.getSymbol(((ReturnTree) finalStatement).getExpression());
StatementTree precedingStatement = statements.get(statements.size() - 2);
return tryMatchAssignment(variableSymbol, precedingStatement)
.filter(resultExpr -> canInlineToReturnStatement(resultExpr, state))
.map(
resultExpr ->
describeMatch(
precedingStatement,
SuggestedFix.builder()
.replace(
precedingStatement,
String.format("return %s;", SourceCode.treeToString(resultExpr, state)))
.delete(finalStatement)
.build()))
.orElse(Description.NO_MATCH);
}
private static Optional<ExpressionTree> tryMatchAssignment(Symbol targetSymbol, Tree tree) {
if (tree instanceof ExpressionStatementTree) {
return tryMatchAssignment(targetSymbol, ((ExpressionStatementTree) tree).getExpression());
}
if (tree instanceof AssignmentTree) {
AssignmentTree assignment = (AssignmentTree) tree;
return targetSymbol.equals(ASTHelpers.getSymbol(assignment.getVariable()))
? Optional.of(assignment.getExpression())
: Optional.empty();
}
if (tree instanceof VariableTree) {
VariableTree declaration = (VariableTree) tree;
return declaration.getModifiers().getAnnotations().isEmpty()
&& targetSymbol.equals(ASTHelpers.getSymbol(declaration))
? Optional.ofNullable(declaration.getInitializer())
: Optional.empty();
}
return Optional.empty();
}
/**
* Tells whether inlining the given expression to the associated return statement can be done
* safely.
*
* <p>Inlining is generally safe, but in rare cases the operation may have a functional impact.
* The sole case considered here is the inlining of a Mockito mock or spy construction without an
* explicit type. In such a case the type created depends on context, such as the method's return
* type.
*/
private static boolean canInlineToReturnStatement(
ExpressionTree expressionTree, VisitorState state) {
return !MOCKITO_MOCK_OR_SPY_WITH_IMPLICIT_TYPE.matches(expressionTree, state)
|| MoreASTHelpers.findMethodExitedOnReturn(state)
.filter(m -> MoreASTHelpers.areSameType(expressionTree, m.getReturnType(), state))
.isPresent();
}
}

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

@@ -0,0 +1,100 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.common.base.Preconditions.checkState;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.CONCURRENCY;
import static com.google.errorprone.BugPattern.StandardTags.PERFORMANCE;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
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.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.MethodInvocationTree;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.util.Position;
import java.util.stream.Stream;
import tech.picnic.errorprone.bugpatterns.util.ThirdPartyLibrary;
/**
* A {@link BugChecker} that flags {@link reactor.core.publisher.Flux} operator usages that may
* implicitly cause the calling thread to be blocked.
*
* <p>Note that the methods flagged here are not themselves blocking, but iterating over the
* resulting {@link Iterable} or {@link Stream} may be.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Avoid iterating over `Flux`es in an implicitly blocking manner",
link = BUG_PATTERNS_BASE_URL + "FluxImplicitBlock",
linkType = CUSTOM,
severity = WARNING,
tags = {CONCURRENCY, PERFORMANCE})
public final class FluxImplicitBlock extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> FLUX_WITH_IMPLICIT_BLOCK =
instanceMethod()
.onDescendantOf("reactor.core.publisher.Flux")
.namedAnyOf("toIterable", "toStream")
.withNoParameters();
private static final Supplier<Type> STREAM = Suppliers.typeFromString(Stream.class.getName());
/** Instantiates a new {@link FluxImplicitBlock} instance. */
public FluxImplicitBlock() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!FLUX_WITH_IMPLICIT_BLOCK.matches(tree, state)) {
return Description.NO_MATCH;
}
Description.Builder description =
buildDescription(tree).addFix(SuggestedFixes.addSuppressWarnings(state, canonicalName()));
if (ThirdPartyLibrary.GUAVA.isIntroductionAllowed(state)) {
description.addFix(
suggestBlockingElementCollection(
tree, "com.google.common.collect.ImmutableList.toImmutableList", state));
}
description.addFix(
suggestBlockingElementCollection(tree, "java.util.stream.Collectors.toList", state));
return description.build();
}
private static SuggestedFix suggestBlockingElementCollection(
MethodInvocationTree tree, String fullyQualifiedCollectorMethod, VisitorState state) {
SuggestedFix.Builder importSuggestion = SuggestedFix.builder();
String replacementMethodInvocation =
SuggestedFixes.qualifyStaticImport(fullyQualifiedCollectorMethod, importSuggestion, state);
boolean isStream =
ASTHelpers.isSubtype(ASTHelpers.getResultType(tree), STREAM.get(state), state);
String replacement =
String.format(
".collect(%s()).block()%s", replacementMethodInvocation, isStream ? ".stream()" : "");
return importSuggestion.merge(replaceMethodInvocation(tree, replacement, state)).build();
}
private static SuggestedFix.Builder replaceMethodInvocation(
MethodInvocationTree tree, String replacement, VisitorState state) {
int startPosition = state.getEndPosition(ASTHelpers.getReceiver(tree));
int endPosition = state.getEndPosition(tree);
checkState(
startPosition != Position.NOPOS && endPosition != Position.NOPOS,
"Cannot locate method to be replaced in source code");
return SuggestedFix.builder().replace(startPosition, endPosition, replacement);
}
}

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,18 @@ 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.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,20 +23,12 @@ 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 java.util.Optional;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
import tech.picnic.errorprone.bugpatterns.util.ConflictDetection;
/** A {@link BugChecker} that flags non-canonical JUnit method declarations. */
// XXX: Consider introducing a class-level check that enforces that test classes:
@@ -56,32 +47,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 +81,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)
ConflictDetection.findMethodRenameBlocker(symbol, newName, state)
.ifPresentOrElse(
blocker -> reportMethodRenameBlocker(tree, blocker, state),
() -> fixBuilder.merge(SuggestedFixes.renameMethod(tree, newName, state))));
@@ -122,63 +101,8 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
.build());
}
/**
* If applicable, returns a human-readable argument against assigning the given name to an
* existing method.
*
* <p>This method implements imperfect heuristics. Things it currently does not consider include
* the following:
*
* <ul>
* <li>Whether the rename would merely introduce a method overload, rather than clashing with an
* existing method declaration.
* <li>Whether the rename would cause a method in a superclass to be overridden.
* <li>Whether the rename would in fact clash with a static import. (It could be that a static
* import of the same name is only referenced from lexical scopes in which the method under
* consideration cannot be referenced directly.)
* </ul>
*/
private static Optional<String> findMethodRenameBlocker(String methodName, VisitorState state) {
if (isMethodInEnclosingClass(methodName, state)) {
return Optional.of(
String.format("a method named `%s` already exists in this class", methodName));
}
if (isSimpleNameStaticallyImported(methodName, state)) {
return Optional.of(String.format("`%s` is already statically imported", methodName));
}
if (isReservedKeyword(methodName)) {
return Optional.of(String.format("`%s` is a reserved keyword", methodName));
}
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 isSimpleNameStaticallyImported(String simpleName, VisitorState state) {
return state.getPath().getCompilationUnit().getImports().stream()
.filter(ImportTree::isStatic)
.map(ImportTree::getQualifiedIdentifier)
.map(tree -> getStaticImportSimpleName(tree, state))
.anyMatch(simpleName::contentEquals);
}
private static CharSequence getStaticImportSimpleName(Tree tree, VisitorState state) {
String source = SourceCode.treeToString(tree, state);
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(MethodSymbol 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 +110,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

@@ -0,0 +1,307 @@
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.ALL;
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.anyOf;
import static com.google.errorprone.matchers.Matchers.anything;
import static com.google.errorprone.matchers.Matchers.argument;
import static com.google.errorprone.matchers.Matchers.argumentCount;
import static com.google.errorprone.matchers.Matchers.classLiteral;
import static com.google.errorprone.matchers.Matchers.hasArguments;
import static com.google.errorprone.matchers.Matchers.isPrimitiveOrBoxedPrimitiveType;
import static com.google.errorprone.matchers.Matchers.isSameType;
import static com.google.errorprone.matchers.Matchers.methodHasParameters;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import static com.google.errorprone.matchers.Matchers.toType;
import static java.util.function.Predicate.not;
import static java.util.stream.Collectors.joining;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.HAS_METHOD_SOURCE;
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.getMethodSourceFactoryNames;
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.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;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags JUnit tests with a {@link
* org.junit.jupiter.params.provider.MethodSource} annotation that can be replaced with an
* equivalent {@link org.junit.jupiter.params.provider.ValueSource} annotation.
*/
// XXX: Where applicable, also flag `@MethodSource` annotations that reference multiple value
// factory methods (or that repeat the same value factory method multiple times).
// XXX: Support inlining of overloaded value factory methods.
// XXX: Support inlining of value factory methods referenced by multiple `@MethodSource`
// annotations.
// XXX: Support value factory return expressions of the form `Stream.of(a, b,
// c).map(Arguments::argument)`.
// XXX: Support simplification of test methods that accept additional injected parameters such as
// `TestInfo`; such parameters should be ignored for the purpose of this check.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Prefer `@ValueSource` over a `@MethodSource` where possible and reasonable",
linkType = CUSTOM,
link = BUG_PATTERNS_BASE_URL + "JUnitValueSource",
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class JUnitValueSource extends BugChecker implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> SUPPORTED_VALUE_FACTORY_VALUES =
anyOf(
isArrayArgumentValueCandidate(),
toType(
MethodInvocationTree.class,
allOf(
staticMethod()
.onClass("org.junit.jupiter.params.provider.Arguments")
.namedAnyOf("arguments", "of"),
argumentCount(1),
argument(0, isArrayArgumentValueCandidate()))));
private static final Matcher<ExpressionTree> ARRAY_OF_SUPPORTED_SINGLE_VALUE_ARGUMENTS =
isSingleDimensionArrayCreationWithAllElementsMatching(SUPPORTED_VALUE_FACTORY_VALUES);
private static final Matcher<ExpressionTree> ENUMERATION_OF_SUPPORTED_SINGLE_VALUE_ARGUMENTS =
toType(
MethodInvocationTree.class,
allOf(
staticMethod()
.onClassAny(
Stream.class.getName(),
IntStream.class.getName(),
LongStream.class.getName(),
DoubleStream.class.getName(),
List.class.getName(),
Set.class.getName(),
"com.google.common.collect.ImmutableList",
"com.google.common.collect.ImmutableSet")
.named("of"),
hasArguments(AT_LEAST_ONE, anything()),
hasArguments(ALL, SUPPORTED_VALUE_FACTORY_VALUES)));
private static final Matcher<MethodTree> IS_UNARY_METHOD_WITH_SUPPORTED_PARAMETER =
methodHasParameters(
anyOf(
isPrimitiveOrBoxedPrimitiveType(),
isSameType(String.class),
isSameType(state -> state.getSymtab().classType)));
/** Instantiates a new {@link JUnitValueSource} instance. */
public JUnitValueSource() {}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (!IS_UNARY_METHOD_WITH_SUPPORTED_PARAMETER.matches(tree, state)) {
return Description.NO_MATCH;
}
Type parameterType = ASTHelpers.getType(Iterables.getOnlyElement(tree.getParameters()));
return findMethodSourceAnnotation(tree, state)
.flatMap(
methodSourceAnnotation ->
getSoleLocalFactoryName(methodSourceAnnotation, tree)
.filter(factory -> !hasSiblingReferencingValueFactory(tree, factory, state))
.flatMap(factory -> findSiblingWithName(tree, factory, state))
.flatMap(
factoryMethod ->
tryConstructValueSourceFix(
parameterType, methodSourceAnnotation, factoryMethod, state))
.map(fix -> describeMatch(methodSourceAnnotation, fix)))
.orElse(Description.NO_MATCH);
}
/**
* Returns the name of the value factory method pointed to by the given {@code @MethodSource}
* annotation, if it (a) is the only one and (b) is a method in the same class as the annotated
* method.
*/
private static Optional<String> getSoleLocalFactoryName(
AnnotationTree methodSourceAnnotation, MethodTree method) {
return getElementIfSingleton(getMethodSourceFactoryNames(methodSourceAnnotation, method))
.filter(name -> name.indexOf('#') < 0);
}
/**
* Tells whether the given method has a sibling method in the same class that depends on the
* specified value factory method.
*/
private static boolean hasSiblingReferencingValueFactory(
MethodTree tree, String valueFactory, VisitorState state) {
return findMatchingSibling(tree, m -> hasValueFactory(m, valueFactory, state), state)
.isPresent();
}
private static Optional<MethodTree> findSiblingWithName(
MethodTree tree, String methodName, VisitorState state) {
return findMatchingSibling(tree, m -> m.getName().contentEquals(methodName), state);
}
private static Optional<MethodTree> findMatchingSibling(
MethodTree tree, Predicate<? super MethodTree> predicate, VisitorState state) {
return state.findEnclosing(ClassTree.class).getMembers().stream()
.filter(MethodTree.class::isInstance)
.map(MethodTree.class::cast)
.filter(not(tree::equals))
.filter(predicate)
.findFirst();
}
private static boolean hasValueFactory(
MethodTree tree, String valueFactoryMethodName, VisitorState state) {
return findMethodSourceAnnotation(tree, state).stream()
.anyMatch(
annotation ->
getMethodSourceFactoryNames(annotation, tree).contains(valueFactoryMethodName));
}
private static Optional<AnnotationTree> findMethodSourceAnnotation(
MethodTree tree, VisitorState state) {
return HAS_METHOD_SOURCE.multiMatchResult(tree, state).matchingNodes().stream().findFirst();
}
private static Optional<SuggestedFix> tryConstructValueSourceFix(
Type parameterType,
AnnotationTree methodSourceAnnotation,
MethodTree valueFactoryMethod,
VisitorState state) {
return getSingleReturnExpression(valueFactoryMethod)
.flatMap(expression -> tryExtractValueSourceAttributeValue(expression, state))
.map(
valueSourceAttributeValue ->
SuggestedFix.builder()
.addImport("org.junit.jupiter.params.provider.ValueSource")
.replace(
methodSourceAnnotation,
String.format(
"@ValueSource(%s = %s)",
toValueSourceAttributeName(parameterType), valueSourceAttributeValue))
.delete(valueFactoryMethod)
.build());
}
// XXX: This pattern also occurs a few times inside Error Prone; contribute upstream.
private static Optional<ExpressionTree> getSingleReturnExpression(MethodTree methodTree) {
List<ExpressionTree> returnExpressions = new ArrayList<>();
new TreeScanner<@Nullable Void, @Nullable Void>() {
@Override
public @Nullable Void visitClass(ClassTree node, @Nullable Void unused) {
/* Ignore `return` statements inside anonymous/local classes. */
return null;
}
@Override
public @Nullable Void visitReturn(ReturnTree node, @Nullable Void unused) {
returnExpressions.add(node.getExpression());
return super.visitReturn(node, unused);
}
@Override
public @Nullable Void visitLambdaExpression(
LambdaExpressionTree node, @Nullable Void unused) {
/* Ignore `return` statements inside lambda expressions. */
return null;
}
}.scan(methodTree, null);
return getElementIfSingleton(returnExpressions);
}
private static Optional<String> tryExtractValueSourceAttributeValue(
ExpressionTree tree, VisitorState state) {
List<? extends ExpressionTree> arguments;
if (ENUMERATION_OF_SUPPORTED_SINGLE_VALUE_ARGUMENTS.matches(tree, state)) {
arguments = ((MethodInvocationTree) tree).getArguments();
} else if (ARRAY_OF_SUPPORTED_SINGLE_VALUE_ARGUMENTS.matches(tree, state)) {
arguments = ((NewArrayTree) tree).getInitializers();
} else {
return Optional.empty();
}
/*
* Join the values into a comma-separated string, unwrapping `Arguments` factory method
* invocations if applicable.
*/
return Optional.of(
arguments.stream()
.map(
arg ->
arg instanceof MethodInvocationTree
? Iterables.getOnlyElement(((MethodInvocationTree) arg).getArguments())
: arg)
.map(argument -> SourceCode.treeToString(argument, state))
.collect(joining(", ")))
.map(value -> arguments.size() > 1 ? String.format("{%s}", value) : value);
}
private static String toValueSourceAttributeName(Type type) {
String typeString = type.tsym.name.toString();
switch (typeString) {
case "Class":
return "classes";
case "Character":
return "chars";
case "Integer":
return "ints";
default:
return typeString.toLowerCase(Locale.ROOT) + 's';
}
}
private static <T> Optional<T> getElementIfSingleton(Collection<T> collection) {
return Optional.of(collection)
.filter(elements -> elements.size() == 1)
.map(Iterables::getOnlyElement);
}
private static Matcher<ExpressionTree> isSingleDimensionArrayCreationWithAllElementsMatching(
Matcher<? super ExpressionTree> elementMatcher) {
return (tree, state) -> {
if (!(tree instanceof NewArrayTree)) {
return false;
}
NewArrayTree newArray = (NewArrayTree) tree;
return newArray.getDimensions().isEmpty()
&& !newArray.getInitializers().isEmpty()
&& newArray.getInitializers().stream()
.allMatch(element -> elementMatcher.matches(element, state));
};
}
private static Matcher<ExpressionTree> isArrayArgumentValueCandidate() {
return anyOf(classLiteral(anything()), (tree, state) -> ASTHelpers.constValue(tree) != null);
}
}

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

@@ -0,0 +1,86 @@
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.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.argument;
import static com.google.errorprone.matchers.Matchers.isSameType;
import static com.google.errorprone.matchers.Matchers.isVariable;
import static com.google.errorprone.matchers.Matchers.not;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
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.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import java.util.List;
import tech.picnic.errorprone.bugpatterns.util.MoreASTHelpers;
/**
* A {@link BugChecker} that flags the use of {@link org.mockito.Mockito#mock(Class)} and {@link
* org.mockito.Mockito#spy(Class)} where instead the type to be mocked or spied can be derived from
* context.
*/
// XXX: This check currently does not flag method invocation arguments. When adding support for
// this, consider that in some cases the type to be mocked or spied must be specified explicitly so
// as to disambiguate between method overloads.
// XXX: This check currently does not flag (implicit or explicit) lambda return expressions.
// XXX: This check currently does not drop suppressions that become obsolete after the
// suggested fix is applied; consider adding support for this.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Don't unnecessarily pass a type to Mockito's `mock(Class)` and `spy(Class)` methods",
link = BUG_PATTERNS_BASE_URL + "MockitoMockClassReference",
linkType = CUSTOM,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class MockitoMockClassReference extends BugChecker
implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<MethodInvocationTree> MOCKITO_MOCK_OR_SPY_WITH_HARDCODED_TYPE =
allOf(
argument(0, allOf(isSameType(Class.class.getName()), not(isVariable()))),
staticMethod().onClass("org.mockito.Mockito").namedAnyOf("mock", "spy"));
/** Instantiates a new {@link MockitoMockClassReference} instance. */
public MockitoMockClassReference() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!MOCKITO_MOCK_OR_SPY_WITH_HARDCODED_TYPE.matches(tree, state)
|| !isTypeDerivableFromContext(tree, state)) {
return Description.NO_MATCH;
}
List<? extends ExpressionTree> arguments = tree.getArguments();
return describeMatch(tree, SuggestedFixes.removeElement(arguments.get(0), arguments, state));
}
private static boolean isTypeDerivableFromContext(MethodInvocationTree tree, VisitorState state) {
Tree parent = state.getPath().getParentPath().getLeaf();
switch (parent.getKind()) {
case VARIABLE:
return !ASTHelpers.hasNoExplicitType((VariableTree) parent, state)
&& MoreASTHelpers.areSameType(tree, parent, state);
case ASSIGNMENT:
return MoreASTHelpers.areSameType(tree, parent, state);
case RETURN:
return MoreASTHelpers.findMethodExitedOnReturn(state)
.filter(m -> MoreASTHelpers.areSameType(tree, m.getReturnType(), state))
.isPresent();
default:
return false;
}
}
}

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

@@ -7,6 +7,7 @@ import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.anyMethod;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.anything;
import static com.google.errorprone.matchers.Matchers.argumentCount;
import static com.google.errorprone.matchers.Matchers.isNonNullUsingDataflow;
import static com.google.errorprone.matchers.Matchers.isSameType;
@@ -49,6 +50,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,13 +65,10 @@ 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";
@SuppressWarnings("UnnecessaryLambda")
private static final Matcher<ExpressionTree> ANY_EXPR = (t, s) -> true;
"RedundantStringConversion:ExtraConversionMethods";
private static final Matcher<ExpressionTree> ANY_EXPR = anything();
private static final Matcher<ExpressionTree> LOCALE = isSameType(Locale.class);
private static final Matcher<ExpressionTree> MARKER = isSubtypeOf("org.slf4j.Marker");
private static final Matcher<ExpressionTree> STRING = isSameType(String.class);
@@ -141,7 +140,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 +373,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

@@ -74,15 +74,22 @@ public final class RequestMappingAnnotation extends BugChecker implements Method
isSameType("java.time.ZoneId"),
isSameType("java.util.Locale"),
isSameType("java.util.TimeZone"),
isSameType("jakarta.servlet.http.HttpServletRequest"),
isSameType("jakarta.servlet.http.HttpServletResponse"),
isSameType("javax.servlet.http.HttpServletRequest"),
isSameType("javax.servlet.http.HttpServletResponse"),
isSameType("org.springframework.http.HttpMethod"),
isSameType("org.springframework.ui.Model"),
isSameType("org.springframework.validation.BindingResult"),
isSameType("org.springframework.web.context.request.NativeWebRequest"),
isSameType("org.springframework.web.context.request.WebRequest"),
isSameType("org.springframework.web.server.ServerWebExchange"),
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,75 @@
package tech.picnic.errorprone.bugpatterns.util;
import static tech.picnic.errorprone.bugpatterns.util.JavaKeywords.isValidIdentifier;
import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Type;
import java.util.Optional;
/** A set of helper methods for detecting conflicts that would be caused when applying fixes. */
public final class ConflictDetection {
private ConflictDetection() {}
/**
* If applicable, returns a human-readable argument against assigning the given name to an
* existing method.
*
* <p>This method implements imperfect heuristics. Things it currently does not consider include
* the following:
*
* <ul>
* <li>Whether the rename would merely introduce a method overload, rather than clashing with an
* existing method declaration in its class or a supertype.
* <li>Whether the rename would in fact clash with a static import. (It could be that a static
* import of the same name is only referenced from lexical scopes in which the method under
* consideration cannot be referenced directly.)
* </ul>
*
* @param method The method considered for renaming.
* @param newName The newly proposed name for the method.
* @param state The {@link VisitorState} to use when searching for blockers.
* @return A human-readable argument against assigning the proposed name to the given method, or
* {@link Optional#empty()} if no blocker was found.
*/
public 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` is already defined in this class or a supertype", newName));
}
if (isSimpleNameStaticallyImported(newName, state)) {
return Optional.of(String.format("`%s` is already statically imported", newName));
}
if (!isValidIdentifier(newName)) {
return Optional.of(String.format("`%s` is not a valid identifier", newName));
}
return Optional.empty();
}
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) {
return state.getPath().getCompilationUnit().getImports().stream()
.filter(ImportTree::isStatic)
.map(ImportTree::getQualifiedIdentifier)
.map(tree -> getStaticImportSimpleName(tree, state))
.anyMatch(simpleName::contentEquals);
}
private static CharSequence getStaticImportSimpleName(Tree tree, VisitorState state) {
String source = SourceCode.treeToString(tree, state);
return source.subSequence(source.lastIndexOf('.') + 1, source.length());
}
}

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,82 @@
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.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import java.util.Optional;
/**
* 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();
}
/**
* Returns the {@link MethodTree} from which control flow would exit if there would be a {@code
* return} statement at the given {@link VisitorState}'s current {@link VisitorState#getPath()
* path}.
*
* @param state The {@link VisitorState} from which to derive the AST location of interest.
* @return A {@link MethodTree}, unless the {@link VisitorState}'s path does not point to an AST
* node located inside a method, or if the (hypothetical) {@code return} statement would exit
* a lambda expression instead.
*/
public static Optional<MethodTree> findMethodExitedOnReturn(VisitorState state) {
return Optional.ofNullable(state.findEnclosing(LambdaExpressionTree.class, MethodTree.class))
.filter(MethodTree.class::isInstance)
.map(MethodTree.class::cast);
}
/**
* Tells whether the given trees are of the same type, after type erasure.
*
* @param treeA The first tree of interest.
* @param treeB The second tree of interest.
* @param state The {@link VisitorState} describing the context in which the given trees were
* found.
* @return Whether the specified trees have the same erased types.
*/
public static boolean areSameType(Tree treeA, Tree treeB, VisitorState state) {
return ASTHelpers.isSameType(ASTHelpers.getType(treeA), ASTHelpers.getType(treeB), state);
}
}

View File

@@ -0,0 +1,115 @@
package tech.picnic.errorprone.bugpatterns.util;
import static com.google.common.collect.ImmutableList.toImmutableList;
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.ImmutableList;
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.
*
* <p>This method differs from {@link #getMethodSourceFactoryDescriptors(AnnotationTree,
* MethodTree)} in that it drops any parenthesized method parameter type enumerations. That is,
* method descriptors such as {@code factoryMethod()} and {@code factoryMethod(java.lang.String)}
* are both simplified to just {@code factoryMethod}. This also means that the returned method
* names may not unambiguously reference a single value factory method; in such a case JUnit will
* throw an error at runtime.
*
* @param methodSourceAnnotation The annotation from which to extract value factory method names.
* @param method The method on which the annotation is located.
* @return One or more value factory descriptors, in the order defined.
* @see #getMethodSourceFactoryDescriptors(AnnotationTree, MethodTree)
*/
// XXX: Drop this method in favour of `#getMethodSourceFactoryDescriptors`. That will require
// callers to either explicitly drop information, or perform a more advanced analysis.
public static ImmutableList<String> getMethodSourceFactoryNames(
AnnotationTree methodSourceAnnotation, MethodTree method) {
return getMethodSourceFactoryDescriptors(methodSourceAnnotation, method).stream()
.map(
descriptor -> {
int index = descriptor.indexOf('(');
return index < 0 ? descriptor : descriptor.substring(0, index);
})
.collect(toImmutableList());
}
/**
* Returns the descriptors 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
* descriptors.
* @param method The method on which the annotation is located.
* @return One or more value factory descriptors, in the order defined.
* @see #getMethodSourceFactoryNames(AnnotationTree, MethodTree)
*/
// XXX: Rather than strings, have this method return instances of a value type capable of
// resolving the value factory method pointed to.
public static ImmutableList<String> getMethodSourceFactoryDescriptors(
AnnotationTree methodSourceAnnotation, MethodTree method) {
String methodName = method.getName().toString();
ExpressionTree value = AnnotationMatcherUtils.getArgument(methodSourceAnnotation, "value");
if (!(value instanceof NewArrayTree)) {
return ImmutableList.of(toMethodSourceFactoryDescriptor(value, methodName));
}
return ((NewArrayTree) value)
.getInitializers().stream()
.map(name -> toMethodSourceFactoryDescriptor(name, methodName))
.collect(toImmutableList());
}
private static String toMethodSourceFactoryDescriptor(
@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

@@ -1,11 +1,16 @@
package tech.picnic.errorprone.refasterrules;
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.AbstractStringAssert;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
@@ -89,4 +94,30 @@ final class AssertJStringRules {
return assertThat(string).doesNotMatch(regex);
}
}
static final class AssertThatPathContent {
@BeforeTemplate
AbstractStringAssert<?> before(Path path, Charset charset) throws IOException {
return assertThat(Files.readString(path, charset));
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractStringAssert<?> after(Path path, Charset charset) {
return assertThat(path).content(charset);
}
}
static final class AssertThatPathContentUtf8 {
@BeforeTemplate
AbstractStringAssert<?> before(Path path) throws IOException {
return assertThat(Files.readString(path));
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractStringAssert<?> after(Path path) {
return assertThat(path).content(UTF_8);
}
}
}

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

@@ -0,0 +1,53 @@
package tech.picnic.errorprone.refasterrules;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChooser;
import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers;
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/** Refaster rules related to {@link com.google.errorprone.bugpatterns.BugChecker} classes. */
@OnlineDocumentation
final class BugCheckerRules {
private BugCheckerRules() {}
/**
* Avoid calling {@link BugCheckerRefactoringTestHelper#setFixChooser(FixChooser)} or {@link
* BugCheckerRefactoringTestHelper#setImportOrder(String)} with their respective default values.
*/
static final class BugCheckerRefactoringTestHelperIdentity {
@BeforeTemplate
BugCheckerRefactoringTestHelper before(BugCheckerRefactoringTestHelper helper) {
return Refaster.anyOf(
helper.setFixChooser(FixChoosers.FIRST), helper.setImportOrder("static-first"));
}
@AfterTemplate
BugCheckerRefactoringTestHelper after(BugCheckerRefactoringTestHelper helper) {
return helper;
}
}
/**
* Prefer {@link BugCheckerRefactoringTestHelper.ExpectOutput#expectUnchanged()} over repeating
* the input.
*/
// XXX: This rule assumes that the full source code is specified as a single string, e.g. using a
// text block. Support for multi-line source code input would require a `BugChecker`
// implementation instead.
static final class BugCheckerRefactoringTestHelperAddInputLinesExpectUnchanged {
@BeforeTemplate
BugCheckerRefactoringTestHelper before(
BugCheckerRefactoringTestHelper helper, String path, String source) {
return helper.addInputLines(path, source).addOutputLines(path, source);
}
@AfterTemplate
BugCheckerRefactoringTestHelper after(
BugCheckerRefactoringTestHelper helper, String path, String source) {
return helper.addInputLines(path, source).expectUnchanged();
}
}
}

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

@@ -6,6 +6,7 @@ import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static java.util.function.Function.identity;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Streams;
@@ -126,8 +127,8 @@ final class ImmutableMapRules {
}
/**
* Don't map a a stream's elements to map entries, only to subsequently collect them into an
* {@link ImmutableMap}. The collection can be performed directly.
* Don't map a stream's elements to map entries, only to subsequently collect them into an {@link
* ImmutableMap}. The collection can be performed directly.
*/
abstract static class StreamOfMapEntriesToImmutableMap<E, K, V> {
@Placeholder(allowsIdentity = true)
@@ -315,6 +316,48 @@ final class ImmutableMapRules {
}
}
/**
* Prefer creation of an immutable submap using {@link Maps#filterKeys(Map, Predicate)} over more
* contrived alternatives.
*/
abstract static class ImmutableMapCopyOfMapsFilterKeys<K, V> {
@Placeholder(allowsIdentity = true)
abstract boolean keyFilter(@MayOptionallyUse K key);
@BeforeTemplate
ImmutableMap<K, V> before(ImmutableMap<K, V> map) {
return map.entrySet().stream()
.filter(e -> keyFilter(e.getKey()))
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
}
@AfterTemplate
ImmutableMap<K, V> after(ImmutableMap<K, V> map) {
return ImmutableMap.copyOf(Maps.filterKeys(map, k -> keyFilter(k)));
}
}
/**
* Prefer creation of an immutable submap using {@link Maps#filterValues(Map, Predicate)} over
* more contrived alternatives.
*/
abstract static class ImmutableMapCopyOfMapsFilterValues<K, V> {
@Placeholder(allowsIdentity = true)
abstract boolean valueFilter(@MayOptionallyUse V value);
@BeforeTemplate
ImmutableMap<K, V> before(ImmutableMap<K, V> map) {
return map.entrySet().stream()
.filter(e -> valueFilter(e.getValue()))
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
}
@AfterTemplate
ImmutableMap<K, V> after(ImmutableMap<K, V> map) {
return ImmutableMap.copyOf(Maps.filterValues(map, v -> valueFilter(v)));
}
}
// XXX: Add a rule for this:
// Maps.transformValues(streamOfEntries.collect(groupBy(fun)), ImmutableMap::copyOf)
// ->
@@ -323,9 +366,4 @@ final class ImmutableMapRules {
// map.entrySet().stream().filter(keyPred).forEach(mapBuilder::put)
// ->
// mapBuilder.putAll(Maps.filterKeys(map, pred))
//
// map.entrySet().stream().filter(entry ->
// pred(entry.getKey())).collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue))
// ->
// ImmutableMap.copyOf(Maps.filterKeys(map, pred))
}

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,140 @@
package tech.picnic.errorprone.refasterrules;
import static java.util.Objects.requireNonNullElse;
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#getOrDefault(Object, Object)} over more contrived alternatives. */
// XXX: Note that `requireNonNullElse` throws an NPE if the second argument is `null`, while the
// alternative does not.
static final class MapGetOrDefault<K, V, T> {
@BeforeTemplate
V before(Map<K, V> map, T key, V defaultValue) {
return requireNonNullElse(map.get(key), defaultValue);
}
@AfterTemplate
V after(Map<K, V> map, T key, V defaultValue) {
return map.getOrDefault(key, defaultValue);
}
}
/** 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. */
@@ -17,11 +21,14 @@ import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
final class NullRules {
private NullRules() {}
/** Prefer the {@code ==} operator over {@link Objects#isNull(Object)}. */
/**
* Prefer the {@code ==} operator (with {@code null} as the second operand) over {@link
* Objects#isNull(Object)}.
*/
static final class IsNull {
@BeforeTemplate
boolean before(@Nullable Object object) {
return Objects.isNull(object);
return Refaster.anyOf(null == object, Objects.isNull(object));
}
@AfterTemplate
@@ -30,11 +37,14 @@ final class NullRules {
}
}
/** Prefer the {@code !=} operator over {@link Objects#nonNull(Object)}. */
/**
* Prefer the {@code !=} operator (with {@code null} as the second operand) over {@link
* Objects#nonNull(Object)}.
*/
static final class IsNotNull {
@BeforeTemplate
boolean before(@Nullable Object object) {
return Objects.nonNull(object);
return Refaster.anyOf(null != object, Objects.nonNull(object));
}
@AfterTemplate
@@ -43,13 +53,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 +74,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,206 @@
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 static java.util.Objects.requireNonNull;
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 java.util.Objects;
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 Objects#requireNonNull(Object)} over non-JDK alternatives. */
static final class RequireNonNull<T> {
@BeforeTemplate
T before(T object) {
return checkNotNull(object);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
T after(T object) {
return requireNonNull(object);
}
}
/** Prefer {@link Objects#requireNonNull(Object)} over more verbose alternatives. */
static final class RequireNonNullStatement<T> {
@BeforeTemplate
void before(T object) {
if (object == null) {
throw new NullPointerException();
}
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(T object) {
requireNonNull(object);
}
}
/** Prefer {@link Objects#requireNonNull(Object, String)} over non-JDK alternatives. */
static final class RequireNonNullWithMessage<T> {
@BeforeTemplate
T before(T object, String message) {
return checkNotNull(object, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
T after(T object, String message) {
return requireNonNull(object, message);
}
}
/** Prefer {@link Objects#requireNonNull(Object, String)} over more verbose alternatives. */
static final class RequireNonNullWithMessageStatement<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) {
requireNonNull(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,11 +1,17 @@
package tech.picnic.errorprone.refasterrules;
import static com.google.common.collect.ImmutableList.toImmutableList;
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 java.util.stream.Collectors.toCollection;
import static org.assertj.core.api.Assertions.assertThat;
import static reactor.function.TupleUtils.function;
import com.google.common.collect.MoreCollectors;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
@@ -14,18 +20,30 @@ 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.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
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 +67,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 +125,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,11 +317,71 @@ 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());
return Refaster.anyOf(
mono.switchIfEmpty(Mono.empty()), mono.flux().next(), mono.flux().singleOrEmpty());
}
@BeforeTemplate
Mono<@Nullable Void> before2(Mono<@Nullable Void> mono) {
return mono.then();
}
@AfterTemplate
@@ -171,7 +407,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 +427,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
@@ -239,17 +480,201 @@ final class ReactorRules {
* Flux}.
*/
abstract static class MonoFlatMapToFlux<T, S> {
// XXX: It would be more expressive if this `@Placeholder` were replaced with a `Function<?
// super T, ? extends Mono<? extends S>>` parameter, so that compatible non-lambda expression
// arguments to `flatMapMany` are also matched. However, the type inferred for lambda and method
// reference expressions passed to `flatMapMany` appears to always be `Function<T, Publisher<?
// extends S>>`, which doesn't match. Find a solution.
@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 +682,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,23 +692,34 @@ final class ReactorRules {
}
}
/**
* Prefer a collection using {@link MoreCollectors#toOptional()} over more contrived alternatives.
*/
/** 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 {@link Mono#singleOptional()} over more contrived alternatives. */
// XXX: Consider creating a plugin that flags/discourages `Mono<Optional<T>>` method return
// types, just as we discourage nullable `Boolean`s and `Optional`s.
static final class MonoCollectToOptional<T> {
static final class MonoSingleOptional<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())));
mono.flux().collect(toOptional()),
mono.map(Optional::of).defaultIfEmpty(Optional.empty()));
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
Mono<Optional<T>> after(Mono<T> mono) {
return mono.flux().collect(toOptional());
return mono.singleOptional();
}
}
@@ -312,6 +749,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 +812,67 @@ final class ReactorRules {
}
}
/**
* Prefer {@link Flux#count()} followed by a conversion from {@code long} to {@code int} over
* collecting into a list and counting its elements.
*/
static final class FluxCountMapMathToIntExact<T> {
@BeforeTemplate
Mono<Integer> before(Flux<T> flux) {
return Refaster.anyOf(
flux.collect(toImmutableList())
.map(
Refaster.anyOf(
Collection::size,
List::size,
ImmutableCollection::size,
ImmutableList::size)),
flux.collect(toCollection(ArrayList::new))
.map(Refaster.anyOf(Collection::size, List::size)));
}
@AfterTemplate
Mono<Integer> after(Flux<T> flux) {
return flux.count().map(Math::toIntExact);
}
}
/**
* 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 +899,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
@@ -414,12 +1204,14 @@ final class ReactorRules {
}
}
/** Don't unnecessarily call {@link StepVerifier.Step#expectNext(Object[])}. */
static final class StepVerifierStepExpectNextEmpty<T> {
/** Don't unnecessarily have {@link StepVerifier.Step} expect no elements. */
// XXX: Given an `IsEmpty` matcher that identifies a wide range of guaranteed-empty `Iterable`
// expressions, consider also simplifying `step.expectNextSequence(someEmptyIterable)`.
static final class StepVerifierStepIdentity<T> {
@BeforeTemplate
@SuppressWarnings("unchecked")
StepVerifier.Step<T> before(StepVerifier.Step<T> step) {
return step.expectNext();
return Refaster.anyOf(step.expectNext(), step.expectNextCount(0));
}
@AfterTemplate

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

@@ -10,6 +10,7 @@ import com.google.common.collect.Streams;
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.Matches;
import com.google.errorprone.refaster.annotation.MayOptionallyUse;
import com.google.errorprone.refaster.annotation.Placeholder;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
@@ -19,10 +20,14 @@ import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.ToDoubleFunction;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
import tech.picnic.errorprone.refaster.matchers.IsLambdaExpressionOrMethodReference;
/** Refaster rules related to expressions dealing with {@link Stream}s. */
@OnlineDocumentation
@@ -77,6 +82,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 +172,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> {
@@ -341,4 +384,46 @@ final class StreamRules {
return stream.allMatch(e -> test(e));
}
}
static final class StreamMapToIntSum<T> {
@BeforeTemplate
int before(
Stream<T> stream,
@Matches(IsLambdaExpressionOrMethodReference.class) Function<? super T, Integer> mapper) {
return stream.map(mapper).reduce(0, Integer::sum);
}
@AfterTemplate
int after(Stream<T> stream, ToIntFunction<T> mapper) {
return stream.mapToInt(mapper).sum();
}
}
static final class StreamMapToDoubleSum<T> {
@BeforeTemplate
double before(
Stream<T> stream,
@Matches(IsLambdaExpressionOrMethodReference.class) Function<? super T, Double> mapper) {
return stream.map(mapper).reduce(0.0, Double::sum);
}
@AfterTemplate
double after(Stream<T> stream, ToDoubleFunction<T> mapper) {
return stream.mapToDouble(mapper).sum();
}
}
static final class StreamMapToLongSum<T> {
@BeforeTemplate
long before(
Stream<T> stream,
@Matches(IsLambdaExpressionOrMethodReference.class) Function<? super T, Long> mapper) {
return stream.map(mapper).reduce(0L, Long::sum);
}
@AfterTemplate
long after(Stream<T> stream, ToLongFunction<T> mapper) {
return stream.mapToLong(mapper).sum();
}
}
}

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