mirror of
https://github.com/jlengrand/error-prone-support.git
synced 2026-03-10 08:11:25 +00:00
Compare commits
90 Commits
pdsoels/we
...
sschroever
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b9ef09c77 | ||
|
|
554a3e634c | ||
|
|
7a3ae7c646 | ||
|
|
6d24540d01 | ||
|
|
7637ffee24 | ||
|
|
12d2b52e38 | ||
|
|
53daabe5df | ||
|
|
ee0884e65f | ||
|
|
3af81d8b10 | ||
|
|
ebd64c1077 | ||
|
|
929f1dd1c7 | ||
|
|
9ddd91a50e | ||
|
|
a1227ca710 | ||
|
|
44e0904357 | ||
|
|
9b54c73dc0 | ||
|
|
8ace5b7e9a | ||
|
|
94ffc5d495 | ||
|
|
977019c5bf | ||
|
|
6514236514 | ||
|
|
6d23fbdd35 | ||
|
|
b6f14c073a | ||
|
|
ae22e0ec5e | ||
|
|
6e6f8d9f7b | ||
|
|
68d0bed36c | ||
|
|
2edbc85c28 | ||
|
|
0fefb6985e | ||
|
|
64f9d6b7a2 | ||
|
|
b0f99e7c0b | ||
|
|
320c4175c9 | ||
|
|
e9829d93bf | ||
|
|
b273502e88 | ||
|
|
8c6bd1b6e7 | ||
|
|
0c1817c589 | ||
|
|
73cf28e7ff | ||
|
|
8a0abf5957 | ||
|
|
5fb4aed3ad | ||
|
|
aef9c5da7a | ||
|
|
7069e7a6d8 | ||
|
|
334c374ca1 | ||
|
|
57cd084f82 | ||
|
|
0b3be1b75b | ||
|
|
902538fd4a | ||
|
|
50f6b770e4 | ||
|
|
47e0a779bd | ||
|
|
973d3c3cd9 | ||
|
|
edb7290e2e | ||
|
|
d5c45e003f | ||
|
|
f784c64150 | ||
|
|
978c90db9d | ||
|
|
ae89a37934 | ||
|
|
8f1d1df747 | ||
|
|
04368e9243 | ||
|
|
156df71616 | ||
|
|
64b1c7eea4 | ||
|
|
80d0d85826 | ||
|
|
d30c99a28f | ||
|
|
29c23542da | ||
|
|
62c1c277ae | ||
|
|
8580e89008 | ||
|
|
06c8b164e9 | ||
|
|
fd9d3157bc | ||
|
|
a623f73c1c | ||
|
|
f9d0cd99d6 | ||
|
|
9bec3de372 | ||
|
|
4164514c5b | ||
|
|
c3cd535b16 | ||
|
|
64195279cc | ||
|
|
61c9f67f66 | ||
|
|
4bb14b01ec | ||
|
|
b267b4dba8 | ||
|
|
03f0e0493b | ||
|
|
2111c81784 | ||
|
|
43d50f2ef9 | ||
|
|
2d972fd975 | ||
|
|
ee265a87ae | ||
|
|
6b4fba62da | ||
|
|
e883e28e34 | ||
|
|
d84de6efba | ||
|
|
4dca61a144 | ||
|
|
dc9597a603 | ||
|
|
ec9853ac88 | ||
|
|
5bb1dd1a10 | ||
|
|
fd6a45ebd8 | ||
|
|
82d4677509 | ||
|
|
1fdf1016b7 | ||
|
|
80e537fce2 | ||
|
|
d85897ea62 | ||
|
|
c5bde3999d | ||
|
|
575d494303 | ||
|
|
844ef84d55 |
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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
|
||||
|
||||
|
||||
18
.github/workflows/build.yaml
vendored
18
.github/workflows/build.yaml
vendored
@@ -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
|
||||
@@ -29,13 +29,15 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
steps:
|
||||
# We run the build twice for each supported JDK: once against the
|
||||
# original Error Prone release, using only Error Prone checks available
|
||||
# on Maven Central, and once against the Picnic Error Prone fork,
|
||||
# additionally enabling all checks defined in this project and any
|
||||
# Error Prone checks available only from other artifact repositories.
|
||||
# We run the build twice for each supported JDK: once against the
|
||||
# original Error Prone release, using only Error Prone checks available
|
||||
# on Maven Central, and once against the Picnic Error Prone fork,
|
||||
# additionally enabling all checks defined in this project and any Error
|
||||
# Prone checks available only from other artifact repositories.
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3.8.0
|
||||
with:
|
||||
|
||||
44
.github/workflows/codeql.yml
vendored
Normal file
44
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# Analyzes the code using GitHub's default CodeQL query database.
|
||||
# Identified issues are registered with GitHub's code scanning dashboard. When
|
||||
# a pull request is analyzed, any offending lines are annotated. See
|
||||
# https://codeql.github.com for details.
|
||||
name: CodeQL analysis
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '0 4 * * 1'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
analyze:
|
||||
strategy:
|
||||
matrix:
|
||||
language: [ java, ruby ]
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3.8.0
|
||||
with:
|
||||
java-version: 17.0.6
|
||||
distribution: temurin
|
||||
cache: maven
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2.2.11
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Perform minimal build
|
||||
if: matrix.language == 'java'
|
||||
run: mvn -T1C clean install -DskipTests -Dverification.skip
|
||||
- name: Perform CodeQL analysis
|
||||
uses: github/codeql-action/analyze@v2.2.11
|
||||
with:
|
||||
category: /language:${{ matrix.language }}
|
||||
25
.github/workflows/deploy-website.yaml
vendored
25
.github/workflows/deploy-website.yaml
vendored
@@ -3,32 +3,37 @@ on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master, website ]
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ruby/setup-ruby@v1.126.0
|
||||
with:
|
||||
working-directory: ./website
|
||||
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` and update `website/README.md` once
|
||||
# module `docgen` has no errors.
|
||||
run: 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:
|
||||
|
||||
36
.github/workflows/openssf-scorecard.yml
vendored
Normal file
36
.github/workflows/openssf-scorecard.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# Analyzes the code base and GitHub project configuration for adherence to
|
||||
# security best practices for open source software. Identified issues are
|
||||
# registered with GitHub's code scanning dashboard. When a pull request is
|
||||
# analyzed, any offending lines are annotated. See
|
||||
# https://securityscorecards.dev for details.
|
||||
name: OpenSSF Scorecard update
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '0 4 * * 1'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
analyze:
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
id-token: write
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run OpenSSF Scorecard analysis
|
||||
uses: ossf/scorecard-action@v2.1.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
publish_results: ${{ github.ref == 'refs/heads/master' }}
|
||||
- name: Update GitHub's code scanning dashboard
|
||||
uses: github/codeql-action/upload-sarif@v2.2.11
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
3
.github/workflows/pitest-analyze-pr.yml
vendored
3
.github/workflows/pitest-analyze-pr.yml
vendored
@@ -15,10 +15,11 @@ jobs:
|
||||
uses: actions/checkout@v3.1.0
|
||||
with:
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
- 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
|
||||
|
||||
12
.github/workflows/pitest-update-pr.yml
vendored
12
.github/workflows/pitest-update-pr.yml
vendored
@@ -9,20 +9,24 @@ on:
|
||||
- completed
|
||||
permissions:
|
||||
actions: read
|
||||
checks: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
jobs:
|
||||
update-pr:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
permissions:
|
||||
actions: read
|
||||
checks: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- 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
|
||||
|
||||
@@ -48,6 +48,17 @@ be accepted. When in doubt, make sure to first raise an
|
||||
|
||||
To the extent possible, the pull request process guards our coding guidelines.
|
||||
Some pointers:
|
||||
- Try to make sure that the
|
||||
[`./run-full-build.sh`][error-prone-support-full-build] script completes
|
||||
successfully, ideally before opening a pull request. See the [development
|
||||
instructions][error-prone-support-developing] for details on how to
|
||||
efficiently resolve many of the errors and warnings that may be reported. (In
|
||||
particular, make sure to run `mvn fmt:format` and
|
||||
[`./apply-error-prone-suggestions.sh`][error-prone-support-patch].) That
|
||||
said, if you feel that the build fails for invalid or debatable reasons, or
|
||||
if you're unsure how to best resolve an issue, don't let that discourage you
|
||||
from opening a PR with a failing build; we can have a look at the issue
|
||||
together!
|
||||
- Checks should be _topical_: ideally they address a single concern.
|
||||
- Where possible checks should provide _fixes_, and ideally these are
|
||||
completely behavior-preserving. In order for a check to be adopted by users
|
||||
@@ -66,6 +77,9 @@ Some pointers:
|
||||
sneak in unrelated changes; instead just open more than one pull request 😉.
|
||||
|
||||
[error-prone-criteria]: https://errorprone.info/docs/criteria
|
||||
[error-prone-support-developing]: https://github.com/PicnicSupermarket/error-prone-support/tree/master#-developing-error-prone-support
|
||||
[error-prone-support-full-build]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-full-build.sh
|
||||
[error-prone-support-issues]: https://github.com/PicnicSupermarket/error-prone-support/issues
|
||||
[error-prone-support-mutation-tests]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-mutation-tests.sh
|
||||
[error-prone-support-patch]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/apply-error-prone-suggestions.sh
|
||||
[error-prone-support-pulls]: https://github.com/PicnicSupermarket/error-prone-support/pulls
|
||||
|
||||
45
README.md
45
README.md
@@ -171,8 +171,15 @@ rules][refaster-rules].
|
||||
## 👷 Developing Error Prone Support
|
||||
|
||||
This is a [Maven][maven] project, so running `mvn clean install` performs a
|
||||
full clean build and installs the library to your local Maven repository. Some
|
||||
relevant flags:
|
||||
full clean build and installs the library to your local Maven repository.
|
||||
|
||||
Once you've made changes, the build may fail due to a warning or error emitted
|
||||
by static code analysis. The flags and commands listed below allow you to
|
||||
suppress or (in a large subset of cases) automatically fix such cases. Make
|
||||
sure to carefully check the available options, as this can save you significant
|
||||
amounts of development time!
|
||||
|
||||
Relevant Maven build parameters:
|
||||
|
||||
- `-Dverification.warn` makes the warnings and errors emitted by various
|
||||
plugins and the Java compiler non-fatal, where possible.
|
||||
@@ -189,19 +196,25 @@ relevant flags:
|
||||
Pending a release of [google/error-prone#3301][error-prone-pull-3301], this
|
||||
flag must currently be used in combination with `-Perror-prone-fork`.
|
||||
|
||||
Some other commands one may find relevant:
|
||||
Other highly relevant commands:
|
||||
|
||||
- `mvn fmt:format` formats the code using
|
||||
[`google-java-format`][google-java-format].
|
||||
- `./run-mutation-tests.sh` runs mutation tests using [Pitest][pitest]. The
|
||||
results can be reviewed by opening the respective
|
||||
- [`./run-full-build.sh`][script-run-full-build] builds the project twice,
|
||||
where the second pass validates compatbility with Picnic's [Error Prone
|
||||
fork][error-prone-fork-repo] and compliance of the code with any rules
|
||||
defined within this project. (Consider running this before [opening a pull
|
||||
request][contributing-pull-request], as the PR checks also perform this
|
||||
validation.)
|
||||
- [`./apply-error-prone-suggestions.sh`][script-apply-error-prone-suggestions]
|
||||
applies Error Prone and Error Prone Support code suggestions to this project.
|
||||
Before running this command, make sure to have installed the project (`mvn
|
||||
clean install`) and make sure that the current working directory does not
|
||||
contain unstaged or uncommited changes.
|
||||
- [`./run-mutation-tests.sh`][script-run-mutation-tests] runs mutation tests
|
||||
using [Pitest][pitest]. The results can be reviewed by opening the respective
|
||||
`target/pit-reports/index.html` files. For more information check the [PIT
|
||||
Maven plugin][pitest-maven].
|
||||
- `./apply-error-prone-suggestions.sh` applies Error Prone and Error Prone
|
||||
Support code suggestions to this project. Before running this command, make
|
||||
sure to have installed the project (`mvn clean install`) and make sure that
|
||||
the current working directory does not contain unstaged or uncommited
|
||||
changes.
|
||||
|
||||
When running the project's tests in IntelliJ IDEA, you might see the following
|
||||
error:
|
||||
@@ -228,9 +241,15 @@ Want to report or fix a bug, suggest or add a new feature, or improve the
|
||||
documentation? That's awesome! Please read our [contribution
|
||||
guidelines][contributing].
|
||||
|
||||
### Security
|
||||
|
||||
If you want to report a security vulnerablity, please do so through a private
|
||||
channel; please see our [security policy][security] for details.
|
||||
|
||||
[bug-checks]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/
|
||||
[bug-checks-identity-conversion]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/IdentityConversion.java
|
||||
[contributing]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/CONTRIBUTING.md
|
||||
[contributing-pull-request]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/CONTRIBUTING.md#-opening-a-pull-request
|
||||
[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
|
||||
@@ -247,8 +266,8 @@ guidelines][contributing].
|
||||
[maven-central-badge]: https://img.shields.io/maven-central/v/tech.picnic.error-prone-support/error-prone-support?color=blue
|
||||
[maven-central-search]: https://search.maven.org/artifact/tech.picnic.error-prone-support/error-prone-support
|
||||
[maven]: https://maven.apache.org
|
||||
[picnic-blog]: https://blog.picnic.nl
|
||||
[picnic-blog-ep-post]: https://blog.picnic.nl/picnic-loves-error-prone-producing-high-quality-and-consistent-java-code-b8a566be6886
|
||||
[picnic-blog]: https://blog.picnic.nl
|
||||
[pitest]: https://pitest.org
|
||||
[pitest-maven]: https://pitest.org/quickstart/maven
|
||||
[pr-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
|
||||
@@ -257,3 +276,7 @@ guidelines][contributing].
|
||||
[refaster-rules]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/
|
||||
[reproducible-builds-badge]: https://img.shields.io/badge/Reproducible_Builds-ok-success?labelColor=1e5b96
|
||||
[reproducible-builds-report]: https://github.com/jvm-repo-rebuild/reproducible-central/blob/master/content/tech/picnic/error-prone-support/error-prone-support/README.md
|
||||
[script-apply-error-prone-suggestions]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/apply-error-prone-suggestions.sh
|
||||
[script-run-full-build]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-full-build.sh
|
||||
[script-run-mutation-tests]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-mutation-tests.sh
|
||||
[security]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/SECURITY.md
|
||||
|
||||
23
SECURITY.md
Normal file
23
SECURITY.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Security policy
|
||||
|
||||
We take security seriously. We are mindful of Error Prone Support's place in
|
||||
the software supply chain, and the risks and responsibilities that come with
|
||||
this.
|
||||
|
||||
## Supported versions
|
||||
|
||||
This project uses [semantic versioning][semantic-versioning]. In general, only
|
||||
the latest version of this software is supported. That said, if users have a
|
||||
compelling reason to ask for patch release of an older major release, then we
|
||||
will seriously consider such a request. We do urge users to stay up-to-date and
|
||||
use the latest release where feasible.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
To report a vulnerability, please visit the [security
|
||||
advisories][security-advisories] page and click _Report a vulnerability_. We
|
||||
will take such reports seriously and work with you to resolve the issue in a
|
||||
timely manner.
|
||||
|
||||
[security-advisories]: https://github.com/PicnicSupermarket/error-prone-support/security/advisories
|
||||
[semantic-versioning]: https://semver.org
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>tech.picnic.error-prone-support</groupId>
|
||||
<artifactId>error-prone-support</artifactId>
|
||||
<version>0.8.1-SNAPSHOT</version>
|
||||
<version>0.9.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>documentation-support</artifactId>
|
||||
@@ -27,6 +27,10 @@
|
||||
<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>
|
||||
|
||||
@@ -5,17 +5,19 @@ 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 com.sun.tools.javac.util.Context;
|
||||
import java.util.Optional;
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
import tech.picnic.errorprone.documentation.BugPatternExtractor.BugPatternDocumentation;
|
||||
|
||||
@@ -23,29 +25,38 @@ import tech.picnic.errorprone.documentation.BugPatternExtractor.BugPatternDocume
|
||||
* An {@link Extractor} that describes how to extract data from a {@code @BugPattern} annotation.
|
||||
*/
|
||||
@Immutable
|
||||
final class BugPatternExtractor implements Extractor<BugPatternDocumentation> {
|
||||
@Override
|
||||
public BugPatternDocumentation extract(ClassTree tree, Context context) {
|
||||
ClassSymbol symbol = ASTHelpers.getSymbol(tree);
|
||||
BugPattern annotation = symbol.getAnnotation(BugPattern.class);
|
||||
requireNonNull(annotation, "BugPattern annotation must be present");
|
||||
@AutoService(Extractor.class)
|
||||
public final class BugPatternExtractor implements Extractor<BugPatternDocumentation> {
|
||||
/** Instantiates a new {@link BugPatternExtractor} instance. */
|
||||
public BugPatternExtractor() {}
|
||||
|
||||
return new AutoValue_BugPatternExtractor_BugPatternDocumentation(
|
||||
symbol.getQualifiedName().toString(),
|
||||
annotation.name().isEmpty() ? tree.getSimpleName().toString() : annotation.name(),
|
||||
ImmutableList.copyOf(annotation.altNames()),
|
||||
annotation.link(),
|
||||
ImmutableList.copyOf(annotation.tags()),
|
||||
annotation.summary(),
|
||||
annotation.explanation(),
|
||||
annotation.severity(),
|
||||
annotation.disableable(),
|
||||
annotation.documentSuppression() ? getSuppressionAnnotations(tree) : ImmutableList.of());
|
||||
@Override
|
||||
public String identifier() {
|
||||
return "bugpattern";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canExtract(ClassTree tree) {
|
||||
return ASTHelpers.hasDirectAnnotationWithSimpleName(tree, BugPattern.class.getSimpleName());
|
||||
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()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,14 @@ 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;
|
||||
@@ -19,6 +23,7 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -27,6 +32,13 @@ import javax.tools.JavaFileObject;
|
||||
*/
|
||||
// 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);
|
||||
|
||||
@@ -51,19 +63,25 @@ final class DocumentationGeneratorTaskListener implements TaskListener {
|
||||
return;
|
||||
}
|
||||
|
||||
ClassTree classTree = JavacTrees.instance(context).getTree(taskEvent.getTypeElement());
|
||||
JavaFileObject sourceFile = taskEvent.getSourceFile();
|
||||
if (classTree == null || sourceFile == null) {
|
||||
CompilationUnitTree compilationUnit = taskEvent.getCompilationUnit();
|
||||
ClassTree classTree = JavacTrees.instance(context).getTree(taskEvent.getTypeElement());
|
||||
if (sourceFile == null || compilationUnit == null || classTree == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExtractorType.findMatchingType(classTree)
|
||||
.ifPresent(
|
||||
extractorType ->
|
||||
writeToFile(
|
||||
extractorType.getIdentifier(),
|
||||
getSimpleClassName(sourceFile.toUri()),
|
||||
extractorType.getExtractor().extract(classTree, context)));
|
||||
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() {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Interface implemented by classes that define how to extract data of some type {@link T} from a
|
||||
@@ -13,21 +14,20 @@ import com.sun.tools.javac.util.Context;
|
||||
@Immutable
|
||||
interface Extractor<T> {
|
||||
/**
|
||||
* Extracts and returns an instance of {@link T} using the provided arguments.
|
||||
* Returns the unique identifier of this extractor.
|
||||
*
|
||||
* @param tree The {@link ClassTree} to analyze and from which to extract instances of {@link T}.
|
||||
* @param context The {@link Context} in which the current compilation takes place.
|
||||
* @return A non-null instance of {@link T}.
|
||||
* @return A non-{@code null} string.
|
||||
*/
|
||||
// XXX: Drop `Context` parameter unless used.
|
||||
T extract(ClassTree tree, Context context);
|
||||
String identifier();
|
||||
|
||||
/**
|
||||
* Tells whether this {@link Extractor} can extract documentation content from the given {@link
|
||||
* ClassTree}.
|
||||
* Attempts to extract an instance of type {@link T} using the provided arguments.
|
||||
*
|
||||
* @param tree The {@link ClassTree} of interest.
|
||||
* @return {@code true} iff data extraction is supported.
|
||||
* @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.
|
||||
*/
|
||||
boolean canExtract(ClassTree tree);
|
||||
Optional<T> tryExtract(ClassTree tree, VisitorState state);
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Optional;
|
||||
|
||||
/** An enumeration of {@link Extractor} types. */
|
||||
enum ExtractorType {
|
||||
BUG_PATTERN("bugpattern", new BugPatternExtractor());
|
||||
|
||||
private static final ImmutableSet<ExtractorType> TYPES =
|
||||
Sets.immutableEnumSet(EnumSet.allOf(ExtractorType.class));
|
||||
|
||||
private final String identifier;
|
||||
private final Extractor<?> extractor;
|
||||
|
||||
ExtractorType(String identifier, Extractor<?> extractor) {
|
||||
this.identifier = identifier;
|
||||
this.extractor = extractor;
|
||||
}
|
||||
|
||||
String getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
Extractor<?> getExtractor() {
|
||||
return extractor;
|
||||
}
|
||||
|
||||
static Optional<ExtractorType> findMatchingType(ClassTree tree) {
|
||||
return TYPES.stream().filter(type -> type.getExtractor().canExtract(tree)).findFirst();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
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_CLASS_NAME_PATTERN = Pattern.compile("(.*)Test");
|
||||
|
||||
@Override
|
||||
public String identifier() {
|
||||
return "refaster-test-input";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<RefasterTemplateCollectionTestData> tryExtract(
|
||||
ClassTree tree, VisitorState state) {
|
||||
if (!state.getPath().getCompilationUnit().getPackageName().toString().contains("input")) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
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_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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
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_CLASS_NAME_PATTERN = Pattern.compile("(.*)Test");
|
||||
|
||||
@Override
|
||||
public String identifier() {
|
||||
return "refaster-test-output";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<RefasterTemplateCollectionTestData> tryExtract(
|
||||
ClassTree tree, VisitorState state) {
|
||||
if (!state.getPath().getCompilationUnit().getPackageName().toString().contains("output")) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
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_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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package tech.picnic.errorprone.documentation;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.errorprone.BugPattern;
|
||||
@@ -14,7 +13,6 @@ 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.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
@@ -120,7 +118,8 @@ final class BugPatternExtractorTest {
|
||||
|
||||
private static void verifyFileMatchesResource(
|
||||
Path outputDirectory, String fileName, String resourceName) throws IOException {
|
||||
assertThat(Files.readString(outputDirectory.resolve(fileName)))
|
||||
assertThat(outputDirectory.resolve(fileName))
|
||||
.content(UTF_8)
|
||||
.isEqualToIgnoringWhitespace(getResource(resourceName));
|
||||
}
|
||||
|
||||
@@ -139,14 +138,10 @@ final class BugPatternExtractorTest {
|
||||
|
||||
@Override
|
||||
public Description matchClass(ClassTree tree, VisitorState state) {
|
||||
BugPatternExtractor extractor = new BugPatternExtractor();
|
||||
|
||||
assertThatThrownBy(() -> extractor.extract(tree, state.context))
|
||||
.isInstanceOf(NullPointerException.class)
|
||||
.hasMessage("BugPattern annotation must be present");
|
||||
|
||||
return buildDescription(tree)
|
||||
.setMessage(String.format("Can extract: %s", extractor.canExtract(tree)))
|
||||
.setMessage(
|
||||
String.format(
|
||||
"Can extract: %s", new BugPatternExtractor().tryExtract(tree, state).isPresent()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,29 @@
|
||||
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;
|
||||
@@ -74,4 +84,55 @@ final class DocumentationGeneratorTaskListenerTest {
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [
|
||||
"class A {}\n"
|
||||
],
|
||||
"replacementTests": [
|
||||
{
|
||||
"inputLines": "class A {}\n",
|
||||
"outputLines": "class A {}\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [
|
||||
"class B {}\n",
|
||||
"class A {}\n"
|
||||
],
|
||||
"replacementTests": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [
|
||||
"class A {}\n"
|
||||
],
|
||||
"replacementTests": []
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [],
|
||||
"replacementTests": []
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [],
|
||||
"replacementTests": []
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [],
|
||||
"replacementTests": [
|
||||
{
|
||||
"inputLines": "class B {}\n",
|
||||
"outputLines": "class B {}\n"
|
||||
},
|
||||
{
|
||||
"inputLines": "class A {}\n",
|
||||
"outputLines": "class A {}\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [],
|
||||
"replacementTests": [
|
||||
{
|
||||
"inputLines": "class A {}\n",
|
||||
"outputLines": "class A {}\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>tech.picnic.error-prone-support</groupId>
|
||||
<artifactId>error-prone-support</artifactId>
|
||||
<version>0.8.1-SNAPSHOT</version>
|
||||
<version>0.9.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>error-prone-contrib</artifactId>
|
||||
@@ -37,7 +37,7 @@
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_test_helpers</artifactId>
|
||||
<scope>test</scope>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
@@ -257,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>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
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.not;
|
||||
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.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.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.matchers.Matchers;
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
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 tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags unnecessarily nested usage of methods that implement an
|
||||
* associative operation.
|
||||
*
|
||||
* <p>The arguments to such methods can be flattened without affecting semantics, while making the
|
||||
* code more readable.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"These methods implement an associative operation, so the list of operands can be flattened",
|
||||
link = BUG_PATTERNS_BASE_URL + "AssociativeMethodInvocation",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class AssociativeMethodInvocation extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Supplier<Type> ITERABLE = Suppliers.typeFromClass(Iterable.class);
|
||||
private static final ImmutableSet<Matcher<ExpressionTree>> ASSOCIATIVE_OPERATIONS =
|
||||
ImmutableSet.of(
|
||||
allOf(
|
||||
staticMethod().onClass(Suppliers.typeFromClass(Matchers.class)).named("allOf"),
|
||||
toType(MethodInvocationTree.class, not(hasArgumentOfType(ITERABLE)))),
|
||||
allOf(
|
||||
staticMethod().onClass(Suppliers.typeFromClass(Matchers.class)).named("anyOf"),
|
||||
toType(MethodInvocationTree.class, not(hasArgumentOfType(ITERABLE)))),
|
||||
staticMethod().onClass(Suppliers.typeFromClass(Refaster.class)).named("anyOf"));
|
||||
|
||||
/** Instantiates a new {@link AssociativeMethodInvocation} instance. */
|
||||
public AssociativeMethodInvocation() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (tree.getArguments().isEmpty()) {
|
||||
/* Absent any arguments, there is nothing to simplify. */
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
for (Matcher<ExpressionTree> matcher : ASSOCIATIVE_OPERATIONS) {
|
||||
if (matcher.matches(tree, state)) {
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder();
|
||||
for (ExpressionTree arg : tree.getArguments()) {
|
||||
if (matcher.matches(arg, state)) {
|
||||
MethodInvocationTree invocation = (MethodInvocationTree) arg;
|
||||
fix.merge(
|
||||
invocation.getArguments().isEmpty()
|
||||
? SuggestedFixes.removeElement(arg, tree.getArguments(), state)
|
||||
: SourceCode.unwrapMethodInvocation(invocation, state));
|
||||
}
|
||||
}
|
||||
|
||||
return fix.isEmpty() ? Description.NO_MATCH : describeMatch(tree, fix.build());
|
||||
}
|
||||
}
|
||||
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
private static Matcher<MethodInvocationTree> hasArgumentOfType(Supplier<Type> type) {
|
||||
return (tree, state) ->
|
||||
tree.getArguments().stream()
|
||||
.anyMatch(arg -> ASTHelpers.isSubtype(ASTHelpers.getType(arg), type.get(state), state));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
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.common.collect.Streams;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.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.IdentifierTree;
|
||||
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.TryTree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import com.sun.source.util.TreeScanner;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
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)
|
||||
&& !isIdentifierSymbolReferencedInAssociatedFinallyBlock(variableSymbol, 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 likely be
|
||||
* done without changing the expression's return type.
|
||||
*
|
||||
* <p>Inlining an expression generally does not change its return type, 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the given identifier {@link Symbol} is referenced in a {@code finally} block that
|
||||
* is executed <em>after</em> control flow returns from the {@link VisitorState#getPath() current
|
||||
* location}.
|
||||
*/
|
||||
private static boolean isIdentifierSymbolReferencedInAssociatedFinallyBlock(
|
||||
Symbol symbol, VisitorState state) {
|
||||
return Streams.zip(
|
||||
Streams.stream(state.getPath()).skip(1),
|
||||
Streams.stream(state.getPath()),
|
||||
(tree, child) -> {
|
||||
if (!(tree instanceof TryTree)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BlockTree finallyBlock = ((TryTree) tree).getFinallyBlock();
|
||||
return !child.equals(finallyBlock) ? finallyBlock : null;
|
||||
})
|
||||
.anyMatch(finallyBlock -> referencesIdentifierSymbol(symbol, finallyBlock));
|
||||
}
|
||||
|
||||
private static boolean referencesIdentifierSymbol(Symbol symbol, @Nullable BlockTree tree) {
|
||||
return Boolean.TRUE.equals(
|
||||
new TreeScanner<Boolean, @Nullable Void>() {
|
||||
@Override
|
||||
public Boolean visitIdentifier(IdentifierTree node, @Nullable Void unused) {
|
||||
return symbol.equals(ASTHelpers.getSymbol(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean reduce(Boolean r1, Boolean r2) {
|
||||
return Boolean.TRUE.equals(r1) || Boolean.TRUE.equals(r2);
|
||||
}
|
||||
}.scan(tree, null));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -82,10 +82,8 @@ public final class FluxFlatMapUsage extends BugChecker
|
||||
|
||||
SuggestedFix serializationFix = SuggestedFixes.renameMethodInvocation(tree, "concatMap", state);
|
||||
SuggestedFix concurrencyCapFix =
|
||||
SuggestedFix.builder()
|
||||
.postfixWith(
|
||||
Iterables.getOnlyElement(tree.getArguments()), ", " + MAX_CONCURRENCY_ARG_NAME)
|
||||
.build();
|
||||
SuggestedFix.postfixWith(
|
||||
Iterables.getOnlyElement(tree.getArguments()), ", " + MAX_CONCURRENCY_ARG_NAME);
|
||||
|
||||
Description.Builder description = buildDescription(tree);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,8 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
// XXX: Consider detecting cases where a flagged expression is passed to a method, and where removal
|
||||
// of the identity conversion would cause a different method overload to be selected. Depending on
|
||||
// the target method such a modification may change the code's semantics or performance.
|
||||
// XXX: Also flag `Stream#map`, `Mono#map` and `Flux#map` invocations where the given transformation
|
||||
// is effectively the identity operation.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Avoid or clarify identity conversions",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
package tech.picnic.errorprone.bugpatterns.util;
|
||||
|
||||
import static com.sun.tools.javac.parser.Tokens.TokenKind.RPAREN;
|
||||
import static com.sun.tools.javac.util.Position.NOPOS;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.util.ErrorProneToken;
|
||||
import com.google.errorprone.util.ErrorProneTokens;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
|
||||
import com.sun.tools.javac.util.Position;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A collection of Error Prone utility methods for dealing with the source code representation of
|
||||
@@ -59,4 +69,57 @@ public final class SourceCode {
|
||||
whitespaceEndPos == -1 ? sourceCode.length() : whitespaceEndPos,
|
||||
"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link SuggestedFix} for the replacement of the given {@link MethodInvocationTree}
|
||||
* with just the arguments to the method invocation, effectively "unwrapping" the method
|
||||
* invocation.
|
||||
*
|
||||
* <p>For example, given the method invocation {@code foo.bar(1, 2, 3)}, this method will return a
|
||||
* {@link SuggestedFix} that replaces the method invocation with {@code 1, 2, 3}.
|
||||
*
|
||||
* <p>This method aims to preserve the original formatting of the method invocation, including
|
||||
* whitespace and comments.
|
||||
*
|
||||
* @param tree The AST node to be unwrapped.
|
||||
* @param state A {@link VisitorState} describing the context in which the given {@link
|
||||
* MethodInvocationTree} is found.
|
||||
* @return A non-{@code null} {@link SuggestedFix}.
|
||||
*/
|
||||
public static SuggestedFix unwrapMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
CharSequence sourceCode = state.getSourceCode();
|
||||
int startPosition = state.getEndPosition(tree.getMethodSelect());
|
||||
int endPosition = state.getEndPosition(tree);
|
||||
|
||||
if (sourceCode == null || startPosition == Position.NOPOS || endPosition == Position.NOPOS) {
|
||||
return unwrapMethodInvocationDroppingWhitespaceAndComments(tree, state);
|
||||
}
|
||||
|
||||
ImmutableList<ErrorProneToken> tokens =
|
||||
ErrorProneTokens.getTokens(
|
||||
sourceCode.subSequence(startPosition, endPosition).toString(), state.context);
|
||||
|
||||
Optional<Integer> leftParenPosition =
|
||||
tokens.stream().findFirst().map(t -> startPosition + t.endPos());
|
||||
Optional<Integer> rightParenPosition =
|
||||
Streams.findLast(tokens.stream().filter(t -> t.kind() == RPAREN))
|
||||
.map(t -> startPosition + t.pos());
|
||||
if (leftParenPosition.isEmpty() || rightParenPosition.isEmpty()) {
|
||||
return unwrapMethodInvocationDroppingWhitespaceAndComments(tree, state);
|
||||
}
|
||||
|
||||
return SuggestedFix.replace(
|
||||
tree,
|
||||
sourceCode
|
||||
.subSequence(leftParenPosition.orElseThrow(), rightParenPosition.orElseThrow())
|
||||
.toString());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static SuggestedFix unwrapMethodInvocationDroppingWhitespaceAndComments(
|
||||
MethodInvocationTree tree, VisitorState state) {
|
||||
return SuggestedFix.replace(
|
||||
tree,
|
||||
tree.getArguments().stream().map(arg -> treeToString(arg, state)).collect(joining(", ")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,15 +16,18 @@ import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
import org.assertj.core.api.AbstractBooleanAssert;
|
||||
import org.assertj.core.api.AbstractCollectionAssert;
|
||||
import org.assertj.core.api.AbstractMapAssert;
|
||||
import org.assertj.core.api.MapAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
@@ -220,6 +223,19 @@ final class AssertJMapRules {
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapContainsOnlyKeys<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractCollectionAssert<?, Collection<? extends K>, K, ?> before(Map<K, V> map, Set<K> keys) {
|
||||
return assertThat(map.keySet()).hasSameElementsAs(keys);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, Set<K> keys) {
|
||||
return assertThat(map).containsOnlyKeys(keys);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapContainsValue<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(Map<K, V> map, V value) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
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.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
@@ -17,8 +22,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;
|
||||
@@ -27,6 +35,7 @@ import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collector;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -88,7 +97,7 @@ final class ReactorRules {
|
||||
}
|
||||
|
||||
/** Prefer {@link Mono#justOrEmpty(Object)} over more contrived alternatives. */
|
||||
static final class MonoJustOrEmpty<@Nullable T> {
|
||||
static final class MonoJustOrEmptyObject<@Nullable T> {
|
||||
@BeforeTemplate
|
||||
Mono<T> before(T value) {
|
||||
return Mono.justOrEmpty(Optional.ofNullable(value));
|
||||
@@ -101,9 +110,25 @@ final class ReactorRules {
|
||||
}
|
||||
|
||||
/** Prefer {@link Mono#justOrEmpty(Optional)} over more verbose alternatives. */
|
||||
static final class MonoJustOrEmptyOptional<T> {
|
||||
@BeforeTemplate
|
||||
Mono<T> before(Optional<T> optional) {
|
||||
return Mono.just(optional).filter(Optional::isPresent).map(Optional::orElseThrow);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<T> after(Optional<T> optional) {
|
||||
return Mono.justOrEmpty(optional);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link Mono#defer(Supplier) deferring} {@link Mono#justOrEmpty(Optional)} over more
|
||||
* verbose alternatives.
|
||||
*/
|
||||
// XXX: If `optional` is a constant and effectively-final expression then the `Mono.defer` can be
|
||||
// dropped. Should look into Refaster support for identifying this.
|
||||
static final class MonoFromOptional<T> {
|
||||
static final class MonoDeferMonoJustOrEmpty<T> {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"MonoFromSupplier" /* `optional` may match a checked exception-throwing expression. */)
|
||||
@@ -378,6 +403,13 @@ final class ReactorRules {
|
||||
return mono.then();
|
||||
}
|
||||
|
||||
// XXX: Replace this rule with an extension of the `IdentityConversion` rule, supporting
|
||||
// `Stream#map`, `Mono#map` and `Flux#map`.
|
||||
@BeforeTemplate
|
||||
Mono<ImmutableList<T>> before3(Mono<ImmutableList<T>> mono) {
|
||||
return mono.map(ImmutableList::copyOf);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<T> after(Mono<T> mono) {
|
||||
return mono;
|
||||
@@ -699,21 +731,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 +838,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.
|
||||
@@ -1119,6 +1176,40 @@ final class ReactorRules {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link Flux#collect(Collector)} with {@link ImmutableList#toImmutableList()} over
|
||||
* alternatives that do not explicitly return an immutable collection.
|
||||
*/
|
||||
static final class FluxCollectToImmutableList<T> {
|
||||
@BeforeTemplate
|
||||
Mono<List<T>> before(Flux<T> flux) {
|
||||
return flux.collectList();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
Mono<ImmutableList<T>> after(Flux<T> flux) {
|
||||
return flux.collect(toImmutableList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link Flux#collect(Collector)} with {@link ImmutableSet#toImmutableSet()} over more
|
||||
* contrived alternatives.
|
||||
*/
|
||||
static final class FluxCollectToImmutableSet<T> {
|
||||
@BeforeTemplate
|
||||
Mono<ImmutableSet<T>> before(Flux<T> flux) {
|
||||
return flux.collect(toImmutableList()).map(ImmutableSet::copyOf);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
Mono<ImmutableSet<T>> after(Flux<T> flux) {
|
||||
return flux.collect(toImmutableSet());
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link reactor.util.context.Context#empty()}} over more verbose alternatives. */
|
||||
// XXX: Consider introducing an `IsEmpty` matcher that identifies a wide range of guaranteed-empty
|
||||
// `Collection` and `Map` expressions.
|
||||
@@ -1173,12 +1264,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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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 java.util.function.Predicate.not;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
@@ -11,21 +13,23 @@ import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.AlsoNegation;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/** Refaster rules related to expressions dealing with {@link String}s. */
|
||||
// XXX: Should we prefer `s -> !s.isEmpty()` or `not(String::isEmpty)`?
|
||||
@OnlineDocumentation
|
||||
final class StringRules {
|
||||
private StringRules() {}
|
||||
|
||||
/** Prefer {@link String#isEmpty()} over alternatives that consult the string's length. */
|
||||
// XXX: Once we target JDK 15+, generalize this rule to cover all `CharSequence` subtypes.
|
||||
static final class StringIsEmpty {
|
||||
@BeforeTemplate
|
||||
boolean before(String str) {
|
||||
@@ -39,6 +43,37 @@ final class StringRules {
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer a method reference to {@link String#isEmpty()} over the equivalent lambda function. */
|
||||
// XXX: Once we target JDK 15+, generalize this rule to cover all `CharSequence` subtypes.
|
||||
// XXX: As it stands, this rule is a special case of what `MethodReferenceUsage` tries to achieve.
|
||||
// If/when `MethodReferenceUsage` becomes production ready, we should simply drop this check.
|
||||
static final class StringIsEmptyPredicate {
|
||||
@BeforeTemplate
|
||||
Predicate<String> before() {
|
||||
return s -> s.isEmpty();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Predicate<String> after() {
|
||||
return String::isEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer a method reference to {@link String#isEmpty()} over the equivalent lambda function. */
|
||||
// XXX: Once we target JDK 15+, generalize this rule to cover all `CharSequence` subtypes.
|
||||
static final class StringIsNotEmptyPredicate {
|
||||
@BeforeTemplate
|
||||
Predicate<String> before() {
|
||||
return s -> !s.isEmpty();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
Predicate<String> after() {
|
||||
return not(String::isEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Strings#isNullOrEmpty(String)} over the more verbose alternative. */
|
||||
static final class StringIsNullOrEmpty {
|
||||
@BeforeTemplate
|
||||
@@ -65,7 +100,7 @@ final class StringRules {
|
||||
|
||||
@AfterTemplate
|
||||
Optional<String> after(String str) {
|
||||
return Optional.ofNullable(str).filter(s -> !s.isEmpty());
|
||||
return Optional.ofNullable(str).filter(not(String::isEmpty));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +111,9 @@ final class StringRules {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
Optional<String> after(Optional<String> optional) {
|
||||
return optional.filter(s -> !s.isEmpty());
|
||||
return optional.filter(not(String::isEmpty));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.sun.source.tree.Tree;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/** Refaster rules related to expressions dealing with {@link SuggestedFix}es. */
|
||||
@OnlineDocumentation
|
||||
final class SuggestedFixRules {
|
||||
private SuggestedFixRules() {}
|
||||
|
||||
/** Prefer {@link SuggestedFix#delete(Tree)} over more contrived alternatives. */
|
||||
static final class SuggestedFixDelete {
|
||||
@BeforeTemplate
|
||||
SuggestedFix before(Tree tree) {
|
||||
return SuggestedFix.builder().delete(tree).build();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
SuggestedFix after(Tree tree) {
|
||||
return SuggestedFix.delete(tree);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link SuggestedFix#replace(Tree, String)}} over more contrived alternatives. */
|
||||
static final class SuggestedFixReplaceTree {
|
||||
@BeforeTemplate
|
||||
SuggestedFix before(Tree tree, String replaceWith) {
|
||||
return SuggestedFix.builder().replace(tree, replaceWith).build();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
SuggestedFix after(Tree tree, String replaceWith) {
|
||||
return SuggestedFix.replace(tree, replaceWith);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link SuggestedFix#replace(int, int, String)}} over more contrived alternatives. */
|
||||
static final class SuggestedFixReplaceStartEnd {
|
||||
@BeforeTemplate
|
||||
SuggestedFix before(int start, int end, String replaceWith) {
|
||||
return SuggestedFix.builder().replace(start, end, replaceWith).build();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
SuggestedFix after(int start, int end, String replaceWith) {
|
||||
return SuggestedFix.replace(start, end, replaceWith);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link SuggestedFix#replace(Tree, String, int, int)}} over more contrived alternatives.
|
||||
*/
|
||||
static final class SuggestedFixReplaceTreeStartEnd {
|
||||
@BeforeTemplate
|
||||
SuggestedFix before(Tree tree, String replaceWith, int start, int end) {
|
||||
return SuggestedFix.builder().replace(tree, replaceWith, start, end).build();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
SuggestedFix after(Tree tree, String replaceWith, int start, int end) {
|
||||
return SuggestedFix.replace(tree, replaceWith, start, end);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link SuggestedFix#swap(Tree, Tree)} over more contrived alternatives. */
|
||||
static final class SuggestedFixSwap {
|
||||
@BeforeTemplate
|
||||
SuggestedFix before(Tree tree1, Tree tree2) {
|
||||
return SuggestedFix.builder().swap(tree1, tree2).build();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
SuggestedFix after(Tree tree1, Tree tree2) {
|
||||
return SuggestedFix.swap(tree1, tree2);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link SuggestedFix#prefixWith(Tree, String)} over more contrived alternatives. */
|
||||
static final class SuggestedFixPrefixWith {
|
||||
@BeforeTemplate
|
||||
SuggestedFix before(Tree tree, String prefix) {
|
||||
return SuggestedFix.builder().prefixWith(tree, prefix).build();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
SuggestedFix after(Tree tree, String prefix) {
|
||||
return SuggestedFix.prefixWith(tree, prefix);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link SuggestedFix#postfixWith(Tree, String)}} over more contrived alternatives. */
|
||||
static final class SuggestedFixPostfixWith {
|
||||
@BeforeTemplate
|
||||
SuggestedFix before(Tree tree, String postfix) {
|
||||
return SuggestedFix.builder().postfixWith(tree, postfix).build();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
SuggestedFix after(Tree tree, String postfix) {
|
||||
return SuggestedFix.postfixWith(tree, postfix);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
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 AssociativeMethodInvocationTest {
|
||||
@Test
|
||||
void identification() {
|
||||
CompilationTestHelper.newInstance(AssociativeMethodInvocation.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import com.google.common.collect.ImmutableList;",
|
||||
"import com.google.errorprone.matchers.Matchers;",
|
||||
"import com.google.errorprone.refaster.Refaster;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" Matchers.allOf();",
|
||||
" Matchers.anyOf();",
|
||||
" Refaster.anyOf();",
|
||||
"",
|
||||
" Matchers.allOf((t, s) -> true);",
|
||||
" Matchers.anyOf((t, s) -> true);",
|
||||
" Refaster.anyOf(0);",
|
||||
"",
|
||||
" Matchers.allOf(Matchers.anyOf((t, s) -> true));",
|
||||
" Matchers.anyOf(Matchers.allOf((t, s) -> true));",
|
||||
" Refaster.anyOf(Matchers.allOf((t, s) -> true));",
|
||||
"",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" Matchers.allOf(Matchers.allOf((t, s) -> true));",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" Matchers.anyOf(Matchers.anyOf((t, s) -> true));",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" Refaster.anyOf(Refaster.anyOf(0));",
|
||||
"",
|
||||
" Matchers.allOf(Matchers.allOf(ImmutableList.of((t, s) -> true)));",
|
||||
" Matchers.anyOf(Matchers.anyOf(ImmutableList.of((t, s) -> true)));",
|
||||
"",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" Matchers.allOf(",
|
||||
" (t, s) -> true, Matchers.allOf((t, s) -> false, (t, s) -> true), (t, s) -> false);",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" Matchers.anyOf(",
|
||||
" (t, s) -> true, Matchers.anyOf((t, s) -> false, (t, s) -> true), (t, s) -> false);",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" Refaster.anyOf(0, Refaster.anyOf(1, 2), 3);",
|
||||
" }",
|
||||
"}")
|
||||
.doTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacement() {
|
||||
BugCheckerRefactoringTestHelper.newInstance(AssociativeMethodInvocation.class, getClass())
|
||||
.addInputLines(
|
||||
"A.java",
|
||||
"import com.google.errorprone.matchers.Matchers;",
|
||||
"import com.google.errorprone.refaster.Refaster;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" Matchers.allOf(Matchers.allOf());",
|
||||
" Matchers.anyOf(Matchers.anyOf());",
|
||||
" Refaster.anyOf(Refaster.anyOf());",
|
||||
"",
|
||||
" Matchers.allOf(Matchers.allOf((t, s) -> true));",
|
||||
" Matchers.anyOf(Matchers.anyOf((t, s) -> true));",
|
||||
" Refaster.anyOf(Refaster.anyOf(0));",
|
||||
"",
|
||||
" Matchers.allOf(",
|
||||
" Matchers.anyOf(),",
|
||||
" Matchers.allOf((t, s) -> false, (t, s) -> true),",
|
||||
" Matchers.allOf(),",
|
||||
" Matchers.anyOf((t, s) -> false));",
|
||||
" Matchers.anyOf(",
|
||||
" Matchers.allOf(),",
|
||||
" Matchers.anyOf((t, s) -> false, (t, s) -> true),",
|
||||
" Matchers.anyOf(),",
|
||||
" Matchers.allOf((t, s) -> false));",
|
||||
" Refaster.anyOf(Matchers.allOf(), Refaster.anyOf(1, 2), Matchers.anyOf());",
|
||||
" }",
|
||||
"}")
|
||||
.addOutputLines(
|
||||
"A.java",
|
||||
"import com.google.errorprone.matchers.Matchers;",
|
||||
"import com.google.errorprone.refaster.Refaster;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" Matchers.allOf();",
|
||||
" Matchers.anyOf();",
|
||||
" Refaster.anyOf();",
|
||||
"",
|
||||
" Matchers.allOf((t, s) -> true);",
|
||||
" Matchers.anyOf((t, s) -> true);",
|
||||
" Refaster.anyOf(0);",
|
||||
"",
|
||||
" Matchers.allOf(",
|
||||
" Matchers.anyOf(), (t, s) -> false, (t, s) -> true, Matchers.anyOf((t, s) -> false));",
|
||||
" Matchers.anyOf(",
|
||||
" Matchers.allOf(), (t, s) -> false, (t, s) -> true, Matchers.allOf((t, s) -> false));",
|
||||
" Refaster.anyOf(Matchers.allOf(), 1, 2, Matchers.anyOf());",
|
||||
" }",
|
||||
"}")
|
||||
.doTest(TestMode.TEXT_MATCH);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
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;",
|
||||
" };",
|
||||
" }",
|
||||
"",
|
||||
" String redundantAssignmentInsideTryBlock(AutoCloseable closeable) throws Exception {",
|
||||
" try (closeable) {",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" String variable = toString();",
|
||||
" return variable;",
|
||||
" }",
|
||||
" }",
|
||||
"",
|
||||
" String redundantAssignmentsInsideTryAndFinallyBlocks() {",
|
||||
" String variable = toString();",
|
||||
" try {",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" variable = \"foo\";",
|
||||
" return variable;",
|
||||
" } finally {",
|
||||
" String variable2 = toString();",
|
||||
" if (true) {",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" String variable3 = toString();",
|
||||
" return variable3;",
|
||||
" }",
|
||||
" return variable2;",
|
||||
" }",
|
||||
" }",
|
||||
"",
|
||||
" String assignmentUsedInsideFinallyBlock() {",
|
||||
" String variable = toString();",
|
||||
" try {",
|
||||
" variable = \"foo\";",
|
||||
" return variable;",
|
||||
" } finally {",
|
||||
" String variable2 = toString();",
|
||||
" return variable + variable2;",
|
||||
" }",
|
||||
" }",
|
||||
"",
|
||||
" String redundantAssignmentToVariableUsedInsideUnexecutedFinallyBlock(AutoCloseable closeable)",
|
||||
" throws Exception {",
|
||||
" String variable = toString();",
|
||||
" try (closeable) {",
|
||||
" if (true) {",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" variable = \"foo\";",
|
||||
" return variable;",
|
||||
" }",
|
||||
" }",
|
||||
" try {",
|
||||
" } finally {",
|
||||
" 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);
|
||||
}
|
||||
}
|
||||
@@ -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;",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,9 @@ import org.junit.jupiter.api.Test;
|
||||
// `String.valueOf(null)` may not. That is because the latter matches `String#valueOf(char[])`. We
|
||||
// could special-case `null` arguments, but that doesn't seem worth the trouble.
|
||||
final class RedundantStringConversionTest {
|
||||
private final CompilationTestHelper compilationTestHelper =
|
||||
CompilationTestHelper.newInstance(RedundantStringConversion.class, getClass());
|
||||
|
||||
@Test
|
||||
void identificationOfIdentityTransformation() {
|
||||
compilationTestHelper
|
||||
CompilationTestHelper.newInstance(RedundantStringConversion.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"class A {",
|
||||
@@ -38,7 +35,7 @@ final class RedundantStringConversionTest {
|
||||
|
||||
@Test
|
||||
void identificationWithinMutatingAssignment() {
|
||||
compilationTestHelper
|
||||
CompilationTestHelper.newInstance(RedundantStringConversion.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import java.math.BigInteger;",
|
||||
@@ -99,7 +96,7 @@ final class RedundantStringConversionTest {
|
||||
|
||||
@Test
|
||||
void identificationWithinBinaryOperation() {
|
||||
compilationTestHelper
|
||||
CompilationTestHelper.newInstance(RedundantStringConversion.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import java.math.BigInteger;",
|
||||
@@ -194,7 +191,7 @@ final class RedundantStringConversionTest {
|
||||
|
||||
@Test
|
||||
void identificationWithinStringBuilderMethod() {
|
||||
compilationTestHelper
|
||||
CompilationTestHelper.newInstance(RedundantStringConversion.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import java.math.BigInteger;",
|
||||
@@ -249,7 +246,7 @@ final class RedundantStringConversionTest {
|
||||
// XXX: Also test the other formatter methods.
|
||||
@Test
|
||||
void identificationWithinFormatterMethod() {
|
||||
compilationTestHelper
|
||||
CompilationTestHelper.newInstance(RedundantStringConversion.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import java.util.Formattable;",
|
||||
@@ -294,7 +291,7 @@ final class RedundantStringConversionTest {
|
||||
|
||||
@Test
|
||||
void identificationWithinGuavaGuardMethod() {
|
||||
compilationTestHelper
|
||||
CompilationTestHelper.newInstance(RedundantStringConversion.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import static com.google.common.base.Preconditions.checkArgument;",
|
||||
@@ -354,7 +351,7 @@ final class RedundantStringConversionTest {
|
||||
|
||||
@Test
|
||||
void identificationWithinSlf4jLoggerMethod() {
|
||||
compilationTestHelper
|
||||
CompilationTestHelper.newInstance(RedundantStringConversion.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import java.util.Formattable;",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -48,7 +48,6 @@ final class StringCaseLocaleUsageTest {
|
||||
@Test
|
||||
void replacementFirstSuggestedFix() {
|
||||
BugCheckerRefactoringTestHelper.newInstance(StringCaseLocaleUsage.class, getClass())
|
||||
.setFixChooser(FixChoosers.FIRST)
|
||||
.addInputLines(
|
||||
"A.java",
|
||||
"class A {",
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
final class JavaKeywordsTest {
|
||||
private static Stream<Arguments> isValidIdentifierTestCases() {
|
||||
/* { str, expected } */
|
||||
/* { string, expected } */
|
||||
return Stream.of(
|
||||
arguments("", false),
|
||||
arguments("public", false),
|
||||
@@ -28,7 +28,7 @@ final class JavaKeywordsTest {
|
||||
|
||||
@MethodSource("isValidIdentifierTestCases")
|
||||
@ParameterizedTest
|
||||
void isValidIdentifier(String str, boolean expected) {
|
||||
assertThat(JavaKeywords.isValidIdentifier(str)).isEqualTo(expected);
|
||||
void isValidIdentifier(String string, boolean expected) {
|
||||
assertThat(JavaKeywords.isValidIdentifier(string)).isEqualTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,22 +16,6 @@ import com.sun.source.tree.MethodInvocationTree;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class MethodMatcherFactoryTest {
|
||||
/** A {@link BugChecker} that flags method invocations matched by {@link #TEST_MATCHER}. */
|
||||
@BugPattern(severity = SUGGESTION, summary = "Flags methods matched by the test matcher.")
|
||||
public static final class MatchedMethodsFlagger extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (TEST_MATCHER.matches(tree, state)) {
|
||||
return buildDescription(tree).build();
|
||||
}
|
||||
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Matcher<ExpressionTree> TEST_MATCHER =
|
||||
new MethodMatcherFactory()
|
||||
.create(
|
||||
@@ -207,4 +191,20 @@ final class MethodMatcherFactoryTest {
|
||||
"}")
|
||||
.doTest();
|
||||
}
|
||||
|
||||
/** A {@link BugChecker} that flags method invocations matched by {@link #TEST_MATCHER}. */
|
||||
@BugPattern(severity = SUGGESTION, summary = "Flags methods matched by the test matcher.")
|
||||
public static final class MatchedMethodsFlagger extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (TEST_MATCHER.matches(tree, state)) {
|
||||
return buildDescription(tree).build();
|
||||
}
|
||||
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,22 +8,22 @@ import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.AnnotationTreeMatcher;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import javax.lang.model.element.Name;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class SourceCodeTest {
|
||||
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
|
||||
BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass());
|
||||
|
||||
@Test
|
||||
void deleteWithTrailingWhitespaceAnnotations() {
|
||||
refactoringTestHelper
|
||||
BugCheckerRefactoringTestHelper.newInstance(
|
||||
DeleteWithTrailingWhitespaceTestChecker.class, getClass())
|
||||
.addInputLines("AnnotationToBeDeleted.java", "@interface AnnotationToBeDeleted {}")
|
||||
.expectUnchanged()
|
||||
.addInputLines(
|
||||
@@ -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() {}",
|
||||
@@ -98,10 +96,10 @@ final class SourceCodeTest {
|
||||
|
||||
@Test
|
||||
void deleteWithTrailingWhitespaceMethods() {
|
||||
refactoringTestHelper
|
||||
BugCheckerRefactoringTestHelper.newInstance(
|
||||
DeleteWithTrailingWhitespaceTestChecker.class, getClass())
|
||||
.addInputLines(
|
||||
"MethodDeletions.java",
|
||||
"",
|
||||
"interface MethodDeletions {",
|
||||
" class SoleMethod {",
|
||||
" void methodToBeDeleted() {}",
|
||||
@@ -141,7 +139,6 @@ final class SourceCodeTest {
|
||||
"}")
|
||||
.addOutputLines(
|
||||
"MethodDeletions.java",
|
||||
"",
|
||||
"interface MethodDeletions {",
|
||||
" class SoleMethod {}",
|
||||
"",
|
||||
@@ -166,13 +163,78 @@ final class SourceCodeTest {
|
||||
.doTest(TestMode.TEXT_MATCH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unwrapMethodInvocation() {
|
||||
BugCheckerRefactoringTestHelper.newInstance(UnwrapMethodInvocationTestChecker.class, getClass())
|
||||
.addInputLines(
|
||||
"A.java",
|
||||
"import com.google.common.collect.ImmutableList;",
|
||||
"",
|
||||
"class A {",
|
||||
" Object[] m() {",
|
||||
" return new Object[][] {",
|
||||
" {ImmutableList.of()},",
|
||||
" {ImmutableList.of(1)},",
|
||||
" {com.google.common.collect.ImmutableList.of(1, 2)},",
|
||||
" {",
|
||||
" 0, /*a*/",
|
||||
" ImmutableList /*b*/./*c*/ <Integer> /*d*/of /*e*/(/*f*/ 1 /*g*/, /*h*/ 2 /*i*/) /*j*/",
|
||||
" }",
|
||||
" };",
|
||||
" }",
|
||||
"}")
|
||||
.addOutputLines(
|
||||
"A.java",
|
||||
"import com.google.common.collect.ImmutableList;",
|
||||
"",
|
||||
"class A {",
|
||||
" Object[] m() {",
|
||||
" return new Object[][] {{}, {1}, {1, 2}, {0, /*a*/ /*f*/ 1 /*g*/, /*h*/ 2 /*i*/ /*j*/}};",
|
||||
" }",
|
||||
"}")
|
||||
.doTest(TestMode.TEXT_MATCH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unwrapMethodInvocationDroppingWhitespaceAndComments() {
|
||||
BugCheckerRefactoringTestHelper.newInstance(
|
||||
UnwrapMethodInvocationDroppingWhitespaceAndCommentsTestChecker.class, getClass())
|
||||
.addInputLines(
|
||||
"A.java",
|
||||
"import com.google.common.collect.ImmutableList;",
|
||||
"",
|
||||
"class A {",
|
||||
" Object[] m() {",
|
||||
" return new Object[][] {",
|
||||
" {ImmutableList.of()},",
|
||||
" {ImmutableList.of(1)},",
|
||||
" {com.google.common.collect.ImmutableList.of(1, 2)},",
|
||||
" {",
|
||||
" 0, /*a*/",
|
||||
" ImmutableList /*b*/./*c*/ <Integer> /*d*/of /*e*/(/*f*/ 1 /*g*/, /*h*/ 2 /*i*/) /*j*/",
|
||||
" }",
|
||||
" };",
|
||||
" }",
|
||||
"}")
|
||||
.addOutputLines(
|
||||
"A.java",
|
||||
"import com.google.common.collect.ImmutableList;",
|
||||
"",
|
||||
"class A {",
|
||||
" Object[] m() {",
|
||||
" return new Object[][] {{}, {1}, {1, 2}, {0, /*a*/ 1, 2 /*j*/}};",
|
||||
" }",
|
||||
"}")
|
||||
.doTest(TestMode.TEXT_MATCH);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that uses {@link SourceCode#deleteWithTrailingWhitespace(Tree,
|
||||
* VisitorState)} to suggest the deletion of annotations and methods with a name containing
|
||||
* {@value DELETION_MARKER}.
|
||||
*/
|
||||
@BugPattern(severity = ERROR, summary = "Interacts with `SourceCode` for testing purposes")
|
||||
public static final class TestChecker extends BugChecker
|
||||
public static final class DeleteWithTrailingWhitespaceTestChecker extends BugChecker
|
||||
implements AnnotationTreeMatcher, MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String DELETION_MARKER = "ToBeDeleted";
|
||||
@@ -196,4 +258,37 @@ final class SourceCodeTest {
|
||||
: Description.NO_MATCH;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that applies {@link
|
||||
* SourceCode#unwrapMethodInvocation(MethodInvocationTree, VisitorState)} to all method
|
||||
* invocations.
|
||||
*/
|
||||
@BugPattern(severity = ERROR, summary = "Interacts with `SourceCode` for testing purposes")
|
||||
public static final class UnwrapMethodInvocationTestChecker extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
return describeMatch(tree, SourceCode.unwrapMethodInvocation(tree, state));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that applies {@link
|
||||
* SourceCode#unwrapMethodInvocationDroppingWhitespaceAndComments(MethodInvocationTree,
|
||||
* VisitorState)} to all method invocations.
|
||||
*/
|
||||
@BugPattern(severity = ERROR, summary = "Interacts with `SourceCode` for testing purposes")
|
||||
public static final class UnwrapMethodInvocationDroppingWhitespaceAndCommentsTestChecker
|
||||
extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
return describeMatch(
|
||||
tree, SourceCode.unwrapMethodInvocationDroppingWhitespaceAndComments(tree, state));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,9 @@ import org.junit.jupiter.params.provider.ValueSource;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
final class ThirdPartyLibraryTest {
|
||||
private final CompilationTestHelper compilationTestHelper =
|
||||
CompilationTestHelper.newInstance(TestChecker.class, getClass());
|
||||
|
||||
@Test
|
||||
void isIntroductionAllowed() {
|
||||
compilationTestHelper
|
||||
CompilationTestHelper.newInstance(TestChecker.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"// BUG: Diagnostic contains: ASSERTJ: true, GUAVA: true, NEW_RELIC_AGENT_API: true, REACTOR: true",
|
||||
@@ -33,7 +30,7 @@ final class ThirdPartyLibraryTest {
|
||||
|
||||
@Test
|
||||
void isIntroductionAllowedWitnessClassesInSymtab() {
|
||||
compilationTestHelper
|
||||
CompilationTestHelper.newInstance(TestChecker.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import com.google.common.collect.ImmutableList;",
|
||||
@@ -55,7 +52,7 @@ final class ThirdPartyLibraryTest {
|
||||
|
||||
@Test
|
||||
void isIntroductionAllowedWitnessClassesPartiallyOnClassPath() {
|
||||
compilationTestHelper
|
||||
CompilationTestHelper.newInstance(TestChecker.class, getClass())
|
||||
.withClasspath(ImmutableList.class, Flux.class)
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
@@ -66,7 +63,7 @@ final class ThirdPartyLibraryTest {
|
||||
|
||||
@Test
|
||||
void isIntroductionAllowedWitnessClassesNotOnClassPath() {
|
||||
compilationTestHelper
|
||||
CompilationTestHelper.newInstance(TestChecker.class, getClass())
|
||||
.withClasspath()
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
@@ -79,7 +76,7 @@ final class ThirdPartyLibraryTest {
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
void isIntroductionAllowedIgnoreClasspathCompat(boolean ignoreClassPath) {
|
||||
compilationTestHelper
|
||||
CompilationTestHelper.newInstance(TestChecker.class, getClass())
|
||||
.setArgs("-XepOpt:ErrorProneSupport:IgnoreClasspathCompat=" + ignoreClassPath)
|
||||
.withClasspath(ImmutableList.class, Flux.class)
|
||||
.addSourceLines(
|
||||
|
||||
@@ -13,7 +13,6 @@ final class RefasterRulesTest {
|
||||
/** The names of all Refaster rule groups defined in this module. */
|
||||
private static final ImmutableSet<Class<?>> RULE_COLLECTIONS =
|
||||
ImmutableSet.of(
|
||||
AssertJRules.class,
|
||||
AssertJBigDecimalRules.class,
|
||||
AssertJBigIntegerRules.class,
|
||||
AssertJBooleanRules.class,
|
||||
@@ -25,16 +24,18 @@ final class RefasterRulesTest {
|
||||
AssertJFloatRules.class,
|
||||
AssertJIntegerRules.class,
|
||||
AssertJLongRules.class,
|
||||
AssertJNumberRules.class,
|
||||
AssertJMapRules.class,
|
||||
AssertJNumberRules.class,
|
||||
AssertJObjectRules.class,
|
||||
AssertJOptionalRules.class,
|
||||
AssertJPrimitiveRules.class,
|
||||
AssertJRules.class,
|
||||
AssertJShortRules.class,
|
||||
AssertJStringRules.class,
|
||||
AssertJThrowingCallableRules.class,
|
||||
AssortedRules.class,
|
||||
BigDecimalRules.class,
|
||||
BugCheckerRules.class,
|
||||
CollectionRules.class,
|
||||
ComparatorRules.class,
|
||||
DoubleStreamRules.class,
|
||||
@@ -64,6 +65,7 @@ final class RefasterRulesTest {
|
||||
RxJava2AdapterRules.class,
|
||||
StreamRules.class,
|
||||
StringRules.class,
|
||||
SuggestedFixRules.class,
|
||||
TestNGToAssertJRules.class,
|
||||
TimeRules.class,
|
||||
WebClientRules.class);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -130,6 +130,10 @@ final class AssertJMapRulesTest implements RefasterRuleCollectionTestCase {
|
||||
return assertThat(ImmutableMap.of(1, 2).containsKey(3)).isFalse();
|
||||
}
|
||||
|
||||
AbstractAssert<?, ?> testAssertThatMapContainsOnlyKeys() {
|
||||
return assertThat(ImmutableMap.of(1, 2).keySet()).hasSameElementsAs(ImmutableSet.of(3));
|
||||
}
|
||||
|
||||
AbstractAssert<?, ?> testAssertThatMapContainsValue() {
|
||||
return assertThat(ImmutableMap.of(1, 2).containsValue(3)).isTrue();
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -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;
|
||||
@@ -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("")));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
|
||||
@@ -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;
|
||||
@@ -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 {}");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user