Compare commits

...

94 Commits

Author SHA1 Message Date
Pieter Dirk Soels
6417a5e9bf Move new rule collection 2023-04-03 15:37:33 +02:00
Pieter Dirk Soels
21effe0aa2 Drop Docgen Maven profile 2023-04-03 13:40:06 +02:00
Pieter Dirk Soels
290ad2c8ee Fit Refaster extractors into new documentation support setup 2023-04-03 13:40:06 +02:00
Rick Ossendrijver
3b2e42cfbb Introduce documentation generation
This is a squash commit of the following previous commits:

---

Introduce `documentation-support` module to extract website data from source code

By adding a compilation `TaskListener` that extracts data from the Bug pattern
and Refaster rule collection (test) classes and writing to JSON output files
in the target directory. This extraction happens as part of the Maven build
using the `docgen` profile.

---

Improve website styling

Co-authored-by: japborst <japborst@gmail.com>
Co-authored-by: Gijs de Jong <berichtaangijs@gmail.com>

---

Generate Markdown files from existing content for the website

---

Upgrade dependencies to the latest versions

---

Compile and install project jars before docgen

---

Run validation in build and exclude self-url

---

Reintroduce `htmlproofer`, improve templates, cleanup setup and dependencies

---

Delete directory if it exists

---

Add SCSS for GitHub button and fix bug pattern sample rendering

---

Fix bug pattern GitHub link

---

Small styling tweaks (incl. for dark theme)

---

Use single mvn command

---

Move mustache templates

---

Hardcode anchors for headings

---

Add supression to bugpatterns

---

Remove self ignore for html-proofer

---

Add refaster supressions and use callouts

---

Use v0.4.1-SNAPSHOT

---

Revert "Use single mvn command"

This reverts commit 594471d1ed23a1c19d7fe88d925d1b7f828716cd.

---

Extract Refaster samples from source code instead of AST

---

Skip verification, for now

---

Add notes on disabling bugpatterns

---

Set default layout and image

---

Fix mobile navigation

---

Revert "Set default layout and image"

This reverts commit 67a4aa7b5b4d14c0f2b783f345f53affe6ef3ec5.

---

Add supression for refaster rules

---

Post-rebase fixes

---

Fix the tests

---

Doh

---

Version bump

---

Exclude ThirdPartyLibraryTest from Bug Pattern test output

---

Remove only last occurence of 'Test' in Bug Pattern tests doc generation

---

Move `MapRulesTest` resources

---

Add exclusion for docgen of `SourceCodeTest`

---

Bump version

---

Extra fixes after rebase

---

Delete custom nav footer and `assets/images/favicon.ico`

---

Post rebase fixes with version bump to `0.8.1-SNAPSHOT`

---

Use new BugPatternTest extraction method
2023-04-03 13:40:05 +02:00
Pieter Dirk Soels
43b8b32d90 PSM-1717 Pass ClassLoader to ServiceLoader 2023-04-03 13:39:11 +02:00
Stephan Schroevers
2c914752cd Fix JDK 11 compatibility 2023-04-03 09:59:51 +02:00
Stephan Schroevers
8d70f1db64 Suggestions 2023-04-03 09:59:51 +02:00
Stephan Schroevers
809b4c6f90 Suggestions 2023-04-03 09:59:51 +02:00
Stephan Schroevers
0ea11dd8ef Suggestions 2023-04-03 09:59:51 +02:00
Rick Ossendrijver
f5f814f49f This actually kills the mutant 2023-04-03 09:59:51 +02:00
Rick Ossendrijver
a8187dbd29 Kill another mutant and drop unused imports 2023-04-03 09:59:51 +02:00
Rick Ossendrijver
a211eaaac4 Further simplify testing setup 2023-04-03 09:59:51 +02:00
Rick Ossendrijver
1fd6e35e00 Simplify tests 2023-04-03 09:59:51 +02:00
Rick Ossendrijver
2cc8c42155 Introduce BugPatternTestExtractor with tests 2023-04-03 09:59:51 +02:00
Picnic-Bot
e9829d93bf Upgrade Forbidden APIs plugin 3.5 -> 3.5.1 (#558)
See:
- https://github.com/policeman-tools/forbidden-apis/wiki/Changes
- https://github.com/policeman-tools/forbidden-apis/compare/3.5...3.5.1
2023-04-01 09:40:16 +02:00
Stephan Schroevers
b273502e88 [maven-release-plugin] prepare for next development iteration 2023-03-31 09:31:01 +02:00
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
210 changed files with 5143 additions and 372 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

@@ -10,16 +10,16 @@ jobs:
strategy:
matrix:
os: [ ubuntu-22.04 ]
jdk: [ 11.0.16, 17.0.4, 19 ]
jdk: [ 11.0.18, 17.0.6, 19.0.2 ]
distribution: [ temurin ]
experimental: [ false ]
include:
- os: macos-12
jdk: 17.0.4
jdk: 17.0.6
distribution: temurin
experimental: false
- os: windows-2022
jdk: 17.0.4
jdk: 17.0.6
distribution: temurin
experimental: false
- os: ubuntu-22.04

View File

@@ -19,16 +19,21 @@ jobs:
bundler-cache: true
- name: Configure Github Pages
uses: actions/configure-pages@v2.1.3
- name: Set up JDK
uses: actions/setup-java@v3.5.1
with:
java-version: 17.0.4
distribution: temurin
cache: maven
- name: Compile project and extract data
# XXX: Remove `-Dverification.{skip|warn}` and update `website/README.md`
# once module `docgen` has no errors.
run: |
mvn -T1C clean install -DskipTests -Dverification.warn
mvn -T1C clean install -DskipTests -Dverification.skip -Pdocgen
- name: Generate documentation
run: ./generate-docs.sh
- name: Build website with Jekyll
working-directory: ./website
run: bundle exec jekyll build
- name: Validate HTML output
working-directory: ./website
# XXX: Drop `--disable_external true` once we fully adopted the
# "Refaster rules" terminology on our website and in the code.
run: bundle exec htmlproofer --disable_external true --check-external-hash false ./_site
run: bundle exec ruby generate-docs.rb
- name: Upload website as artifact
uses: actions/upload-pages-artifact@v1.0.5
with:

View File

@@ -18,7 +18,7 @@ jobs:
- name: Set up JDK
uses: actions/setup-java@v3.8.0
with:
java-version: 17.0.4
java-version: 17.0.6
distribution: temurin
cache: maven
- name: Run Pitest

View File

@@ -22,7 +22,7 @@ jobs:
- name: Set up JDK
uses: actions/setup-java@v3.8.0
with:
java-version: 17.0.4
java-version: 17.0.6
distribution: temurin
cache: maven
- name: Download Pitest analysis artifact

View File

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

View File

@@ -36,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].
@@ -94,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:
@@ -207,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

View File

@@ -0,0 +1,91 @@
<?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.1-SNAPSHOT</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_core</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,114 @@
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.service.AutoService;
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.VisitorState;
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 java.util.Optional;
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
@AutoService(Extractor.class)
public final class BugPatternExtractor implements Extractor<BugPatternDocumentation> {
/** Instantiates a new {@link BugPatternExtractor} instance. */
public BugPatternExtractor() {}
@Override
public String identifier() {
return "bugpattern";
}
@Override
public Optional<BugPatternDocumentation> tryExtract(ClassTree tree, VisitorState state) {
ClassSymbol symbol = ASTHelpers.getSymbol(tree);
BugPattern annotation = symbol.getAnnotation(BugPattern.class);
if (annotation == null) {
return Optional.empty();
}
return Optional.of(
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()));
}
/**
* 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,200 @@
package tech.picnic.errorprone.documentation;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.anything;
import static com.google.errorprone.matchers.Matchers.argument;
import static com.google.errorprone.matchers.Matchers.classLiteral;
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
import static com.google.errorprone.matchers.Matchers.instanceMethod;
import static com.google.errorprone.matchers.Matchers.toType;
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
import com.google.auto.service.AutoService;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreeScanner;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.BugPatternTestDocumentation;
/**
* An {@link Extractor} that describes how to extract data from classes that test a {@code
* BugChecker}.
*/
@Immutable
@AutoService(Extractor.class)
public final class BugPatternTestExtractor implements Extractor<BugPatternTestDocumentation> {
private static final Pattern TEST_CLASS_NAME_PATTERN = Pattern.compile("(.*)Test");
private static final Matcher<Tree> JUNIT_TEST_METHOD =
toType(MethodTree.class, hasAnnotation("org.junit.jupiter.api.Test"));
private static final Matcher<MethodInvocationTree> BUG_PATTERN_TEST_METHOD =
allOf(
staticMethod()
.onDescendantOfAny(
"com.google.errorprone.CompilationTestHelper",
"com.google.errorprone.BugCheckerRefactoringTestHelper")
.named("newInstance"),
argument(0, classLiteral(anything())));
/** Instantiates a new {@link BugPatternTestExtractor} instance. */
public BugPatternTestExtractor() {}
@Override
public String identifier() {
return "bugpattern-test";
}
// XXX: Improve support for correctly extracting multiple sources from a single
// `{BugCheckerRefactoring,Compilation}TestHelper` test.
@Override
public Optional<BugPatternTestDocumentation> tryExtract(ClassTree tree, VisitorState state) {
return getClassUnderTest(tree)
.filter(bugPatternName -> testsBugPattern(bugPatternName, tree, state))
.map(
bugPatternName -> {
CollectBugPatternTests scanner = new CollectBugPatternTests();
for (Tree m : tree.getMembers()) {
if (JUNIT_TEST_METHOD.matches(m, state)) {
scanner.scan(m, state);
}
}
return new AutoValue_BugPatternTestExtractor_BugPatternTestDocumentation(
bugPatternName, scanner.getIdentificationTests(), scanner.getReplacementTests());
});
}
private static boolean testsBugPattern(
String bugPatternName, ClassTree tree, VisitorState state) {
AtomicBoolean result = new AtomicBoolean(false);
new TreeScanner<@Nullable Void, @Nullable Void>() {
@Override
public @Nullable Void visitMethodInvocation(MethodInvocationTree node, @Nullable Void v) {
if (BUG_PATTERN_TEST_METHOD.matches(node, state)) {
MemberSelectTree firstArgumentTree = (MemberSelectTree) node.getArguments().get(0);
result.compareAndSet(
/* expectedValue= */ false,
bugPatternName.equals(firstArgumentTree.getExpression().toString()));
}
return super.visitMethodInvocation(node, v);
}
}.scan(tree, null);
return result.get();
}
private static Optional<String> getClassUnderTest(ClassTree tree) {
return Optional.of(TEST_CLASS_NAME_PATTERN.matcher(tree.getSimpleName().toString()))
.filter(java.util.regex.Matcher::matches)
.map(m -> m.group(1));
}
private static final class CollectBugPatternTests
extends TreeScanner<@Nullable Void, VisitorState> {
private static final Matcher<ExpressionTree> IDENTIFICATION_SOURCE_LINES =
instanceMethod()
.onDescendantOf("com.google.errorprone.CompilationTestHelper")
.named("addSourceLines");
private static final Matcher<ExpressionTree> REPLACEMENT_INPUT =
instanceMethod()
.onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper")
.named("addInputLines");
private static final Matcher<ExpressionTree> REPLACEMENT_OUTPUT =
instanceMethod()
.onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper.ExpectOutput")
.named("addOutputLines");
private final List<String> identificationTests = new ArrayList<>();
private final List<BugPatternReplacementTestDocumentation> replacementTests = new ArrayList<>();
public ImmutableList<String> getIdentificationTests() {
return ImmutableList.copyOf(identificationTests);
}
public ImmutableList<BugPatternReplacementTestDocumentation> getReplacementTests() {
return ImmutableList.copyOf(replacementTests);
}
// XXX: Consider:
// - Whether to omit or handle differently identification tests without `// BUG: Diagnostic
// (contains|matches)` markers.
// - Whether to omit or handle differently replacement tests with identical input and output.
// (Though arguably we should have a separate checker which replaces such cases with
// `.expectUnchanged()`.)
// - Whether to track `.expectUnchanged()` test cases.
@Override
public @Nullable Void visitMethodInvocation(MethodInvocationTree node, VisitorState state) {
if (IDENTIFICATION_SOURCE_LINES.matches(node, state)) {
getSourceCode(node).ifPresent(identificationTests::add);
} else if (REPLACEMENT_OUTPUT.matches(node, state)) {
ExpressionTree receiver = ASTHelpers.getReceiver(node);
// XXX: Make this code nicer.
if (REPLACEMENT_INPUT.matches(receiver, state)) {
getSourceCode(node)
.ifPresent(
output ->
getSourceCode((MethodInvocationTree) receiver)
.ifPresent(
input ->
replacementTests.add(
new AutoValue_BugPatternTestExtractor_BugPatternReplacementTestDocumentation(
input, output))));
}
}
return super.visitMethodInvocation(node, state);
}
// XXX: Duplicated from `ErrorProneTestSourceFormat`. Can we do better?
private static Optional<String> getSourceCode(MethodInvocationTree tree) {
List<? extends ExpressionTree> sourceLines =
tree.getArguments().subList(1, tree.getArguments().size());
StringBuilder source = new StringBuilder();
for (ExpressionTree sourceLine : sourceLines) {
String value = ASTHelpers.constValue(sourceLine, String.class);
if (value == null) {
return Optional.empty();
}
source.append(value).append('\n');
}
return Optional.of(source.toString());
}
}
// XXX: Rename?
@AutoValue
abstract static class BugPatternTestDocumentation {
abstract String name();
abstract ImmutableList<String> identificationTests();
abstract ImmutableList<BugPatternReplacementTestDocumentation> replacementTests();
}
// XXX: Rename?
@AutoValue
abstract static class BugPatternReplacementTestDocumentation {
abstract String inputLines();
abstract String outputLines();
}
}

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,109 @@
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.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskEvent.Kind;
import com.sun.source.util.TaskListener;
import com.sun.source.util.TreePath;
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 java.util.ServiceLoader;
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 {
@SuppressWarnings({"rawtypes", "unchecked"})
private static final ImmutableList<Extractor<?>> EXTRACTORS =
(ImmutableList)
ImmutableList.copyOf(
ServiceLoader.load(
Extractor.class, DocumentationGeneratorTaskListener.class.getClassLoader()));
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;
}
JavaFileObject sourceFile = taskEvent.getSourceFile();
CompilationUnitTree compilationUnit = taskEvent.getCompilationUnit();
ClassTree classTree = JavacTrees.instance(context).getTree(taskEvent.getTypeElement());
if (sourceFile == null || compilationUnit == null || classTree == null) {
return;
}
VisitorState state =
VisitorState.createForUtilityPurposes(context)
.withPath(new TreePath(new TreePath(compilationUnit), classTree));
for (Extractor<?> extractor : EXTRACTORS) {
extractor
.tryExtract(classTree, state)
.ifPresent(
data ->
writeToFile(
extractor.identifier(), getSimpleClassName(sourceFile.toUri()), data));
}
}
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.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.sun.source.tree.ClassTree;
import java.util.Optional;
/**
* 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> {
/**
* Returns the unique identifier of this extractor.
*
* @return A non-{@code null} string.
*/
String identifier();
/**
* Attempts to extract an instance of type {@link T} using the provided arguments.
*
* @param tree The {@link ClassTree} to analyze and from which to extract an instance of type
* {@link T}.
* @param state A {@link VisitorState} describing the context in which the given {@link ClassTree}
* is found.
* @return An instance of type {@link T}, if possible.
*/
Optional<T> tryExtract(ClassTree tree, VisitorState state);
}

View File

@@ -0,0 +1,61 @@
package tech.picnic.errorprone.documentation;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import java.util.Optional;
import java.util.regex.Pattern;
import tech.picnic.errorprone.documentation.models.RefasterTemplateCollectionTestData;
import tech.picnic.errorprone.documentation.models.RefasterTemplateTestData;
@Immutable
@AutoService(Extractor.class)
public final class RefasterTestInputExtractor
implements Extractor<RefasterTemplateCollectionTestData> {
private static final Pattern TEST_INPUT_CLASS_NAME_PATTERN = Pattern.compile("(.*)TestInput");
@Override
public String identifier() {
return "refaster-test-input";
}
@Override
public Optional<RefasterTemplateCollectionTestData> tryExtract(
ClassTree tree, VisitorState state) {
Optional<String> className = getClassUnderTest(tree);
if (className.isEmpty()) {
return Optional.empty();
}
ImmutableList<RefasterTemplateTestData> templateTests =
tree.getMembers().stream()
.filter(m -> m instanceof MethodTree)
.map(MethodTree.class::cast)
.filter(m -> m.getName().toString().startsWith("test"))
.map(
m ->
RefasterTemplateTestData.create(
m.getName().toString().replace("test", ""), getSourceCode(m, state)))
.collect(toImmutableList());
return Optional.of(
RefasterTemplateCollectionTestData.create(className.orElseThrow(), true, templateTests));
}
private static Optional<String> getClassUnderTest(ClassTree tree) {
return Optional.of(TEST_INPUT_CLASS_NAME_PATTERN.matcher(tree.getSimpleName().toString()))
.filter(java.util.regex.Matcher::matches)
.map(m -> m.group(1));
}
// XXX: Duplicated from `SourceCode`. Can we do better?
private String getSourceCode(MethodTree tree, VisitorState state) {
String src = state.getSourceForNode(tree);
return src != null ? src : tree.toString();
}
}

View File

@@ -0,0 +1,61 @@
package tech.picnic.errorprone.documentation;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import java.util.Optional;
import java.util.regex.Pattern;
import tech.picnic.errorprone.documentation.models.RefasterTemplateCollectionTestData;
import tech.picnic.errorprone.documentation.models.RefasterTemplateTestData;
@Immutable
@AutoService(Extractor.class)
public final class RefasterTestOutputExtractor
implements Extractor<RefasterTemplateCollectionTestData> {
private static final Pattern TEST_INPUT_CLASS_NAME_PATTERN = Pattern.compile("(.*)TestOutput");
@Override
public String identifier() {
return "refaster-test-output";
}
@Override
public Optional<RefasterTemplateCollectionTestData> tryExtract(
ClassTree tree, VisitorState state) {
Optional<String> className = getClassUnderTest(tree);
if (className.isEmpty()) {
return Optional.empty();
}
ImmutableList<RefasterTemplateTestData> templateTests =
tree.getMembers().stream()
.filter(m -> m instanceof MethodTree)
.map(MethodTree.class::cast)
.filter(m -> m.getName().toString().startsWith("test"))
.map(
m ->
RefasterTemplateTestData.create(
m.getName().toString().replace("test", ""), getSourceCode(m, state)))
.collect(toImmutableList());
return Optional.of(
RefasterTemplateCollectionTestData.create(className.orElseThrow(), true, templateTests));
}
private static Optional<String> getClassUnderTest(ClassTree tree) {
return Optional.of(TEST_INPUT_CLASS_NAME_PATTERN.matcher(tree.getSimpleName().toString()))
.filter(java.util.regex.Matcher::matches)
.map(m -> m.group(1));
}
// XXX: Duplicated from `SourceCode`. Can we do better?
private String getSourceCode(MethodTree tree, VisitorState state) {
String src = state.getSourceForNode(tree);
return src != null ? src : tree.toString();
}
}

View File

@@ -0,0 +1,25 @@
package tech.picnic.errorprone.documentation.models;
import com.google.auto.value.AutoValue;
import java.util.List;
import tech.picnic.errorprone.documentation.models.AutoValue_RefasterTemplateCollectionData;
/**
* Object containing all data related to a Refaster template collection. This is solely used for
* serialization.
*/
@AutoValue
public abstract class RefasterTemplateCollectionData {
public static RefasterTemplateCollectionData create(
String name, String description, String link, List<RefasterTemplateData> templates) {
return new AutoValue_RefasterTemplateCollectionData(name, description, link, templates);
}
abstract String name();
abstract String description();
abstract String link();
abstract List<RefasterTemplateData> templates();
}

View File

@@ -0,0 +1,20 @@
package tech.picnic.errorprone.documentation.models;
import com.google.auto.value.AutoValue;
import java.util.List;
import tech.picnic.errorprone.documentation.models.AutoValue_RefasterTemplateCollectionTestData;
@AutoValue
public abstract class RefasterTemplateCollectionTestData {
public static RefasterTemplateCollectionTestData create(
String templateCollection, boolean isInput, List<RefasterTemplateTestData> templatesTests) {
return new AutoValue_RefasterTemplateCollectionTestData(
templateCollection, isInput, templatesTests);
}
abstract String templateCollection();
abstract boolean isInput();
abstract List<RefasterTemplateTestData> templateTests();
}

View File

@@ -0,0 +1,21 @@
package tech.picnic.errorprone.documentation.models;
import com.google.auto.value.AutoValue;
import com.google.errorprone.BugPattern.SeverityLevel;
import tech.picnic.errorprone.documentation.models.AutoValue_RefasterTemplateData;
@AutoValue
public abstract class RefasterTemplateData {
public static RefasterTemplateData create(
String name, String description, String link, SeverityLevel severityLevel) {
return new AutoValue_RefasterTemplateData(name, description, link, severityLevel);
}
abstract String name();
abstract String description();
abstract String link();
abstract SeverityLevel severityLevel();
}

View File

@@ -0,0 +1,15 @@
package tech.picnic.errorprone.documentation.models;
import com.google.auto.value.AutoValue;
import tech.picnic.errorprone.documentation.models.AutoValue_RefasterTemplateTestData;
@AutoValue
public abstract class RefasterTemplateTestData {
public static RefasterTemplateTestData create(String templateName, String templateTestContent) {
return new AutoValue_RefasterTemplateTestData(templateName, templateTestContent);
}
abstract String templateName();
abstract String templateTestContent();
}

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,148 @@
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 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) {
return buildDescription(tree)
.setMessage(
String.format(
"Can extract: %s", new BugPatternExtractor().tryExtract(tree, state).isPresent()))
.build();
}
}
}

View File

@@ -0,0 +1,352 @@
package tech.picnic.errorprone.documentation;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.common.io.Resources;
import java.io.IOException;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
final class BugPatternTestExtractorTest {
@Test
void noBugPatternTest(@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 bugPatternTestFileWithoutTestSuffix(@TempDir Path outputDirectory) {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"TestCheckerWithWrongSuffix.java",
"import com.google.errorprone.bugpatterns.BugChecker;",
"import com.google.errorprone.CompilationTestHelper;",
"",
"final class TestCheckerWithoutTestSuffix {",
" private static class TestCheckerWithout extends BugChecker {}",
"",
" CompilationTestHelper compilationTestHelper = CompilationTestHelper.newInstance(TestCheckerWithout.class, getClass());",
"}");
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
}
@Test
void minimalBugPatternTest(@TempDir Path outputDirectory) throws IOException {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"TestCheckerTest.java",
"import com.google.errorprone.bugpatterns.BugChecker;",
"import com.google.errorprone.CompilationTestHelper;",
"",
"final class TestCheckerTest {",
" private static class TestChecker extends BugChecker {}",
"",
" CompilationTestHelper compilationTestHelper = CompilationTestHelper.newInstance(TestChecker.class, getClass());",
"}");
verifyFileMatchesResource(
outputDirectory,
"bugpattern-test-TestCheckerTest.json",
"bugpattern-test-documentation-minimal.json");
}
@Test
void differentBugPatternAsClassVariableTest(@TempDir Path outputDirectory) {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"TestCheckerTest.java",
"import com.google.errorprone.bugpatterns.BugChecker;",
"import com.google.errorprone.CompilationTestHelper;",
"",
"final class TestCheckerTest {",
" private static class DifferentChecker extends BugChecker {}",
"",
" CompilationTestHelper compilationTestHelper = CompilationTestHelper.newInstance(DifferentChecker.class, getClass());",
"}");
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
}
@Test
void differentBugPatternAsLocalVariable(@TempDir Path outputDirectory) {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"TestCheckerTest.java",
"import com.google.errorprone.bugpatterns.BugChecker;",
"import com.google.errorprone.CompilationTestHelper;",
"import org.junit.jupiter.api.Test;",
"",
"final class TestCheckerTest {",
" private static class DifferentChecker extends BugChecker {}",
"",
" @Test",
" void identification() {",
" CompilationTestHelper.newInstance(DifferentChecker.class, getClass())",
" .addSourceLines(\"A.java\", \"class A {}\")",
" .doTest();",
" }",
"}");
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
}
@Test
void bugPatternTestSingleIdentification(@TempDir Path outputDirectory) throws IOException {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"TestCheckerTest.java",
"import com.google.errorprone.bugpatterns.BugChecker;",
"import com.google.errorprone.CompilationTestHelper;",
"import org.junit.jupiter.api.Test;",
"",
"final class TestCheckerTest {",
" private static class TestChecker extends BugChecker {}",
"",
" @Test",
" void identification() {",
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
" .addSourceLines(\"A.java\", \"class A {}\")",
" .doTest();",
" }",
"}");
verifyFileMatchesResource(
outputDirectory,
"bugpattern-test-TestCheckerTest.json",
"bugpattern-test-documentation-identification.json");
}
@Test
void bugPatternTestIdentificationMultipleSourceLines(@TempDir Path outputDirectory)
throws IOException {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"TestCheckerTest.java",
"package pkg;",
"",
"import com.google.errorprone.bugpatterns.BugChecker;",
"import com.google.errorprone.CompilationTestHelper;",
"import org.junit.jupiter.api.Test;",
"",
"final class TestCheckerTest {",
" private static class TestChecker extends BugChecker {}",
"",
" @Test",
" void identification() {",
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
" .addSourceLines(\"A.java\", \"class A {}\")",
" .addSourceLines(\"B.java\", \"class B {}\")",
" .doTest();",
" }",
"}");
verifyFileMatchesResource(
outputDirectory,
"bugpattern-test-TestCheckerTest.json",
"bugpattern-test-documentation-identification-two-sources.json");
}
@Test
void bugPatternTestSingleReplacement(@TempDir Path outputDirectory) throws IOException {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"TestCheckerTest.java",
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
"import com.google.errorprone.bugpatterns.BugChecker;",
"import org.junit.jupiter.api.Test;",
"",
"final class TestCheckerTest {",
" private static class TestChecker extends BugChecker {}",
"",
" @Test",
" void replacement() {",
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
" .addInputLines(\"A.java\", \"class A {}\")",
" .addOutputLines(\"A.java\", \"class A {}\")",
" .doTest(TestMode.TEXT_MATCH);",
" }",
"}");
verifyFileMatchesResource(
outputDirectory,
"bugpattern-test-TestCheckerTest.json",
"bugpattern-test-documentation-replacement.json");
}
@Test
void bugPatternTestMultipleReplacementSources(@TempDir Path outputDirectory) throws IOException {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"TestCheckerTest.java",
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
"import com.google.errorprone.bugpatterns.BugChecker;",
"import org.junit.jupiter.api.Test;",
"",
"final class TestCheckerTest {",
" private static class TestChecker extends BugChecker {}",
"",
" @Test",
" void replacement() {",
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
" .addInputLines(\"A.java\", \"class A {}\")",
" .addOutputLines(\"A.java\", \"class A {}\")",
" .addInputLines(\"B.java\", \"class B {}\")",
" .addOutputLines(\"B.java\", \"class B {}\")",
" .doTest(TestMode.TEXT_MATCH);",
" }",
"}");
verifyFileMatchesResource(
outputDirectory,
"bugpattern-test-TestCheckerTest.json",
"bugpattern-test-documentation-replacement-two-sources.json");
}
@Test
void bugPatternReplacementExpectUnchanged(@TempDir Path outputDirectory) throws IOException {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"TestCheckerTest.java",
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
"import com.google.errorprone.bugpatterns.BugChecker;",
"import org.junit.jupiter.api.Test;",
"",
"final class TestCheckerTest {",
" private static class TestChecker extends BugChecker {}",
"",
" @Test",
" void replacement() {",
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
" .addInputLines(\"A.java\", \"class A {}\")",
" .expectUnchanged()",
" .doTest(TestMode.TEXT_MATCH);",
" }",
"}");
verifyFileMatchesResource(
outputDirectory,
"bugpattern-test-TestCheckerTest.json",
"bugpattern-test-documentation-replacement-expect-unchanged.json");
}
@Test
void bugPatternTestIdentificationAndReplacement(@TempDir Path outputDirectory)
throws IOException {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"TestCheckerTest.java",
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
"import com.google.errorprone.bugpatterns.BugChecker;",
"import com.google.errorprone.CompilationTestHelper;",
"import org.junit.jupiter.api.Test;",
"",
"final class TestCheckerTest {",
" private static class TestChecker extends BugChecker {}",
"",
" @Test",
" void identification() {",
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
" .addSourceLines(\"A.java\", \"class A {}\")",
" .doTest();",
" }",
"",
" @Test",
" void replacement() {",
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
" .addInputLines(\"A.java\", \"class A {}\")",
" .addOutputLines(\"A.java\", \"class A {}\")",
" .doTest(TestMode.TEXT_MATCH);",
" }",
"}");
verifyFileMatchesResource(
outputDirectory,
"bugpattern-test-TestCheckerTest.json",
"bugpattern-test-documentation-identification-and-replacement.json");
}
@Test
void bugPatternTestMultipleIdentificationAndReplacement(@TempDir Path outputDirectory)
throws IOException {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"TestCheckerTest.java",
"package pkg;",
"",
"import static com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers.SECOND;",
"",
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
"import com.google.errorprone.bugpatterns.BugChecker;",
"import com.google.errorprone.CompilationTestHelper;",
"import org.junit.jupiter.api.Test;",
"",
"final class TestCheckerTest {",
" private static class TestChecker extends BugChecker {}",
"",
" @Test",
" void identification() {",
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
" .addSourceLines(\"A.java\", \"class A {}\")",
" .doTest();",
" }",
"",
" @Test",
" void identification2() {",
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
" .addSourceLines(\"B.java\", \"class B {}\")",
" .doTest();",
" }",
"",
" @Test",
" void replacementFirstSuggestedFix() {",
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
" .addInputLines(\"A.java\", \"class A {}\")",
" .addOutputLines(\"A.java\", \"class A {}\")",
" .doTest(TestMode.TEXT_MATCH);",
" }",
"",
" @Test",
" void replacementSecondSuggestedFix() {",
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
" .setFixChooser(SECOND)",
" .addInputLines(\"B.java\", \"class B {}\")",
" .addOutputLines(\"B.java\", \"class B {}\")",
" .doTest(TestMode.TEXT_MATCH);",
" }",
"}");
verifyFileMatchesResource(
outputDirectory,
"bugpattern-test-TestCheckerTest.json",
"bugpattern-test-documentation-multiple-identification-and-replacement.json");
}
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(BugPatternTestExtractorTest.class, resourceName), UTF_8);
}
}

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,138 @@
package tech.picnic.errorprone.documentation;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.nio.charset.StandardCharsets.UTF_8;
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.auto.service.AutoService;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.Tree;
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 java.util.Optional;
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");
}
@Test
void extraction(@TempDir Path outputDirectory) {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"DocumentationGeneratorTaskListenerTestClass.java",
"class DocumentationGeneratorTaskListenerTestClass {}");
// XXX: Once we support only JDK 15+, use a text block for the `expected` string.
assertThat(
outputDirectory.resolve(
"documentation-generator-task-listener-test-DocumentationGeneratorTaskListenerTestClass.json"))
.content(UTF_8)
.isEqualToIgnoringWhitespace(
"{\"className\":\"DocumentationGeneratorTaskListenerTestClass\",\"path\":[\"CLASS: DocumentationGeneratorTaskListenerTestClass\",\"COMPILATION_UNIT\"]}");
}
@Immutable
@AutoService(Extractor.class)
public static final class TestExtractor implements Extractor<ExtractionParameters> {
@Override
public String identifier() {
return "documentation-generator-task-listener-test";
}
@Override
public Optional<ExtractionParameters> tryExtract(ClassTree tree, VisitorState state) {
return Optional.of(tree.getSimpleName().toString())
.filter(n -> n.contains(DocumentationGeneratorTaskListenerTest.class.getSimpleName()))
.map(
className ->
new AutoValue_DocumentationGeneratorTaskListenerTest_ExtractionParameters(
className,
Streams.stream(state.getPath())
.map(TestExtractor::describeTree)
.collect(toImmutableList())));
}
private static String describeTree(Tree tree) {
return (tree instanceof ClassTree)
? String.join(": ", String.valueOf(tree.getKind()), ((ClassTree) tree).getSimpleName())
: tree.getKind().toString();
}
}
@AutoValue
abstract static class ExtractionParameters {
abstract String className();
abstract ImmutableList<String> path();
}
}

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

@@ -0,0 +1,12 @@
{
"name": "TestChecker",
"identificationTests": [
"class A {}\n"
],
"replacementTests": [
{
"inputLines": "class A {}\n",
"outputLines": "class A {}\n"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"name": "TestChecker",
"identificationTests": [
"class B {}\n",
"class A {}\n"
],
"replacementTests": []
}

View File

@@ -0,0 +1,7 @@
{
"name": "TestChecker",
"identificationTests": [
"class A {}\n"
],
"replacementTests": []
}

View File

@@ -0,0 +1,5 @@
{
"name": "TestChecker",
"identificationTests": [],
"replacementTests": []
}

View File

@@ -0,0 +1,17 @@
{
"name": "TestChecker",
"identificationTests": [
"class A {}\n",
"class B {}\n"
],
"replacementTests": [
{
"inputLines": "class A {}\n",
"outputLines": "class A {}\n"
},
{
"inputLines": "class B {}\n",
"outputLines": "class B {}\n"
}
]
}

View File

@@ -0,0 +1,5 @@
{
"name": "TestChecker",
"identificationTests": [],
"replacementTests": []
}

View File

@@ -0,0 +1,14 @@
{
"name": "TestChecker",
"identificationTests": [],
"replacementTests": [
{
"inputLines": "class B {}\n",
"outputLines": "class B {}\n"
},
{
"inputLines": "class A {}\n",
"outputLines": "class A {}\n"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"name": "TestChecker",
"identificationTests": [],
"replacementTests": [
{
"inputLines": "class A {}\n",
"outputLines": "class A {}\n"
}
]
}

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>tech.picnic.error-prone-support</groupId>
<artifactId>error-prone-support</artifactId>
<version>0.8.0</version>
<version>0.9.1-SNAPSHOT</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>
@@ -213,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>
@@ -226,6 +239,7 @@
</annotationProcessorPaths>
<compilerArgs combine.children="append">
<arg>-Xplugin:RefasterRuleCompiler</arg>
<arg>-Xplugin:DocumentationGenerator -XoutputDirectory=${project.build.directory}/docs</arg>
</compilerArgs>
</configuration>
</plugin>
@@ -243,4 +257,33 @@
</plugins>
</pluginManagement>
</build>
<profiles>
<profile>
<id>docgen</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>add-test-source</id>
<goals>
<goal>add-test-source</goal>
</goals>
<phase>generate-test-sources</phase>
<configuration>
<sources>
<source>src/test/resources</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

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

@@ -152,7 +152,7 @@ public final class ErrorProneTestHelperSourceFormat extends BugChecker
StringBuilder source = new StringBuilder();
for (ExpressionTree sourceLine : sourceLines) {
Object value = ASTHelpers.constValue(sourceLine);
String value = ASTHelpers.constValue(sourceLine, String.class);
if (value == null) {
return Optional.empty();
}

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

@@ -9,7 +9,6 @@ import static com.google.errorprone.matchers.Matchers.hasModifier;
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.isValidIdentifier;
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.SETUP_OR_TEARDOWN_METHOD;
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.TEST_METHOD;
@@ -25,15 +24,11 @@ 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.ImportTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Type;
import java.util.Optional;
import javax.lang.model.element.Modifier;
import 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:
@@ -90,7 +85,7 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
tryCanonicalizeMethodName(symbol)
.ifPresent(
newName ->
findMethodRenameBlocker(symbol, newName, state)
ConflictDetection.findMethodRenameBlocker(symbol, newName, state)
.ifPresentOrElse(
blocker -> reportMethodRenameBlocker(tree, blocker, state),
() -> fixBuilder.merge(SuggestedFixes.renameMethod(tree, newName, state))));
@@ -106,61 +101,7 @@ 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(
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());
}
private static Optional<String> tryCanonicalizeMethodName(Symbol symbol) {
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()))

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

@@ -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

@@ -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;
@@ -67,9 +68,7 @@ public final class RedundantStringConversion extends BugChecker
private static final String EXTRA_STRING_CONVERSION_METHODS_FLAG =
"RedundantStringConversion:ExtraConversionMethods";
@SuppressWarnings("UnnecessaryLambda")
private static final Matcher<ExpressionTree> ANY_EXPR = (t, s) -> true;
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);

View File

@@ -74,9 +74,13 @@ 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"),

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

@@ -5,8 +5,12 @@ 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.
@@ -46,4 +50,33 @@ public final class MoreASTHelpers {
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

@@ -1,6 +1,6 @@
package tech.picnic.errorprone.bugpatterns.util;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
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;
@@ -9,7 +9,7 @@ import static java.util.Objects.requireNonNullElse;
import static tech.picnic.errorprone.bugpatterns.util.MoreMatchers.hasMetaAnnotation;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.matchers.AnnotationMatcherUtils;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.MultiMatcher;
@@ -55,25 +55,59 @@ public final class 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.
* @return One or more value factory 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)
*/
static ImmutableSet<String> getMethodSourceFactoryNames(
// 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 ImmutableSet.of(toMethodSourceFactoryName(value, methodName));
return ImmutableList.of(toMethodSourceFactoryDescriptor(value, methodName));
}
return ((NewArrayTree) value)
.getInitializers().stream()
.map(name -> toMethodSourceFactoryName(name, methodName))
.collect(toImmutableSet());
.map(name -> toMethodSourceFactoryDescriptor(name, methodName))
.collect(toImmutableList());
}
private static String toMethodSourceFactoryName(
private static String toMethodSourceFactoryDescriptor(
@Nullable ExpressionTree tree, String annotatedMethodName) {
return requireNonNullElse(
Strings.emptyToNull(ASTHelpers.constValue(tree, String.class)), annotatedMethodName);

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

@@ -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

@@ -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

@@ -21,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
@@ -34,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

View File

@@ -6,11 +6,13 @@ 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}. */
@@ -72,8 +74,22 @@ final class PreconditionsRules {
}
}
/** Prefer {@link Preconditions#checkNotNull(Object)} over more verbose alternatives. */
static final class CheckNotNull<T> {
/** 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) {
@@ -84,12 +100,26 @@ final class PreconditionsRules {
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(T object) {
checkNotNull(object);
requireNonNull(object);
}
}
/** Prefer {@link Preconditions#checkNotNull(Object, Object)} over more verbose alternatives. */
static final class CheckNotNullWithMessage<T> {
/** 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) {
@@ -100,7 +130,7 @@ final class PreconditionsRules {
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(T object, String message) {
checkNotNull(object, message);
requireNonNull(object, message);
}
}

View File

@@ -1,14 +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.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.MoreCollectors;
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
@@ -17,8 +20,11 @@ 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;
@@ -699,21 +705,21 @@ final class ReactorRules {
}
}
/**
* Prefer a collection using {@link MoreCollectors#toOptional()} over more contrived alternatives.
*/
/** 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 mono.map(Optional::of).defaultIfEmpty(Optional.empty());
return Refaster.anyOf(
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();
}
}
@@ -806,6 +812,31 @@ 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.
@@ -1173,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

@@ -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
@@ -379,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();
}
}
}

View File

@@ -0,0 +1,174 @@
package tech.picnic.errorprone.bugpatterns;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class DirectReturnTest {
@Test
void identification() {
CompilationTestHelper.newInstance(DirectReturn.class, getClass())
.addSourceLines(
"A.java",
"import static org.mockito.Mockito.mock;",
"import static org.mockito.Mockito.spy;",
"",
"import java.util.function.Supplier;",
"",
"class A {",
" private String field;",
"",
" void emptyMethod() {}",
"",
" void voidMethod() {",
" toString();",
" return;",
" }",
"",
" String directReturnOfParam(String param) {",
" return param;",
" }",
"",
" String assignmentToField() {",
" field = toString();",
" return field;",
" }",
"",
" Object redundantAssignmentToParam(String param) {",
" // BUG: Diagnostic contains:",
" param = toString();",
" return param;",
" }",
"",
" String redundantMockAssignmentToParam(String param) {",
" // BUG: Diagnostic contains:",
" param = mock();",
" return param;",
" }",
"",
" Object redundantMockWithExplicitTypeAssignmentToParam(String param) {",
" // BUG: Diagnostic contains:",
" param = mock(String.class);",
" return param;",
" }",
"",
" Object salientMockAssignmentToParam(String param) {",
" param = mock();",
" return param;",
" }",
"",
" String redundantAssignmentToLocalVariable() {",
" String variable = null;",
" // BUG: Diagnostic contains:",
" variable = toString();",
" return variable;",
" }",
"",
" String unusedAssignmentToLocalVariable(String param) {",
" String variable = null;",
" variable = toString();",
" return param;",
" }",
"",
" String redundantVariableDeclaration() {",
" // BUG: Diagnostic contains:",
" String variable = toString();",
" return variable;",
" }",
"",
" String redundantSpyVariableDeclaration() {",
" // BUG: Diagnostic contains:",
" String variable = spy();",
" return variable;",
" }",
"",
" Object redundantSpyWithExplicitTypeVariableDeclaration() {",
" // BUG: Diagnostic contains:",
" String variable = spy(String.class);",
" return variable;",
" }",
"",
" Object salientSpyTypeVariableDeclaration() {",
" String variable = spy(\"name\");",
" return variable;",
" }",
"",
" String unusedVariableDeclaration(String param) {",
" String variable = toString();",
" return param;",
" }",
"",
" String assignmentToAnnotatedVariable() {",
" @SuppressWarnings(\"HereBeDragons\")",
" String variable = toString();",
" return variable;",
" }",
"",
" String complexReturnStatement() {",
" String variable = toString();",
" return variable + toString();",
" }",
"",
" String assignmentInsideIfClause() {",
" String variable = null;",
" if (true) {",
" variable = toString();",
" }",
" return variable;",
" }",
"",
" String redundantAssignmentInsideElseClause() {",
" String variable = toString();",
" if (true) {",
" return variable;",
" } else {",
" // BUG: Diagnostic contains:",
" variable = \"foo\";",
" return variable;",
" }",
" }",
"",
" Supplier<String> redundantAssignmentInsideLambda() {",
" return () -> {",
" // BUG: Diagnostic contains:",
" String variable = toString();",
" return variable;",
" };",
" }",
"}")
.doTest();
}
@Test
void replacement() {
BugCheckerRefactoringTestHelper.newInstance(DirectReturn.class, getClass())
.addInputLines(
"A.java",
"class A {",
" String m1() {",
" String variable = null;",
" variable = toString();",
" return variable;",
" }",
"",
" String m2() {",
" String variable = toString();",
" return variable;",
" }",
"}")
.addOutputLines(
"A.java",
"class A {",
" String m1() {",
" String variable = null;",
" return toString();",
" }",
"",
" String m2() {",
" return toString();",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -67,7 +67,6 @@ final class FluxFlatMapUsageTest {
@Test
void replacementFirstSuggestedFix() {
BugCheckerRefactoringTestHelper.newInstance(FluxFlatMapUsage.class, getClass())
.setFixChooser(FixChoosers.FIRST)
.addInputLines(
"A.java",
"import reactor.core.publisher.Flux;",

View File

@@ -0,0 +1,182 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.common.base.Predicates.and;
import static com.google.common.base.Predicates.containsPattern;
import static com.google.common.base.Predicates.not;
import static com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers.SECOND;
import static com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers.THIRD;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
import reactor.core.CorePublisher;
import reactor.core.publisher.Flux;
final class FluxImplicitBlockTest {
@Test
void identification() {
CompilationTestHelper.newInstance(FluxImplicitBlock.class, getClass())
.expectErrorMessage(
"X",
and(
containsPattern("SuppressWarnings"),
containsPattern("toImmutableList"),
containsPattern("toList")))
.addSourceLines(
"A.java",
"import com.google.common.collect.ImmutableList;",
"import java.util.stream.Stream;",
"import reactor.core.publisher.Flux;",
"",
"class A {",
" void m() {",
" // BUG: Diagnostic matches: X",
" Flux.just(1).toIterable();",
" // BUG: Diagnostic matches: X",
" Flux.just(2).toStream();",
" // BUG: Diagnostic matches: X",
" long count = Flux.just(3).toStream().count();",
"",
" Flux.just(4).toIterable(1);",
" Flux.just(5).toIterable(2, null);",
" Flux.just(6).toStream(3);",
" new Foo().toIterable();",
" new Foo().toStream();",
" }",
"",
" class Foo<T> {",
" Iterable<T> toIterable() {",
" return ImmutableList.of();",
" }",
"",
" Stream<T> toStream() {",
" return Stream.empty();",
" }",
" }",
"}")
.doTest();
}
@Test
void identificationWithoutGuavaOnClasspath() {
CompilationTestHelper.newInstance(FluxImplicitBlock.class, getClass())
.withClasspath(CorePublisher.class, Flux.class, Publisher.class)
.expectErrorMessage("X", not(containsPattern("toImmutableList")))
.addSourceLines(
"A.java",
"import reactor.core.publisher.Flux;",
"",
"class A {",
" void m() {",
" // BUG: Diagnostic matches: X",
" Flux.just(1).toIterable();",
" // BUG: Diagnostic matches: X",
" Flux.just(2).toStream();",
" }",
"}")
.doTest();
}
@Test
void replacementFirstSuggestedFix() {
BugCheckerRefactoringTestHelper.newInstance(FluxImplicitBlock.class, getClass())
.addInputLines(
"A.java",
"import reactor.core.publisher.Flux;",
"",
"class A {",
" void m() {",
" Flux.just(1).toIterable();",
" Flux.just(2).toStream();",
" }",
"}")
.addOutputLines(
"A.java",
"import reactor.core.publisher.Flux;",
"",
"class A {",
" @SuppressWarnings(\"FluxImplicitBlock\")",
" void m() {",
" Flux.just(1).toIterable();",
" Flux.just(2).toStream();",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
@Test
void replacementSecondSuggestedFix() {
BugCheckerRefactoringTestHelper.newInstance(FluxImplicitBlock.class, getClass())
.setFixChooser(SECOND)
.addInputLines(
"A.java",
"import reactor.core.publisher.Flux;",
"",
"class A {",
" void m() {",
" Flux.just(1).toIterable();",
" Flux.just(2).toStream();",
" Flux.just(3).toIterable().iterator();",
" Flux.just(4).toStream().count();",
" Flux.just(5) /* a */./* b */ toIterable /* c */(/* d */ ) /* e */;",
" Flux.just(6) /* a */./* b */ toStream /* c */(/* d */ ) /* e */;",
" }",
"}")
.addOutputLines(
"A.java",
"import static com.google.common.collect.ImmutableList.toImmutableList;",
"",
"import reactor.core.publisher.Flux;",
"",
"class A {",
" void m() {",
" Flux.just(1).collect(toImmutableList()).block();",
" Flux.just(2).collect(toImmutableList()).block().stream();",
" Flux.just(3).collect(toImmutableList()).block().iterator();",
" Flux.just(4).collect(toImmutableList()).block().stream().count();",
" Flux.just(5).collect(toImmutableList()).block() /* e */;",
" Flux.just(6).collect(toImmutableList()).block().stream() /* e */;",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
@Test
void replacementThirdSuggestedFix() {
BugCheckerRefactoringTestHelper.newInstance(FluxImplicitBlock.class, getClass())
.setFixChooser(THIRD)
.addInputLines(
"A.java",
"import reactor.core.publisher.Flux;",
"",
"class A {",
" void m() {",
" Flux.just(1).toIterable();",
" Flux.just(2).toStream();",
" Flux.just(3).toIterable().iterator();",
" Flux.just(4).toStream().count();",
" Flux.just(5) /* a */./* b */ toIterable /* c */(/* d */ ) /* e */;",
" Flux.just(6) /* a */./* b */ toStream /* c */(/* d */ ) /* e */;",
" }",
"}")
.addOutputLines(
"A.java",
"import static java.util.stream.Collectors.toList;",
"",
"import reactor.core.publisher.Flux;",
"",
"class A {",
" void m() {",
" Flux.just(1).collect(toList()).block();",
" Flux.just(2).collect(toList()).block().stream();",
" Flux.just(3).collect(toList()).block().iterator();",
" Flux.just(4).collect(toList()).block().stream().count();",
" Flux.just(5).collect(toList()).block() /* e */;",
" Flux.just(6).collect(toList()).block().stream() /* e */;",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -180,7 +180,6 @@ final class IdentityConversionTest {
@Test
void replacementFirstSuggestedFix() {
BugCheckerRefactoringTestHelper.newInstance(IdentityConversion.class, getClass())
.setFixChooser(FixChoosers.FIRST)
.addInputLines(
"A.java",
"import static com.google.errorprone.matchers.Matchers.staticMethod;",

View File

@@ -0,0 +1,496 @@
package tech.picnic.errorprone.bugpatterns;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class JUnitValueSourceTest {
@Test
void identification() {
CompilationTestHelper.newInstance(JUnitValueSource.class, getClass())
.addSourceLines(
"A.java",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import java.util.Optional;",
"import java.util.stream.Stream;",
"import org.junit.jupiter.params.ParameterizedTest;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.junit.jupiter.params.provider.MethodSource;",
"",
"class A {",
" private static Stream<Arguments> identificationTestCases() {",
" return Stream.of(arguments(1), Arguments.of(2));",
" }",
"",
" @ParameterizedTest",
" // BUG: Diagnostic contains:",
" @MethodSource(\"identificationTestCases\")",
" void identification(int foo) {}",
"",
" private static int[] identificationWithParensTestCases() {",
" return new int[] {1, 2};",
" }",
"",
" @ParameterizedTest",
" // BUG: Diagnostic contains:",
" @MethodSource(\"identificationWithParensTestCases()\")",
" void identificationWithParens(int foo) {}",
"",
" @ParameterizedTest",
" @MethodSource(\"valueFactoryMissingTestCases\")",
" void valueFactoryMissing(int foo) {}",
"",
" private static Stream<Arguments> multipleUsagesTestCases() {",
" return Stream.of(arguments(1), Arguments.of(2));",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"multipleUsagesTestCases\")",
" void multipleUsages1(int foo) {}",
"",
" @ParameterizedTest",
" @MethodSource(\"multipleUsagesTestCases()\")",
" void multipleUsages2(int bar) {}",
"",
" private static Stream<Arguments> valueFactoryRepeatedTestCases() {",
" return Stream.of(arguments(1), arguments(2));",
" }",
"",
" @ParameterizedTest",
" @MethodSource({\"valueFactoryRepeatedTestCases\", \"valueFactoryRepeatedTestCases\"})",
" void valueFactoryRepeated(int foo) {}",
"",
" private static Stream<Arguments> multipleParametersTestCases() {",
" return Stream.of(arguments(1, 2), arguments(3, 4));",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"multipleParametersTestCases\")",
" void multipleParameters(int first, int second) {}",
"",
" private static int[] arrayWithoutInitializersTestCases() {",
" return new int[1];",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"arrayWithoutInitializersTestCases\")",
" void arrayWithoutInitializers(int foo) {}",
"",
" private static Stream<Arguments> runtimeValueTestCases() {",
" int second = 2;",
" return Stream.of(arguments(1), arguments(second));",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"runtimeValueTestCases\")",
" void runtimeValue(int foo) {}",
"",
" private static Stream<Arguments> streamChainTestCases() {",
" return Stream.of(1, 2).map(Arguments::arguments);",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"streamChainTestCases\")",
" void streamChain(int number) {}",
"",
" private static Stream<Arguments> multipleReturnsTestCases() {",
" if (true) {",
" return Stream.of(arguments(1), arguments(2));",
" } else {",
" return Stream.of(arguments(3), arguments(4));",
" }",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"multipleReturnsTestCases\")",
" void multipleReturns(int number) {}",
"",
" private static Stream<Arguments> multipleFactoriesFooTestCases() {",
" return Stream.of(arguments(1));",
" }",
"",
" private static Stream<Arguments> multipleFactoriesBarTestCases() {",
" return Stream.of(arguments(1));",
" }",
"",
" @ParameterizedTest",
" @MethodSource({\"multipleFactoriesFooTestCases\", \"multipleFactoriesBarTestCases\"})",
" void multipleFactories(int i) {}",
"",
" private static Stream<Arguments> extraArgsTestCases() {",
" return Stream.of(arguments(1), arguments(1, 2));",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"extraArgsTestCases\")",
" void extraArgs(int... i) {}",
"",
" private static Stream<Arguments> localClassTestCases() {",
" class Foo {",
" Stream<Arguments> foo() {",
" return Stream.of(arguments(1), arguments(2));",
" }",
" }",
" return Stream.of(arguments(1), arguments(2));",
" }",
"",
" @ParameterizedTest",
" // BUG: Diagnostic contains:",
" @MethodSource(\"localClassTestCases\")",
" void localClass(int i) {}",
"",
" private static Stream<Arguments> lambdaReturnTestCases() {",
" int foo =",
" Optional.of(10)",
" .map(",
" i -> {",
" return i / 2;",
" })",
" .orElse(0);",
" return Stream.of(arguments(1), arguments(1));",
" }",
"",
" @ParameterizedTest",
" // BUG: Diagnostic contains:",
" @MethodSource(\"lambdaReturnTestCases\")",
" void lambdaReturn(int i) {}",
"",
" @ParameterizedTest",
" @MethodSource(\"tech.picnic.errorprone.Foo#fooTestCases\")",
" void staticMethodReference(int foo) {}",
"",
" private static Stream<Arguments> valueFactoryWithArgumentTestCases(int amount) {",
" return Stream.of(arguments(1), arguments(2));",
" }",
"",
" @ParameterizedTest",
" // BUG: Diagnostic contains:",
" @MethodSource(\"valueFactoryWithArgumentTestCases\")",
" void valueFactoryWithArgument(int foo) {}",
"",
" private static Arguments[] emptyArrayValueFactoryTestCases() {",
" return new Arguments[] {};",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"emptyArrayValueFactoryTestCases\")",
" void emptyArrayValueFactory(int foo) {}",
"",
" private static Stream<Arguments> emptyStreamValueFactoryTestCases() {",
" return Stream.of();",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"emptyStreamValueFactoryTestCases\")",
" void emptyStreamValueFactory(int foo) {}",
"",
" private static Arguments[] invalidValueFactoryArgumentsTestCases() {",
" return new Arguments[] {arguments(1), arguments(new Object() {})};",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"invalidValueFactoryArgumentsTestCases\")",
" void invalidValueFactoryArguments(int foo) {}",
"}")
.doTest();
}
@Test
void replacement() {
BugCheckerRefactoringTestHelper.newInstance(JUnitValueSource.class, getClass())
.addInputLines(
"A.java",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import com.google.common.collect.ImmutableList;",
"import com.google.common.collect.ImmutableSet;",
"import java.util.List;",
"import java.util.Set;",
"import java.util.stream.DoubleStream;",
"import java.util.stream.IntStream;",
"import java.util.stream.LongStream;",
"import java.util.stream.Stream;",
"import org.junit.jupiter.params.ParameterizedTest;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.junit.jupiter.params.provider.MethodSource;",
"",
"class A {",
" private static final boolean CONST_BOOLEAN = false;",
" private static final byte CONST_BYTE = 42;",
" private static final char CONST_CHARACTER = 'a';",
" private static final short CONST_SHORT = 42;",
" private static final int CONST_INTEGER = 42;",
" private static final long CONST_LONG = 42;",
" private static final float CONST_FLOAT = 42;",
" private static final double CONST_DOUBLE = 42;",
" private static final String CONST_STRING = \"foo\";",
"",
" private static Stream<Arguments> streamOfBooleanArguments() {",
" return Stream.of(arguments(false), arguments(true), arguments(CONST_BOOLEAN));",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"streamOfBooleanArguments\")",
" void primitiveBoolean(boolean b) {}",
"",
" private static Stream<Object> streamOfBooleansAndBooleanArguments() {",
" return Stream.of(false, arguments(true), CONST_BOOLEAN);",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"streamOfBooleansAndBooleanArguments\")",
" void boxedBoolean(Boolean b) {}",
"",
" private static List<Arguments> listOfByteArguments() {",
" return List.of(arguments((byte) 0), arguments((byte) 1), arguments(CONST_BYTE));",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"listOfByteArguments\")",
" void primitiveByte(byte b) {}",
"",
" private static List<Object> listOfBytesAndByteArguments() {",
" return List.of((byte) 0, arguments((byte) 1), CONST_BYTE);",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"listOfBytesAndByteArguments\")",
" void boxedByte(Byte b) {}",
"",
" private static Set<Arguments> setOfCharacterArguments() {",
" return Set.of(arguments((char) 0), arguments((char) 1), arguments(CONST_CHARACTER));",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"setOfCharacterArguments\")",
" void primitiveCharacter(char c) {}",
"",
" private static Set<Object> setOfCharactersAndCharacterArguments() {",
" return Set.of((char) 0, arguments((char) 1), CONST_CHARACTER);",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"setOfCharactersAndCharacterArguments\")",
" void boxedCharacter(Character c) {}",
"",
" private static Arguments[] arrayOfShortArguments() {",
" return new Arguments[] {arguments((short) 0), arguments((short) 1), arguments(CONST_SHORT)};",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"arrayOfShortArguments\")",
" void primitiveShort(short s) {}",
"",
" private static Object[] arrayOfShortsAndShortArguments() {",
" return new Object[] {(short) 0, arguments((short) 1), CONST_SHORT};",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"arrayOfShortsAndShortArguments\")",
" void boxedShort(Short s) {}",
"",
" private static IntStream intStream() {",
" return IntStream.of(0, 1, CONST_INTEGER);",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"intStream\")",
" void primitiveInteger(int i) {}",
"",
" private static int[] intArray() {",
" return new int[] {0, 1, CONST_INTEGER};",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"intArray\")",
" void boxedInteger(Integer i) {}",
"",
" private static LongStream longStream() {",
" return LongStream.of(0, 1, CONST_LONG);",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"longStream\")",
" void primitiveLong(long l) {}",
"",
" private static long[] longArray() {",
" return new long[] {0, 1, CONST_LONG};",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"longArray\")",
" void boxedLong(Long l) {}",
"",
" private static ImmutableList<Arguments> immutableListOfFloatArguments() {",
" return ImmutableList.of(arguments(0.0F), arguments(1.0F), arguments(CONST_FLOAT));",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"immutableListOfFloatArguments\")",
" void primitiveFloat(float f) {}",
"",
" private static Stream<Object> streamOfFloatsAndFloatArguments() {",
" return Stream.of(0.0F, arguments(1.0F), CONST_FLOAT);",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"streamOfFloatsAndFloatArguments\")",
" void boxedFloat(Float f) {}",
"",
" private static DoubleStream doubleStream() {",
" return DoubleStream.of(0, 1, CONST_DOUBLE);",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"doubleStream\")",
" void primitiveDouble(double d) {}",
"",
" private static double[] doubleArray() {",
" return new double[] {0, 1, CONST_DOUBLE};",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"doubleArray\")",
" void boxedDouble(Double d) {}",
"",
" private static ImmutableSet<Arguments> immutableSetOfStringArguments() {",
" return ImmutableSet.of(arguments(\"foo\"), arguments(\"bar\"), arguments(CONST_STRING));",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"immutableSetOfStringArguments\")",
" void string(String s) {}",
"",
" private static Stream<Class<?>> streamOfClasses() {",
" return Stream.of(Stream.class, java.util.Map.class);",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"streamOfClasses\")",
" void clazz(Class<?> c) {}",
"",
" private static Stream<Arguments> sameNameFactoryTestCases() {",
" return Stream.of(arguments(1));",
" }",
"",
" private static Stream<Arguments> sameNameFactoryTestCases(int overload) {",
" return Stream.of(arguments(overload));",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"sameNameFactoryTestCases\")",
" void sameNameFactory(int i) {}",
"}")
.addOutputLines(
"A.java",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import com.google.common.collect.ImmutableList;",
"import com.google.common.collect.ImmutableSet;",
"import java.util.List;",
"import java.util.Set;",
"import java.util.stream.DoubleStream;",
"import java.util.stream.IntStream;",
"import java.util.stream.LongStream;",
"import java.util.stream.Stream;",
"import org.junit.jupiter.params.ParameterizedTest;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.junit.jupiter.params.provider.MethodSource;",
"import org.junit.jupiter.params.provider.ValueSource;",
"",
"class A {",
" private static final boolean CONST_BOOLEAN = false;",
" private static final byte CONST_BYTE = 42;",
" private static final char CONST_CHARACTER = 'a';",
" private static final short CONST_SHORT = 42;",
" private static final int CONST_INTEGER = 42;",
" private static final long CONST_LONG = 42;",
" private static final float CONST_FLOAT = 42;",
" private static final double CONST_DOUBLE = 42;",
" private static final String CONST_STRING = \"foo\";",
"",
" @ParameterizedTest",
" @ValueSource(booleans = {false, true, CONST_BOOLEAN})",
" void primitiveBoolean(boolean b) {}",
"",
" @ParameterizedTest",
" @ValueSource(booleans = {false, true, CONST_BOOLEAN})",
" void boxedBoolean(Boolean b) {}",
"",
" @ParameterizedTest",
" @ValueSource(bytes = {(byte) 0, (byte) 1, CONST_BYTE})",
" void primitiveByte(byte b) {}",
"",
" @ParameterizedTest",
" @ValueSource(bytes = {(byte) 0, (byte) 1, CONST_BYTE})",
" void boxedByte(Byte b) {}",
"",
" @ParameterizedTest",
" @ValueSource(chars = {(char) 0, (char) 1, CONST_CHARACTER})",
" void primitiveCharacter(char c) {}",
"",
" @ParameterizedTest",
" @ValueSource(chars = {(char) 0, (char) 1, CONST_CHARACTER})",
" void boxedCharacter(Character c) {}",
"",
" @ParameterizedTest",
" @ValueSource(shorts = {(short) 0, (short) 1, CONST_SHORT})",
" void primitiveShort(short s) {}",
"",
" @ParameterizedTest",
" @ValueSource(shorts = {(short) 0, (short) 1, CONST_SHORT})",
" void boxedShort(Short s) {}",
"",
" @ParameterizedTest",
" @ValueSource(ints = {0, 1, CONST_INTEGER})",
" void primitiveInteger(int i) {}",
"",
" @ParameterizedTest",
" @ValueSource(ints = {0, 1, CONST_INTEGER})",
" void boxedInteger(Integer i) {}",
"",
" @ParameterizedTest",
" @ValueSource(longs = {0, 1, CONST_LONG})",
" void primitiveLong(long l) {}",
"",
" @ParameterizedTest",
" @ValueSource(longs = {0, 1, CONST_LONG})",
" void boxedLong(Long l) {}",
"",
" @ParameterizedTest",
" @ValueSource(floats = {0.0F, 1.0F, CONST_FLOAT})",
" void primitiveFloat(float f) {}",
"",
" @ParameterizedTest",
" @ValueSource(floats = {0.0F, 1.0F, CONST_FLOAT})",
" void boxedFloat(Float f) {}",
"",
" @ParameterizedTest",
" @ValueSource(doubles = {0, 1, CONST_DOUBLE})",
" void primitiveDouble(double d) {}",
"",
" @ParameterizedTest",
" @ValueSource(doubles = {0, 1, CONST_DOUBLE})",
" void boxedDouble(Double d) {}",
"",
" @ParameterizedTest",
" @ValueSource(strings = {\"foo\", \"bar\", CONST_STRING})",
" void string(String s) {}",
"",
" @ParameterizedTest",
" @ValueSource(classes = {Stream.class, java.util.Map.class})",
" void clazz(Class<?> c) {}",
"",
" private static Stream<Arguments> sameNameFactoryTestCases(int overload) {",
" return Stream.of(arguments(overload));",
" }",
"",
" @ParameterizedTest",
" @ValueSource(ints = 1)",
" void sameNameFactory(int i) {}",
"}")
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -0,0 +1,136 @@
package tech.picnic.errorprone.bugpatterns;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class MockitoMockClassReferenceTest {
@Test
void identification() {
CompilationTestHelper.newInstance(MockitoMockClassReference.class, getClass())
.addSourceLines(
"A.java",
"import static org.mockito.Mockito.mock;",
"import static org.mockito.Mockito.spy;",
"import static org.mockito.Mockito.withSettings;",
"",
"import java.util.List;",
"import java.util.Objects;",
"import org.mockito.invocation.InvocationOnMock;",
"",
"class A {",
" {",
" Double d = Objects.requireNonNullElseGet(null, () -> mock(Double.class));",
" Double d2 =",
" Objects.requireNonNullElseGet(",
" null,",
" () -> {",
" return mock(Double.class);",
" });",
" }",
"",
" void m() {",
" Number variableMock = 42;",
" // BUG: Diagnostic contains:",
" variableMock = mock(Number.class);",
" // BUG: Diagnostic contains:",
" variableMock = mock(Number.class, \"name\");",
" // BUG: Diagnostic contains:",
" variableMock = mock(Number.class, InvocationOnMock::callRealMethod);",
" // BUG: Diagnostic contains:",
" variableMock = mock(Number.class, withSettings());",
" variableMock = mock(Integer.class);",
" variableMock = 42;",
" // BUG: Diagnostic contains:",
" List rawMock = mock(List.class);",
" // BUG: Diagnostic contains:",
" List<String> genericMock = mock(List.class);",
" var varMock = mock(Integer.class);",
" Class<? extends Number> numberType = Integer.class;",
" Number variableTypeMock = mock(numberType);",
" Object subtypeMock = mock(Integer.class);",
"",
" Number variableSpy = 42;",
" // BUG: Diagnostic contains:",
" variableSpy = spy(Number.class);",
" variableSpy = spy(Integer.class);",
" variableSpy = 42;",
" // BUG: Diagnostic contains:",
" List rawSpy = spy(List.class);",
" // BUG: Diagnostic contains:",
" List<String> genericSpy = spy(List.class);",
" var varSpy = spy(Integer.class);",
" Number variableTypeSpy = spy(numberType);",
" Object subtypeSpy = spy(Integer.class);",
" Object objectSpy = spy(new Object());",
"",
" Objects.hash(mock(Integer.class));",
" Integer i = mock(mock(Integer.class));",
" String s = new String(mock(String.class));",
" }",
"",
" Double getDoubleMock() {",
" return Objects.requireNonNullElseGet(",
" null,",
" () -> {",
" return mock(Double.class);",
" });",
" }",
"",
" Integer getIntegerMock() {",
" // BUG: Diagnostic contains:",
" return mock(Integer.class);",
" }",
"",
" <T> T getGenericMock(Class<T> clazz) {",
" return mock(clazz);",
" }",
"",
" Number getSubTypeMock() {",
" return mock(Integer.class);",
" }",
"}")
.doTest();
}
@Test
void replacement() {
BugCheckerRefactoringTestHelper.newInstance(MockitoMockClassReference.class, getClass())
.addInputLines(
"A.java",
"import static org.mockito.Mockito.mock;",
"import static org.mockito.Mockito.spy;",
"import static org.mockito.Mockito.withSettings;",
"",
"import org.mockito.invocation.InvocationOnMock;",
"",
"class A {",
" void m() {",
" Number simpleMock = mock(Number.class);",
" Number namedMock = mock(Number.class, \"name\");",
" Number customAnswerMock = mock(Number.class, InvocationOnMock::callRealMethod);",
" Number customSettingsMock = mock(Number.class, withSettings());",
" Number simpleSpy = spy(Number.class);",
" }",
"}")
.addOutputLines(
"A.java",
"import static org.mockito.Mockito.mock;",
"import static org.mockito.Mockito.spy;",
"import static org.mockito.Mockito.withSettings;",
"",
"import org.mockito.invocation.InvocationOnMock;",
"",
"class A {",
" void m() {",
" Number simpleMock = mock();",
" Number namedMock = mock(\"name\");",
" Number customAnswerMock = mock(InvocationOnMock::callRealMethod);",
" Number customSettingsMock = mock(withSettings());",
" Number simpleSpy = spy();",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -16,6 +16,8 @@ final class RequestMappingAnnotationTest {
"import javax.servlet.http.HttpServletRequest;",
"import javax.servlet.http.HttpServletResponse;",
"import org.springframework.http.HttpMethod;",
"import org.springframework.ui.Model;",
"import org.springframework.validation.BindingResult;",
"import org.springframework.web.bind.annotation.DeleteMapping;",
"import org.springframework.web.bind.annotation.GetMapping;",
"import org.springframework.web.bind.annotation.PatchMapping;",
@@ -82,6 +84,12 @@ final class RequestMappingAnnotationTest {
" A properHttpMethod(HttpMethod method);",
"",
" @RequestMapping",
" A properModel(Model model);",
"",
" @RequestMapping",
" A properBindingResult(BindingResult result);",
"",
" @RequestMapping",
" A properNativeWebRequest(NativeWebRequest request);",
"",
" @RequestMapping",

View File

@@ -48,7 +48,6 @@ final class StringCaseLocaleUsageTest {
@Test
void replacementFirstSuggestedFix() {
BugCheckerRefactoringTestHelper.newInstance(StringCaseLocaleUsage.class, getClass())
.setFixChooser(FixChoosers.FIRST)
.addInputLines(
"A.java",
"class A {",

View File

@@ -0,0 +1,73 @@
package tech.picnic.errorprone.bugpatterns.util;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import com.google.errorprone.BugPattern;
import com.google.errorprone.CompilationTestHelper;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.MethodTree;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import org.junit.jupiter.api.Test;
final class ConflictDetectionTest {
@Test
void matcher() {
CompilationTestHelper.newInstance(RenameBlockerFlagger.class, getClass())
.addSourceLines(
"pkg/A.java",
"package pkg;",
"",
"import static pkg.A.B.method3t;",
"",
"import pkg.A.method4t;",
"",
"class A {",
" void method1() {",
" method3t();",
" method4(method4t.class);",
" }",
"",
" // BUG: Diagnostic contains: a method named `method2t` is already defined in this class or a",
" // supertype",
" void method2() {}",
"",
" void method2t() {}",
"",
" // BUG: Diagnostic contains: `method3t` is already statically imported",
" void method3() {}",
"",
" void method4(Object o) {}",
"",
" // BUG: Diagnostic contains: `int` is not a valid identifier",
" void in() {}",
"",
" static class B {",
" static void method3t() {}",
" }",
"",
" class method4t {}",
"}")
.doTest();
}
/**
* A {@link BugChecker} that uses {@link ConflictDetection#findMethodRenameBlocker(MethodSymbol,
* String, VisitorState)} to flag methods of which the name cannot be suffixed with a {@code t}.
*/
@BugPattern(summary = "Interacts with `ConflictDetection` for testing purposes", severity = ERROR)
public static final class RenameBlockerFlagger extends BugChecker implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
return ConflictDetection.findMethodRenameBlocker(
ASTHelpers.getSymbol(tree), tree.getName() + "t", state)
.map(blocker -> buildDescription(tree).setMessage(blocker).build())
.orElse(Description.NO_MATCH);
}
}
}

View File

@@ -8,9 +8,16 @@ 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.ExpressionStatementTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.ReturnTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import java.util.List;
import java.util.function.BiFunction;
import org.junit.jupiter.api.Test;
@@ -67,7 +74,70 @@ final class MoreASTHelpersTest {
.doTest();
}
private static String createDiagnosticsMessage(
@Test
void findMethodExitedOnReturn() {
CompilationTestHelper.newInstance(FindMethodReturnTestChecker.class, getClass())
.addSourceLines(
"A.java",
"import java.util.stream.Stream;",
"",
"class A {",
" {",
" toString();",
" }",
"",
" String topLevelMethod() {",
" // BUG: Diagnostic contains: topLevelMethod",
" toString();",
" // BUG: Diagnostic contains: topLevelMethod",
" return toString();",
" }",
"",
" Stream<String> anotherMethod() {",
" // BUG: Diagnostic contains: anotherMethod",
" return Stream.of(1)",
" .map(",
" n -> {",
" toString();",
" return toString();",
" });",
" }",
"",
" void recursiveMethod(Runnable r) {",
" // BUG: Diagnostic contains: recursiveMethod",
" recursiveMethod(",
" new Runnable() {",
" @Override",
" public void run() {",
" // BUG: Diagnostic contains: run",
" toString();",
" }",
" });",
" }",
"}")
.doTest();
}
@Test
void areSameType() {
CompilationTestHelper.newInstance(AreSameTypeTestChecker.class, getClass())
.addSourceLines(
"A.java",
"class A {",
" void negative1(String a, Integer b) {}",
"",
" void negative2(Integer a, Number b) {}",
"",
" // BUG: Diagnostic contains:",
" void positive1(String a, String b) {}",
"",
" // BUG: Diagnostic contains:",
" void positive2(Iterable<String> a, Iterable<Integer> b) {}",
"}")
.doTest();
}
private static String createMethodSearchDiagnosticsMessage(
BiFunction<String, VisitorState, Object> valueFunction, VisitorState state) {
return Maps.toMap(ImmutableSet.of("foo", "bar", "baz"), key -> valueFunction.apply(key, state))
.toString();
@@ -85,7 +155,7 @@ final class MoreASTHelpersTest {
public Description matchMethod(MethodTree tree, VisitorState state) {
return buildDescription(tree)
.setMessage(
createDiagnosticsMessage(
createMethodSearchDiagnosticsMessage(
(methodName, s) -> MoreASTHelpers.findMethods(methodName, s).size(), state))
.build();
}
@@ -103,8 +173,55 @@ final class MoreASTHelpersTest {
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
return buildDescription(tree)
.setMessage(createDiagnosticsMessage(MoreASTHelpers::methodExistsInEnclosingClass, state))
.setMessage(
createMethodSearchDiagnosticsMessage(
MoreASTHelpers::methodExistsInEnclosingClass, state))
.build();
}
}
/**
* A {@link BugChecker} that delegates to {@link
* MoreASTHelpers#findMethodExitedOnReturn(VisitorState)}.
*/
@BugPattern(summary = "Interacts with `MoreASTHelpers` for testing purposes", severity = ERROR)
public static final class FindMethodReturnTestChecker extends BugChecker
implements ExpressionStatementTreeMatcher, ReturnTreeMatcher {
private static final long serialVersionUID = 1L;
@Override
public Description matchExpressionStatement(ExpressionStatementTree tree, VisitorState state) {
return flagMethodReturnLocation(tree, state);
}
@Override
public Description matchReturn(ReturnTree tree, VisitorState state) {
return flagMethodReturnLocation(tree, state);
}
private Description flagMethodReturnLocation(Tree tree, VisitorState state) {
return MoreASTHelpers.findMethodExitedOnReturn(state)
.map(m -> buildDescription(tree).setMessage(m.getName().toString()).build())
.orElse(Description.NO_MATCH);
}
}
/**
* A {@link BugChecker} that delegates to {@link MoreASTHelpers#areSameType(Tree, Tree,
* VisitorState)}.
*/
@BugPattern(summary = "Interacts with `MoreASTHelpers` for testing purposes", severity = ERROR)
public static final class AreSameTypeTestChecker extends BugChecker implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
List<? extends VariableTree> parameters = tree.getParameters();
return parameters.stream()
.skip(1)
.allMatch(p -> MoreASTHelpers.areSameType(p, parameters.get(0), state))
? describeMatch(tree)
: Description.NO_MATCH;
}
}
}

View File

@@ -83,6 +83,40 @@ final class MoreJUnitMatchersTest {
@Test
void getMethodSourceFactoryNames() {
CompilationTestHelper.newInstance(MethodSourceFactoryNamesTestChecker.class, getClass())
.addSourceLines(
"A.java",
"import org.junit.jupiter.params.provider.MethodSource;",
"",
"class A {",
" @MethodSource",
" // BUG: Diagnostic contains: [matchingMethodSource]",
" void matchingMethodSource(boolean b) {}",
"",
" @MethodSource(\"myValueFactory\")",
" // BUG: Diagnostic contains: [myValueFactory]",
" void singleCustomMethodSource(boolean b) {}",
"",
" @MethodSource({",
" \"nullary()\",",
" \"nullary()\",",
" \"\",",
" \"withStringParam(java.lang.String)\",",
" \"paramsUnspecified\"",
" })",
" // BUG: Diagnostic contains: [nullary, nullary, multipleMethodSources, withStringParam,",
" // paramsUnspecified]",
" void multipleMethodSources(boolean b) {}",
"",
" @MethodSource({\"foo\", \"()\", \"bar\"})",
" // BUG: Diagnostic contains: [foo, , bar]",
" void methodSourceWithoutName(boolean b) {}",
"}")
.doTest();
}
@Test
void getMethodSourceFactoryDescriptors() {
CompilationTestHelper.newInstance(MethodSourceFactoryDescriptorsTestChecker.class, getClass())
.addSourceLines(
"A.java",
"import org.junit.jupiter.params.provider.MethodSource;",
@@ -119,6 +153,14 @@ final class MoreJUnitMatchersTest {
" @MethodSource({\"myValueFactory\", \"\"})",
" // BUG: Diagnostic contains: [myValueFactory, customAndMatchingMethodSources]",
" void customAndMatchingMethodSources(boolean b) {}",
"",
" @MethodSource({\"factory\", \"\", \"factory\", \"\"})",
" // BUG: Diagnostic contains: [factory, repeatedMethodSources, factory, repeatedMethodSources]",
" void repeatedMethodSources(boolean b) {}",
"",
" @MethodSource({\"nullary()\", \"withStringParam(java.lang.String)\"})",
" // BUG: Diagnostic contains: [nullary(), withStringParam(java.lang.String)]",
" void methodSourcesWithParameterSpecification(boolean b) {}",
"}")
.doTest();
}
@@ -170,4 +212,25 @@ final class MoreJUnitMatchersTest {
.build();
}
}
/**
* A {@link BugChecker} that flags methods with a JUnit {@code @MethodSource} annotation by
* enumerating the associated value factory method descriptors.
*/
@BugPattern(summary = "Interacts with `MoreJUnitMatchers` for testing purposes", severity = ERROR)
public static final class MethodSourceFactoryDescriptorsTestChecker extends BugChecker
implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
AnnotationTree annotation =
Iterables.getOnlyElement(HAS_METHOD_SOURCE.multiMatchResult(tree, state).matchingNodes());
return buildDescription(tree)
.setMessage(
MoreJUnitMatchers.getMethodSourceFactoryDescriptors(annotation, tree).toString())
.build();
}
}
}

View File

@@ -31,7 +31,6 @@ final class SourceCodeTest {
.expectUnchanged()
.addInputLines(
"AnnotationDeletions.java",
"",
"interface AnnotationDeletions {",
" class SoleAnnotation {",
" @AnnotationToBeDeleted",
@@ -66,7 +65,6 @@ final class SourceCodeTest {
"}")
.addOutputLines(
"AnnotationDeletions.java",
"",
"interface AnnotationDeletions {",
" class SoleAnnotation {",
" void m() {}",
@@ -101,7 +99,6 @@ final class SourceCodeTest {
refactoringTestHelper
.addInputLines(
"MethodDeletions.java",
"",
"interface MethodDeletions {",
" class SoleMethod {",
" void methodToBeDeleted() {}",
@@ -141,7 +138,6 @@ final class SourceCodeTest {
"}")
.addOutputLines(
"MethodDeletions.java",
"",
"interface MethodDeletions {",
" class SoleMethod {}",
"",

View File

@@ -35,6 +35,7 @@ final class RefasterRulesTest {
AssertJThrowingCallableRules.class,
AssortedRules.class,
BigDecimalRules.class,
BugCheckerRules.class,
CollectionRules.class,
ComparatorRules.class,
DoubleStreamRules.class,

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;

View File

@@ -1,12 +1,22 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.AbstractStringAssert;
import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;
final class AssertJStringRulesTest implements RefasterRuleCollectionTestCase {
@Override
public ImmutableSet<?> elidedTypesAndStaticImports() {
return ImmutableSet.of(Files.class);
}
void testAbstractStringAssertStringIsEmpty() {
assertThat("foo").isEqualTo("");
}
@@ -30,4 +40,12 @@ final class AssertJStringRulesTest implements RefasterRuleCollectionTestCase {
AbstractAssert<?, ?> testAssertThatDoesNotMatch() {
return assertThat("foo".matches(".*")).isFalse();
}
AbstractStringAssert<?> testAssertThatPathContent() throws IOException {
return assertThat(Files.readString(Paths.get(""), Charset.defaultCharset()));
}
AbstractStringAssert<?> testAssertThatPathContentUtf8() throws IOException {
return assertThat(Files.readString(Paths.get("")));
}
}

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIOException;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static com.google.common.collect.ImmutableSet.toImmutableSet;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import com.google.common.collect.ImmutableSet;
import java.math.BigDecimal;

View File

@@ -0,0 +1,29 @@
package tech.picnic.errorprone.refasterrules.input;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers;
import com.google.errorprone.bugpatterns.BugChecker;
import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;
final class BugCheckerRulesTest implements RefasterRuleCollectionTestCase {
@Override
public ImmutableSet<?> elidedTypesAndStaticImports() {
return ImmutableSet.of(FixChoosers.class);
}
ImmutableSet<BugCheckerRefactoringTestHelper> testBugCheckerRefactoringTestHelperIdentity() {
return ImmutableSet.of(
BugCheckerRefactoringTestHelper.newInstance(BugChecker.class, getClass())
.setFixChooser(FixChoosers.FIRST),
BugCheckerRefactoringTestHelper.newInstance(BugChecker.class, getClass())
.setImportOrder("static-first"));
}
BugCheckerRefactoringTestHelper
testBugCheckerRefactoringTestHelperAddInputLinesExpectUnchanged() {
return BugCheckerRefactoringTestHelper.newInstance(BugChecker.class, getClass())
.addInputLines("A.java", "class A {}")
.addOutputLines("A.java", "class A {}");
}
}

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.reverseOrder;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import com.google.common.collect.BoundType;
import com.google.common.collect.ImmutableSet;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static com.google.common.collect.ImmutableListMultimap.flatteningToImmutableListMultimap;
import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.Comparator.naturalOrder;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static java.util.function.Function.identity;
@@ -107,4 +107,16 @@ final class ImmutableMapRulesTest implements RefasterRuleCollectionTestCase {
Map<String, String> testImmutableMapOf5() {
return Map.of("k1", "v1", "k2", "v2", "k3", "v3", "k4", "v4", "k5", "v5");
}
ImmutableMap<String, Integer> testImmutableMapCopyOfMapsFilterKeys() {
return ImmutableMap.of("foo", 1).entrySet().stream()
.filter(entry -> entry.getKey().length() > 1)
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
}
ImmutableMap<String, Integer> testImmutableMapCopyOfMapsFilterValues() {
return ImmutableMap.of("foo", 1).entrySet().stream()
.filter(entry -> entry.getValue() > 0)
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
}
}

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static com.google.common.collect.ImmutableMultiset.toImmutableMultiset;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static com.google.common.collect.ImmutableSetMultimap.flatteningToImmutableSetMultimap;
import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.refasterrules;
package tech.picnic.errorprone.refasterrules.input;
import static com.google.common.collect.ImmutableSet.toImmutableSet;

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