mirror of
https://github.com/jlengrand/error-prone-support.git
synced 2026-03-10 15:49:33 +00:00
Compare commits
15 Commits
rossendrij
...
rossendrij
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d98178fc8 | ||
|
|
a1b92ef92a | ||
|
|
857e912061 | ||
|
|
f87b916329 | ||
|
|
3b15c3bf7d | ||
|
|
a3c828fe94 | ||
|
|
0cbcdb8c8b | ||
|
|
7548278b91 | ||
|
|
5b2a391523 | ||
|
|
6f98868171 | ||
|
|
0ad09268e0 | ||
|
|
7f246785cf | ||
|
|
7dab0eb562 | ||
|
|
fa004e74b4 | ||
|
|
e9fcfc7624 |
51
.github/ISSUE_TEMPLATE/bug_report.md
vendored
51
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,51 +0,0 @@
|
||||
---
|
||||
name: 🐛 Bug report
|
||||
about: Create a report to help us improve.
|
||||
title: ""
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
### Describe the bug
|
||||
|
||||
<!-- Provide a clear and concise description of what the bug or issue is. -->
|
||||
|
||||
- [ ] I have verified that the issue is reproducible against the latest version
|
||||
of the project.
|
||||
- [ ] I have searched through existing issues to verify that this issue is not
|
||||
already known.
|
||||
|
||||
### Minimal Reproducible Example
|
||||
|
||||
<!-- Provide a clear and concise description of what happened. Please include
|
||||
steps on how to reproduce the issue. -->
|
||||
|
||||
```java
|
||||
If applicable and possible, please replace this section with the code that
|
||||
triggered the isue.
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Logs</summary>
|
||||
|
||||
```sh
|
||||
Please replace this sentence with log output, if applicable.
|
||||
```
|
||||
</details>
|
||||
|
||||
### Expected behavior
|
||||
|
||||
<!-- Provide a clear and concise description of what you expected to happen. -->
|
||||
|
||||
### Setup
|
||||
|
||||
<!-- Please complete the following information: -->
|
||||
|
||||
- Operating system (e.g. MacOS Monterey).
|
||||
- Java version (i.e. `java --version`, e.g. `17.0.10`).
|
||||
- Error Prone version (e.g. `2.25.0`).
|
||||
- Error Prone Support version (e.g. `0.15.0`).
|
||||
|
||||
### Additional context
|
||||
|
||||
<!-- Provide any other context about the problem here. -->
|
||||
58
.github/ISSUE_TEMPLATE/feature_request.md
vendored
58
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,58 +0,0 @@
|
||||
---
|
||||
name: 🚀 Request a new feature
|
||||
about: Suggest a new feature.
|
||||
title: ""
|
||||
labels: new feature
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
### Problem
|
||||
|
||||
<!-- Here, describe the context of the problem that you're facing, and which
|
||||
you'd like to be solved through Error Prone Support. -->
|
||||
|
||||
### Description of the proposed new feature
|
||||
|
||||
<!-- Please indicate the type of improvement. -->
|
||||
|
||||
- [ ] Support a stylistic preference.
|
||||
- [ ] Avoid a common gotcha, or potential problem.
|
||||
- [ ] Improve performance.
|
||||
|
||||
<!--
|
||||
Here, provide a clear and concise description of the desired change.
|
||||
|
||||
If possible, provide a simple and minimal example using the following format:
|
||||
-->
|
||||
|
||||
I would like to rewrite the following code:
|
||||
```java
|
||||
// XXX: Write the code to match here.
|
||||
```
|
||||
|
||||
to:
|
||||
```java
|
||||
// XXX: Write the desired code here.
|
||||
```
|
||||
|
||||
### Considerations
|
||||
|
||||
<!--
|
||||
Here, mention any other aspects to consider. Relevant questions:
|
||||
|
||||
- If applicable, is the rewrite operation a clear improvement in all cases?
|
||||
- Are there special cases to consider?
|
||||
- Can we further generalize the proposed solution?
|
||||
- Are there alternative solutions we should consider?
|
||||
-->
|
||||
|
||||
### Participation
|
||||
|
||||
<!-- Pull requests are very welcome, and we happily review contributions. Are
|
||||
you up for the challenge? :D -->
|
||||
|
||||
- [ ] I am willing to submit a pull request to implement this improvement.
|
||||
|
||||
### Additional context
|
||||
|
||||
<!-- Provide any other context about the request here. -->
|
||||
29
.github/release.yml
vendored
29
.github/release.yml
vendored
@@ -1,29 +0,0 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- "ignore-changelog"
|
||||
categories:
|
||||
- title: ":warning: Update considerations and deprecations"
|
||||
labels:
|
||||
- "breaking change"
|
||||
- "deprecation"
|
||||
- title: ":rocket: New Error Prone checks and Refaster rules"
|
||||
labels:
|
||||
- "new feature"
|
||||
- title: ":sparkles: Improvements"
|
||||
labels:
|
||||
- "improvement"
|
||||
- title: ":bug: Bug fixes"
|
||||
labels:
|
||||
- "bug"
|
||||
- "bug fix"
|
||||
- title: ":books: Documentation, test and build improvements"
|
||||
labels:
|
||||
- "documentation"
|
||||
- "chore"
|
||||
- title: ":chart_with_upwards_trend: Dependency upgrades"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- title: "Other changes"
|
||||
labels:
|
||||
- "*"
|
||||
59
.github/workflows/build.yml
vendored
59
.github/workflows/build.yml
vendored
@@ -1,59 +0,0 @@
|
||||
name: Build and verify
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master ]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-22.04 ]
|
||||
jdk: [ 17.0.10, 21.0.2, 22.0.2 ]
|
||||
distribution: [ temurin ]
|
||||
experimental: [ false ]
|
||||
include:
|
||||
- os: macos-14
|
||||
jdk: 17.0.10
|
||||
distribution: temurin
|
||||
experimental: false
|
||||
- os: windows-2022
|
||||
jdk: 17.0.10
|
||||
distribution: temurin
|
||||
experimental: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
steps:
|
||||
- name: Install Harden-Runner
|
||||
uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
||||
with:
|
||||
disable-sudo: true
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.adoptium.net:443
|
||||
github.com:443
|
||||
jitpack.io:443
|
||||
objects.githubusercontent.com:443
|
||||
repo.maven.apache.org:443
|
||||
# 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 and set up JDK and Maven
|
||||
uses: s4u/setup-maven-action@489441643219d2b93ee2a127b2402eb640a1b947 # v1.13.0
|
||||
with:
|
||||
java-version: ${{ matrix.jdk }}
|
||||
java-distribution: ${{ matrix.distribution }}
|
||||
maven-version: 3.9.9
|
||||
- name: Display build environment details
|
||||
run: mvn --version
|
||||
- name: Build project against vanilla Error Prone, compile Javadoc
|
||||
run: mvn -T1C install javadoc:jar
|
||||
- name: Build project with self-check against Error Prone fork
|
||||
run: mvn -T1C clean verify -Perror-prone-fork -Pnon-maven-central -Pself-check -s settings.xml
|
||||
- name: Remove installed project artifacts
|
||||
run: mvn build-helper:remove-project-artifact
|
||||
|
||||
# XXX: Enable Codecov once we "go public".
|
||||
52
.github/workflows/codeql.yml
vendored
52
.github/workflows/codeql.yml
vendored
@@ -1,52 +0,0 @@
|
||||
# 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: Install Harden-Runner
|
||||
uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
||||
with:
|
||||
disable-sudo: true
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.adoptium.net:443
|
||||
api.github.com:443
|
||||
github.com:443
|
||||
objects.githubusercontent.com:443
|
||||
repo.maven.apache.org:443
|
||||
uploads.github.com:443
|
||||
- name: Check out code and set up JDK and Maven
|
||||
uses: s4u/setup-maven-action@489441643219d2b93ee2a127b2402eb640a1b947 # v1.13.0
|
||||
with:
|
||||
java-version: 17.0.10
|
||||
java-distribution: temurin
|
||||
maven-version: 3.9.9
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Perform minimal build
|
||||
if: matrix.language == 'java'
|
||||
run: mvn -T1C clean package -DskipTests -Dverification.skip
|
||||
- name: Perform CodeQL analysis
|
||||
uses: github/codeql-action/analyze@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
|
||||
with:
|
||||
category: /language:${{ matrix.language }}
|
||||
85
.github/workflows/deploy-website.yml
vendored
85
.github/workflows/deploy-website.yml
vendored
@@ -1,85 +0,0 @@
|
||||
name: Update `error-prone.picnic.tech` website content
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master, website ]
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Install Harden-Runner
|
||||
uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
||||
with:
|
||||
disable-sudo: true
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.adoptium.net:443
|
||||
api.github.com:443
|
||||
bestpractices.coreinfrastructure.org:443
|
||||
blog.picnic.nl:443
|
||||
errorprone.info:443
|
||||
github.com:443
|
||||
img.shields.io:443
|
||||
index.rubygems.org:443
|
||||
jitpack.io:443
|
||||
maven.apache.org:443
|
||||
objects.githubusercontent.com:443
|
||||
pitest.org:443
|
||||
repo.maven.apache.org:443
|
||||
rubygems.org:443
|
||||
search.maven.org:443
|
||||
securityscorecards.dev:443
|
||||
sonarcloud.io:443
|
||||
www.baeldung.com:443
|
||||
www.bestpractices.dev:443
|
||||
www.youtube.com:443
|
||||
youtrack.jetbrains.com:443
|
||||
- name: Check out code
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ruby/setup-ruby@6bd3d993c602f6b675728ebaecb2b569ff86e99b # v1.174.0
|
||||
with:
|
||||
working-directory: ./website
|
||||
bundler-cache: true
|
||||
- name: Configure Github Pages
|
||||
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0
|
||||
- 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
|
||||
- name: Upload website as artifact
|
||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
|
||||
with:
|
||||
path: ./website/_site
|
||||
deploy:
|
||||
if: github.ref == 'refs/heads/website'
|
||||
needs: build
|
||||
permissions:
|
||||
id-token: write
|
||||
pages: write
|
||||
runs-on: ubuntu-22.04
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Install Harden-Runner
|
||||
uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
||||
with:
|
||||
disable-sudo: true
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.github.com:443
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
|
||||
50
.github/workflows/openssf-scorecard.yml
vendored
50
.github/workflows/openssf-scorecard.yml
vendored
@@ -1,50 +0,0 @@
|
||||
# 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: Install Harden-Runner
|
||||
uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
||||
with:
|
||||
disable-sudo: true
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.github.com:443
|
||||
api.osv.dev:443
|
||||
api.scorecard.dev:443
|
||||
api.securityscorecards.dev:443
|
||||
github.com:443
|
||||
oss-fuzz-build-logs.storage.googleapis.com:443
|
||||
*.sigstore.dev:443
|
||||
www.bestpractices.dev:443
|
||||
- name: Check out code
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run OpenSSF Scorecard analysis
|
||||
uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.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@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
44
.github/workflows/pitest-analyze-pr.yml
vendored
44
.github/workflows/pitest-analyze-pr.yml
vendored
@@ -1,44 +0,0 @@
|
||||
# Performs mutation testing analysis on the files changed by a pull request and
|
||||
# uploads the results. The associated PR is subsequently updated by the
|
||||
# `pitest-update-pr.yml` workflow. See https://blog.pitest.org/oss-pitest-pr/
|
||||
# for details.
|
||||
name: "Mutation testing"
|
||||
on:
|
||||
pull_request:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
analyze-pr:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Install Harden-Runner
|
||||
uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
||||
with:
|
||||
disable-sudo: true
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.adoptium.net:443
|
||||
github.com:443
|
||||
objects.githubusercontent.com:443
|
||||
repo.maven.apache.org:443
|
||||
- name: Check out code and set up JDK and Maven
|
||||
uses: s4u/setup-maven-action@489441643219d2b93ee2a127b2402eb640a1b947 # v1.13.0
|
||||
with:
|
||||
checkout-fetch-depth: 2
|
||||
java-version: 17.0.10
|
||||
java-distribution: temurin
|
||||
maven-version: 3.9.9
|
||||
- name: Run Pitest
|
||||
# By running with features `+GIT(from[HEAD~1]), +gitci`, Pitest only
|
||||
# analyzes lines changed in the associated pull request, as GitHub
|
||||
# exposes the changes unique to the PR as a single commit on top of the
|
||||
# target branch. See https://blog.pitest.org/pitest-pr-setup for
|
||||
# details.
|
||||
run: mvn test pitest:mutationCoverage -DargLine.xmx=2048m -Dverification.skip -Dfeatures="+GIT(from[HEAD~1]), +gitci"
|
||||
- name: Aggregate Pitest reports
|
||||
run: mvn pitest-git:aggregate -DkilledEmoji=":tada:" -DmutantEmoji=":zombie:" -DtrailingText="Mutation testing report by [Pitest](https://pitest.org/). Review any surviving mutants by inspecting the line comments under [_Files changed_](${{ github.event.number }}/files)."
|
||||
- name: Upload Pitest reports as artifact
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: pitest-reports
|
||||
path: ./target/pit-reports-ci
|
||||
46
.github/workflows/pitest-update-pr.yml
vendored
46
.github/workflows/pitest-update-pr.yml
vendored
@@ -1,46 +0,0 @@
|
||||
# Updates a pull request based on the corresponding mutation testing analysis
|
||||
# performed by the `pitest-analyze-pr.yml` workflow. See
|
||||
# https://blog.pitest.org/oss-pitest-pr/ for details.
|
||||
name: "Mutation testing: post results"
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Mutation testing"]
|
||||
types:
|
||||
- completed
|
||||
permissions:
|
||||
actions: read
|
||||
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: Install Harden-Runner
|
||||
uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
||||
with:
|
||||
disable-sudo: true
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.adoptium.net:443
|
||||
api.github.com:443
|
||||
github.com:443
|
||||
objects.githubusercontent.com:443
|
||||
repo.maven.apache.org:443
|
||||
- name: Check out code and set up JDK and Maven
|
||||
uses: s4u/setup-maven-action@489441643219d2b93ee2a127b2402eb640a1b947 # v1.13.0
|
||||
with:
|
||||
java-version: 17.0.10
|
||||
java-distribution: temurin
|
||||
maven-version: 3.9.9
|
||||
- name: Download Pitest analysis artifact
|
||||
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
|
||||
with:
|
||||
workflow: ${{ github.event.workflow_run.workflow_id }}
|
||||
name: pitest-reports
|
||||
path: ./target/pit-reports-ci
|
||||
- name: Update PR
|
||||
run: mvn -DrepoToken="${{ secrets.GITHUB_TOKEN }}" pitest-github:updatePR
|
||||
53
.github/workflows/run-integration-tests.yml
vendored
53
.github/workflows/run-integration-tests.yml
vendored
@@ -1,53 +0,0 @@
|
||||
# If requested by means of a pull request comment, runs integration tests
|
||||
# against the project, using the code found on the pull request branch.
|
||||
# XXX: Generalize this to a matrix build of multiple integration tests,
|
||||
# possibly using multiple JDK or OS versions.
|
||||
# XXX: Investigate whether the comment can specify which integration tests run
|
||||
# run. See this example of a dynamic build matrix:
|
||||
# https://docs.github.com/en/actions/learn-github-actions/expressions#example-returning-a-json-object
|
||||
name: "Integration tests"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [ created ]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
run-integration-tests:
|
||||
name: On-demand integration test
|
||||
if: |
|
||||
github.event.issue.pull_request && contains(github.event.comment.body, '/integration-test')
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Install Harden-Runner
|
||||
uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
||||
with:
|
||||
disable-sudo: true
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.adoptium.net:443
|
||||
checkstyle.org:443
|
||||
github.com:443
|
||||
objects.githubusercontent.com:443
|
||||
oss.sonatype.org:443
|
||||
raw.githubusercontent.com:443
|
||||
repo.maven.apache.org:443
|
||||
repository.sonatype.org:443
|
||||
- name: Check out code and set up JDK and Maven
|
||||
uses: s4u/setup-maven-action@489441643219d2b93ee2a127b2402eb640a1b947 # v1.13.0
|
||||
with:
|
||||
checkout-ref: "refs/pull/${{ github.event.issue.number }}/head"
|
||||
java-version: 17.0.10
|
||||
java-distribution: temurin
|
||||
maven-version: 3.9.9
|
||||
- name: Install project to local Maven repository
|
||||
run: mvn -T1C install -DskipTests -Dverification.skip
|
||||
- name: Run integration test
|
||||
run: xvfb-run ./integration-tests/checkstyle.sh "${{ runner.temp }}/artifacts"
|
||||
- name: Upload artifacts on failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: integration-test-checkstyle
|
||||
path: "${{ runner.temp }}/artifacts"
|
||||
- name: Remove installed project artifacts
|
||||
run: mvn build-helper:remove-project-artifact
|
||||
51
.github/workflows/sonarcloud.yml
vendored
51
.github/workflows/sonarcloud.yml
vendored
@@ -1,51 +0,0 @@
|
||||
# Analyzes the code base using SonarCloud. See
|
||||
# https://sonarcloud.io/project/overview?id=PicnicSupermarket_error-prone-support.
|
||||
name: SonarCloud analysis
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '0 4 * * 1'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
analyze:
|
||||
# Analysis of code in forked repositories is skipped, as such workflow runs
|
||||
# do not have access to the requisite secrets.
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Install Harden-Runner
|
||||
uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
||||
with:
|
||||
disable-sudo: true
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
analysis-sensorcache-eu-central-1-prod.s3.amazonaws.com:443
|
||||
api.adoptium.net:443
|
||||
api.nuget.org:443
|
||||
ea6ne4j2sb.execute-api.eu-central-1.amazonaws.com:443
|
||||
github.com:443
|
||||
objects.githubusercontent.com:443
|
||||
repo.maven.apache.org:443
|
||||
sc-cleancode-sensorcache-eu-central-1-prod.s3.amazonaws.com:443
|
||||
*.sonarcloud.io:443
|
||||
sonarcloud.io:443
|
||||
- name: Check out code and set up JDK and Maven
|
||||
uses: s4u/setup-maven-action@489441643219d2b93ee2a127b2402eb640a1b947 # v1.13.0
|
||||
with:
|
||||
checkout-fetch-depth: 0
|
||||
java-version: 17.0.10
|
||||
java-distribution: temurin
|
||||
maven-version: 3.9.9
|
||||
- name: Create missing `test` directory
|
||||
# XXX: Drop this step in favour of actually having a test.
|
||||
run: mkdir refaster-compiler/src/test
|
||||
- name: Perform SonarCloud analysis
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
run: mvn -T1C jacoco:prepare-agent verify jacoco:report sonar:sonar -Dverification.skip -Dsonar.projectKey=PicnicSupermarket_error-prone-support
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,12 +4,8 @@
|
||||
.DS_Store
|
||||
.factorypath
|
||||
.idea
|
||||
!.idea/icon.svg
|
||||
.project
|
||||
.settings
|
||||
target
|
||||
*.iml
|
||||
*.swp
|
||||
|
||||
# The Git repositories checked out by the integration test framework.
|
||||
integration-tests/.repos
|
||||
|
||||
65
.idea/icon.svg
generated
65
.idea/icon.svg
generated
@@ -1,65 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 1259 1199" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-624.154,-988.431)">
|
||||
<g transform="matrix(1,0,0,1,-70.1122,-35.0561)">
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1227.03,1988.79C1237.78,2070.45 1225.83,2190.1 1192.24,2194.53C1158.65,2198.95 1116.14,2086.46 1105.39,2004.81C1094.64,1923.16 1128.44,1902.11 1153.32,1898.84C1178.18,1895.56 1216.28,1907.14 1227.03,1988.79Z" style="fill:rgb(219,220,211);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1151.08,1881.86C1134.93,1883.99 1114.77,1892.69 1101.6,1913.17C1088.42,1933.64 1082.71,1963.73 1088.42,2007.04C1094.04,2049.75 1107.59,2099.16 1124.51,2138.68C1132.97,2158.45 1142.15,2175.68 1152.59,2188.86C1163.04,2202.05 1176.31,2213.89 1194.48,2211.5C1212.65,2209.11 1222.39,2194.23 1229.07,2178.8C1235.75,2163.36 1240.15,2144.34 1243.21,2123.05C1249.32,2080.5 1249.63,2029.27 1244,1986.56C1238.3,1943.24 1225,1915.66 1206.98,1899.29C1188.95,1882.93 1167.22,1879.74 1151.08,1881.86ZM1155.55,1915.81C1164.27,1914.66 1174.03,1915.62 1183.96,1924.64C1193.89,1933.66 1205.01,1952.69 1210.06,1991.03C1215.18,2029.97 1214.89,2079.4 1209.32,2118.19C1206.53,2137.58 1202.32,2154.4 1197.65,2165.2C1194.14,2173.29 1190.82,2176.3 1189.96,2177.22C1188.89,2176.55 1184.91,2174.51 1179.43,2167.6C1172.12,2158.38 1163.7,2143.22 1155.99,2125.21C1140.57,2089.18 1127.49,2041.51 1122.36,2002.57C1117.32,1964.24 1123.13,1942.97 1130.39,1931.69C1137.65,1920.42 1146.82,1916.96 1155.55,1915.81Z" style="fill:rgb(26,26,26);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1516.33,1963.1C1466.19,1897.75 1427.4,1906.77 1407.5,1922.05C1387.59,1937.32 1368.84,1972.45 1418.98,2037.8C1431.75,2054.44 1447.26,2071.84 1463.69,2088.19C1495.18,2119.52 1534.33,2139.39 1582.98,2126.14C1606.4,2119.76 1622.19,2110.46 1623.75,2098.64C1625.79,2083.16 1603,2065.78 1569.69,2050.47C1554.75,2019.83 1535.59,1988.2 1516.33,1963.1Z" style="fill:rgb(219,220,211);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1397.07,1908.46C1409.99,1898.55 1430.41,1890.44 1454.2,1895.61C1478,1900.77 1503.31,1918 1529.91,1952.67C1548.92,1977.44 1567.3,2007.65 1582.28,2037.56C1597.47,2044.87 1610.74,2052.64 1621.09,2061.47C1632.68,2071.35 1642.93,2084.12 1640.73,2100.88C1639.05,2113.64 1630.31,2122.66 1620.9,2128.78C1611.49,2134.9 1600.29,2139.17 1587.48,2142.66C1532.39,2157.66 1485.57,2134.11 1451.61,2100.32C1434.7,2083.49 1418.73,2065.6 1405.39,2048.22C1378.79,2013.56 1368.69,1984.64 1369.86,1960.32C1371.04,1936 1384.15,1918.38 1397.07,1908.46ZM1417.92,1935.63C1410.94,1940.99 1404.71,1948.57 1404.07,1961.97C1403.43,1975.37 1409.02,1996.69 1432.56,2027.38C1444.76,2043.27 1459.82,2060.18 1475.77,2076.05C1504.8,2104.93 1536.26,2121.12 1578.48,2109.62C1589.1,2106.73 1597.5,2103.16 1602.23,2100.08C1605.14,2098.18 1606.16,2096.97 1606.54,2096.46C1606.07,2095.66 1604.57,2092.39 1598.86,2087.52C1591.24,2081.02 1578.31,2073.28 1562.54,2066.03L1556.98,2063.47L1554.29,2057.97C1539.86,2028.35 1521.12,1997.46 1502.75,1973.52C1479.2,1942.84 1460.05,1931.91 1446.94,1929.07C1433.84,1926.23 1424.9,1930.27 1417.92,1935.63Z" style="fill:rgb(26,26,26);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M917.121,1633.14C845.801,1674.32 730.68,1709.07 713.738,1679.73C696.797,1650.39 784.453,1568.07 855.777,1526.89C927.102,1485.71 959.48,1508.89 972.02,1530.62C984.562,1552.34 988.445,1591.97 917.121,1633.14Z" style="fill:rgb(219,220,211);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M986.848,1522.06C978.707,1507.95 962.949,1492.66 938.992,1488.32C915.031,1483.98 885.055,1490.22 847.219,1512.07C809.906,1533.61 769.453,1565.03 739.41,1595.79C724.391,1611.17 711.977,1626.24 703.797,1640.94C695.617,1655.64 689.746,1672.42 698.914,1688.29C708.078,1704.17 725.547,1707.48 742.363,1707.74C759.184,1708.01 778.445,1704.79 799.273,1699.47C840.934,1688.83 888.375,1669.51 925.684,1647.97C963.52,1626.12 983.91,1603.29 992.137,1580.37C1000.36,1557.45 994.988,1536.16 986.848,1522.06ZM957.195,1539.18C961.594,1546.79 964.438,1556.18 959.906,1568.8C955.379,1581.43 942.047,1598.98 908.562,1618.32C874.551,1637.96 828.77,1656.6 790.801,1666.3C771.816,1671.14 754.664,1673.69 742.902,1673.5C734.082,1673.37 730.035,1671.45 728.859,1671C729.062,1669.76 729.426,1665.3 733.715,1657.59C739.434,1647.31 750.215,1633.73 763.906,1619.72C791.285,1591.68 830.324,1561.36 864.336,1541.72C897.824,1522.38 919.695,1519.62 932.895,1522.01C946.09,1524.4 952.797,1531.56 957.195,1539.18Z" style="fill:rgb(26,26,26);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1791.57,1526.89C1862.89,1568.07 1950.54,1650.39 1933.61,1679.74C1916.66,1709.08 1801.54,1674.33 1730.22,1633.15C1658.9,1591.97 1662.78,1552.34 1675.32,1530.62C1687.86,1508.89 1720.24,1485.72 1791.57,1526.89Z" style="fill:rgb(219,220,211);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1660.5,1522.06C1652.35,1536.16 1646.98,1557.45 1655.21,1580.37C1663.43,1603.29 1683.82,1626.13 1721.66,1647.97C1758.97,1669.52 1806.41,1688.84 1848.07,1699.48C1868.9,1704.79 1888.16,1708.01 1904.98,1707.75C1921.79,1707.48 1939.27,1704.17 1948.43,1688.3C1957.59,1672.42 1951.73,1655.64 1943.55,1640.94C1935.37,1626.25 1922.95,1611.17 1907.93,1595.79C1877.89,1565.04 1837.43,1533.61 1800.12,1512.07C1762.29,1490.22 1732.31,1483.98 1708.35,1488.32C1684.39,1492.66 1668.64,1507.95 1660.5,1522.06ZM1690.15,1539.18C1694.55,1531.56 1701.25,1524.4 1714.45,1522.02C1727.64,1519.62 1749.52,1522.39 1783,1541.72C1817.02,1561.36 1856.06,1591.68 1883.44,1619.72C1897.12,1633.73 1907.91,1647.32 1913.63,1657.59C1917.92,1665.3 1918.28,1669.77 1918.48,1671.01C1917.31,1671.45 1913.26,1673.37 1904.44,1673.51C1892.68,1673.69 1875.52,1671.15 1856.54,1666.3C1818.57,1656.61 1772.79,1637.96 1738.78,1618.32C1705.29,1598.99 1691.97,1581.43 1687.43,1568.81C1682.91,1556.18 1685.75,1546.8 1690.15,1539.18Z" style="fill:rgb(26,26,26);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1322.81,1013.67C1058.16,1014.17 843.293,1229.45 843.297,1494.11L843.223,1862.19C843.223,1955.32 919.055,2031.16 1012.19,2031.16C1054.55,2031.16 1093.39,2015.51 1123.09,1989.7C1169.54,2049.44 1242.17,2087.79 1323.7,2087.79C1405.22,2087.79 1477.84,2049.45 1524.28,1989.73C1553.98,2015.52 1592.81,2031.16 1635.15,2031.16C1728.29,2031.16 1804.12,1955.32 1804.12,1862.19L1804.12,1494.1C1804.12,1229.09 1588.7,1013.67 1323.69,1013.67L1322.84,1013.67L1322.81,1013.67ZM1322.92,1068.46L1323.69,1068.46C1559.09,1068.46 1749.33,1258.7 1749.33,1494.11L1749.33,1862.19C1749.33,1925.92 1698.88,1976.37 1635.15,1976.37C1596.91,1976.37 1563.67,1958.03 1542.94,1929.68L1517.91,1895.48L1497,1932.34C1462.85,1992.53 1398.48,2033 1323.7,2033C1248.9,2033 1184.52,1992.51 1150.38,1932.3L1129.45,1895.41L1104.43,1929.65C1083.69,1958.02 1050.44,1976.37 1012.19,1976.37C948.461,1976.37 898.016,1925.93 898.012,1862.2L898.086,1494.11C898.086,1259.03 1087.84,1068.92 1322.92,1068.47L1322.92,1068.46Z" style="fill:rgb(26,26,26);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1322.86,1041.07C1072.99,1041.54 870.684,1244.23 870.688,1494.11L870.648,1862.19C870.648,1940.62 933.789,2003.77 1012.22,2003.77C1073.65,2003.77 1125.69,1965.03 1145.37,1910.56C1201.73,1934.69 1262.41,1947.14 1323.72,1947.14C1385.02,1947.14 1445.69,1934.69 1502.04,1910.57C1521.72,1965.04 1573.76,2003.77 1635.19,2003.77C1713.62,2003.77 1776.76,1940.62 1776.76,1862.19L1776.76,1494.11C1776.76,1243.9 1573.93,1041.07 1323.72,1041.07L1322.86,1041.07Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1323.7,1494.1C1449.2,1494.1 1550.24,1595.14 1550.24,1720.64L1550.24,1833.86C1550.24,1959.36 1449.2,2060.4 1323.7,2060.4C1198.2,2060.4 1097.16,1959.36 1097.16,1833.86L1097.16,1720.64C1097.16,1595.14 1198.2,1494.1 1323.7,1494.1Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1322.86,1041.07C1262.14,1041.18 1204.25,1053.27 1151.36,1075.05C1316.07,1142.87 1432.03,1304.93 1432.03,1494.11L1432.03,1811.95C1432.03,2003.63 1209.62,2024.85 1126.54,1945.82C1177.52,2034.1 1254.55,2060.4 1323.7,2060.4C1408.4,2060.4 1481.95,2014.37 1520.82,1945.86C1546.53,1981.01 1588.08,2003.77 1635.15,2003.77C1713.58,2003.77 1776.72,1940.62 1776.72,1862.19L1776.72,1494.11C1776.72,1243.9 1573.89,1041.07 1323.68,1041.07L1322.86,1041.07Z" style="fill:rgb(219,220,211);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1322.86,1041.07C1304.33,1041.1 1286.06,1042.28 1268.11,1044.48C1492.04,1071.93 1665.46,1262.75 1665.46,1494.11L1665.46,1862.19C1665.46,1920.85 1630.14,1970.94 1579.54,1992.48C1596.59,1999.74 1615.38,2003.77 1635.15,2003.77C1713.58,2003.77 1776.72,1940.62 1776.72,1862.19L1776.72,1494.11C1776.72,1243.9 1573.89,1041.07 1323.68,1041.07L1322.86,1041.07Z" style="fill:rgb(189,191,175);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1322.85,1034.22C1069.29,1034.69 863.84,1240.54 863.84,1494.11L863.766,1862.19C863.766,1944.3 930.078,2010.61 1012.19,2010.61C1057.88,2010.61 1098.53,1989.94 1125.71,1957.58C1166.88,2023.5 1240.02,2067.25 1323.7,2067.25C1407.36,2067.25 1480.48,2023.52 1521.66,1957.62C1548.84,1989.96 1589.47,2010.61 1635.15,2010.61C1717.25,2010.61 1783.57,1944.3 1783.57,1862.19L1783.57,1494.11C1783.57,1240.2 1577.59,1034.21 1323.68,1034.22L1322.85,1034.22ZM1322.86,1047.92L1323.68,1047.92C1570.19,1047.92 1769.87,1247.6 1769.87,1494.11L1769.87,1862.19C1769.87,1936.95 1709.91,1996.92 1635.15,1996.92C1590.29,1996.92 1550.82,1975.26 1526.36,1941.82L1520.1,1933.27L1514.87,1942.48C1477.18,2008.91 1405.92,2053.55 1323.7,2053.55C1241.46,2053.55 1170.19,2008.89 1132.5,1942.44L1127.27,1933.21L1121.02,1941.77C1096.56,1975.24 1057.07,1996.92 1012.19,1996.92C937.43,1996.92 877.469,1936.95 877.465,1862.19L877.539,1494.11C877.539,1247.94 1076.7,1048.39 1322.86,1047.92Z" style="fill:rgb(26,26,26);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1576.29,1470.72C1576.29,1481.36 1568.75,1491.56 1555.31,1499.08C1541.88,1506.6 1523.67,1510.83 1504.68,1510.83C1465.12,1510.83 1433.06,1492.87 1433.06,1470.72C1433.06,1448.57 1465.12,1430.62 1504.68,1430.62C1523.67,1430.62 1541.88,1434.84 1555.31,1442.36C1568.75,1449.89 1576.29,1460.09 1576.29,1470.72Z" style="fill:rgb(255,155,173);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1214.28,1470.72C1214.28,1481.36 1206.73,1491.56 1193.31,1499.08C1179.87,1506.6 1161.66,1510.83 1142.66,1510.83C1103.11,1510.83 1071.05,1492.87 1071.05,1470.72C1071.05,1448.57 1103.11,1430.62 1142.66,1430.62C1161.66,1430.62 1179.87,1434.84 1193.31,1442.36C1206.73,1449.89 1214.28,1460.09 1214.28,1470.72Z" style="fill:rgb(255,155,173);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1104.39,1401.46C1104.39,1375.15 1118.45,1350.79 1141.24,1337.63C1164.03,1324.48 1192.16,1324.48 1214.95,1337.63C1237.74,1350.79 1251.81,1375.15 1251.81,1401.46L1224.41,1401.46C1224.41,1384.9 1215.6,1369.64 1201.25,1361.36C1186.9,1353.07 1169.29,1353.07 1154.94,1361.36C1140.59,1369.64 1131.78,1384.9 1131.78,1401.46L1104.39,1401.46Z" style="fill:rgb(26,26,26);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1395.54,1401.46C1395.54,1375.15 1409.61,1350.79 1432.39,1337.63C1455.18,1324.48 1483.32,1324.48 1506.11,1337.63C1528.89,1350.79 1542.96,1375.15 1542.96,1401.46L1515.56,1401.46C1515.56,1384.9 1506.75,1369.64 1492.41,1361.36C1478.06,1353.07 1460.44,1353.07 1446.09,1361.36C1431.74,1369.64 1422.93,1384.9 1422.93,1401.46L1395.54,1401.46Z" style="fill:rgb(26,26,26);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,9.81959)">
|
||||
<path d="M1256.81,1448.61C1256.81,1472.48 1269.56,1494.57 1290.24,1506.51C1310.92,1518.45 1336.42,1518.45 1357.1,1506.51C1377.78,1494.57 1390.53,1472.48 1390.53,1448.61L1376.83,1448.61C1376.83,1467.61 1366.71,1485.15 1350.25,1494.65C1333.79,1504.15 1313.55,1504.15 1297.09,1494.65C1280.63,1485.15 1270.51,1467.61 1270.51,1448.61L1256.81,1448.61Z" style="fill:rgb(26,26,26);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,13 +0,0 @@
|
||||
-XX:ReservedCodeCacheSize=512m
|
||||
-XX:SoftRefLRUPolicyMSPerMB=10
|
||||
-XX:+UseParallelGC
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
|
||||
@@ -1,3 +0,0 @@
|
||||
--batch-mode
|
||||
--errors
|
||||
--strict-checksums
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"helpers:pinGitHubActionDigests"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"^org\\.springframework:spring-framework-bom$",
|
||||
"^org\\.springframework\\.boot:spring-boot[a-z-]*$"
|
||||
],
|
||||
"separateMinorPatch": true
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"dawidd6/action-download-artifact",
|
||||
"github/codeql-action",
|
||||
"ruby/setup-ruby"
|
||||
],
|
||||
"schedule": "every 4 weeks on Monday"
|
||||
}
|
||||
],
|
||||
"reviewers": [
|
||||
"rickie",
|
||||
"Stephan202"
|
||||
]
|
||||
}
|
||||
37
.travis.yml
Normal file
37
.travis.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
dist: bionic
|
||||
language: java
|
||||
jdk: openjdk11
|
||||
addons:
|
||||
sonarcloud:
|
||||
organization: picnic-technologies
|
||||
token: "${SONARCLOUD_TOKEN}"
|
||||
install:
|
||||
- mvn io.takari:maven:wrapper
|
||||
script:
|
||||
# We run the build twice: 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 Error Prone checks
|
||||
# available from other artifact repositories.
|
||||
- ./mvnw clean install
|
||||
- ./mvnw clean install -Perror-prone-fork -Pnon-maven-central -s settings.xml
|
||||
# XXX: Enable SonarCloud once we "go public".
|
||||
# ./mvnw jacoco:prepare-agent surefire:test jacoco:report sonar:sonar
|
||||
- ./mvnw jacoco:prepare-agent surefire:test jacoco:report
|
||||
before_cache:
|
||||
# Don't cache the artifacts we just generated, for multiple reasons: (1) we
|
||||
# shouldn't need them next time around and (2) if we do, that indicates a
|
||||
# dependency issue which might otherwise go unnoticed until next time we bump
|
||||
# the project's version (i.e., when tagging).
|
||||
- find "${HOME}/.m2/repository" -depth -name '*-SNAPSHOT' -exec rm -r '{}' \;
|
||||
cache:
|
||||
directories:
|
||||
# The local Maven repository in which third party dependencies are stored.
|
||||
- ${HOME}/.m2/repository
|
||||
# The Takari Maven Wrapper's storage for downloaded Maven distributions.
|
||||
- ${HOME}/.m2/wrapper
|
||||
# The SonarQube analysis cache.
|
||||
- ${HOME}/.sonar/cache
|
||||
# XXX: Enable Codecov once we "go public".
|
||||
#after_success:
|
||||
# - bash <(curl -s https://codecov.io/bash)
|
||||
@@ -1,85 +0,0 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for checking this document! This project is free software, and we
|
||||
(the maintainers) encourage and value any contribution.
|
||||
|
||||
Here are some guidelines to help you get started.
|
||||
|
||||
## 🐛 Reporting a bug
|
||||
|
||||
Like any non-trivial piece of software, this library is probably not bug-free.
|
||||
If you found a bug, feel free to [report the issue][error-prone-support-issues]
|
||||
on GitHub.
|
||||
|
||||
Before doing so, please:
|
||||
- Verify that the issue is reproducible against the latest version of the
|
||||
project.
|
||||
- Search through the existing set of issues to see whether the problem is
|
||||
already known. With some luck a solution is already in place, or a workaround
|
||||
may have been provided.
|
||||
|
||||
When filing a bug report, please include the following:
|
||||
- Any relevant information about your environment. This should generally
|
||||
include the output of `java --version`, as well as the version of Error Prone
|
||||
you're using.
|
||||
- A description of what is going on (e.g. logging output, stacktraces).
|
||||
- A minimum reproducible example, so that other developers can try to reproduce
|
||||
(and optionally fix) the bug.
|
||||
- Any additional information that may be relevant.
|
||||
|
||||
## 💡 Reporting an improvement
|
||||
|
||||
If you would like to see an improvement, you can file a [GitHub
|
||||
issue][error-prone-support-issues]. This is also a good idea when you're
|
||||
already working towards opening a pull request, as this allows for discussion
|
||||
around the idea.
|
||||
|
||||
## 🚀 Opening a pull request
|
||||
|
||||
All submissions, including submissions by project members, require approval by
|
||||
at least two reviewers. We use [GitHub pull
|
||||
requests][error-prone-support-pulls] for this purpose.
|
||||
|
||||
Before opening a pull request, please check whether there are any existing
|
||||
(open or closed) issues or pull requests addressing the same problem. This
|
||||
avoids double work or lots of time spent on a solution that may ultimately not
|
||||
be accepted. When in doubt, make sure to first raise an
|
||||
[issue][error-prone-support-issues] to discuss the idea.
|
||||
|
||||
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
|
||||
it must not "get in the way". So for a check that addresses a relatively
|
||||
trivial stylistic concern it is doubly important that the violations it
|
||||
detects can be auto-patched.
|
||||
- Make sure you have read Error Prone's [criteria for new
|
||||
checks][error-prone-criteria]. Most guidelines described there apply to this
|
||||
project as well, except that this project _does_ focus quite heavy on style
|
||||
enforcement. But that just makes the previous point doubly important.
|
||||
- Make sure that a check's [(mutation) test
|
||||
coverage][error-prone-support-mutation-tests] is or remains about as high as
|
||||
it can be. Not only does this lead to better tests, it also points out
|
||||
opportunities to simplify the code.
|
||||
- Please restrict the scope of a pull request to a single feature or fix. Don't
|
||||
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-branch-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
|
||||
21
LICENSE.md
21
LICENSE.md
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2024 Picnic Technologies BV
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
325
README.md
325
README.md
@@ -1,325 +0,0 @@
|
||||
<div align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="website/assets/images/logo-dark.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="website/assets/images/logo.svg">
|
||||
<img alt="Error Prone Support logo" src="website/assets/images/logo.svg" width="50%">
|
||||
</picture>
|
||||
</div>
|
||||
|
||||
# Error Prone Support
|
||||
|
||||
Error Prone Support is a [Picnic][picnic-blog]-opinionated extension of
|
||||
Google's [Error Prone][error-prone-orig-repo]. It aims to improve code quality,
|
||||
focussing on maintainability, consistency and avoidance of common pitfalls.
|
||||
|
||||
> Error Prone is a static analysis tool for Java that catches common
|
||||
> programming mistakes at compile-time.
|
||||
|
||||
To learn more about Error Prone (Support), how you can start using Error Prone
|
||||
in practice, and how we use it at Picnic, watch the conference talk
|
||||
[_Automating away bugs with Error Prone in practice_][conference-talk]. Also
|
||||
consider checking out the blog post [_Picnic loves Error Prone: producing
|
||||
high-quality and consistent Java code_][picnic-blog-ep-post].
|
||||
|
||||
[![Maven Central][maven-central-badge]][maven-central-search]
|
||||
[![Reproducible Builds][reproducible-builds-badge]][reproducible-builds-report]
|
||||
[![OpenSSF Best Practices][openssf-best-practices-badge]][openssf-best-practices-checklist]
|
||||
[![OpenSSF Scorecard][openssf-scorecard-badge]][openssf-scorecard-report]
|
||||
[![CodeQL Analysis][codeql-badge]][codeql-master]
|
||||
[![GitHub Actions][github-actions-build-badge]][github-actions-build-master]
|
||||
[![Mutation tested with PIT][pitest-badge]][pitest]
|
||||
[![Quality Gate Status][sonarcloud-quality-badge]][sonarcloud-quality-master]
|
||||
[![Maintainability Rating][sonarcloud-maintainability-badge]][sonarcloud-maintainability-master]
|
||||
[![Reliability Rating][sonarcloud-reliability-badge]][sonarcloud-reliability-master]
|
||||
[![Security Rating][sonarcloud-security-badge]][sonarcloud-security-master]
|
||||
[![Coverage][sonarcloud-coverage-badge]][sonarcloud-coverage-master]
|
||||
[![Duplicated Lines (%)][sonarcloud-duplication-badge]][sonarcloud-duplication-master]
|
||||
[![Technical Debt][sonarcloud-technical-debt-badge]][sonarcloud-technical-debt-master]
|
||||
[![License][license-badge]][license]
|
||||
[![PRs Welcome][pr-badge]][contributing]
|
||||
|
||||
[Getting started](#-getting-started) •
|
||||
[Developing Error Prone Support](#-developing-error-prone-support) •
|
||||
[How it works](#-how-it-works) • [Contributing](#%EF%B8%8F-contributing)
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Getting started
|
||||
|
||||
### Installation
|
||||
|
||||
This library is built on top of [Error Prone][error-prone-orig-repo]. To use
|
||||
it, read the installation guide for Maven or Gradle below. The library requires
|
||||
that your build is executed using JDK 17 or above, but supports builds that
|
||||
[target][baeldung-java-source-target-options] older versions of Java.
|
||||
|
||||
#### Maven
|
||||
|
||||
1. First, follow Error Prone's [installation
|
||||
guide][error-prone-installation-guide].
|
||||
2. Next, edit your `pom.xml` file to add one or more Error Prone Support
|
||||
modules to the `annotationProcessorPaths` of the `maven-compiler-plugin`:
|
||||
|
||||
```xml
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<!-- Prefer using the latest release. -->
|
||||
<version>3.12.0</version>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<!-- Error Prone itself. -->
|
||||
<path>
|
||||
<groupId>com.google.errorprone</groupId>
|
||||
<artifactId>error_prone_core</artifactId>
|
||||
<version>${error-prone.version}</version>
|
||||
</path>
|
||||
<!-- Error Prone Support's additional bug checkers. -->
|
||||
<path>
|
||||
<groupId>tech.picnic.error-prone-support</groupId>
|
||||
<artifactId>error-prone-contrib</artifactId>
|
||||
<version>${error-prone-support.version}</version>
|
||||
</path>
|
||||
<!-- Error Prone Support's Refaster rules. -->
|
||||
<path>
|
||||
<groupId>tech.picnic.error-prone-support</groupId>
|
||||
<artifactId>refaster-runner</artifactId>
|
||||
<version>${error-prone-support.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
<compilerArgs>
|
||||
<arg>
|
||||
-Xplugin:ErrorProne
|
||||
<!-- Add other Error Prone flags here. See
|
||||
https://errorprone.info/docs/flags. -->
|
||||
</arg>
|
||||
<arg>-XDcompilePolicy=simple</arg>
|
||||
</compilerArgs>
|
||||
<!-- Enable this if you'd like to fail your build upon warnings. -->
|
||||
<!-- <failOnWarning>true</failOnWarning> -->
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
```
|
||||
|
||||
<!-- XXX: Reference `oss-parent`'s `pom.xml` once that project also uses Error
|
||||
Prone Support. Alternatively reference this project's `self-check` profile
|
||||
definition. -->
|
||||
|
||||
#### Gradle
|
||||
|
||||
1. First, follow the [installation
|
||||
guide][error-prone-gradle-installation-guide] of the
|
||||
`gradle-errorprone-plugin`.
|
||||
2. Next, edit your `build.gradle` file to add one or more Error Prone Support
|
||||
modules:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
// Error Prone itself.
|
||||
errorprone("com.google.errorprone:error_prone_core:${errorProneVersion}")
|
||||
// Error Prone Support's additional bug checkers.
|
||||
errorprone("tech.picnic.error-prone-support:error-prone-contrib:${errorProneSupportVersion}")
|
||||
// Error Prone Support's Refaster rules.
|
||||
errorprone("tech.picnic.error-prone-support:refaster-runner:${errorProneSupportVersion}")
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.errorprone.disableWarningsInGeneratedCode = true
|
||||
// Add other Error Prone flags here. See:
|
||||
// - https://github.com/tbroyer/gradle-errorprone-plugin#configuration
|
||||
// - https://errorprone.info/docs/flags
|
||||
}
|
||||
```
|
||||
|
||||
### Seeing it in action
|
||||
|
||||
Consider the following example code:
|
||||
|
||||
```java
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class Example {
|
||||
static BigDecimal getNumber() {
|
||||
return BigDecimal.valueOf(0);
|
||||
}
|
||||
|
||||
public ImmutableSet<Integer> getSet() {
|
||||
ImmutableSet<Integer> set = ImmutableSet.of(1);
|
||||
return ImmutableSet.copyOf(set);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If the [installation](#installation) was successful, then building the above
|
||||
code with Maven should yield two compiler warnings:
|
||||
|
||||
```sh
|
||||
$ mvn clean install
|
||||
...
|
||||
[INFO] Example.java:[9,34] [Refaster Rule] BigDecimalRules.BigDecimalZero: Refactoring opportunity
|
||||
(see https://error-prone.picnic.tech/refasterrules/BigDecimalRules#BigDecimalZero)
|
||||
Did you mean 'return BigDecimal.ZERO;'?
|
||||
...
|
||||
[WARNING] Example.java:[13,35] [IdentityConversion] This method invocation appears redundant; remove it or suppress this warning and add a comment explaining its purpose
|
||||
(see https://error-prone.picnic.tech/bugpatterns/IdentityConversion)
|
||||
Did you mean 'return set;' or '@SuppressWarnings("IdentityConversion") public ImmutableSet<Integer> getSet() {'?
|
||||
...
|
||||
```
|
||||
|
||||
Two things are kicking in here:
|
||||
|
||||
1. An Error Prone [`BugChecker`][error-prone-bugchecker] that flags unnecessary
|
||||
[identity conversions][bug-checks-identity-conversion].
|
||||
2. A [Refaster][refaster] rule capable of
|
||||
[rewriting][refaster-rules-bigdecimal] expressions of the form
|
||||
`BigDecimal.valueOf(0)` and `new BigDecimal(0)` to `BigDecimal.ZERO`.
|
||||
|
||||
Be sure to check out all [bug checks][bug-checks] and [refaster
|
||||
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.
|
||||
|
||||
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.
|
||||
- `-Dverification.skip` disables various non-essential plugins and compiles the
|
||||
code with minimal checks (i.e. without linting, Error Prone checks, etc.).
|
||||
- `-Dversion.error-prone=some-version` runs the build using the specified
|
||||
version of Error Prone. This is useful e.g. when testing a locally built
|
||||
Error Prone SNAPSHOT.
|
||||
- `-Perror-prone-fork` runs the build using Picnic's [Error Prone
|
||||
fork][error-prone-fork-repo], hosted on [Jitpack][error-prone-fork-jitpack].
|
||||
This fork generally contains a few changes on top of the latest Error Prone
|
||||
release.
|
||||
- `-Pself-check` runs the checks defined by this project against itself.
|
||||
Pending a release of [google/error-prone#3301][error-prone-pull-3301], this
|
||||
flag must currently be used in combination with `-Perror-prone-fork`.
|
||||
|
||||
Other highly relevant commands:
|
||||
|
||||
- `mvn fmt:format` formats the code using
|
||||
[`google-java-format`][google-java-format].
|
||||
- [`./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-branch-mutation-tests.sh`][script-run-branch-mutation-tests] uses
|
||||
[Pitest][pitest] to run mutation tests against code that is modified relative
|
||||
to the upstream default branch. The results can be reviewed by opening the
|
||||
respective `target/pit-reports/index.html` files. One can use
|
||||
[`./run-mutation-tests.sh`][script-run-mutation-tests] to run mutation tests
|
||||
against _all_ code in the current working directory. For more information
|
||||
check the [PIT Maven plugin][pitest-maven].
|
||||
|
||||
When running the project's tests in IntelliJ IDEA, you might see the following
|
||||
error:
|
||||
|
||||
```
|
||||
java: exporting a package from system module jdk.compiler is not allowed with --release
|
||||
```
|
||||
|
||||
If this happens, go to _Settings -> Build, Execution, Deployment -> Compiler ->
|
||||
Java Compiler_ and deselect the option _Use '--release' option for
|
||||
cross-compilation (Java 9 and later)_. See [IDEA-288052][idea-288052] for
|
||||
details.
|
||||
|
||||
## 💡 How it works
|
||||
|
||||
This project provides additional [`BugChecker`][error-prone-bugchecker]
|
||||
implementations.
|
||||
|
||||
<!-- XXX: Extend this section. -->
|
||||
|
||||
## ✍️ Contributing
|
||||
|
||||
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 vulnerability, please do so through a private
|
||||
channel; please see our [security policy][security] for details.
|
||||
|
||||
[baeldung-java-source-target-options]: https://www.baeldung.com/java-source-target-options
|
||||
[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
|
||||
[codeql-badge]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/codeql.yml/badge.svg?branch=master&event=push
|
||||
[codeql-master]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/codeql.yml?query=branch:master+event:push
|
||||
[conference-talk]: https://www.youtube.com/watch?v=-47WD-3wKBs
|
||||
[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
|
||||
[error-prone-gradle-installation-guide]: https://github.com/tbroyer/gradle-errorprone-plugin
|
||||
[error-prone-installation-guide]: https://errorprone.info/docs/installation#maven
|
||||
[error-prone-orig-repo]: https://github.com/google/error-prone
|
||||
[error-prone-pull-3301]: https://github.com/google/error-prone/pull/3301
|
||||
[github-actions-build-badge]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/build.yml/badge.svg
|
||||
[github-actions-build-master]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/build.yml?query=branch:master&event=push
|
||||
[google-java-format]: https://github.com/google/google-java-format
|
||||
[idea-288052]: https://youtrack.jetbrains.com/issue/IDEA-288052
|
||||
[license-badge]: https://img.shields.io/github/license/PicnicSupermarket/error-prone-support
|
||||
[license]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/LICENSE.md
|
||||
[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
|
||||
[openssf-best-practices-badge]: https://bestpractices.coreinfrastructure.org/projects/7199/badge
|
||||
[openssf-best-practices-checklist]: https://bestpractices.coreinfrastructure.org/projects/7199
|
||||
[openssf-scorecard-badge]: https://img.shields.io/ossf-scorecard/github.com/PicnicSupermarket/error-prone-support?label=openssf%20scorecard
|
||||
[openssf-scorecard-report]: https://securityscorecards.dev/viewer/?uri=github.com/PicnicSupermarket/error-prone-support
|
||||
[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-badge]: https://img.shields.io/badge/-Mutation%20tested%20with%20PIT-blue.svg
|
||||
[pitest]: https://pitest.org
|
||||
[pitest-maven]: https://pitest.org/quickstart/maven
|
||||
[pr-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
|
||||
[refaster]: https://errorprone.info/docs/refaster
|
||||
[refaster-rules-bigdecimal]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/BigDecimalRules.java
|
||||
[refaster-rules]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/
|
||||
[reproducible-builds-badge]: https://img.shields.io/badge/Reproducible_Builds-ok-success?labelColor=1e5b96
|
||||
[reproducible-builds-report]: https://github.com/jvm-repo-rebuild/reproducible-central/blob/master/content/tech/picnic/error-prone-support/error-prone-support/README.md
|
||||
[script-apply-error-prone-suggestions]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/apply-error-prone-suggestions.sh
|
||||
[script-run-branch-mutation-tests]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-branch-mutation-tests.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
|
||||
[sonarcloud-coverage-badge]: https://sonarcloud.io/api/project_badges/measure?project=PicnicSupermarket_error-prone-support&metric=coverage
|
||||
[sonarcloud-coverage-master]: https://sonarcloud.io/component_measures?id=PicnicSupermarket_error-prone-support&metric=coverage
|
||||
[sonarcloud-duplication-badge]: https://sonarcloud.io/api/project_badges/measure?project=PicnicSupermarket_error-prone-support&metric=duplicated_lines_density
|
||||
[sonarcloud-duplication-master]: https://sonarcloud.io/component_measures?id=PicnicSupermarket_error-prone-support&metric=duplicated_lines_density
|
||||
[sonarcloud-maintainability-badge]: https://sonarcloud.io/api/project_badges/measure?project=PicnicSupermarket_error-prone-support&metric=sqale_rating
|
||||
[sonarcloud-maintainability-master]: https://sonarcloud.io/component_measures?id=PicnicSupermarket_error-prone-support&metric=sqale_rating
|
||||
[sonarcloud-quality-badge]: https://sonarcloud.io/api/project_badges/measure?project=PicnicSupermarket_error-prone-support&metric=alert_status
|
||||
[sonarcloud-quality-master]: https://sonarcloud.io/summary/new_code?id=PicnicSupermarket_error-prone-support
|
||||
[sonarcloud-reliability-badge]: https://sonarcloud.io/api/project_badges/measure?project=PicnicSupermarket_error-prone-support&metric=reliability_rating
|
||||
[sonarcloud-reliability-master]: https://sonarcloud.io/component_measures?id=PicnicSupermarket_error-prone-support&metric=reliability_rating
|
||||
[sonarcloud-security-badge]: https://sonarcloud.io/api/project_badges/measure?project=PicnicSupermarket_error-prone-support&metric=security_rating
|
||||
[sonarcloud-security-master]: https://sonarcloud.io/component_measures?id=PicnicSupermarket_error-prone-support&metric=security_rating
|
||||
[sonarcloud-technical-debt-badge]: https://sonarcloud.io/api/project_badges/measure?project=PicnicSupermarket_error-prone-support&metric=sqale_index
|
||||
[sonarcloud-technical-debt-master]: https://sonarcloud.io/component_measures?id=PicnicSupermarket_error-prone-support&metric=sqale_index
|
||||
23
SECURITY.md
23
SECURITY.md
@@ -1,23 +0,0 @@
|
||||
# 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
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Compiles the code using Error Prone and applies its suggestions. The set of
|
||||
# checks applied can optionally be restricted by name.
|
||||
#
|
||||
# As this script may modify the project's code, it is important to execute it
|
||||
# in a clean Git working directory.
|
||||
|
||||
set -e -u -o pipefail
|
||||
|
||||
if [ "${#}" -gt 1 ]; then
|
||||
echo "Usage: ${0} [PatchChecks]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
patchChecks=${1:-}
|
||||
|
||||
mvn clean test-compile fmt:format \
|
||||
-s "$(dirname "${0}")/settings.xml" \
|
||||
-T 1.0C \
|
||||
-Perror-prone \
|
||||
-Perror-prone-fork \
|
||||
-Ppatch \
|
||||
-Pself-check \
|
||||
-Derror-prone.patch-checks="${patchChecks}" \
|
||||
-Dverification.skip
|
||||
@@ -1,7 +0,0 @@
|
||||
# Arcmutate license for Error Prone Support, requested by sending an email to
|
||||
# support@arcmutate.com.
|
||||
expires=27/10/2025
|
||||
keyVersion=1
|
||||
signature=aQLVt0J//7nXDAlJuBe9ru7Dq6oApcLh17rxsgHoJqBkUCd2zvrtRxkcPm5PmF1I8gBDT9PHb+kTf6Kcfz+D1fT0wRrZv38ip56521AQPD6zjRSqak9O2Gisjo+QTPMD7oS009aSWKzQ1SMdtuZ+KIRvw4Gl+frtYzexqnGWEUQ\=
|
||||
packages=tech.picnic.errorprone.*
|
||||
type=OSSS
|
||||
@@ -1,98 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>tech.picnic.error-prone-support</groupId>
|
||||
<artifactId>error-prone-support</artifactId>
|
||||
<version>0.18.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>documentation-support</artifactId>
|
||||
|
||||
<name>Picnic :: Error Prone Support :: Documentation Support</name>
|
||||
<description>Data extraction support for the purpose of documentation generation.</description>
|
||||
<url>https://error-prone.picnic.tech</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_annotation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_check_api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_test_helpers</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.module</groupId>
|
||||
<artifactId>jackson-module-parameter-names</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto</groupId>
|
||||
<artifactId>auto-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.service</groupId>
|
||||
<artifactId>auto-service-annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.value</groupId>
|
||||
<artifactId>auto-value-annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jspecify</groupId>
|
||||
<artifactId>jspecify</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- XXX: Explicitly declared as a workaround for
|
||||
https://github.com/pitest/pitest-junit5-plugin/issues/105. -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-params</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,147 +0,0 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.google.auto.common.AnnotationMirrors;
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.tools.javac.code.Attribute;
|
||||
import com.sun.tools.javac.code.Symbol.ClassSymbol;
|
||||
import java.net.URI;
|
||||
import java.util.Optional;
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
import tech.picnic.errorprone.documentation.BugPatternExtractor.BugPatternDocumentation;
|
||||
|
||||
/**
|
||||
* An {@link Extractor} that describes how to extract data from a {@code @BugPattern} annotation.
|
||||
*/
|
||||
@Immutable
|
||||
@AutoService(Extractor.class)
|
||||
@SuppressWarnings("rawtypes" /* See https://github.com/google/auto/issues/870. */)
|
||||
public final class BugPatternExtractor implements Extractor<BugPatternDocumentation> {
|
||||
/** Instantiates a new {@link BugPatternExtractor} instance. */
|
||||
public BugPatternExtractor() {}
|
||||
|
||||
@Override
|
||||
public String identifier() {
|
||||
return "bugpattern";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<BugPatternDocumentation> tryExtract(ClassTree tree, VisitorState state) {
|
||||
ClassSymbol symbol = ASTHelpers.getSymbol(tree);
|
||||
BugPattern annotation = symbol.getAnnotation(BugPattern.class);
|
||||
if (annotation == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(
|
||||
BugPatternDocumentation.create(
|
||||
state.getPath().getCompilationUnit().getSourceFile().toUri(),
|
||||
symbol.getQualifiedName().toString(),
|
||||
annotation.name().isEmpty() ? tree.getSimpleName().toString() : annotation.name(),
|
||||
ImmutableList.copyOf(annotation.altNames()),
|
||||
annotation.link(),
|
||||
ImmutableList.copyOf(annotation.tags()),
|
||||
annotation.summary(),
|
||||
annotation.explanation(),
|
||||
annotation.severity(),
|
||||
annotation.disableable(),
|
||||
annotation.documentSuppression()
|
||||
? getSuppressionAnnotations(tree)
|
||||
: ImmutableList.of()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fully-qualified class names of suppression annotations specified by the {@link
|
||||
* BugPattern} annotation located on the given tree.
|
||||
*
|
||||
* @implNote This method cannot simply invoke {@link BugPattern#suppressionAnnotations()}, as that
|
||||
* will yield an "Attempt to access Class objects for TypeMirrors" exception.
|
||||
*/
|
||||
private static ImmutableList<String> getSuppressionAnnotations(ClassTree tree) {
|
||||
AnnotationTree annotationTree =
|
||||
ASTHelpers.getAnnotationWithSimpleName(
|
||||
ASTHelpers.getAnnotations(tree), BugPattern.class.getSimpleName());
|
||||
requireNonNull(annotationTree, "BugPattern annotation must be present");
|
||||
|
||||
Attribute.Array types =
|
||||
doCast(
|
||||
AnnotationMirrors.getAnnotationValue(
|
||||
ASTHelpers.getAnnotationMirror(annotationTree), "suppressionAnnotations"),
|
||||
Attribute.Array.class);
|
||||
|
||||
return types.getValue().stream()
|
||||
.map(v -> doCast(v, Attribute.Class.class).classType.toString())
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T extends AnnotationValue> T doCast(AnnotationValue value, Class<T> target) {
|
||||
verify(target.isInstance(value), "Value '%s' is not of type '%s'", value, target);
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
@JsonDeserialize(as = AutoValue_BugPatternExtractor_BugPatternDocumentation.class)
|
||||
abstract static class BugPatternDocumentation {
|
||||
static BugPatternDocumentation create(
|
||||
URI source,
|
||||
String fullyQualifiedName,
|
||||
String name,
|
||||
ImmutableList<String> altNames,
|
||||
String link,
|
||||
ImmutableList<String> tags,
|
||||
String summary,
|
||||
String explanation,
|
||||
SeverityLevel severityLevel,
|
||||
boolean canDisable,
|
||||
ImmutableList<String> suppressionAnnotations) {
|
||||
return new AutoValue_BugPatternExtractor_BugPatternDocumentation(
|
||||
source,
|
||||
fullyQualifiedName,
|
||||
name,
|
||||
altNames,
|
||||
link,
|
||||
tags,
|
||||
summary,
|
||||
explanation,
|
||||
severityLevel,
|
||||
canDisable,
|
||||
suppressionAnnotations);
|
||||
}
|
||||
|
||||
abstract URI source();
|
||||
|
||||
abstract String fullyQualifiedName();
|
||||
|
||||
abstract String name();
|
||||
|
||||
abstract ImmutableList<String> altNames();
|
||||
|
||||
abstract String link();
|
||||
|
||||
abstract ImmutableList<String> tags();
|
||||
|
||||
abstract String summary();
|
||||
|
||||
abstract String explanation();
|
||||
|
||||
abstract SeverityLevel severityLevel();
|
||||
|
||||
abstract boolean canDisable();
|
||||
|
||||
abstract ImmutableList<String> suppressionAnnotations();
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static java.util.function.Predicate.not;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
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.MethodInvocationTree;
|
||||
import com.sun.source.util.TreeScanner;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.TestCases;
|
||||
|
||||
/**
|
||||
* An {@link Extractor} that describes how to extract data from classes that test a {@code
|
||||
* BugChecker}.
|
||||
*/
|
||||
// XXX: Handle other methods from `{BugCheckerRefactoring,Compilation}TestHelper`:
|
||||
// - Indicate which custom arguments are specified, if any.
|
||||
// - For replacement tests, indicate which `FixChooser` is used.
|
||||
// - ... (We don't use all optional features; TBD what else to support.)
|
||||
@Immutable
|
||||
@AutoService(Extractor.class)
|
||||
@SuppressWarnings("rawtypes" /* See https://github.com/google/auto/issues/870. */)
|
||||
public final class BugPatternTestExtractor implements Extractor<TestCases> {
|
||||
/** Instantiates a new {@link BugPatternTestExtractor} instance. */
|
||||
public BugPatternTestExtractor() {}
|
||||
|
||||
@Override
|
||||
public String identifier() {
|
||||
return "bugpattern-test";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<TestCases> tryExtract(ClassTree tree, VisitorState state) {
|
||||
BugPatternTestCollector collector = new BugPatternTestCollector();
|
||||
|
||||
collector.scan(tree, state);
|
||||
|
||||
return Optional.of(collector.getCollectedTests())
|
||||
.filter(not(ImmutableList::isEmpty))
|
||||
.map(
|
||||
tests ->
|
||||
new AutoValue_BugPatternTestExtractor_TestCases(
|
||||
state.getPath().getCompilationUnit().getSourceFile().toUri(),
|
||||
ASTHelpers.getSymbol(tree).className(),
|
||||
tests));
|
||||
}
|
||||
|
||||
private static final class BugPatternTestCollector
|
||||
extends TreeScanner<@Nullable Void, VisitorState> {
|
||||
private static final Matcher<ExpressionTree> COMPILATION_HELPER_DO_TEST =
|
||||
instanceMethod()
|
||||
.onDescendantOf("com.google.errorprone.CompilationTestHelper")
|
||||
.named("doTest");
|
||||
private static final Matcher<ExpressionTree> TEST_HELPER_NEW_INSTANCE =
|
||||
staticMethod()
|
||||
.onDescendantOfAny(
|
||||
"com.google.errorprone.CompilationTestHelper",
|
||||
"com.google.errorprone.BugCheckerRefactoringTestHelper")
|
||||
.named("newInstance")
|
||||
.withParameters(Class.class.getCanonicalName(), Class.class.getCanonicalName());
|
||||
private static final Matcher<ExpressionTree> IDENTIFICATION_SOURCE_LINES =
|
||||
instanceMethod()
|
||||
.onDescendantOf("com.google.errorprone.CompilationTestHelper")
|
||||
.named("addSourceLines");
|
||||
private static final Matcher<ExpressionTree> REPLACEMENT_DO_TEST =
|
||||
instanceMethod()
|
||||
.onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper")
|
||||
.named("doTest");
|
||||
private static final Matcher<ExpressionTree> REPLACEMENT_EXPECT_UNCHANGED =
|
||||
instanceMethod()
|
||||
.onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper.ExpectOutput")
|
||||
.named("expectUnchanged");
|
||||
private static final Matcher<ExpressionTree> REPLACEMENT_OUTPUT_SOURCE_LINES =
|
||||
instanceMethod()
|
||||
.onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper.ExpectOutput")
|
||||
.namedAnyOf("addOutputLines", "expectUnchanged");
|
||||
|
||||
private final List<TestCase> collectedTestCases = new ArrayList<>();
|
||||
|
||||
private ImmutableList<TestCase> getCollectedTests() {
|
||||
return ImmutableList.copyOf(collectedTestCases);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Void visitMethodInvocation(MethodInvocationTree node, VisitorState state) {
|
||||
boolean isReplacementTest = REPLACEMENT_DO_TEST.matches(node, state);
|
||||
if (isReplacementTest || COMPILATION_HELPER_DO_TEST.matches(node, state)) {
|
||||
getClassUnderTest(node, state)
|
||||
.ifPresent(
|
||||
classUnderTest -> {
|
||||
List<TestEntry> entries = new ArrayList<>();
|
||||
if (isReplacementTest) {
|
||||
extractReplacementTestCases(node, entries, state);
|
||||
} else {
|
||||
extractIdentificationTestCases(node, entries, state);
|
||||
}
|
||||
|
||||
if (!entries.isEmpty()) {
|
||||
collectedTestCases.add(
|
||||
new AutoValue_BugPatternTestExtractor_TestCase(
|
||||
classUnderTest, ImmutableList.copyOf(entries).reverse()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return super.visitMethodInvocation(node, state);
|
||||
}
|
||||
|
||||
private static Optional<String> getClassUnderTest(
|
||||
MethodInvocationTree tree, VisitorState state) {
|
||||
if (TEST_HELPER_NEW_INSTANCE.matches(tree, state)) {
|
||||
return Optional.ofNullable(ASTHelpers.getSymbol(tree.getArguments().get(0)))
|
||||
.filter(s -> !s.type.allparams().isEmpty())
|
||||
.map(s -> s.type.allparams().get(0).tsym.getQualifiedName().toString());
|
||||
}
|
||||
|
||||
ExpressionTree receiver = ASTHelpers.getReceiver(tree);
|
||||
return receiver instanceof MethodInvocationTree methodInvocation
|
||||
? getClassUnderTest(methodInvocation, state)
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
private static void extractIdentificationTestCases(
|
||||
MethodInvocationTree tree, List<TestEntry> sink, VisitorState state) {
|
||||
if (IDENTIFICATION_SOURCE_LINES.matches(tree, state)) {
|
||||
String path = ASTHelpers.constValue(tree.getArguments().get(0), String.class);
|
||||
Optional<String> sourceCode =
|
||||
getSourceCode(tree).filter(s -> s.contains("// BUG: Diagnostic"));
|
||||
if (path != null && sourceCode.isPresent()) {
|
||||
sink.add(
|
||||
new AutoValue_BugPatternTestExtractor_IdentificationTestEntry(
|
||||
path, sourceCode.orElseThrow()));
|
||||
}
|
||||
}
|
||||
|
||||
ExpressionTree receiver = ASTHelpers.getReceiver(tree);
|
||||
if (receiver instanceof MethodInvocationTree methodInvocation) {
|
||||
extractIdentificationTestCases(methodInvocation, sink, state);
|
||||
}
|
||||
}
|
||||
|
||||
private static void extractReplacementTestCases(
|
||||
MethodInvocationTree tree, List<TestEntry> sink, VisitorState state) {
|
||||
if (REPLACEMENT_OUTPUT_SOURCE_LINES.matches(tree, state)) {
|
||||
/*
|
||||
* Retrieve the method invocation that contains the input source code. Note that this cast
|
||||
* is safe, because this code is guarded by an earlier call to `#getClassUnderTest(..)`,
|
||||
* which ensures that `tree` is part of a longer method invocation chain.
|
||||
*/
|
||||
MethodInvocationTree inputTree = (MethodInvocationTree) ASTHelpers.getReceiver(tree);
|
||||
|
||||
String path = ASTHelpers.constValue(inputTree.getArguments().get(0), String.class);
|
||||
Optional<String> inputCode = getSourceCode(inputTree);
|
||||
if (path != null && inputCode.isPresent()) {
|
||||
Optional<String> outputCode =
|
||||
REPLACEMENT_EXPECT_UNCHANGED.matches(tree, state) ? inputCode : getSourceCode(tree);
|
||||
|
||||
if (outputCode.isPresent() && !inputCode.equals(outputCode)) {
|
||||
sink.add(
|
||||
new AutoValue_BugPatternTestExtractor_ReplacementTestEntry(
|
||||
path, inputCode.orElseThrow(), outputCode.orElseThrow()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExpressionTree receiver = ASTHelpers.getReceiver(tree);
|
||||
if (receiver instanceof MethodInvocationTree methodInvocation) {
|
||||
extractReplacementTestCases(methodInvocation, sink, state);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: This logic is duplicated in `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());
|
||||
}
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
@JsonDeserialize(as = AutoValue_BugPatternTestExtractor_TestCases.class)
|
||||
abstract static class TestCases {
|
||||
static TestCases create(URI source, String testClass, ImmutableList<TestCase> testCases) {
|
||||
return new AutoValue_BugPatternTestExtractor_TestCases(source, testClass, testCases);
|
||||
}
|
||||
|
||||
abstract URI source();
|
||||
|
||||
abstract String testClass();
|
||||
|
||||
abstract ImmutableList<TestCase> testCases();
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
@JsonDeserialize(as = AutoValue_BugPatternTestExtractor_TestCase.class)
|
||||
abstract static class TestCase {
|
||||
static TestCase create(String classUnderTest, ImmutableList<TestEntry> entries) {
|
||||
return new AutoValue_BugPatternTestExtractor_TestCase(classUnderTest, entries);
|
||||
}
|
||||
|
||||
abstract String classUnderTest();
|
||||
|
||||
abstract ImmutableList<TestEntry> entries();
|
||||
}
|
||||
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(AutoValue_BugPatternTestExtractor_IdentificationTestEntry.class),
|
||||
@JsonSubTypes.Type(AutoValue_BugPatternTestExtractor_ReplacementTestEntry.class)
|
||||
})
|
||||
@JsonTypeInfo(include = As.EXISTING_PROPERTY, property = "type", use = JsonTypeInfo.Id.DEDUCTION)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonPropertyOrder("type")
|
||||
interface TestEntry {
|
||||
TestType type();
|
||||
|
||||
String path();
|
||||
|
||||
enum TestType {
|
||||
IDENTIFICATION,
|
||||
REPLACEMENT
|
||||
}
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class IdentificationTestEntry implements TestEntry {
|
||||
static IdentificationTestEntry create(String path, String code) {
|
||||
return new AutoValue_BugPatternTestExtractor_IdentificationTestEntry(path, code);
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
@Override
|
||||
public final TestType type() {
|
||||
return TestType.IDENTIFICATION;
|
||||
}
|
||||
|
||||
abstract String code();
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class ReplacementTestEntry implements TestEntry {
|
||||
static ReplacementTestEntry create(String path, String input, String output) {
|
||||
return new AutoValue_BugPatternTestExtractor_ReplacementTestEntry(path, input, output);
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
@Override
|
||||
public final TestType type() {
|
||||
return TestType.REPLACEMENT;
|
||||
}
|
||||
|
||||
abstract String input();
|
||||
|
||||
abstract String output();
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.sun.source.util.JavacTask;
|
||||
import com.sun.source.util.Plugin;
|
||||
import com.sun.tools.javac.api.BasicJavacTask;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A compiler {@link Plugin} that analyzes and extracts relevant information for documentation
|
||||
* purposes from processed files.
|
||||
*/
|
||||
// XXX: Find a better name for this class; it doesn't generate documentation per se.
|
||||
@AutoService(Plugin.class)
|
||||
public final class DocumentationGenerator implements Plugin {
|
||||
@VisibleForTesting static final String OUTPUT_DIRECTORY_FLAG = "-XoutputDirectory";
|
||||
private static final Pattern OUTPUT_DIRECTORY_FLAG_PATTERN =
|
||||
Pattern.compile(Pattern.quote(OUTPUT_DIRECTORY_FLAG) + "=(.*)");
|
||||
|
||||
/** Instantiates a new {@link DocumentationGenerator} instance. */
|
||||
public DocumentationGenerator() {}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(JavacTask javacTask, String... args) {
|
||||
checkArgument(args.length == 1, "Precisely one path must be provided");
|
||||
|
||||
javacTask.addTaskListener(
|
||||
new DocumentationGeneratorTaskListener(
|
||||
((BasicJavacTask) javacTask).getContext(), getOutputPath(args[0])));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static Path getOutputPath(String pathArg) {
|
||||
Matcher matcher = OUTPUT_DIRECTORY_FLAG_PATTERN.matcher(pathArg);
|
||||
checkArgument(
|
||||
matcher.matches(), "'%s' must be of the form '%s=<value>'", pathArg, OUTPUT_DIRECTORY_FLAG);
|
||||
|
||||
String path = matcher.group(1);
|
||||
try {
|
||||
return Path.of(path);
|
||||
} catch (InvalidPathException e) {
|
||||
throw new IllegalArgumentException(String.format("Invalid path '%s'", path), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
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.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ServiceLoader;
|
||||
import javax.tools.JavaFileObject;
|
||||
|
||||
/**
|
||||
* A {@link TaskListener} that identifies and extracts relevant content for documentation generation
|
||||
* and writes it to disk.
|
||||
*/
|
||||
// XXX: Find a better name for this class; it doesn't generate documentation per se.
|
||||
final class DocumentationGeneratorTaskListener implements TaskListener {
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private static final ImmutableList<Extractor<?>> EXTRACTORS =
|
||||
(ImmutableList)
|
||||
ImmutableList.copyOf(
|
||||
ServiceLoader.load(
|
||||
Extractor.class, DocumentationGeneratorTaskListener.class.getClassLoader()));
|
||||
|
||||
private final Context context;
|
||||
private final Path docsPath;
|
||||
|
||||
DocumentationGeneratorTaskListener(Context context, Path path) {
|
||||
this.context = context;
|
||||
this.docsPath = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void started(TaskEvent taskEvent) {
|
||||
if (taskEvent.getKind() == Kind.ANALYZE) {
|
||||
createDocsDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finished(TaskEvent taskEvent) {
|
||||
if (taskEvent.getKind() != Kind.ANALYZE) {
|
||||
return;
|
||||
}
|
||||
|
||||
JavaFileObject sourceFile = taskEvent.getSourceFile();
|
||||
CompilationUnitTree compilationUnit = taskEvent.getCompilationUnit();
|
||||
ClassTree classTree = JavacTrees.instance(context).getTree(taskEvent.getTypeElement());
|
||||
if (sourceFile == null || compilationUnit == null || classTree == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
VisitorState state =
|
||||
VisitorState.createForUtilityPurposes(context)
|
||||
.withPath(new TreePath(new TreePath(compilationUnit), classTree));
|
||||
|
||||
for (Extractor<?> extractor : EXTRACTORS) {
|
||||
extractor
|
||||
.tryExtract(classTree, state)
|
||||
.ifPresent(
|
||||
data ->
|
||||
writeToFile(
|
||||
extractor.identifier(), getSimpleClassName(sourceFile.toUri()), data));
|
||||
}
|
||||
}
|
||||
|
||||
private void createDocsDirectory() {
|
||||
try {
|
||||
Files.createDirectories(docsPath);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Error while creating directory with path '%s'", docsPath), e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> void writeToFile(String identifier, String className, T data) {
|
||||
Json.write(docsPath.resolve(String.format("%s-%s.json", identifier, className)), data);
|
||||
}
|
||||
|
||||
private static String getSimpleClassName(URI path) {
|
||||
return Path.of(path).getFileName().toString().replace(".java", "");
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Interface implemented by classes that define how to extract data of some type {@link T} from a
|
||||
* given {@link ClassTree}.
|
||||
*
|
||||
* @param <T> The type of data that is extracted.
|
||||
*/
|
||||
@Immutable
|
||||
interface Extractor<T> {
|
||||
/**
|
||||
* Returns the unique identifier of this extractor.
|
||||
*
|
||||
* @return A non-{@code null} string.
|
||||
*/
|
||||
String identifier();
|
||||
|
||||
/**
|
||||
* Attempts to extract an instance of type {@link T} using the provided arguments.
|
||||
*
|
||||
* @param tree The {@link ClassTree} to analyze and from which to extract an instance of type
|
||||
* {@link T}.
|
||||
* @param state A {@link VisitorState} describing the context in which the given {@link ClassTree}
|
||||
* is found.
|
||||
* @return An instance of type {@link T}, if possible.
|
||||
*/
|
||||
Optional<T> tryExtract(ClassTree tree, VisitorState state);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.guava.GuavaModule;
|
||||
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
|
||||
import com.google.errorprone.annotations.FormatMethod;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Utility class that offers mutually consistent JSON serialization and deserialization operations,
|
||||
* without further specifying the exact schema used.
|
||||
*/
|
||||
final class Json {
|
||||
private static final ObjectMapper OBJECT_MAPPER =
|
||||
new ObjectMapper()
|
||||
.setVisibility(PropertyAccessor.FIELD, Visibility.ANY)
|
||||
.registerModules(new GuavaModule(), new ParameterNamesModule());
|
||||
|
||||
private Json() {}
|
||||
|
||||
static <T> T read(Path path, Class<T> clazz) {
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(path.toFile(), clazz);
|
||||
} catch (IOException e) {
|
||||
throw failure(e, "Failure reading from '%s'", path);
|
||||
}
|
||||
}
|
||||
|
||||
static <T> void write(Path path, T object) {
|
||||
try {
|
||||
OBJECT_MAPPER.writeValue(path.toFile(), object);
|
||||
} catch (IOException e) {
|
||||
throw failure(e, "Failure writing to '%s'", path);
|
||||
}
|
||||
}
|
||||
|
||||
@FormatMethod
|
||||
private static UncheckedIOException failure(IOException cause, String format, Object... args) {
|
||||
return new UncheckedIOException(String.format(format, args), cause);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* A Java compiler plugin that extracts data from compiled classes, in support of the Error Prone
|
||||
* Support documentation.
|
||||
*/
|
||||
@com.google.errorprone.annotations.CheckReturnValue
|
||||
@org.jspecify.annotations.NullMarked
|
||||
package tech.picnic.errorprone.documentation;
|
||||
@@ -1,142 +0,0 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import tech.picnic.errorprone.documentation.BugPatternExtractor.BugPatternDocumentation;
|
||||
|
||||
final class BugPatternExtractorTest {
|
||||
@Test
|
||||
void noBugPattern(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerWithoutAnnotation.java",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"public final class TestCheckerWithoutAnnotation extends BugChecker {}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void minimalBugPattern(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"MinimalBugChecker.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.BugPattern;",
|
||||
"import com.google.errorprone.BugPattern.SeverityLevel;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"@BugPattern(summary = \"MinimalBugChecker summary\", severity = SeverityLevel.ERROR)",
|
||||
"public final class MinimalBugChecker extends BugChecker {}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"MinimalBugChecker",
|
||||
BugPatternDocumentation.create(
|
||||
URI.create("file:///MinimalBugChecker.java"),
|
||||
"pkg.MinimalBugChecker",
|
||||
"MinimalBugChecker",
|
||||
ImmutableList.of(),
|
||||
"",
|
||||
ImmutableList.of(),
|
||||
"MinimalBugChecker summary",
|
||||
"",
|
||||
ERROR,
|
||||
/* canDisable= */ true,
|
||||
ImmutableList.of(SuppressWarnings.class.getCanonicalName())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void completeBugPattern(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"CompleteBugChecker.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.BugPattern;",
|
||||
"import com.google.errorprone.BugPattern.SeverityLevel;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import org.junit.jupiter.api.Test;",
|
||||
"",
|
||||
"@BugPattern(",
|
||||
" name = \"OtherName\",",
|
||||
" summary = \"CompleteBugChecker summary\",",
|
||||
" linkType = BugPattern.LinkType.CUSTOM,",
|
||||
" link = \"https://error-prone.picnic.tech\",",
|
||||
" explanation = \"Example explanation\",",
|
||||
" severity = SeverityLevel.SUGGESTION,",
|
||||
" altNames = \"Check\",",
|
||||
" tags = BugPattern.StandardTags.SIMPLIFICATION,",
|
||||
" disableable = false,",
|
||||
" suppressionAnnotations = {BugPattern.class, Test.class})",
|
||||
"public final class CompleteBugChecker extends BugChecker {}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"CompleteBugChecker",
|
||||
BugPatternDocumentation.create(
|
||||
URI.create("file:///CompleteBugChecker.java"),
|
||||
"pkg.CompleteBugChecker",
|
||||
"OtherName",
|
||||
ImmutableList.of("Check"),
|
||||
"https://error-prone.picnic.tech",
|
||||
ImmutableList.of("Simplification"),
|
||||
"CompleteBugChecker summary",
|
||||
"Example explanation",
|
||||
SUGGESTION,
|
||||
/* canDisable= */ false,
|
||||
ImmutableList.of(BugPattern.class.getCanonicalName(), "org.junit.jupiter.api.Test")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void undocumentedSuppressionBugPattern(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"UndocumentedSuppressionBugPattern.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.BugPattern;",
|
||||
"import com.google.errorprone.BugPattern.SeverityLevel;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"@BugPattern(",
|
||||
" summary = \"UndocumentedSuppressionBugPattern summary\",",
|
||||
" severity = SeverityLevel.WARNING,",
|
||||
" documentSuppression = false)",
|
||||
"public final class UndocumentedSuppressionBugPattern extends BugChecker {}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"UndocumentedSuppressionBugPattern",
|
||||
BugPatternDocumentation.create(
|
||||
URI.create("file:///UndocumentedSuppressionBugPattern.java"),
|
||||
"pkg.UndocumentedSuppressionBugPattern",
|
||||
"UndocumentedSuppressionBugPattern",
|
||||
ImmutableList.of(),
|
||||
"",
|
||||
ImmutableList.of(),
|
||||
"UndocumentedSuppressionBugPattern summary",
|
||||
"",
|
||||
WARNING,
|
||||
/* canDisable= */ true,
|
||||
ImmutableList.of()));
|
||||
}
|
||||
|
||||
private static void verifyGeneratedFileContent(
|
||||
Path outputDirectory, String testClass, BugPatternDocumentation expected) {
|
||||
assertThat(outputDirectory.resolve(String.format("bugpattern-%s.json", testClass)))
|
||||
.exists()
|
||||
.returns(expected, path -> Json.read(path, BugPatternDocumentation.class));
|
||||
}
|
||||
}
|
||||
@@ -1,558 +0,0 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.IdentificationTestEntry;
|
||||
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.ReplacementTestEntry;
|
||||
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.TestCase;
|
||||
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.TestCases;
|
||||
|
||||
final class BugPatternTestExtractorTest {
|
||||
@Test
|
||||
void noTestClass(@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 noDoTestInvocation(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\");",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\");",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullBugCheckerInstance(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance((Class<BugChecker>) null, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance((Class<BugChecker>) null, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rawBugCheckerInstance(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" @SuppressWarnings(\"unchecked\")",
|
||||
" void m() {",
|
||||
" @SuppressWarnings(\"rawtypes\")",
|
||||
" Class bugChecker = TestChecker.class;",
|
||||
"",
|
||||
" CompilationTestHelper.newInstance(bugChecker, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(bugChecker, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void scannerSupplierInstance(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import com.google.errorprone.scanner.ScannerSupplier;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(",
|
||||
" ScannerSupplier.fromBugCheckerClasses(TestChecker.class), getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(",
|
||||
" ScannerSupplier.fromBugCheckerClasses(TestChecker.class), getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonCompileTimeConstantStrings(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(toString() + \"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .addSourceLines(\"B.java\", \"// BUG: Diagnostic contains:\", \"class B {}\", toString())",
|
||||
" .doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(toString() + \"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .addInputLines(\"B.java\", \"class B {}\", toString())",
|
||||
" .addOutputLines(\"B.java\", \"class B { /* This is a change. */ }\")",
|
||||
" .addInputLines(\"C.java\", \"class C {}\")",
|
||||
" .addOutputLines(\"C.java\", \"class C { /* This is a change. */ }\", toString())",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonFluentTestHelperExpressions(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper testHelper =",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"class A {}\");",
|
||||
" testHelper.doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.ExpectOutput expectedOutput =",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\");",
|
||||
" expectedOutput.addOutputLines(\"A.java\", \"class A {}\").doTest();",
|
||||
" expectedOutput.expectUnchanged().doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noSource(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass()).doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass()).doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noDiagnostics(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A {}\")",
|
||||
" .addInputLines(\"B.java\", \"class B {}\")",
|
||||
" .expectUnchanged()",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleFileCompilationTestHelper(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"SingleFileCompilationTestHelperTest.java",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class SingleFileCompilationTestHelperTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"SingleFileCompilationTestHelperTest",
|
||||
TestCases.create(
|
||||
URI.create("file:///SingleFileCompilationTestHelperTest.java"),
|
||||
"SingleFileCompilationTestHelperTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"SingleFileCompilationTestHelperTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
IdentificationTestEntry.create(
|
||||
"A.java", "// BUG: Diagnostic contains:\nclass A {}\n"))))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleFileCompilationTestHelperWithSetArgs(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"SingleFileCompilationTestHelperWithSetArgsTest.java",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class SingleFileCompilationTestHelperWithSetArgsTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .setArgs(\"-XepAllSuggestionsAsWarnings\")",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"SingleFileCompilationTestHelperWithSetArgsTest",
|
||||
TestCases.create(
|
||||
URI.create("file:///SingleFileCompilationTestHelperWithSetArgsTest.java"),
|
||||
"SingleFileCompilationTestHelperWithSetArgsTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"SingleFileCompilationTestHelperWithSetArgsTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
IdentificationTestEntry.create(
|
||||
"A.java", "// BUG: Diagnostic contains:\nclass A {}\n"))))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiFileCompilationTestHelper(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"MultiFileCompilationTestHelperTest.java",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class MultiFileCompilationTestHelperTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .addSourceLines(\"B.java\", \"// BUG: Diagnostic contains:\", \"class B {}\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"MultiFileCompilationTestHelperTest",
|
||||
TestCases.create(
|
||||
URI.create("file:///MultiFileCompilationTestHelperTest.java"),
|
||||
"MultiFileCompilationTestHelperTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"MultiFileCompilationTestHelperTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
IdentificationTestEntry.create(
|
||||
"A.java", "// BUG: Diagnostic contains:\nclass A {}\n"),
|
||||
IdentificationTestEntry.create(
|
||||
"B.java", "// BUG: Diagnostic contains:\nclass B {}\n"))))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleFileBugCheckerRefactoringTestHelper(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"SingleFileBugCheckerRefactoringTestHelperTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class SingleFileBugCheckerRefactoringTestHelperTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"SingleFileBugCheckerRefactoringTestHelperTest",
|
||||
TestCases.create(
|
||||
URI.create("file:///SingleFileBugCheckerRefactoringTestHelperTest.java"),
|
||||
"SingleFileBugCheckerRefactoringTestHelperTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"SingleFileBugCheckerRefactoringTestHelperTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
ReplacementTestEntry.create(
|
||||
"A.java", "class A {}\n", "class A { /* This is a change. */ }\n"))))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestMode(
|
||||
@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers;",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .setArgs(\"-XepAllSuggestionsAsWarnings\")",
|
||||
" .setFixChooser(FixChoosers.SECOND)",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .doTest(TestMode.TEXT_MATCH);",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest",
|
||||
TestCases.create(
|
||||
URI.create(
|
||||
"file:///SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest.java"),
|
||||
"SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
ReplacementTestEntry.create(
|
||||
"A.java", "class A {}\n", "class A { /* This is a change. */ }\n"))))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiFileBugCheckerRefactoringTestHelper(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"MultiFileBugCheckerRefactoringTestHelperTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class MultiFileBugCheckerRefactoringTestHelperTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .addInputLines(\"B.java\", \"class B {}\")",
|
||||
" .addOutputLines(\"B.java\", \"class B { /* This is a change. */ }\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"MultiFileBugCheckerRefactoringTestHelperTest",
|
||||
TestCases.create(
|
||||
URI.create("file:///MultiFileBugCheckerRefactoringTestHelperTest.java"),
|
||||
"MultiFileBugCheckerRefactoringTestHelperTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"MultiFileBugCheckerRefactoringTestHelperTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
ReplacementTestEntry.create(
|
||||
"A.java", "class A {}\n", "class A { /* This is a change. */ }\n"),
|
||||
ReplacementTestEntry.create(
|
||||
"B.java", "class B {}\n", "class B { /* This is a change. */ }\n"))))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void compilationAndBugCheckerRefactoringTestHelpers(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"CompilationAndBugCheckerRefactoringTestHelpersTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class CompilationAndBugCheckerRefactoringTestHelpersTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"CompilationAndBugCheckerRefactoringTestHelpersTest",
|
||||
TestCases.create(
|
||||
URI.create("file:///CompilationAndBugCheckerRefactoringTestHelpersTest.java"),
|
||||
"CompilationAndBugCheckerRefactoringTestHelpersTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"CompilationAndBugCheckerRefactoringTestHelpersTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
IdentificationTestEntry.create(
|
||||
"A.java", "// BUG: Diagnostic contains:\nclass A {}\n"))),
|
||||
TestCase.create(
|
||||
"CompilationAndBugCheckerRefactoringTestHelpersTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
ReplacementTestEntry.create(
|
||||
"A.java", "class A {}\n", "class A { /* This is a change. */ }\n"))))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void compilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNames(
|
||||
@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest {",
|
||||
" private static class CustomTestChecker extends BugChecker {}",
|
||||
"",
|
||||
" private static class CustomTestChecker2 extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(CustomTestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(CustomTestChecker2.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest",
|
||||
TestCases.create(
|
||||
URI.create(
|
||||
"file:///CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest.java"),
|
||||
"pkg.CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"pkg.CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest.CustomTestChecker",
|
||||
ImmutableList.of(
|
||||
IdentificationTestEntry.create(
|
||||
"A.java", "// BUG: Diagnostic contains:\nclass A {}\n"))),
|
||||
TestCase.create(
|
||||
"pkg.CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest.CustomTestChecker2",
|
||||
ImmutableList.of(
|
||||
ReplacementTestEntry.create(
|
||||
"A.java", "class A {}\n", "class A { /* This is a change. */ }\n"))))));
|
||||
}
|
||||
|
||||
private static void verifyGeneratedFileContent(
|
||||
Path outputDirectory, String testClass, TestCases expected) {
|
||||
assertThat(outputDirectory.resolve(String.format("bugpattern-test-%s.json", testClass)))
|
||||
.exists()
|
||||
.returns(expected, path -> Json.read(path, TestCases.class));
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.FileManagers;
|
||||
import com.google.errorprone.FileObjects;
|
||||
import com.sun.tools.javac.api.JavacTaskImpl;
|
||||
import com.sun.tools.javac.api.JavacTool;
|
||||
import com.sun.tools.javac.file.JavacFileManager;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.tools.Diagnostic;
|
||||
import javax.tools.JavaCompiler;
|
||||
import javax.tools.JavaFileObject;
|
||||
|
||||
// XXX: Generalize and move this class so that it can also be used by `refaster-compiler`.
|
||||
// XXX: This class is supported by the `ErrorProneTestHelperSourceFormat` check, but until that
|
||||
// support is covered by unit tests, make sure to update that logic if this class or its methods are
|
||||
// moved/renamed.
|
||||
public final class Compilation {
|
||||
private Compilation() {}
|
||||
|
||||
public static void compileWithDocumentationGenerator(
|
||||
Path outputDirectory, String path, String... lines) {
|
||||
compileWithDocumentationGenerator(outputDirectory.toAbsolutePath().toString(), path, lines);
|
||||
}
|
||||
|
||||
public static void compileWithDocumentationGenerator(
|
||||
String outputDirectory, String path, String... lines) {
|
||||
/*
|
||||
* The compiler options specified here largely match those used by Error Prone's
|
||||
* `CompilationTestHelper`. A key difference is the stricter linting configuration. When
|
||||
* compiling using JDK 21+, these lint options also require that certain JDK modules are
|
||||
* explicitly exported.
|
||||
*/
|
||||
compile(
|
||||
ImmutableList.of(
|
||||
"--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
|
||||
"--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
|
||||
"--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
|
||||
"-encoding",
|
||||
"UTF-8",
|
||||
"-parameters",
|
||||
"-proc:none",
|
||||
"-Werror",
|
||||
"-Xlint:all,-serial",
|
||||
"-Xplugin:DocumentationGenerator -XoutputDirectory=" + outputDirectory,
|
||||
"-XDdev",
|
||||
"-XDcompilePolicy=simple"),
|
||||
FileObjects.forSourceLines(path, lines));
|
||||
}
|
||||
|
||||
private static void compile(ImmutableList<String> options, JavaFileObject javaFileObject) {
|
||||
JavacFileManager javacFileManager = FileManagers.testFileManager();
|
||||
JavaCompiler compiler = JavacTool.create();
|
||||
|
||||
List<Diagnostic<?>> diagnostics = new ArrayList<>();
|
||||
JavacTaskImpl task =
|
||||
(JavacTaskImpl)
|
||||
compiler.getTask(
|
||||
null,
|
||||
javacFileManager,
|
||||
diagnostics::add,
|
||||
options,
|
||||
ImmutableList.of(),
|
||||
ImmutableList.of(javaFileObject));
|
||||
|
||||
Boolean result = task.call();
|
||||
assertThat(diagnostics).isEmpty();
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.nio.file.attribute.AclEntryPermission.ADD_SUBDIRECTORY;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.condition.OS.WINDOWS;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystemException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.AclEntry;
|
||||
import java.nio.file.attribute.AclFileAttributeView;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.EnabledOnOs;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
final class DocumentationGeneratorTaskListenerTest {
|
||||
@EnabledOnOs(WINDOWS)
|
||||
@Test
|
||||
void readOnlyFileSystemWindows(@TempDir Path outputDirectory) throws IOException {
|
||||
AclFileAttributeView view =
|
||||
Files.getFileAttributeView(outputDirectory, AclFileAttributeView.class);
|
||||
view.setAcl(
|
||||
view.getAcl().stream()
|
||||
.map(
|
||||
entry ->
|
||||
AclEntry.newBuilder(entry)
|
||||
.setPermissions(
|
||||
Sets.difference(entry.permissions(), ImmutableSet.of(ADD_SUBDIRECTORY)))
|
||||
.build())
|
||||
.collect(toImmutableList()));
|
||||
|
||||
readOnlyFileSystemFailsToWrite(outputDirectory.resolve("nonexistent"));
|
||||
}
|
||||
|
||||
@DisabledOnOs(WINDOWS)
|
||||
@Test
|
||||
void readOnlyFileSystemNonWindows(@TempDir Path outputDirectory) {
|
||||
assertThat(outputDirectory.toFile().setWritable(false))
|
||||
.describedAs("Failed to make test directory unwritable")
|
||||
.isTrue();
|
||||
|
||||
readOnlyFileSystemFailsToWrite(outputDirectory.resolve("nonexistent"));
|
||||
}
|
||||
|
||||
private static void readOnlyFileSystemFailsToWrite(Path outputDirectory) {
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory, "A.java", "class A {}"))
|
||||
.hasRootCauseInstanceOf(FileSystemException.class)
|
||||
.hasCauseInstanceOf(IllegalStateException.class)
|
||||
.hasMessageEndingWith("Error while creating directory with path '%s'", outputDirectory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noClassNoOutput(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(outputDirectory, "A.java", "package pkg;");
|
||||
|
||||
assertThat(outputDirectory).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void excessArguments(@TempDir Path outputDirectory) {
|
||||
String actualOutputDirectory = outputDirectory.toAbsolutePath() + " extra-arg";
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
actualOutputDirectory, "A.java", "package pkg;"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Precisely one path must be provided");
|
||||
}
|
||||
|
||||
@Test
|
||||
void extraction(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"DocumentationGeneratorTaskListenerTestClass.java",
|
||||
"class DocumentationGeneratorTaskListenerTestClass {}");
|
||||
|
||||
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)
|
||||
@SuppressWarnings("rawtypes" /* See https://github.com/google/auto/issues/870. */)
|
||||
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 clazz)
|
||||
? String.join(": ", String.valueOf(tree.getKind()), clazz.getSimpleName())
|
||||
: tree.getKind().toString();
|
||||
}
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class ExtractionParameters {
|
||||
abstract String className();
|
||||
|
||||
abstract ImmutableList<String> path();
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static tech.picnic.errorprone.documentation.DocumentationGenerator.OUTPUT_DIRECTORY_FLAG;
|
||||
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
final class DocumentationGeneratorTest {
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"bar", "foo"})
|
||||
void getOutputPath(String path) {
|
||||
assertThat(DocumentationGenerator.getOutputPath(OUTPUT_DIRECTORY_FLAG + '=' + path))
|
||||
.isEqualTo(Path.of(path));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"", "-XoutputDirectory", "invalidOption=Test", "nothing"})
|
||||
void getOutputPathWithInvalidArgument(String pathArg) {
|
||||
assertThatThrownBy(() -> DocumentationGenerator.getOutputPath(pathArg))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("'%s' must be of the form '%s=<value>'", pathArg, OUTPUT_DIRECTORY_FLAG);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOutputPathWithInvalidPath() {
|
||||
String basePath = "path-with-null-char-\0";
|
||||
assertThatThrownBy(
|
||||
() -> DocumentationGenerator.getOutputPath(OUTPUT_DIRECTORY_FLAG + '=' + basePath))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasCauseInstanceOf(InvalidPathException.class)
|
||||
.hasMessageEndingWith("Invalid path '%s'", basePath);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
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.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
final class JsonTest {
|
||||
private static final TestObject TEST_OBJECT = new AutoValue_JsonTest_TestObject("foo", 42);
|
||||
private static final String TEST_JSON = "{\"string\":\"foo\",\"number\":42}";
|
||||
|
||||
@Test
|
||||
void write(@TempDir Path directory) {
|
||||
Path file = directory.resolve("test.json");
|
||||
|
||||
Json.write(file, TEST_OBJECT);
|
||||
|
||||
assertThat(file).content(UTF_8).isEqualTo(TEST_JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeFailure(@TempDir Path directory) {
|
||||
assertThatThrownBy(() -> Json.write(directory, TEST_OBJECT))
|
||||
.isInstanceOf(UncheckedIOException.class)
|
||||
.hasMessageContaining("Failure writing to '%s'", directory)
|
||||
.hasCauseInstanceOf(FileNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void read(@TempDir Path directory) throws IOException {
|
||||
Path file = directory.resolve("test.json");
|
||||
|
||||
Files.writeString(file, TEST_JSON, UTF_8);
|
||||
|
||||
assertThat(Json.read(file, TestObject.class)).isEqualTo(TEST_OBJECT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readFailure(@TempDir Path directory) {
|
||||
assertThatThrownBy(() -> Json.read(directory, TestObject.class))
|
||||
.isInstanceOf(UncheckedIOException.class)
|
||||
.hasMessageContaining("Failure reading from '%s'", directory)
|
||||
.hasCauseInstanceOf(FileNotFoundException.class);
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
@JsonDeserialize(as = AutoValue_JsonTest_TestObject.class)
|
||||
abstract static class TestObject {
|
||||
abstract String string();
|
||||
|
||||
abstract int number();
|
||||
}
|
||||
}
|
||||
@@ -11,21 +11,71 @@ request.
|
||||
|
||||
### Building
|
||||
|
||||
See the main [readme][main-readme].
|
||||
This is a [Maven][maven] project, so running `mvn clean install` performs a
|
||||
full clean build. Some relevant flags:
|
||||
- `-Dverification.warn` makes the warnings and errors emitted by various
|
||||
plugins and the Java compiler non-fatal, where possible.
|
||||
- `-Dverification.skip` disables various non-essential plugins and compiles the
|
||||
code with minimal checks (i.e. without linting, Error Prone checks, etc.)
|
||||
- `-Dversion.error-prone=some-version` runs the build using the specified
|
||||
version of Error Prone. This is useful e.g. when testing a locally built
|
||||
Error Prone SNAPSHOT.
|
||||
- `-Perror-prone-fork` run the build using Picnic's [Error Prone
|
||||
fork][error-prone-fork-repo], hosted on [Jitpack][error-prone-fork-jitpack].
|
||||
This fork generally contains a few changes on top of the latest Error Prone
|
||||
release.
|
||||
|
||||
Two other goals that one may find relevant:
|
||||
- `mvn fmt:format` formats the code using
|
||||
[`google-java-format`][google-java-format].
|
||||
- `mvn pitest:mutationCoverage` runs mutation tests using [PIT][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].
|
||||
|
||||
When loading the project in IntelliJ IDEA (and perhaps other IDEs) errors about
|
||||
the inaccessibility of `com.sun.tools.javac.*` classes may be reported. If this
|
||||
happens, configure your IDE to enable the `add-exports` profile.
|
||||
|
||||
### Contribution guidelines
|
||||
|
||||
See our [contributing guidelines][main-contributing].
|
||||
To the extend possible, the pull request process guards our coding guidelines.
|
||||
Some pointers:
|
||||
- Checks should we _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
|
||||
it must not "get in the way". So for a check which addresses a relatively
|
||||
trivial stylistic concern it is doubly important that the violations it
|
||||
detects can be auto-patched.
|
||||
- Make sure you have read Error Prone's [criteria for new
|
||||
checks][error-prone-criteria]. Most guidelines described there apply to this
|
||||
project as well, except that this project _does_ focus quite heavy on style
|
||||
enforcement. But that just makes the previous point doubly important.
|
||||
- Make sure that a check's (mutation) coverage is or remains about as high as
|
||||
it can be. Not only does this lead to better tests, it also points out
|
||||
opportunities to simplify the code.
|
||||
- Please restrict the scope of a pull request to a single feature or fix. Don't
|
||||
sneak in unrelated changes.
|
||||
- When in doubt about whether a pull request will be accepted, please first
|
||||
file an issue to discuss it.
|
||||
|
||||
### Our wishlist
|
||||
|
||||
We expect the following tasks to help improve the quality of this open source
|
||||
project:
|
||||
|
||||
- Publish the artifact to Maven Central, then document the coordinates in this
|
||||
`README.md`.
|
||||
- Document how to enable the checks.
|
||||
- Document how to apply patches.
|
||||
- Document each of the checks.
|
||||
- Add Travis CI, [SonarQube][sonarcloud] and [Codecov][codecov]
|
||||
integrations.
|
||||
- Investigate whether it makes sense to include license headers in each file.
|
||||
If so, set that up and enforce it.
|
||||
- Add non-Java file formatting support, like we have internally at Picnic.
|
||||
(I.e., somehow open-source that stuff.)
|
||||
- Add relevant "badges" at the top of this `README.md`.
|
||||
- Auto-generate a website listing each of the checks, just like the Error Prone
|
||||
[bug patterns page][error-prone-bug-patterns]. The [Error Prone
|
||||
repository][error-prone-repo] contains code for this.
|
||||
@@ -43,7 +93,7 @@ project:
|
||||
- Improve an existing check (see `XXX`-marked comments in the code) or write a
|
||||
new one (see the list of suggestions below).
|
||||
|
||||
### BugChecker extension ideas
|
||||
### Ideas for new checks
|
||||
|
||||
The following is a list of checks we'd like to see implemented:
|
||||
|
||||
@@ -53,7 +103,7 @@ The following is a list of checks we'd like to see implemented:
|
||||
signature groups. Using Error Prone's method matchers forbidden method calls
|
||||
can easily be identified. But Error Prone can go one step further by
|
||||
auto-patching violations. For each violation two fixes can be proposed: a
|
||||
purely behavior-preserving fix, which makes the platform-dependent behavior
|
||||
purely behavior-preserving fix which makes the platform-dependent behavior
|
||||
explicit, and another which replaces the platform-dependent behavior with the
|
||||
preferred alternative. (Such as using `UTF-8` instead of the system default
|
||||
charset.)
|
||||
@@ -63,125 +113,128 @@ The following is a list of checks we'd like to see implemented:
|
||||
functionality.
|
||||
- A subset of the refactor operations provided by the Eclipse-specific
|
||||
[AutoRefactor][autorefactor] plugin.
|
||||
- A check that replaces fully qualified types with simple types in contexts
|
||||
- A check which replaces fully qualified types with simple types in contexts
|
||||
where this does not introduce ambiguity. Should consider both actual Java
|
||||
code and Javadoc `@link` references.
|
||||
- A check that simplifies array expressions. It would replace empty array
|
||||
- A check which simplifies array expressions. It would replace empty array
|
||||
expressions of the form `new int[] {}` with `new int[0]`. Statements of the
|
||||
form `byte[] arr = new byte[] {'c'};` would be shortened to `byte[] arr =
|
||||
{'c'};`.
|
||||
- A check that replaces expressions of the form `String.format("some prefix
|
||||
- A check which replaces expressions of the form `String.format("some prefix
|
||||
%s", arg)` with `"some prefix " + arg`, and similar for simple suffixes. Can
|
||||
perhaps be generalized further, though it's unclear how far. (Well, a
|
||||
`String.format` call without arguments can certainly be simplified, too.)
|
||||
- A check that replaces single-character strings with `char`s where possible.
|
||||
- A check which replaces single-character strings with `char`s where possible.
|
||||
For example as argument to `StringBuilder.append` and in string
|
||||
concatenations.
|
||||
- A check that adds or removes the first `Locale` argument to `String.format`
|
||||
- A check which adds or removes the first `Locale` argument to `String.format`
|
||||
and similar calls as necessary. (For example, a format string containing only
|
||||
`%s` placeholders is locale-insensitive unless any of the arguments is a
|
||||
`Formattable`, while `%f` placeholders _are_ locale-sensitive.)
|
||||
- A check that replaces `String.replaceAll` with `String.replace` if the first
|
||||
- A check which replaces `String.replaceAll` with `String.replace` if the first
|
||||
argument is certainly not a regular expression. And if both arguments are
|
||||
single-character strings then the `(char, char)` overload can be invoked
|
||||
instead.
|
||||
- A check that flags (and ideally, replaces) `try-finally` constructs with
|
||||
- A check which flags (and ideally, replaces) `try-finally` constructs with
|
||||
equivalent `try-with-resources` constructs.
|
||||
- A check that drops exceptions declared in `throws` clauses if they are (a)
|
||||
- A check which drops exceptions declared in `throws` clauses if they are (a)
|
||||
not actually thrown and (b) the associated method cannot be overridden.
|
||||
- A check that tries to statically import certain methods whenever used, if
|
||||
- A check which tries to statically import certain methods whenever used, if
|
||||
possible. The set of targeted methods should be configurable, but may default
|
||||
to e.g. `java.util.Function.identity()`, the static methods exposed by
|
||||
`java.util.stream.Collectors` and the various Guava collector factory
|
||||
methods.
|
||||
- A check that replaces `new Random().someMethod()` calls with
|
||||
`ThreadLocalRandom.current().someMethod()` calls, to avoid unnecessary
|
||||
- A check which replaces `new Random().someMethod()` calls with
|
||||
`ThreadLocalRandom.current().someMethod()` calls, so as to avoid unnecessary
|
||||
synchronization.
|
||||
- A check that drops `this.` from `this.someMethod()` calls and which
|
||||
- A check which drops `this.` from `this.someMethod()` calls and which
|
||||
optionally does the same for fields, if no ambiguity arises.
|
||||
- A check that replaces `Integer.valueOf` calls with `Integer.parseInt` or vice
|
||||
versa in order to prevent auto (un)boxing, and likewise for other number
|
||||
- A check which replaces `Integer.valueOf` calls with `Integer.parseInt` or
|
||||
vice versa in order to prevent auto (un)boxing, and likewise for other number
|
||||
types.
|
||||
- A check that flags nullable collections.
|
||||
- A check that flags `AutoCloseable` resources not managed by a
|
||||
- A check which flags nullable collections.
|
||||
- A check which flags `AutoCloseable` resources not managed by a
|
||||
`try-with-resources` construct. Certain subtypes, such as jOOQ's `DSLContext`
|
||||
should be excluded.
|
||||
- A check that flags `java.time` methods which implicitly consult the system
|
||||
- A check which flags `java.time` methods which implicitly consult the system
|
||||
clock, suggesting that a passed-in `Clock` is used instead.
|
||||
- A check that flags public methods on public classes which reference
|
||||
- A check which flags public methods on public classes which reference
|
||||
non-public types. This can cause `IllegalAccessError`s and
|
||||
`BootstrapMethodError`s at runtime.
|
||||
- A check that swaps the LHS and RHS in expressions of the form
|
||||
- A check which swaps the LHS and RHS in expressions of the form
|
||||
`nonConstant.equals(someNonNullConstant)`.
|
||||
- A check that annotates methods which only throw an exception with
|
||||
- A check which annotates methods which only throw an exception with
|
||||
`@Deprecated` or ` @DoNotCall`.
|
||||
- A check that flags imports from other test classes.
|
||||
- A Guava-specific check that replaces `Joiner.join` calls with `String.join`
|
||||
- A check which flags imports from other test classes.
|
||||
- A Guava-specific check which replaces `Joiner.join` calls with `String.join`
|
||||
calls in those cases where the latter is a proper substitute for the former.
|
||||
- A Guava-specific check that flags `{Immutable,}Multimap` type usages where
|
||||
`{Immutable,}{List,Set}Multimap` would be more appropriate.
|
||||
- A Guava-specific check that rewrites `if (conditional) { throw new
|
||||
- A Guava-specific check which flags `{Immutable,}Multimap` type usages
|
||||
where `{Immutable,}{List,Set}Multimap` would be more appropriate.
|
||||
- A Guava-specific check which rewrites `if (conditional) { throw new
|
||||
IllegalArgumentException(); }` and variants to an equivalent `checkArgument`
|
||||
statement. Idem for other exception types.
|
||||
- A Guava-specific check that replaces simple anonymous `CacheLoader` subclass
|
||||
- A Guava-specific check which replaces simple anonymous `CacheLoader` subclass
|
||||
declarations with `CacheLoader.from(someLambda)`.
|
||||
- A Spring-specific check that enforces that `@RequestMapping` annotations,
|
||||
- A Spring-specific check which enforces that methods with the `@Scheduled`
|
||||
annotation are also annotated with New Relic's `@Trace` annotation. Such
|
||||
methods should ideally not also represent Spring MVC endpoints.
|
||||
- A Spring-specific check which enforces that `@RequestMapping` annotations,
|
||||
when applied to a method, explicitly specify one or more target HTTP methods.
|
||||
- A Spring-specific check that looks for classes in which all `@RequestMapping`
|
||||
annotations (and the various aliases) specify the same `path`/`value`
|
||||
property and then moves that path to the class level.
|
||||
- A Spring-specific check that flags `@Value("some.property")` annotations, as
|
||||
- A Spring-specific check which looks for classes in which all
|
||||
`@RequestMapping` annotations (and the various aliases) specify the same
|
||||
`path`/`value` property and then moves that path to the class level.
|
||||
- A Spring-specific check which flags `@Value("some.property")` annotations, as
|
||||
these almost certainly should be `@Value("${some.property}")`.
|
||||
- A Spring-specific check that drops the `required` attribute from
|
||||
- A Spring-specific check which drops the `required` attribute from
|
||||
`@RequestParam` annotations when the `defaultValue` attribute is also
|
||||
specified.
|
||||
- A Spring-specific check that rewrites a class which uses field injection to
|
||||
- A Spring-specific check which rewrites a class which uses field injection to
|
||||
one which uses constructor injection. This check wouldn't be strictly
|
||||
behavior preserving, but could be used for a one-off code base migration.
|
||||
- A Spring-specific check that disallows field injection, except in
|
||||
- A Spring-specific check which disallows field injection, except in
|
||||
`AbstractTestNGSpringContextTests` subclasses. (One known edge case:
|
||||
self-injections so that a bean can call itself through an implicit proxy.)
|
||||
- A Spring-specific check that verifies that public methods on all classes
|
||||
- A Spring-specific check which verifies that public methods on all classes
|
||||
whose name matches a certain pattern, e.g. `.*Service`, are annotated
|
||||
`@Secured`.
|
||||
- A Spring-specific check that verifies that annotations such as
|
||||
- A Spring-specific check which verifies that annotations such as
|
||||
`@RequestParam` are only present in `@RestController` classes.
|
||||
- A Spring-specific check that disallows `@ResponseStatus` on MVC endpoint
|
||||
- A Spring-specific check which disallows `@ResponseStatus` on MVC endpoint
|
||||
methods, as this prevents communication of error status codes.
|
||||
- A Hibernate Validator-specific check that looks for `@UnwrapValidatedValue`
|
||||
- A Hibernate Validator-specific check which looks for `@UnwrapValidatedValue`
|
||||
usages and migrates the associated constraint annotations to the generic type
|
||||
argument to which they (are presumed to) apply.
|
||||
- A TestNG-specific check that drops method-level `@Test` annotations if a
|
||||
- A TestNG-specific check which drops method-level `@Test` annotations if a
|
||||
matching/more specific annotation is already present at the class level.
|
||||
- A TestNG-specific check that enforces that all tests are in a group.
|
||||
- A TestNG-specific check that flags field assignments in
|
||||
- A TestNG-specific check which enforces that all tests are in a group.
|
||||
- A TestNG-specific check which flags field assignments in
|
||||
`@BeforeMethod`-annotated methods unless the class is annotated
|
||||
`@Test(singleThreaded = true)`.
|
||||
- A TestNG-specific check that flags usages of the `expectedExceptions`
|
||||
- A TestNG-specific check which flags usages of the `expectedExceptions`
|
||||
attribute of the `@Test` annotation, pointing to `assertThrows`.
|
||||
- A Jongo-specific check that disallows the creation of sparse indices, in
|
||||
- A Jongo-specific check which disallows the creation of sparse indices, in
|
||||
favour of partial indices.
|
||||
- An Immutables-specific check that replaces
|
||||
- An Immutables-specific check which replaces
|
||||
`checkState`/`IllegalStateException` usages inside a `@Value.Check`-annotated
|
||||
method with `checkArgument`/`IllegalArgument`, since the method is invoked
|
||||
when a caller attempts to create an immutable instance.
|
||||
- An Immutables-specific check that disallows references to collection types
|
||||
- An Immutables-specific check which disallows references to collection types
|
||||
other than the Guava immutable collections, including inside generic type
|
||||
arguments.
|
||||
- An SLF4J-specific check that drops or adds a trailing dot from log messages,
|
||||
- An SLF4J-specific check which drops or adds a trailing dot from log messages,
|
||||
as applicable.
|
||||
- A Mockito-specific check that identifies sequences of statements which mock a
|
||||
significant number of methods on a single object with "default data"; such
|
||||
- A Mockito-specific check which identifies sequences of statements which mock
|
||||
a significant number of methods on a single object with "default data"; such
|
||||
constructions can often benefit from a different type of default answer, such
|
||||
as `Answers.RETURNS_MOCKS`.
|
||||
- An RxJava-specific check that flags `.toCompletable()` calls on expressions
|
||||
- An RxJava-specific check which flags `.toCompletable()` calls on expressions
|
||||
of type `Single<Completable>` etc., as most likely
|
||||
`.flatMapCompletable(Functions.identity())` was meant instead. Idem for other
|
||||
variations.
|
||||
- An RxJava-specific check that flags `expr.firstOrError()` calls and suggests
|
||||
- An RxJava-specific check which flags `expr.firstOrError()` calls and suggests
|
||||
`expr.switchIfEmpty(Single.error(...))`, so that an application-specific
|
||||
exception is thrown instead of `NoSuchElementException`.
|
||||
- An RxJava-specific check that flags use of `#assertValueSet` without
|
||||
- An RxJava-specific check which flags use of `#assertValueSet` without
|
||||
`#assertValueCount`, as the former method doesn't do what one may intuitively
|
||||
expect it to do. See ReactiveX/RxJava#6151.
|
||||
|
||||
@@ -194,7 +247,6 @@ but on the flip side Refaster is much less expressive. While this gap can never
|
||||
be fully closed, there are some ways in which Refaster's scope of utility could
|
||||
be extended. The following is a non-exhaustive list of ideas on how to extend
|
||||
Refaster's expressiveness:
|
||||
|
||||
- Allow more control over _which_ methods are statically imported by
|
||||
`@UseImportPolicy`. Sometimes the `@AfterTemplate` contains more than one
|
||||
static method invocation, and only a subset should be statically imported.
|
||||
@@ -209,9 +261,9 @@ Refaster's expressiveness:
|
||||
pure expression. Introduce a way to express such a constraint. For example,
|
||||
rewriting `optional1.map(Optional::of).orElse(optional2)` to `optional1.or(()
|
||||
-> optional2)` is not behavior preserving if evaluation of `optional2` has
|
||||
side effects.
|
||||
side-effects.
|
||||
- Similarly, certain refactoring operations are only valid if one of the
|
||||
matched expressions is not `@Nullable`. It'd be nice to be able to express
|
||||
matches expressions is not `@Nullable`. It'd be nice to be able to express
|
||||
this.
|
||||
- Generalize `@Placeholder` support such that rules can reference e.g. "any
|
||||
concrete unary method". This would allow refactorings such as
|
||||
@@ -224,6 +276,10 @@ Refaster's expressiveness:
|
||||
to be lost. In such a case don't statically import the method, so that the
|
||||
generic type information can be retained. (There may be cases where generic
|
||||
type information should even be _added_. Find an example.)
|
||||
- Upon application of a template Refaster can throw a _No binding for
|
||||
Key{identifier=someAfterTemplateParam}_ exception. When this happens the
|
||||
template is invalid. Instead perform this check at compile time, such that
|
||||
such malformed templates cannot be defined in the first place.
|
||||
- Provide a way to express "match if (not) annotated (with _X_)". See #1 for a
|
||||
motivating example.
|
||||
- Provide a way to place match constraints on compile time constants. For
|
||||
@@ -232,49 +288,59 @@ Refaster's expressiveness:
|
||||
- Provide a way to express transformations of compile-time constants. This
|
||||
would allow one to e.g. rewrite single-character strings to chars or vice
|
||||
versa, thereby accommodating a target API. Another example would be to
|
||||
replace SLF4J's `{}` placeholders with `%s` or vice versa. Yet another
|
||||
replace SLF4J's `{}` place holders with `%s` or vice versa. Yet another
|
||||
example would be to rewrite `BigDecimal.valueOf("<some-long-value>")` to
|
||||
`BigDecimal.valueOf(theParsedLongValue)`.
|
||||
- More generally, investigate ways to plug in fully dynamic behavior, e.g. by
|
||||
providing hooks which enable plugging in arbitrary
|
||||
predicates/transformations. The result would be a Refaster/`BugChecker`
|
||||
hybrid. A feature like this could form the basis for many other features
|
||||
listed here. (As a concrete example, consider the ability to reference
|
||||
- More generally, investigate ways to plug in in fully dynamic behavior, e.g.
|
||||
by providing hooks using which arbitrary predicates/transformations can be
|
||||
plugged in. The result would be a Refaster/`BugChecker` hybrid. A feature
|
||||
such as this could form the basis for many other features listed here. (As a
|
||||
concrete example, consider the ability to reference
|
||||
`com.google.errorprone.matchers.Matcher` implementations.)
|
||||
- Provide an extension API that enables defining methods or expressions based
|
||||
on functional properties. A motivating example is the Java Collections
|
||||
- Provide a way to match lambda expressions and method references which match a
|
||||
specified functional interface. This would allow rewrites such as
|
||||
`Mono.fromCallable(this::doesNotThrowCheckException)` ->
|
||||
`Mono.fromSupplier(this::doesNotThrowCheckException)`.
|
||||
- Provide an extension API using which methods or expressions can be defined
|
||||
based on functional properties. A motivating example is the Java Collections
|
||||
framework, which allows many ways to define (im)mutable (un)ordered
|
||||
collections with(out) duplicates. One could then express things like "match
|
||||
any method call that collects its inputs into an immutable ordered list". An
|
||||
any method call with collects its inputs into an immutable ordered list". An
|
||||
enum analogous to `java.util.stream.Collector.Characteristics` could be used.
|
||||
Out of the box JDK and Guava collection factory methods could be classified,
|
||||
with the user having the option to extend the classification.
|
||||
- Refaster currently unconditionally ignores expressions containing comments.
|
||||
Provide two additional modes: (a) match and drop the comments or (b)
|
||||
transport the comments to before/after the replaced expression.
|
||||
- Extend Refaster to drop imports that become unnecessary as a result of a
|
||||
refactoring. This e.g. allows one to replace a statically imported TestNG
|
||||
- Extend Refaster to drop imports that come become unnecessary as a result of a
|
||||
refactoring. This e.g. allows one to replace a statically import TestNG
|
||||
`fail(...)` invocation with a statically imported equivalent AssertJ
|
||||
`fail(...)` invocation. (Observe that without an import cleanup this
|
||||
`fail(...)` invocation. (Observe that without an impor cleanup this
|
||||
replacement would cause a compilation error.)
|
||||
- Extend the `@Repeated` match semantics such that it also covers non-varargs
|
||||
methods. For a motivating example see google/error-prone#568.
|
||||
- When matching explicit type references, also match super types. For a
|
||||
motivating example, see the two subtly different loop definitions in
|
||||
`CollectionRemoveAllFromCollectionExpression`.
|
||||
motivating example, see the two subtly difference loop definitions in
|
||||
`CollectionRemoveAllFromCollectionBlock`.
|
||||
- Figure out why Refaster sometimes doesn't match the correct generic overload.
|
||||
See the `AssertThatIterableHasOneComparableElementEqualTo` rule for an
|
||||
See the `AssertThatIterableHasOneComparableElementEqualTo` template for an
|
||||
example.
|
||||
|
||||
[autorefactor]: https://autorefactor.org
|
||||
[bettercodehub]: https://bettercodehub.com
|
||||
[checkstyle-external-project-tests]: https://github.com/checkstyle/checkstyle/blob/master/wercker.yml
|
||||
[codecov]: https://codecov.io
|
||||
[error-prone-bug-patterns]: https://errorprone.info/bugpatterns
|
||||
[error-prone-criteria]: https://errorprone.info/docs/criteria
|
||||
[error-prone-fork-jitpack]: https://jitpack.io/#PicnicSupermarket/error-prone
|
||||
[error-prone-fork-repo]: https://github.com/PicnicSupermarket/error-prone
|
||||
[error-prone]: https://errorprone.info
|
||||
[error-prone-repo]: https://github.com/google/error-prone
|
||||
[forbidden-apis]: https://github.com/policeman-tools/forbidden-apis
|
||||
[fossa]: https://fossa.io
|
||||
[google-java-format]: https://github.com/google/google-java-format
|
||||
[main-contributing]: ../CONTRIBUTING.md
|
||||
[main-readme]: ../README.md
|
||||
[maven]: https://maven.apache.org
|
||||
[modernizer-maven-plugin]: https://github.com/gaul/modernizer-maven-plugin
|
||||
[sonarcloud]: https://sonarcloud.io
|
||||
[pitest]: https://pitest.org
|
||||
[pitest-maven]: https://pitest.org/quickstart/maven
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
<parent>
|
||||
<groupId>tech.picnic.error-prone-support</groupId>
|
||||
<artifactId>error-prone-support</artifactId>
|
||||
<version>0.18.1-SNAPSHOT</version>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>error-prone-contrib</artifactId>
|
||||
|
||||
<name>Picnic :: Error Prone Support :: Contrib</name>
|
||||
<description>Extra Error Prone plugins by Picnic.</description>
|
||||
<url>https://error-prone.picnic.tech</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
@@ -38,39 +37,20 @@
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_test_helpers</artifactId>
|
||||
<scope>provided</scope>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>documentation-support</artifactId>
|
||||
<artifactId>refaster-resource-compiler</artifactId>
|
||||
<!-- This dependency is declared only as a hint to Maven that
|
||||
compilation depends on it; see the `maven-compiler-plugin`'s
|
||||
`annotationProcessorPaths` configuration below. -->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>error-prone-utils</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>refaster-support</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>refaster-test-support</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
<scope>provided</scope>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.service</groupId>
|
||||
@@ -78,8 +58,13 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.value</groupId>
|
||||
<artifactId>auto-value-annotations</artifactId>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.errorprone</groupId>
|
||||
<artifactId>javac</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -102,11 +87,6 @@
|
||||
<artifactId>reactor-adapter</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor.addons</groupId>
|
||||
<artifactId>reactor-extra</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.reactivex.rxjava2</groupId>
|
||||
<artifactId>rxjava</artifactId>
|
||||
@@ -127,16 +107,6 @@
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.annotation</groupId>
|
||||
<artifactId>javax.annotation-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.inject</groupId>
|
||||
<artifactId>javax.inject</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
@@ -153,22 +123,15 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.immutables</groupId>
|
||||
<artifactId>value-annotations</artifactId>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-library</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jspecify</groupId>
|
||||
<artifactId>jspecify</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- XXX: Explicitly declared as a workaround for
|
||||
https://github.com/pitest/pitest-junit5-plugin/issues/105. -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
@@ -184,36 +147,6 @@
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
<artifactId>mongodb-driver-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openrewrite</groupId>
|
||||
<artifactId>rewrite-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openrewrite</groupId>
|
||||
<artifactId>rewrite-java</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openrewrite</groupId>
|
||||
<artifactId>rewrite-java-11</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openrewrite</groupId>
|
||||
<artifactId>rewrite-templating</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openrewrite</groupId>
|
||||
<artifactId>rewrite-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.reactivestreams</groupId>
|
||||
<artifactId>reactive-streams</artifactId>
|
||||
@@ -224,34 +157,19 @@
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-beans</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webflux</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-test</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -264,32 +182,28 @@
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.coveo</groupId>
|
||||
<artifactId>fmt-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<additionalSourceDirectories>
|
||||
<additionalSourceDirectory>${basedir}/src/test/resources</additionalSourceDirectory>
|
||||
</additionalSourceDirectories>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths combine.children="append">
|
||||
<!-- XXX: Drop the version declarations once
|
||||
properly supported. See
|
||||
https://youtrack.jetbrains.com/issue/IDEA-342187. -->
|
||||
<path>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>documentation-support</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>refaster-compiler</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>refaster-support</artifactId>
|
||||
<artifactId>refaster-resource-compiler</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
<compilerArgs combine.children="append">
|
||||
<arg>-Xplugin:DocumentationGenerator -XoutputDirectory=${project.build.directory}/docs</arg>
|
||||
<arg>-Xplugin:RefasterRuleResourceCompiler</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static tech.picnic.errorprone.utils.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.AnnotationTreeMatcher;
|
||||
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.MethodTree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import java.util.Map;
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
|
||||
/** A {@link BugChecker} that flags ambiguous {@code @JsonCreator}s in enums. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "`JsonCreator.Mode` should be set for single-argument creators",
|
||||
link = BUG_PATTERNS_BASE_URL + "AmbiguousJsonCreator",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class AmbiguousJsonCreator extends BugChecker implements AnnotationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<AnnotationTree> IS_JSON_CREATOR_ANNOTATION =
|
||||
isType("com.fasterxml.jackson.annotation.JsonCreator");
|
||||
|
||||
/** Instantiates a new {@link AmbiguousJsonCreator} instance. */
|
||||
public AmbiguousJsonCreator() {}
|
||||
|
||||
@Override
|
||||
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
|
||||
if (!IS_JSON_CREATOR_ANNOTATION.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
ClassTree clazz = state.findEnclosing(ClassTree.class);
|
||||
if (clazz == null || clazz.getKind() != Kind.ENUM) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
MethodTree method = state.findEnclosing(MethodTree.class);
|
||||
if (method == null || method.getParameters().size() != 1) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
boolean customMode =
|
||||
ASTHelpers.getAnnotationMirror(tree).getElementValues().entrySet().stream()
|
||||
.filter(entry -> entry.getKey().getSimpleName().contentEquals("mode"))
|
||||
.map(Map.Entry::getValue)
|
||||
.map(AnnotationValue::getValue)
|
||||
.filter(Symbol.VarSymbol.class::isInstance)
|
||||
.map(Symbol.VarSymbol.class::cast)
|
||||
.anyMatch(varSymbol -> !varSymbol.getSimpleName().contentEquals("DEFAULT"));
|
||||
|
||||
return customMode
|
||||
? Description.NO_MATCH
|
||||
: describeMatch(
|
||||
tree, SuggestedFix.replace(tree, "@JsonCreator(mode = JsonCreator.Mode.DELEGATING)"));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.utils;
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.HashMultimap;
|
||||
@@ -9,6 +9,7 @@ import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.AssignmentTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.io.Serializable;
|
||||
import java.util.HashSet;
|
||||
@@ -27,7 +28,7 @@ import java.util.stream.Stream;
|
||||
* <p>This class allows one to define a whitelist or blacklist of annotations or their attributes.
|
||||
* Annotations are identified by their fully qualified name.
|
||||
*/
|
||||
public final class AnnotationAttributeMatcher implements Serializable {
|
||||
final class AnnotationAttributeMatcher implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final boolean complement;
|
||||
@@ -58,7 +59,7 @@ public final class AnnotationAttributeMatcher implements Serializable {
|
||||
* @param exclusions The listed annotations or annotation attributes are not matched.
|
||||
* @return A non-{@code null} {@link AnnotationAttributeMatcher}.
|
||||
*/
|
||||
public static AnnotationAttributeMatcher create(
|
||||
static AnnotationAttributeMatcher create(
|
||||
Optional<? extends List<String>> inclusions, Iterable<String> exclusions) {
|
||||
Set<String> includedWholeTypes = new HashSet<>();
|
||||
Set<String> excludedWholeTypes = new HashSet<>();
|
||||
@@ -96,13 +97,7 @@ public final class AnnotationAttributeMatcher implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subset of arguments of the given {@link AnnotationTree} matched by this instance.
|
||||
*
|
||||
* @param tree The annotation AST node to be inspected.
|
||||
* @return Any matching annotation arguments.
|
||||
*/
|
||||
public Stream<ExpressionTree> extractMatchingArguments(AnnotationTree tree) {
|
||||
Stream<? extends ExpressionTree> extractMatchingArguments(AnnotationTree tree) {
|
||||
Type type = ASTHelpers.getType(tree.getAnnotationType());
|
||||
if (type == null) {
|
||||
return Stream.empty();
|
||||
@@ -110,13 +105,12 @@ public final class AnnotationAttributeMatcher implements Serializable {
|
||||
|
||||
String annotationType = type.toString();
|
||||
return tree.getArguments().stream()
|
||||
.map(ExpressionTree.class::cast)
|
||||
.filter(a -> matches(annotationType, extractAttributeName(a)));
|
||||
}
|
||||
|
||||
private static String extractAttributeName(ExpressionTree expr) {
|
||||
return (expr instanceof AssignmentTree assignment)
|
||||
? ASTHelpers.getSymbol(assignment.getVariable()).getSimpleName().toString()
|
||||
return (expr.getKind() == Kind.ASSIGNMENT)
|
||||
? ASTHelpers.getSymbol(((AssignmentTree) expr).getVariable()).getSimpleName().toString()
|
||||
: "value";
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
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.argumentCount;
|
||||
import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.nullLiteral;
|
||||
import static tech.picnic.errorprone.utils.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.sun.source.tree.MethodInvocationTree;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags AssertJ {@code isEqualTo(null)} checks for simplification.
|
||||
*
|
||||
* <p>This bug checker cannot be replaced with a simple Refaster rule, as the Refaster approach
|
||||
* would require that all overloads of {@link org.assertj.core.api.Assert#isEqualTo(Object)} (such
|
||||
* as {@link org.assertj.core.api.AbstractStringAssert#isEqualTo(String)}) are explicitly
|
||||
* enumerated. This bug checker generically matches all such current and future overloads.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Prefer `.isNull()` over `.isEqualTo(null)`",
|
||||
link = BUG_PATTERNS_BASE_URL + "AssertJIsNull",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class AssertJIsNull extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<MethodInvocationTree> ASSERT_IS_EQUAL_TO_NULL =
|
||||
allOf(
|
||||
instanceMethod().onDescendantOf("org.assertj.core.api.Assert").named("isEqualTo"),
|
||||
argumentCount(1),
|
||||
argument(0, nullLiteral()));
|
||||
|
||||
/** Instantiates a new {@link AssertJIsNull} instance. */
|
||||
public AssertJIsNull() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!ASSERT_IS_EQUAL_TO_NULL.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix.Builder fix =
|
||||
SuggestedFix.builder().merge(SuggestedFixes.renameMethodInvocation(tree, "isNull", state));
|
||||
tree.getArguments().forEach(arg -> fix.merge(SuggestedFix.delete(arg)));
|
||||
|
||||
return describeMatch(tree, fix.build());
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,41 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.MultiMatcher;
|
||||
import com.google.errorprone.matchers.MultiMatcher.MultiMatchResult;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import java.util.List;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/** A {@link BugChecker} that flags redundant {@code @Autowired} constructor annotations. */
|
||||
/** A {@link BugChecker} which flags redundant {@code @Autowired} constructor annotations. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "AutowiredConstructor",
|
||||
summary = "Omit `@Autowired` on a class' sole constructor, as it is redundant",
|
||||
link = BUG_PATTERNS_BASE_URL + "AutowiredConstructor",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class AutowiredConstructor extends BugChecker implements ClassTreeMatcher {
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class AutowiredConstructorCheck extends BugChecker implements ClassTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final MultiMatcher<Tree, AnnotationTree> AUTOWIRED_ANNOTATION =
|
||||
annotations(AT_LEAST_ONE, isType("org.springframework.beans.factory.annotation.Autowired"));
|
||||
|
||||
/** Instantiates a new {@link AutowiredConstructor} instance. */
|
||||
public AutowiredConstructor() {}
|
||||
|
||||
@Override
|
||||
public Description matchClass(ClassTree tree, VisitorState state) {
|
||||
List<MethodTree> constructors = ASTHelpers.getConstructors(tree);
|
||||
@@ -48,18 +43,20 @@ public final class AutowiredConstructor extends BugChecker implements ClassTreeM
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
MultiMatchResult<AnnotationTree> hasAutowiredAnnotation =
|
||||
AUTOWIRED_ANNOTATION.multiMatchResult(Iterables.getOnlyElement(constructors), state);
|
||||
if (!hasAutowiredAnnotation.matches()) {
|
||||
List<AnnotationTree> annotations =
|
||||
AUTOWIRED_ANNOTATION
|
||||
.multiMatchResult(Iterables.getOnlyElement(constructors), state)
|
||||
.matchingNodes();
|
||||
if (annotations.size() != 1) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
/*
|
||||
* This is the only `@Autowired` constructor: suggest that it be removed. Note that this likely
|
||||
* means that the associated import can be removed as well. Rather than adding code for this
|
||||
* case we leave flagging the unused import to Error Prone's `RemoveUnusedImports` check.
|
||||
* means that the associated import can be removed as well. Rather than adding code for this case we
|
||||
* leave flagging the unused import to Error Prone's `RemoveUnusedImports` check.
|
||||
*/
|
||||
AnnotationTree annotation = hasAutowiredAnnotation.onlyMatchingNode();
|
||||
return describeMatch(annotation, SourceCode.deleteWithTrailingWhitespace(annotation, state));
|
||||
AnnotationTree annotation = Iterables.getOnlyElement(annotations);
|
||||
return describeMatch(annotation, SuggestedFix.delete(annotation));
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static tech.picnic.errorprone.utils.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.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.AnnotationTreeMatcher;
|
||||
@@ -19,34 +17,32 @@ import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.AssignmentTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.NewArrayTree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/** A {@link BugChecker} that flags annotations that could be written more concisely. */
|
||||
/** A {@link BugChecker} which flags annotations that could be written more concisely. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "CanonicalAnnotationSyntax",
|
||||
summary = "Omit redundant syntax from annotation declarations",
|
||||
link = BUG_PATTERNS_BASE_URL + "CanonicalAnnotationSyntax",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class CanonicalAnnotationSyntax extends BugChecker implements AnnotationTreeMatcher {
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class CanonicalAnnotationSyntaxCheck extends BugChecker
|
||||
implements AnnotationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Pattern TRAILING_ARRAY_COMMA = Pattern.compile(",\\s*}$");
|
||||
private static final ImmutableSet<BiFunction<AnnotationTree, VisitorState, Optional<Fix>>>
|
||||
FIX_FACTORIES =
|
||||
ImmutableSet.of(
|
||||
CanonicalAnnotationSyntax::dropRedundantParentheses,
|
||||
CanonicalAnnotationSyntax::dropRedundantValueAttribute,
|
||||
CanonicalAnnotationSyntax::dropRedundantCurlies);
|
||||
|
||||
/** Instantiates a new {@link CanonicalAnnotationSyntax} instance. */
|
||||
public CanonicalAnnotationSyntax() {}
|
||||
CanonicalAnnotationSyntaxCheck::dropRedundantParentheses,
|
||||
CanonicalAnnotationSyntaxCheck::dropRedundantValueAttribute,
|
||||
CanonicalAnnotationSyntaxCheck::dropRedundantCurlies);
|
||||
|
||||
@Override
|
||||
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
|
||||
@@ -90,8 +86,8 @@ public final class CanonicalAnnotationSyntax extends BugChecker implements Annot
|
||||
ExpressionTree arg = args.get(0);
|
||||
if (state.getSourceForNode(arg) == null) {
|
||||
/*
|
||||
* The annotation argument doesn't have a source representation, e.g. because `value` isn't
|
||||
* assigned explicitly.
|
||||
* The annotation argument isn't doesn't have a source representation, e.g. because `value`
|
||||
* isn't assigned to explicitly.
|
||||
*/
|
||||
return Optional.empty();
|
||||
}
|
||||
@@ -106,8 +102,7 @@ public final class CanonicalAnnotationSyntax extends BugChecker implements Annot
|
||||
return Optional.of(
|
||||
SuggestedFix.replace(
|
||||
arg,
|
||||
simplifyAttributeValue(expr, state)
|
||||
.orElseGet(() -> SourceCode.treeToString(expr, state))));
|
||||
simplifyAttributeValue(expr, state).orElseGet(() -> Util.treeToString(expr, state))));
|
||||
}
|
||||
|
||||
private static Optional<Fix> dropRedundantCurlies(AnnotationTree tree, VisitorState state) {
|
||||
@@ -118,7 +113,7 @@ public final class CanonicalAnnotationSyntax extends BugChecker implements Annot
|
||||
* the expression as a whole.
|
||||
*/
|
||||
ExpressionTree value =
|
||||
(arg instanceof AssignmentTree assignment) ? assignment.getExpression() : arg;
|
||||
(arg.getKind() == Kind.ASSIGNMENT) ? ((AssignmentTree) arg).getExpression() : arg;
|
||||
|
||||
/* Store a fix for each expression that was successfully simplified. */
|
||||
simplifyAttributeValue(value, state)
|
||||
@@ -129,21 +124,24 @@ public final class CanonicalAnnotationSyntax extends BugChecker implements Annot
|
||||
}
|
||||
|
||||
private static Optional<String> simplifyAttributeValue(ExpressionTree expr, VisitorState state) {
|
||||
/* Drop curly braces or commas if possible. */
|
||||
return expr instanceof NewArrayTree newArray
|
||||
? simplifySingletonArray(newArray, state).or(() -> dropTrailingComma(newArray, state))
|
||||
: Optional.empty();
|
||||
if (expr.getKind() != Kind.NEW_ARRAY) {
|
||||
/* There are no curly braces or commas to be dropped here. */
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
NewArrayTree array = (NewArrayTree) expr;
|
||||
return simplifySingletonArray(array, state).or(() -> dropTrailingComma(array, state));
|
||||
}
|
||||
|
||||
/** Returns the expression describing the array's sole element, if any. */
|
||||
private static Optional<String> simplifySingletonArray(NewArrayTree array, VisitorState state) {
|
||||
return Optional.of(array.getInitializers())
|
||||
.filter(initializers -> initializers.size() == 1)
|
||||
.map(initializers -> SourceCode.treeToString(initializers.get(0), state));
|
||||
.map(initializers -> Util.treeToString(initializers.get(0), state));
|
||||
}
|
||||
|
||||
private static Optional<String> dropTrailingComma(NewArrayTree array, VisitorState state) {
|
||||
String src = SourceCode.treeToString(array, state);
|
||||
String src = Util.treeToString(array, state);
|
||||
return Optional.of(TRAILING_ARRAY_COMMA.matcher(src))
|
||||
.filter(Matcher::find)
|
||||
.map(m -> src.substring(0, m.start()) + '}');
|
||||
@@ -1,91 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anything;
|
||||
import static com.google.errorprone.matchers.Matchers.classLiteral;
|
||||
import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.receiverOfInvocation;
|
||||
import static com.google.errorprone.matchers.Matchers.toType;
|
||||
import static tech.picnic.errorprone.utils.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.annotations.Var;
|
||||
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.BinaryTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.util.TreePath;
|
||||
import com.sun.tools.javac.code.Symbol.MethodSymbol;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags invocations of {@link Class#getName()} where {@link
|
||||
* Class#getCanonicalName()} was likely meant.
|
||||
*
|
||||
* <p>For top-level types these two methods generally return the same result, but for nested types
|
||||
* the former separates identifiers using a dollar sign ({@code $}) rather than a dot ({@code .}).
|
||||
*
|
||||
* @implNote This check currently only flags {@link Class#getName()} invocations on class literals,
|
||||
* and doesn't flag method references. This avoids false positives, such as suggesting use of
|
||||
* {@link Class#getCanonicalName()} in contexts where the canonical name is {@code null}.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "This code should likely use the type's canonical name",
|
||||
link = BUG_PATTERNS_BASE_URL + "CanonicalClassNameUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class CanonicalClassNameUsage extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> GET_NAME_INVOCATION =
|
||||
toType(
|
||||
MethodInvocationTree.class,
|
||||
allOf(
|
||||
receiverOfInvocation(classLiteral(anything())),
|
||||
instanceMethod().onExactClass(Class.class.getCanonicalName()).named("getName")));
|
||||
private static final Pattern CANONICAL_NAME_USING_TYPES =
|
||||
Pattern.compile("(com\\.google\\.errorprone|tech\\.picnic\\.errorprone)\\..*");
|
||||
|
||||
/** Instantiates a new {@link CanonicalClassNameUsage} instance. */
|
||||
public CanonicalClassNameUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!GET_NAME_INVOCATION.matches(tree, state) || !isPassedToCanonicalNameUsingType(state)) {
|
||||
/*
|
||||
* This is not a `class.getName()` invocation of which the result is passed to another method
|
||||
* known to accept canonical type names.
|
||||
*/
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return describeMatch(
|
||||
tree, SuggestedFixes.renameMethodInvocation(tree, "getCanonicalName", state));
|
||||
}
|
||||
|
||||
private static boolean isPassedToCanonicalNameUsingType(VisitorState state) {
|
||||
@Var TreePath path = state.getPath().getParentPath();
|
||||
while (path.getLeaf() instanceof BinaryTree) {
|
||||
path = path.getParentPath();
|
||||
}
|
||||
|
||||
return path.getLeaf() instanceof MethodInvocationTree methodInvocation
|
||||
&& isOwnedByCanonicalNameUsingType(ASTHelpers.getSymbol(methodInvocation));
|
||||
}
|
||||
|
||||
private static boolean isOwnedByCanonicalNameUsingType(MethodSymbol symbol) {
|
||||
return CANONICAL_NAME_USING_TYPES.matcher(symbol.owner.getQualifiedName()).matches();
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
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.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
import tech.picnic.errorprone.utils.ThirdPartyLibrary;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@link Collector Collectors} that don't clearly express
|
||||
* (im)mutability.
|
||||
*
|
||||
* <p>Replacing such collectors with alternatives that produce immutable collections is preferred.
|
||||
* Do note that Guava's immutable collections are null-hostile.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"Avoid `Collectors.to{List,Map,Set}` in favor of collectors that emphasize (im)mutability",
|
||||
link = BUG_PATTERNS_BASE_URL + "CollectorMutability",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class CollectorMutability extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> COLLECTOR_METHOD =
|
||||
staticMethod().onClass(Collectors.class.getCanonicalName());
|
||||
private static final Matcher<ExpressionTree> LIST_COLLECTOR =
|
||||
staticMethod().anyClass().named("toList");
|
||||
private static final Matcher<ExpressionTree> MAP_COLLECTOR =
|
||||
staticMethod().anyClass().named("toMap");
|
||||
private static final Matcher<ExpressionTree> SET_COLLECTOR =
|
||||
staticMethod().anyClass().named("toSet");
|
||||
|
||||
/** Instantiates a new {@link CollectorMutability} instance. */
|
||||
public CollectorMutability() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!ThirdPartyLibrary.GUAVA.isIntroductionAllowed(state)
|
||||
|| !COLLECTOR_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (LIST_COLLECTOR.matches(tree, state)) {
|
||||
return suggestToCollectionAlternatives(
|
||||
tree,
|
||||
ImmutableList.class.getCanonicalName() + ".toImmutableList",
|
||||
ArrayList.class.getCanonicalName(),
|
||||
state);
|
||||
}
|
||||
|
||||
if (MAP_COLLECTOR.matches(tree, state)) {
|
||||
return suggestToMapAlternatives(tree, state);
|
||||
}
|
||||
|
||||
if (SET_COLLECTOR.matches(tree, state)) {
|
||||
return suggestToCollectionAlternatives(
|
||||
tree,
|
||||
ImmutableSet.class.getCanonicalName() + ".toImmutableSet",
|
||||
HashSet.class.getCanonicalName(),
|
||||
state);
|
||||
}
|
||||
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
private Description suggestToCollectionAlternatives(
|
||||
MethodInvocationTree tree,
|
||||
String immutableReplacement,
|
||||
String mutableReplacement,
|
||||
VisitorState state) {
|
||||
SuggestedFix.Builder mutableFix = SuggestedFix.builder();
|
||||
String toCollectionSelect =
|
||||
SuggestedFixes.qualifyStaticImport(
|
||||
Collectors.class.getCanonicalName() + ".toCollection", mutableFix, state);
|
||||
String mutableCollection = SuggestedFixes.qualifyType(state, mutableFix, mutableReplacement);
|
||||
|
||||
return buildDescription(tree)
|
||||
.addFix(replaceMethodInvocation(tree, immutableReplacement, state))
|
||||
.addFix(
|
||||
mutableFix
|
||||
.replace(tree, String.format("%s(%s::new)", toCollectionSelect, mutableCollection))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Description suggestToMapAlternatives(MethodInvocationTree tree, VisitorState state) {
|
||||
int argCount = tree.getArguments().size();
|
||||
if (argCount > 3) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix.Builder mutableFix = SuggestedFix.builder();
|
||||
String hashMap =
|
||||
SuggestedFixes.qualifyType(state, mutableFix, HashMap.class.getCanonicalName());
|
||||
|
||||
return buildDescription(tree)
|
||||
.addFix(
|
||||
replaceMethodInvocation(
|
||||
tree, ImmutableMap.class.getCanonicalName() + ".toImmutableMap", state))
|
||||
.addFix(
|
||||
mutableFix
|
||||
.postfixWith(
|
||||
tree.getArguments().get(argCount - 1),
|
||||
(argCount == 2 ? ", (a, b) -> { throw new IllegalStateException(); }" : "")
|
||||
+ String.format(", %s::new", hashMap))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static SuggestedFix replaceMethodInvocation(
|
||||
MethodInvocationTree tree, String fullyQualifiedReplacement, VisitorState state) {
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder();
|
||||
String replacement = SuggestedFixes.qualifyStaticImport(fullyQualifiedReplacement, fix, state);
|
||||
fix.merge(SuggestedFix.replace(tree.getMethodSelect(), replacement));
|
||||
return fix.build();
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
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.utils.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.utils.MoreASTHelpers;
|
||||
import tech.picnic.errorprone.utils.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.getCanonicalName())))),
|
||||
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 expressionStatement) {
|
||||
return tryMatchAssignment(targetSymbol, expressionStatement.getExpression());
|
||||
}
|
||||
|
||||
if (tree instanceof AssignmentTree assignment) {
|
||||
return targetSymbol.equals(ASTHelpers.getSymbol(assignment.getVariable()))
|
||||
? Optional.of(assignment.getExpression())
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
if (tree instanceof VariableTree declaration) {
|
||||
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 tryTree)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BlockTree finallyBlock = tryTree.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));
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static tech.picnic.errorprone.utils.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.MethodTreeMatcher;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import java.util.Optional;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/** A {@link BugChecker} that flags empty methods that seemingly can simply be deleted. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Empty method can likely be deleted",
|
||||
link = BUG_PATTERNS_BASE_URL + "EmptyMethod",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class EmptyMethod extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<Tree> PERMITTED_ANNOTATION =
|
||||
annotations(
|
||||
AT_LEAST_ONE,
|
||||
anyOf(
|
||||
isType(Override.class.getCanonicalName()),
|
||||
isType("org.aspectj.lang.annotation.Pointcut")));
|
||||
|
||||
/** Instantiates a new {@link EmptyMethod} instance. */
|
||||
public EmptyMethod() {}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("java:S1067" /* Chaining disjunctions like this does not impact readability. */)
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (tree.getBody() == null
|
||||
|| !tree.getBody().getStatements().isEmpty()
|
||||
|| ASTHelpers.containsComments(tree, state)
|
||||
|| PERMITTED_ANNOTATION.matches(tree, state)
|
||||
|| isInPossibleTestHelperClass(state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (ASTHelpers.methodCanBeOverridden(ASTHelpers.getSymbol(tree))) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return describeMatch(tree, SourceCode.deleteWithTrailingWhitespace(tree, state));
|
||||
}
|
||||
|
||||
private static boolean isInPossibleTestHelperClass(VisitorState state) {
|
||||
return Optional.ofNullable(ASTHelpers.findEnclosingNode(state.getPath(), ClassTree.class))
|
||||
.map(ClassTree::getSimpleName)
|
||||
.filter(name -> name.toString().contains("Test"))
|
||||
.isPresent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
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.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.tools.javac.code.Symbol.MethodSymbol;
|
||||
|
||||
/** A {@link BugChecker} which flags empty methods that seemingly can simply be deleted. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "EmptyMethod",
|
||||
summary = "Empty method can likely be deleted",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class EmptyMethodCheck extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<Tree> HAS_PERMITTED_ANNOTATION =
|
||||
annotations(
|
||||
AT_LEAST_ONE,
|
||||
anyOf(isType("java.lang.Override"), isType("org.aspectj.lang.annotation.Pointcut")));
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (tree.getBody() == null
|
||||
|| !tree.getBody().getStatements().isEmpty()
|
||||
|| ASTHelpers.containsComments(tree, state)
|
||||
|| HAS_PERMITTED_ANNOTATION.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
MethodSymbol sym = ASTHelpers.getSymbol(tree);
|
||||
if (sym == null || ASTHelpers.methodCanBeOverridden(sym)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return describeMatch(tree, SuggestedFix.delete(tree));
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.typePredicateMatcher;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreTypePredicates.isSubTypeOf;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.generic;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.type;
|
||||
|
||||
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.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Supplier;
|
||||
import com.google.errorprone.suppliers.Suppliers;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@link Mono#zip} and {@link Mono#zipWith} invocations with a
|
||||
* {@code Mono<Void>} or {@link Mono#empty()} argument or receiver.
|
||||
*
|
||||
* <p>When a zipped reactive stream completes empty, then the other zipped streams will be cancelled
|
||||
* (or not subscribed to), and the operation as a whole will complete empty as well. This is
|
||||
* generally not what was intended.
|
||||
*/
|
||||
// XXX: Generalize this check to also cover `Flux` zip operations.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Don't pass a `Mono<Void>` or `Mono.empty()` argument to `Mono#{zip,With}`",
|
||||
link = BUG_PATTERNS_BASE_URL + "EmptyMonoZip",
|
||||
linkType = CUSTOM,
|
||||
severity = ERROR,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class EmptyMonoZip extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Supplier<Type> MONO =
|
||||
Suppliers.typeFromString("reactor.core.publisher.Mono");
|
||||
private static final Matcher<ExpressionTree> MONO_ZIP_OR_ZIP_WITH =
|
||||
anyOf(
|
||||
instanceMethod().onDescendantOf(MONO).named("zipWith"),
|
||||
staticMethod().onClass(MONO).named("zip"));
|
||||
private static final Matcher<ExpressionTree> EMPTY_MONO =
|
||||
anyOf(
|
||||
staticMethod().onDescendantOf(MONO).named("empty"),
|
||||
typePredicateMatcher(isSubTypeOf(generic(MONO, type(Void.class.getCanonicalName())))));
|
||||
|
||||
/** Instantiates a new {@link EmptyMonoZip} instance. */
|
||||
public EmptyMonoZip() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!MONO_ZIP_OR_ZIP_WITH.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (hasEmptyReceiver(tree, state)) {
|
||||
return buildDescription(tree)
|
||||
.setMessage("Invoking `Mono#zipWith` on `Mono#empty()` or a `Mono<Void>` is a no-op")
|
||||
.build();
|
||||
}
|
||||
|
||||
if (hasEmptyArguments(tree, state)) {
|
||||
return describeMatch(tree);
|
||||
}
|
||||
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
private static boolean hasEmptyReceiver(MethodInvocationTree tree, VisitorState state) {
|
||||
return tree.getMethodSelect() instanceof MemberSelectTree memberSelect
|
||||
&& EMPTY_MONO.matches(memberSelect.getExpression(), state);
|
||||
}
|
||||
|
||||
private static boolean hasEmptyArguments(MethodInvocationTree tree, VisitorState state) {
|
||||
return tree.getArguments().stream().anyMatch(arg -> EMPTY_MONO.matches(arg, state));
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static java.util.stream.Collectors.collectingAndThen;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSetMultimap;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.common.collect.Sets;
|
||||
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.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.tools.javac.code.Symbol;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@link Ordering#explicit(Object, Object[])}} invocations listing
|
||||
* a subset of an enum type's values.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Make sure `Ordering#explicit` lists all of an enum's values",
|
||||
link = BUG_PATTERNS_BASE_URL + "ExplicitEnumOrdering",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class ExplicitEnumOrdering extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> EXPLICIT_ORDERING =
|
||||
staticMethod().onClass(Ordering.class.getCanonicalName()).named("explicit");
|
||||
|
||||
/** Instantiates a new {@link ExplicitEnumOrdering} instance. */
|
||||
public ExplicitEnumOrdering() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!EXPLICIT_ORDERING.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
ImmutableSet<String> missingEnumValues = getMissingEnumValues(tree.getArguments());
|
||||
if (missingEnumValues.isEmpty()) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return buildDescription(tree)
|
||||
.setMessage(
|
||||
String.format(
|
||||
"Explicit ordering lacks some enum values: %s",
|
||||
String.join(", ", missingEnumValues)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static ImmutableSet<String> getMissingEnumValues(
|
||||
List<? extends ExpressionTree> expressions) {
|
||||
return expressions.stream()
|
||||
.map(ASTHelpers::getSymbol)
|
||||
.filter(s -> s != null && s.isEnum())
|
||||
.collect(
|
||||
collectingAndThen(
|
||||
toImmutableSetMultimap(Symbol::asType, Symbol::toString),
|
||||
ExplicitEnumOrdering::getMissingEnumValues));
|
||||
}
|
||||
|
||||
private static ImmutableSet<String> getMissingEnumValues(
|
||||
ImmutableSetMultimap<Type, String> valuesByType) {
|
||||
return Multimaps.asMap(valuesByType).entrySet().stream()
|
||||
.flatMap(e -> getMissingEnumValues(e.getKey(), e.getValue()))
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
|
||||
private static Stream<String> getMissingEnumValues(Type enumType, Set<String> values) {
|
||||
Symbol.TypeSymbol typeSymbol = enumType.asElement();
|
||||
return Sets.difference(ASTHelpers.enumValues(typeSymbol), values).stream()
|
||||
.map(v -> String.join(".", typeSymbol.getSimpleName(), v));
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.generic;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.subOf;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.type;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.unbound;
|
||||
|
||||
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.MemberReferenceTreeMatcher;
|
||||
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.MemberReferenceTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.util.function.Function;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags usages of {@link Flux#flatMap(Function)} and {@link
|
||||
* Flux#flatMapSequential(Function)}.
|
||||
*
|
||||
* <p>{@link Flux#flatMap(Function)} and {@link Flux#flatMapSequential(Function)} eagerly perform up
|
||||
* to {@link reactor.util.concurrent.Queues#SMALL_BUFFER_SIZE} subscriptions. Additionally, the
|
||||
* former interleaves values as they are emitted, yielding nondeterministic results. In most cases
|
||||
* {@link Flux#concatMap(Function)} should be preferred, as it produces consistent results and
|
||||
* avoids potentially saturating the thread pool on which subscription happens. If {@code
|
||||
* concatMap}'s sequential-subscription semantics are undesirable one should invoke a {@code
|
||||
* flatMap} or {@code flatMapSequential} overload with an explicit concurrency level.
|
||||
*
|
||||
* <p>NB: The rarely-used overload {@link Flux#flatMap(Function, Function,
|
||||
* java.util.function.Supplier)} is not flagged by this check because there is no clear alternative
|
||||
* to point to.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"""
|
||||
`Flux#flatMap` and `Flux#flatMapSequential` have subtle semantics; please use \
|
||||
`Flux#concatMap` or explicitly specify the desired amount of concurrency""",
|
||||
link = BUG_PATTERNS_BASE_URL + "FluxFlatMapUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = ERROR,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class FluxFlatMapUsage extends BugChecker
|
||||
implements MethodInvocationTreeMatcher, MemberReferenceTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String MAX_CONCURRENCY_ARG_NAME = "MAX_CONCURRENCY";
|
||||
private static final Supplier<Type> FLUX =
|
||||
Suppliers.typeFromString("reactor.core.publisher.Flux");
|
||||
private static final Matcher<ExpressionTree> FLUX_FLATMAP =
|
||||
instanceMethod()
|
||||
.onDescendantOf(FLUX)
|
||||
.namedAnyOf("flatMap", "flatMapSequential")
|
||||
.withParameters(Function.class.getCanonicalName());
|
||||
private static final Supplier<Type> FLUX_OF_PUBLISHERS =
|
||||
VisitorState.memoize(
|
||||
generic(FLUX, subOf(generic(type("org.reactivestreams.Publisher"), unbound()))));
|
||||
|
||||
/** Instantiates a new {@link FluxFlatMapUsage} instance. */
|
||||
public FluxFlatMapUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!FLUX_FLATMAP.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix serializationFix = SuggestedFixes.renameMethodInvocation(tree, "concatMap", state);
|
||||
SuggestedFix concurrencyCapFix =
|
||||
SuggestedFix.postfixWith(
|
||||
Iterables.getOnlyElement(tree.getArguments()), ", " + MAX_CONCURRENCY_ARG_NAME);
|
||||
|
||||
Description.Builder description = buildDescription(tree);
|
||||
|
||||
if (state.getTypes().isSubtype(ASTHelpers.getType(tree), FLUX_OF_PUBLISHERS.get(state))) {
|
||||
/*
|
||||
* Nested publishers may need to be subscribed to eagerly in order to avoid a deadlock, e.g.
|
||||
* if they are produced by `Flux#groupBy`. In this case we suggest specifying an explicit
|
||||
* concurrently bound, in favour of sequential subscriptions using `Flux#concatMap`.
|
||||
*/
|
||||
description.addFix(concurrencyCapFix).addFix(serializationFix);
|
||||
} else {
|
||||
description.addFix(serializationFix).addFix(concurrencyCapFix);
|
||||
}
|
||||
|
||||
return description.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) {
|
||||
if (!FLUX_FLATMAP.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
// Method references are expected to occur very infrequently; generating both variants of
|
||||
// suggested fixes is not worth the trouble.
|
||||
return describeMatch(tree);
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
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.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
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.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import tech.picnic.errorprone.utils.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.getCanonicalName());
|
||||
|
||||
/** 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, ImmutableList.class.getCanonicalName() + ".toImmutableList", state));
|
||||
}
|
||||
description.addFix(
|
||||
suggestBlockingElementCollection(
|
||||
tree, Collectors.class.getCanonicalName() + ".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);
|
||||
}
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.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.instanceMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Verify;
|
||||
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.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.BinaryTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.LiteralTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.ParenthesizedTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import com.sun.source.util.SimpleTreeVisitor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Formatter;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags string concatenations that produce a format string; in such cases
|
||||
* the string concatenation should instead be deferred to the invoked method.
|
||||
*
|
||||
* @implNote This checker is based on the implementation of {@link
|
||||
* com.google.errorprone.bugpatterns.flogger.FloggerStringConcatenation}.
|
||||
*/
|
||||
// XXX: Support arbitrary `@FormatMethod`-annotated methods.
|
||||
// XXX: For (explicit or delegated) invocations of `java.util.Formatter` _strictly speaking_ we
|
||||
// should introduce special handling of `Formattable` arguments, as this check would replace a
|
||||
// `Formattable#toString` invocation with a `Formattable#formatTo` invocation. But likely that
|
||||
// should be considered a bug fix, too.
|
||||
// XXX: Introduce a separate check that adds/removes the `Locale` parameter to `String.format`
|
||||
// invocations, as necessary. See also a comment in the `StringJoin` check.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Defer string concatenation to the invoked method",
|
||||
link = BUG_PATTERNS_BASE_URL + "FormatStringConcatenation",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class FormatStringConcatenation extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* AssertJ exposes varargs {@code fail} methods with a {@link Throwable}-accepting overload, the
|
||||
* latter of which should not be flagged.
|
||||
*/
|
||||
private static final Matcher<ExpressionTree> ASSERTJ_FAIL_WITH_THROWABLE_METHOD =
|
||||
anyMethod()
|
||||
.anyClass()
|
||||
.withAnyName()
|
||||
.withParameters(String.class.getCanonicalName(), Throwable.class.getCanonicalName());
|
||||
|
||||
// XXX: Drop some of these methods if we use Refaster to replace some with others.
|
||||
private static final Matcher<ExpressionTree> ASSERTJ_FORMAT_METHOD =
|
||||
anyOf(
|
||||
instanceMethod()
|
||||
.onDescendantOf("org.assertj.core.api.AbstractAssert")
|
||||
.namedAnyOf("overridingErrorMessage", "withFailMessage"),
|
||||
allOf(
|
||||
instanceMethod()
|
||||
.onDescendantOf("org.assertj.core.api.AbstractSoftAssertions")
|
||||
.named("fail"),
|
||||
not(ASSERTJ_FAIL_WITH_THROWABLE_METHOD)),
|
||||
instanceMethod()
|
||||
.onDescendantOf("org.assertj.core.api.AbstractStringAssert")
|
||||
.named("isEqualTo"),
|
||||
instanceMethod()
|
||||
.onDescendantOf("org.assertj.core.api.AbstractThrowableAssert")
|
||||
.namedAnyOf(
|
||||
"hasMessage",
|
||||
"hasMessageContaining",
|
||||
"hasMessageEndingWith",
|
||||
"hasMessageStartingWith",
|
||||
"hasRootCauseMessage",
|
||||
"hasStackTraceContaining"),
|
||||
instanceMethod()
|
||||
.onDescendantOf("org.assertj.core.api.Descriptable")
|
||||
.namedAnyOf("as", "describedAs"),
|
||||
instanceMethod()
|
||||
.onDescendantOf("org.assertj.core.api.ThrowableAssertAlternative")
|
||||
.namedAnyOf(
|
||||
"withMessage",
|
||||
"withMessageContaining",
|
||||
"withMessageEndingWith",
|
||||
"withMessageStartingWith",
|
||||
"withStackTraceContaining"),
|
||||
allOf(
|
||||
instanceMethod().onDescendantOf("org.assertj.core.api.WithAssertions").named("fail"),
|
||||
not(ASSERTJ_FAIL_WITH_THROWABLE_METHOD)),
|
||||
allOf(
|
||||
staticMethod()
|
||||
.onClassAny(
|
||||
"org.assertj.core.api.Assertions",
|
||||
"org.assertj.core.api.BDDAssertions",
|
||||
"org.assertj.core.api.Fail")
|
||||
.named("fail"),
|
||||
not(ASSERTJ_FAIL_WITH_THROWABLE_METHOD)));
|
||||
private static final Matcher<ExpressionTree> GUAVA_FORMAT_METHOD =
|
||||
anyOf(
|
||||
staticMethod()
|
||||
.onClass(Preconditions.class.getCanonicalName())
|
||||
.namedAnyOf("checkArgument", "checkNotNull", "checkState"),
|
||||
staticMethod().onClass(Verify.class.getCanonicalName()).named("verify"));
|
||||
// XXX: Add `PrintWriter`, maybe others.
|
||||
private static final Matcher<ExpressionTree> JDK_FORMAT_METHOD =
|
||||
anyOf(
|
||||
staticMethod().onClass(String.class.getCanonicalName()).named("format"),
|
||||
instanceMethod().onExactClass(Formatter.class.getCanonicalName()).named("format"));
|
||||
private static final Matcher<ExpressionTree> SLF4J_FORMAT_METHOD =
|
||||
instanceMethod()
|
||||
.onDescendantOf("org.slf4j.Logger")
|
||||
.namedAnyOf("debug", "error", "info", "trace", "warn");
|
||||
|
||||
/** Instantiates a new {@link FormatStringConcatenation} instance. */
|
||||
public FormatStringConcatenation() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (hasNonConstantStringConcatenationArgument(tree, 0, state)) {
|
||||
return flagViolation(tree, ASSERTJ_FORMAT_METHOD, 0, "%s", state)
|
||||
.or(() -> flagViolation(tree, JDK_FORMAT_METHOD, 0, "%s", state))
|
||||
.or(() -> flagViolation(tree, SLF4J_FORMAT_METHOD, 0, "{}", state))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
if (hasNonConstantStringConcatenationArgument(tree, 1, state)) {
|
||||
return flagViolation(tree, GUAVA_FORMAT_METHOD, 1, "%s", state)
|
||||
.or(() -> flagViolation(tree, JDK_FORMAT_METHOD, 1, "%s", state))
|
||||
.or(() -> flagViolation(tree, SLF4J_FORMAT_METHOD, 1, "{}", state))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flags the given method invocation if it matches a targeted method and passes a non-compile time
|
||||
* constant string concatenation as a format string.
|
||||
*/
|
||||
private Optional<Description> flagViolation(
|
||||
MethodInvocationTree tree,
|
||||
Matcher<ExpressionTree> matcher,
|
||||
int formatStringParam,
|
||||
String formatSpecifier,
|
||||
VisitorState state) {
|
||||
if (!matcher.matches(tree, state)) {
|
||||
/* The invoked method is not targeted by this check. */
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
List<? extends ExpressionTree> arguments = tree.getArguments();
|
||||
|
||||
if (arguments.size() > formatStringParam + 1) {
|
||||
/*
|
||||
* This method invocation uses explicit string concatenation but _also_ already relies on
|
||||
* format specifiers: flag but don't suggest a fix.
|
||||
*/
|
||||
return Optional.of(describeMatch(tree));
|
||||
}
|
||||
|
||||
ExpressionTree formatStringArg = arguments.get(formatStringParam);
|
||||
ReplacementArgumentsConstructor replacementConstructor =
|
||||
new ReplacementArgumentsConstructor(formatSpecifier);
|
||||
formatStringArg.accept(replacementConstructor, state);
|
||||
|
||||
return Optional.of(
|
||||
describeMatch(
|
||||
tree,
|
||||
SuggestedFix.replace(
|
||||
formatStringArg, replacementConstructor.getReplacementArguments(state))));
|
||||
}
|
||||
|
||||
private static boolean hasNonConstantStringConcatenationArgument(
|
||||
MethodInvocationTree tree, int argPosition, VisitorState state) {
|
||||
List<? extends ExpressionTree> arguments = tree.getArguments();
|
||||
if (arguments.size() <= argPosition) {
|
||||
/* This method doesn't accept enough parameters. */
|
||||
return false;
|
||||
}
|
||||
|
||||
ExpressionTree argument = ASTHelpers.stripParentheses(arguments.get(argPosition));
|
||||
return argument instanceof BinaryTree
|
||||
&& isStringTyped(argument, state)
|
||||
&& ASTHelpers.constValue(argument, String.class) == null;
|
||||
}
|
||||
|
||||
private static boolean isStringTyped(ExpressionTree tree, VisitorState state) {
|
||||
return ASTHelpers.isSameType(ASTHelpers.getType(tree), state.getSymtab().stringType, state);
|
||||
}
|
||||
|
||||
private static class ReplacementArgumentsConstructor
|
||||
extends SimpleTreeVisitor<@Nullable Void, VisitorState> {
|
||||
private final StringBuilder formatString = new StringBuilder();
|
||||
private final List<Tree> formatArguments = new ArrayList<>();
|
||||
private final String formatSpecifier;
|
||||
|
||||
ReplacementArgumentsConstructor(String formatSpecifier) {
|
||||
this.formatSpecifier = formatSpecifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Void visitBinary(BinaryTree tree, VisitorState state) {
|
||||
if (tree.getKind() == Kind.PLUS && isStringTyped(tree, state)) {
|
||||
tree.getLeftOperand().accept(this, state);
|
||||
tree.getRightOperand().accept(this, state);
|
||||
} else {
|
||||
appendExpression(tree);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Void visitParenthesized(ParenthesizedTree tree, VisitorState state) {
|
||||
return tree.getExpression().accept(this, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable Void defaultAction(Tree tree, VisitorState state) {
|
||||
appendExpression(tree);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void appendExpression(Tree tree) {
|
||||
if (tree instanceof LiteralTree literal) {
|
||||
formatString.append(literal.getValue());
|
||||
} else {
|
||||
formatString.append(formatSpecifier);
|
||||
formatArguments.add(tree);
|
||||
}
|
||||
}
|
||||
|
||||
private String getReplacementArguments(VisitorState state) {
|
||||
return state.getConstantExpression(formatString.toString())
|
||||
+ ", "
|
||||
+ formatArguments.stream()
|
||||
.map(tree -> SourceCode.treeToString(tree, state))
|
||||
.collect(joining(", "));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static com.google.errorprone.suppliers.Suppliers.OBJECT_TYPE;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableBiMap;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableMultiset;
|
||||
import com.google.common.collect.ImmutableRangeMap;
|
||||
import com.google.common.collect.ImmutableRangeSet;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSetMultimap;
|
||||
import com.google.common.collect.ImmutableTable;
|
||||
import com.google.common.primitives.Primitives;
|
||||
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.bugpatterns.TypesWithUndefinedEquality;
|
||||
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.util.ASTHelpers;
|
||||
import com.google.errorprone.util.ASTHelpers.TargetType;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import com.sun.tools.javac.code.Types;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/** A {@link BugChecker} that flags redundant identity conversions. */
|
||||
// XXX: Consider detecting cases where a flagged expression is passed to a method, and where removal
|
||||
// of the 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.
|
||||
// XXX: Also flag nullary instance method invocations that represent an identity conversion, such as
|
||||
// `Boolean#booleanValue()`, `Byte#byteValue()` and friends.
|
||||
// XXX: Also flag redundant round-trip conversions such as `path.toFile().toPath()`.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Avoid or clarify identity conversions",
|
||||
link = BUG_PATTERNS_BASE_URL + "IdentityConversion",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class IdentityConversion extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> IS_CONVERSION_METHOD =
|
||||
anyOf(
|
||||
staticMethod()
|
||||
.onClassAny(
|
||||
Primitives.allWrapperTypes().stream()
|
||||
.map(Class::getName)
|
||||
.collect(toImmutableSet()))
|
||||
.named("valueOf"),
|
||||
staticMethod().onClass(String.class.getCanonicalName()).named("valueOf"),
|
||||
staticMethod()
|
||||
.onClassAny(
|
||||
ImmutableBiMap.class.getCanonicalName(),
|
||||
ImmutableList.class.getCanonicalName(),
|
||||
ImmutableListMultimap.class.getCanonicalName(),
|
||||
ImmutableMap.class.getCanonicalName(),
|
||||
ImmutableMultimap.class.getCanonicalName(),
|
||||
ImmutableMultiset.class.getCanonicalName(),
|
||||
ImmutableRangeMap.class.getCanonicalName(),
|
||||
ImmutableRangeSet.class.getCanonicalName(),
|
||||
ImmutableSet.class.getCanonicalName(),
|
||||
ImmutableSetMultimap.class.getCanonicalName(),
|
||||
ImmutableTable.class.getCanonicalName())
|
||||
.named("copyOf"),
|
||||
staticMethod().onClass(Matchers.class.getCanonicalName()).namedAnyOf("allOf", "anyOf"),
|
||||
staticMethod().onClass("reactor.adapter.rxjava.RxJava2Adapter"),
|
||||
staticMethod()
|
||||
.onClass("reactor.core.publisher.Flux")
|
||||
.namedAnyOf("concat", "firstWithSignal", "from", "merge"),
|
||||
staticMethod().onClass("reactor.core.publisher.Mono").namedAnyOf("from", "fromDirect"));
|
||||
|
||||
/** Instantiates a new {@link IdentityConversion} instance. */
|
||||
public IdentityConversion() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
List<? extends ExpressionTree> arguments = tree.getArguments();
|
||||
if (arguments.size() != 1 || !IS_CONVERSION_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
ExpressionTree sourceTree = arguments.get(0);
|
||||
Type sourceType = ASTHelpers.getType(sourceTree);
|
||||
Type resultType = ASTHelpers.getType(tree);
|
||||
TargetType targetType = ASTHelpers.targetType(state);
|
||||
if (sourceType == null || resultType == null || targetType == null) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (!state.getTypes().isSameType(sourceType, resultType)
|
||||
&& !isConvertibleWithWellDefinedEquality(sourceType, targetType.type(), state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (sourceType.isPrimitive()
|
||||
&& state.getPath().getParentPath().getLeaf() instanceof MemberSelectTree) {
|
||||
/*
|
||||
* The result of the conversion method is dereferenced, while the source type is a primitive:
|
||||
* dropping the conversion would yield uncompilable code.
|
||||
*/
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return buildDescription(tree)
|
||||
.setMessage(
|
||||
"""
|
||||
This method invocation appears redundant; remove it or suppress this warning and add a \
|
||||
comment explaining its purpose""")
|
||||
.addFix(SuggestedFix.replace(tree, SourceCode.treeToString(sourceTree, state)))
|
||||
.addFix(SuggestedFixes.addSuppressWarnings(state, canonicalName()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static boolean isConvertibleWithWellDefinedEquality(
|
||||
Type sourceType, Type targetType, VisitorState state) {
|
||||
Types types = state.getTypes();
|
||||
return !types.isSameType(targetType, OBJECT_TYPE.get(state))
|
||||
&& types.isConvertible(sourceType, targetType)
|
||||
&& Arrays.stream(TypesWithUndefinedEquality.values())
|
||||
.noneMatch(b -> b.matchesType(sourceType, state) || b.matchesType(targetType, state));
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.enclosingClass;
|
||||
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
|
||||
import static com.google.errorprone.matchers.Matchers.hasModifier;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.Matchers.methodReturns;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static tech.picnic.errorprone.utils.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.MethodTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import java.util.SortedSet;
|
||||
import javax.lang.model.element.Modifier;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@link SortedSet} property declarations inside
|
||||
* {@code @Value.Immutable}- and {@code @Value.Modifiable}-annotated types that lack a
|
||||
* {@code @Value.NaturalOrder} or {@code @Value.ReverseOrder} annotation.
|
||||
*
|
||||
* <p>Without such an annotation:
|
||||
*
|
||||
* <ul>
|
||||
* <li>deserialization of the enclosing type requires that the associated JSON property is
|
||||
* present, contrary to the way in which Immutables handles other collection properties; and
|
||||
* <li>different instances may use different comparator implementations (e.g. deserialization
|
||||
* would default to natural order sorting), potentially leading to subtle bugs.
|
||||
* </ul>
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"""
|
||||
`SortedSet` properties of a `@Value.Immutable` or `@Value.Modifiable` type must be \
|
||||
annotated with `@Value.NaturalOrder` or `@Value.ReverseOrder`""",
|
||||
link = BUG_PATTERNS_BASE_URL + "ImmutablesSortedSetComparator",
|
||||
linkType = CUSTOM,
|
||||
severity = ERROR,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class ImmutablesSortedSetComparator extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<MethodTree> METHOD_LACKS_ANNOTATION =
|
||||
allOf(
|
||||
methodReturns(isSubtypeOf(SortedSet.class)),
|
||||
anyOf(
|
||||
allOf(
|
||||
hasModifier(Modifier.ABSTRACT),
|
||||
enclosingClass(
|
||||
anyOf(
|
||||
hasAnnotation("org.immutables.value.Value.Immutable"),
|
||||
hasAnnotation("org.immutables.value.Value.Modifiable")))),
|
||||
hasAnnotation("org.immutables.value.Value.Default")),
|
||||
not(
|
||||
anyOf(
|
||||
hasAnnotation("org.immutables.value.Value.NaturalOrder"),
|
||||
hasAnnotation("org.immutables.value.Value.ReverseOrder"))));
|
||||
|
||||
/** Instantiates a new {@link ImmutablesSortedSetComparator} instance. */
|
||||
public ImmutablesSortedSetComparator() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (!METHOD_LACKS_ANNOTATION.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix.Builder builder = SuggestedFix.builder();
|
||||
String valueTypeIdentifier =
|
||||
SuggestedFixes.qualifyType(state, builder, "org.immutables.value.Value");
|
||||
return describeMatch(
|
||||
tree,
|
||||
builder.prefixWith(tree, String.format("@%s.NaturalOrder ", valueTypeIdentifier)).build());
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.LambdaExpressionTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.InstanceOfTree;
|
||||
import com.sun.source.tree.LambdaExpressionTree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags lambda expressions that can be replaced with a method reference
|
||||
* of the form {@code T.class::isInstance}.
|
||||
*/
|
||||
// XXX: Consider folding this logic into the `MethodReferenceUsage` check of the
|
||||
// `error-prone-experimental` module.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Prefer `Class::isInstance` method reference over equivalent lambda expression",
|
||||
link = BUG_PATTERNS_BASE_URL + "IsInstanceLambdaUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class IsInstanceLambdaUsage extends BugChecker implements LambdaExpressionTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** Instantiates a new {@link IsInstanceLambdaUsage} instance. */
|
||||
public IsInstanceLambdaUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) {
|
||||
if (tree.getParameters().size() != 1
|
||||
|| !(tree.getBody() instanceof InstanceOfTree instanceOf)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
VariableTree param = Iterables.getOnlyElement(tree.getParameters());
|
||||
if (!ASTHelpers.getSymbol(param).equals(ASTHelpers.getSymbol(instanceOf.getExpression()))) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return describeMatch(
|
||||
tree,
|
||||
SuggestedFix.replace(
|
||||
tree, SourceCode.treeToString(instanceOf.getType(), state) + ".class::isInstance"));
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.hasMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.hasModifier;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreJUnitMatchers.TEST_METHOD;
|
||||
import static tech.picnic.errorprone.utils.MoreMatchers.hasMetaAnnotation;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import javax.lang.model.element.Modifier;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags non-final and non package-private JUnit test class declarations,
|
||||
* unless abstract.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Non-abstract JUnit test classes should be declared package-private and final",
|
||||
linkType = CUSTOM,
|
||||
link = BUG_PATTERNS_BASE_URL + "JUnitClassModifiers",
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
public final class JUnitClassModifiers extends BugChecker implements ClassTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ClassTree> HAS_SPRING_CONFIGURATION_ANNOTATION =
|
||||
annotations(
|
||||
AT_LEAST_ONE,
|
||||
anyOf(
|
||||
isType("org.springframework.context.annotation.Configuration"),
|
||||
hasMetaAnnotation("org.springframework.context.annotation.Configuration")));
|
||||
private static final Matcher<ClassTree> TEST_CLASS_WITH_INCORRECT_MODIFIERS =
|
||||
allOf(
|
||||
hasMethod(TEST_METHOD),
|
||||
not(hasModifier(Modifier.ABSTRACT)),
|
||||
anyOf(
|
||||
hasModifier(Modifier.PRIVATE),
|
||||
hasModifier(Modifier.PROTECTED),
|
||||
hasModifier(Modifier.PUBLIC),
|
||||
allOf(not(hasModifier(Modifier.FINAL)), not(HAS_SPRING_CONFIGURATION_ANNOTATION))));
|
||||
|
||||
/** Instantiates a new {@link JUnitClassModifiers} instance. */
|
||||
public JUnitClassModifiers() {}
|
||||
|
||||
@Override
|
||||
public Description matchClass(ClassTree tree, VisitorState state) {
|
||||
if (!TEST_CLASS_WITH_INCORRECT_MODIFIERS.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
|
||||
SuggestedFixes.removeModifiers(
|
||||
tree.getModifiers(),
|
||||
state,
|
||||
ImmutableSet.of(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC))
|
||||
.ifPresent(fixBuilder::merge);
|
||||
|
||||
if (!HAS_SPRING_CONFIGURATION_ANNOTATION.matches(tree, state)) {
|
||||
SuggestedFixes.addModifiers(tree, state, Modifier.FINAL).ifPresent(fixBuilder::merge);
|
||||
}
|
||||
|
||||
return describeMatch(tree, fixBuilder.build());
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
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.enclosingClass;
|
||||
import static com.google.errorprone.matchers.Matchers.hasModifier;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static java.util.function.Predicate.not;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreJUnitMatchers.SETUP_OR_TEARDOWN_METHOD;
|
||||
import static tech.picnic.errorprone.utils.MoreJUnitMatchers.TEST_METHOD;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
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.util.ASTHelpers;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.tools.javac.code.Symbol.MethodSymbol;
|
||||
import java.util.Optional;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import tech.picnic.errorprone.utils.ConflictDetection;
|
||||
|
||||
/** A {@link BugChecker} that flags non-canonical JUnit method declarations. */
|
||||
// XXX: Consider introducing a class-level check that enforces that test classes:
|
||||
// 1. Are named `*Test` or `Abstract*TestCase`.
|
||||
// 2. If not `abstract`, are package-private and don't have public methods and subclasses.
|
||||
// 3. Only have private fields.
|
||||
// XXX: If implemented, the current logic could flag only `private` JUnit methods.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "JUnit method declaration can likely be improved",
|
||||
link = BUG_PATTERNS_BASE_URL + "JUnitMethodDeclaration",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class JUnitMethodDeclaration extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String TEST_PREFIX = "test";
|
||||
private static final ImmutableSet<Modifier> ILLEGAL_MODIFIERS =
|
||||
Sets.immutableEnumSet(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC);
|
||||
private static final Matcher<MethodTree> IS_LIKELY_OVERRIDDEN =
|
||||
allOf(
|
||||
not(hasModifier(Modifier.FINAL)),
|
||||
not(hasModifier(Modifier.PRIVATE)),
|
||||
enclosingClass(hasModifier(Modifier.ABSTRACT)));
|
||||
|
||||
/** Instantiates a new {@link JUnitMethodDeclaration} instance. */
|
||||
public JUnitMethodDeclaration() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (IS_LIKELY_OVERRIDDEN.matches(tree, state) || isOverride(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
boolean isTestMethod = TEST_METHOD.matches(tree, state);
|
||||
if (!isTestMethod && !SETUP_OR_TEARDOWN_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
|
||||
SuggestedFixes.removeModifiers(tree.getModifiers(), state, ILLEGAL_MODIFIERS)
|
||||
.ifPresent(fixBuilder::merge);
|
||||
|
||||
if (isTestMethod) {
|
||||
suggestTestMethodRenameIfApplicable(tree, fixBuilder, state);
|
||||
}
|
||||
|
||||
return fixBuilder.isEmpty() ? Description.NO_MATCH : describeMatch(tree, fixBuilder.build());
|
||||
}
|
||||
|
||||
private void suggestTestMethodRenameIfApplicable(
|
||||
MethodTree tree, SuggestedFix.Builder fixBuilder, VisitorState state) {
|
||||
MethodSymbol symbol = ASTHelpers.getSymbol(tree);
|
||||
tryCanonicalizeMethodName(symbol)
|
||||
.ifPresent(
|
||||
newName ->
|
||||
ConflictDetection.findMethodRenameBlocker(symbol, newName, state)
|
||||
.ifPresentOrElse(
|
||||
blocker -> reportMethodRenameBlocker(tree, blocker, state),
|
||||
() -> fixBuilder.merge(SuggestedFixes.renameMethod(tree, newName, state))));
|
||||
}
|
||||
|
||||
private void reportMethodRenameBlocker(MethodTree tree, String reason, VisitorState state) {
|
||||
state.reportMatch(
|
||||
buildDescription(tree)
|
||||
.setMessage(
|
||||
String.format(
|
||||
"This method's name should not redundantly start with `%s` (but note that %s)",
|
||||
TEST_PREFIX, reason))
|
||||
.build());
|
||||
}
|
||||
|
||||
private static Optional<String> tryCanonicalizeMethodName(MethodSymbol symbol) {
|
||||
return Optional.of(symbol.getQualifiedName().toString())
|
||||
.filter(name -> name.startsWith(TEST_PREFIX))
|
||||
.map(name -> name.substring(TEST_PREFIX.length()))
|
||||
.filter(not(String::isEmpty))
|
||||
.map(name -> Character.toLowerCase(name.charAt(0)) + name.substring(1))
|
||||
.filter(name -> !Character.isDigit(name.charAt(0)));
|
||||
}
|
||||
|
||||
private static boolean isOverride(MethodTree tree, VisitorState state) {
|
||||
return ASTHelpers.streamSuperMethods(ASTHelpers.getSymbol(tree), state.getTypes())
|
||||
.findAny()
|
||||
.isPresent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static java.util.function.Predicate.not;
|
||||
|
||||
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.MethodTreeMatcher;
|
||||
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.MultiMatcher;
|
||||
import com.google.errorprone.predicates.TypePredicate;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import java.util.Optional;
|
||||
import javax.lang.model.element.Modifier;
|
||||
|
||||
/** A {@link BugChecker} which flags non-canonical JUnit method declarations. */
|
||||
// XXX: Consider introducing a class-level check which enforces that test classes:
|
||||
// 1. Are named `*Test` or `Abstract*TestCase`.
|
||||
// 2. If not `abstract`, don't have public methods and subclasses.
|
||||
// 3. Only have private fields.
|
||||
// XXX: If implemented, the current logic could flag only `private` JUnit methods.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "JUnitMethodDeclaration",
|
||||
summary = "JUnit method declaration can likely be improved",
|
||||
linkType = BugPattern.LinkType.NONE,
|
||||
severity = BugPattern.SeverityLevel.SUGGESTION,
|
||||
tags = BugPattern.StandardTags.SIMPLIFICATION)
|
||||
public final class JUnitMethodDeclarationCheck extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String TEST_PREFIX = "test";
|
||||
private static final ImmutableSet<Modifier> ILLEGAL_MODIFIERS =
|
||||
ImmutableSet.of(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC);
|
||||
private static final MultiMatcher<MethodTree, AnnotationTree> IS_OVERRIDE_METHOD =
|
||||
annotations(AT_LEAST_ONE, isType("java.lang.Override"));
|
||||
private static final MultiMatcher<MethodTree, AnnotationTree> IS_TEST_METHOD =
|
||||
annotations(
|
||||
AT_LEAST_ONE,
|
||||
anyOf(
|
||||
isType("org.junit.jupiter.api.Test"),
|
||||
hasMetaAnnotation("org.junit.jupiter.api.TestTemplate")));
|
||||
private static final MultiMatcher<MethodTree, AnnotationTree> IS_SETUP_OR_TEARDOWN_METHOD =
|
||||
annotations(
|
||||
AT_LEAST_ONE,
|
||||
anyOf(
|
||||
isType("org.junit.jupiter.api.AfterAll"),
|
||||
isType("org.junit.jupiter.api.AfterEach"),
|
||||
isType("org.junit.jupiter.api.BeforeAll"),
|
||||
isType("org.junit.jupiter.api.BeforeEach")));
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
// XXX: Perhaps we should also skip analysis of non-`private` non-`final` methods in abstract
|
||||
// classes?
|
||||
if (IS_OVERRIDE_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
boolean isTestMethod = IS_TEST_METHOD.matches(tree, state);
|
||||
if (!isTestMethod && !IS_SETUP_OR_TEARDOWN_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix.Builder builder = SuggestedFix.builder();
|
||||
SuggestedFixes.removeModifiers(tree.getModifiers(), state, ILLEGAL_MODIFIERS)
|
||||
.ifPresent(builder::merge);
|
||||
|
||||
if (isTestMethod) {
|
||||
// XXX: In theory this rename could clash with an existing method or static import. In that
|
||||
// case we should emit a warning without a suggested replacement.
|
||||
tryCanonicalizeMethodName(tree, state).ifPresent(builder::merge);
|
||||
}
|
||||
|
||||
return builder.isEmpty() ? Description.NO_MATCH : describeMatch(tree, builder.build());
|
||||
}
|
||||
|
||||
private static Optional<SuggestedFix> tryCanonicalizeMethodName(
|
||||
MethodTree tree, VisitorState state) {
|
||||
return Optional.ofNullable(ASTHelpers.getSymbol(tree))
|
||||
.map(sym -> sym.getQualifiedName().toString())
|
||||
.filter(name -> name.startsWith(TEST_PREFIX))
|
||||
.map(name -> name.substring(TEST_PREFIX.length()))
|
||||
.filter(not(String::isEmpty))
|
||||
.map(name -> Character.toLowerCase(name.charAt(0)) + name.substring(1))
|
||||
.filter(name -> !Character.isDigit(name.charAt(0)))
|
||||
.map(name -> SuggestedFixes.renameMethod(tree, name, state));
|
||||
}
|
||||
|
||||
// XXX: Move to a `MoreMatchers` utility class.
|
||||
private static Matcher<AnnotationTree> hasMetaAnnotation(String annotationClassName) {
|
||||
TypePredicate typePredicate = hasAnnotation(annotationClassName);
|
||||
return (tree, state) -> {
|
||||
Symbol sym = ASTHelpers.getDeclaredSymbol(tree);
|
||||
return sym != null && typePredicate.apply(sym.type, state);
|
||||
};
|
||||
}
|
||||
|
||||
// XXX: Move to a `MoreTypePredicates` utility class.
|
||||
private static TypePredicate hasAnnotation(String annotationClassName) {
|
||||
return (type, state) -> ASTHelpers.hasAnnotation(type.tsym, annotationClassName, state);
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreMatchers.hasMetaAnnotation;
|
||||
|
||||
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.MethodTreeMatcher;
|
||||
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.MultiMatcher;
|
||||
import com.google.errorprone.matchers.MultiMatcher.MultiMatchResult;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags nullary {@link
|
||||
* org.junit.jupiter.params.ParameterizedTest @ParameterizedTest} test methods.
|
||||
*
|
||||
* <p>Such tests are unnecessarily executed more than necessary. This checker suggests annotating
|
||||
* the method with {@link org.junit.jupiter.api.Test @Test}, and to drop all declared {@link
|
||||
* org.junit.jupiter.params.provider.ArgumentsSource argument sources}.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Nullary JUnit test methods should not be parameterized",
|
||||
link = BUG_PATTERNS_BASE_URL + "JUnitNullaryParameterizedTestDeclaration",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class JUnitNullaryParameterizedTestDeclaration extends BugChecker
|
||||
implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final MultiMatcher<MethodTree, AnnotationTree> IS_PARAMETERIZED_TEST =
|
||||
annotations(AT_LEAST_ONE, isType("org.junit.jupiter.params.ParameterizedTest"));
|
||||
private static final Matcher<AnnotationTree> IS_ARGUMENT_SOURCE =
|
||||
anyOf(
|
||||
isType("org.junit.jupiter.params.provider.ArgumentsSource"),
|
||||
isType("org.junit.jupiter.params.provider.ArgumentsSources"),
|
||||
hasMetaAnnotation("org.junit.jupiter.params.provider.ArgumentsSource"));
|
||||
|
||||
/** Instantiates a new {@link JUnitNullaryParameterizedTestDeclaration} instance. */
|
||||
public JUnitNullaryParameterizedTestDeclaration() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (!tree.getParameters().isEmpty()) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
MultiMatchResult<AnnotationTree> isParameterizedTest =
|
||||
IS_PARAMETERIZED_TEST.multiMatchResult(tree, state);
|
||||
if (!isParameterizedTest.matches()) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
/*
|
||||
* This method is vacuously parameterized. Suggest replacing `@ParameterizedTest` with `@Test`.
|
||||
* (As each method is checked independently, we cannot in general determine whether this
|
||||
* suggestion makes a `ParameterizedTest` type import obsolete; that task is left to Error
|
||||
* Prone's `RemoveUnusedImports` check.)
|
||||
*/
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder();
|
||||
fix.merge(
|
||||
SuggestedFix.replace(
|
||||
isParameterizedTest.onlyMatchingNode(),
|
||||
'@' + SuggestedFixes.qualifyType(state, fix, "org.junit.jupiter.api.Test")));
|
||||
|
||||
/*
|
||||
* Also suggest dropping all (explicit and implicit) `@ArgumentsSource`s. No attempt is made to
|
||||
* assess whether a dropped `@MethodSource` also makes the referenced factory method(s) unused.
|
||||
*/
|
||||
tree.getModifiers().getAnnotations().stream()
|
||||
.filter(a -> IS_ARGUMENT_SOURCE.matches(a, state))
|
||||
.forEach(a -> fix.merge(SourceCode.deleteWithTrailingWhitespace(a, state)));
|
||||
|
||||
return describeMatch(tree, fix.build());
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
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.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreJUnitMatchers.HAS_METHOD_SOURCE;
|
||||
import static tech.picnic.errorprone.utils.MoreJUnitMatchers.getMethodSourceFactoryNames;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
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.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.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.utils.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.getCanonicalName(),
|
||||
IntStream.class.getCanonicalName(),
|
||||
LongStream.class.getCanonicalName(),
|
||||
DoubleStream.class.getCanonicalName(),
|
||||
List.class.getCanonicalName(),
|
||||
Set.class.getCanonicalName(),
|
||||
ImmutableList.class.getCanonicalName(),
|
||||
ImmutableSet.class.getCanonicalName())
|
||||
.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 fix = SuggestedFix.builder();
|
||||
String valueSource =
|
||||
SuggestedFixes.qualifyType(
|
||||
state, fix, "org.junit.jupiter.params.provider.ValueSource");
|
||||
return fix.replace(
|
||||
methodSourceAnnotation,
|
||||
String.format(
|
||||
"@%s(%s = %s)",
|
||||
valueSource,
|
||||
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, null);
|
||||
}
|
||||
|
||||
@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 methodInvocation
|
||||
? Iterables.getOnlyElement(methodInvocation.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();
|
||||
|
||||
return switch (typeString) {
|
||||
case "Class" -> "classes";
|
||||
case "Character" -> "chars";
|
||||
case "Integer" -> "ints";
|
||||
default -> 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 newArray)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,16 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
|
||||
import static java.util.Comparator.comparing;
|
||||
import static java.util.Comparator.naturalOrder;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Comparators;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.ErrorProneFlags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
@@ -31,76 +27,57 @@ import com.sun.source.tree.LiteralTree;
|
||||
import com.sun.source.tree.NewArrayTree;
|
||||
import com.sun.source.tree.PrimitiveTypeTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import com.sun.source.util.TreeScanner;
|
||||
import com.sun.tools.javac.code.Symtab;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import javax.inject.Inject;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.utils.AnnotationAttributeMatcher;
|
||||
import tech.picnic.errorprone.utils.Flags;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags annotation array listings which aren't sorted lexicographically.
|
||||
* A {@link BugChecker} which flags annotation array listings which aren't sorted lexicographically.
|
||||
*
|
||||
* <p>The idea behind this checker is that maintaining a sorted sequence simplifies conflict
|
||||
* resolution, and can even avoid it if two branches add the same entry.
|
||||
*/
|
||||
// XXX: In some places we declare a `@SuppressWarnings` annotation with a final value of
|
||||
// `key-to-resolve-AnnotationUseStyle-and-TrailingComment-check-conflict`. That entry must stay
|
||||
// last. Consider adding (generic?) support for such cases.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "LexicographicalAnnotationAttributeListing",
|
||||
summary = "Where possible, sort annotation array attributes lexicographically",
|
||||
link = BUG_PATTERNS_BASE_URL + "LexicographicalAnnotationAttributeListing",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
@SuppressWarnings("java:S2160" /* Super class equality definition suffices. */)
|
||||
public final class LexicographicalAnnotationAttributeListing extends BugChecker
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.STYLE)
|
||||
public final class LexicographicalAnnotationAttributeListingCheck extends BugChecker
|
||||
implements AnnotationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final ImmutableSet<String> BLACKLISTED_ANNOTATIONS =
|
||||
ImmutableSet.of(
|
||||
// XXX: Unless `JsonPropertyOrder#alphabetic` is true...
|
||||
// XXX: unless JsonPropertyOrder#alphabetic is true...
|
||||
"com.fasterxml.jackson.annotation.JsonPropertyOrder#value",
|
||||
"io.swagger.annotations.ApiImplicitParams#value",
|
||||
"io.swagger.v3.oas.annotations.Parameters#value",
|
||||
"javax.xml.bind.annotation.XmlType#propOrder",
|
||||
"org.springframework.context.annotation.PropertySource#value",
|
||||
"org.springframework.test.context.TestPropertySource#locations",
|
||||
"org.springframework.test.context.TestPropertySource#value",
|
||||
"picocli.CommandLine.Option#names");
|
||||
"javax.xml.bind.annotation.XmlType#propOrder");
|
||||
private static final String FLAG_PREFIX = "LexicographicalAnnotationAttributeListing:";
|
||||
private static final String INCLUDED_ANNOTATIONS_FLAG = FLAG_PREFIX + "Includes";
|
||||
private static final String EXCLUDED_ANNOTATIONS_FLAG = FLAG_PREFIX + "Excludes";
|
||||
|
||||
/**
|
||||
* The splitter applied to string-typed annotation arguments prior to lexicographical sorting. By
|
||||
* splitting on {@code =}, strings that represent e.g. inline Spring property declarations are
|
||||
* properly sorted by key, then value.
|
||||
*/
|
||||
private static final Splitter STRING_ARGUMENT_SPLITTER = Splitter.on('=');
|
||||
|
||||
private final AnnotationAttributeMatcher matcher;
|
||||
|
||||
/** Instantiates a default {@link LexicographicalAnnotationAttributeListing} instance. */
|
||||
public LexicographicalAnnotationAttributeListing() {
|
||||
/** Instantiates the default {@link LexicographicalAnnotationAttributeListingCheck}. */
|
||||
public LexicographicalAnnotationAttributeListingCheck() {
|
||||
this(ErrorProneFlags.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a customized {@link LexicographicalAnnotationAttributeListing}.
|
||||
* Instantiates a customized {@link LexicographicalAnnotationAttributeListingCheck}.
|
||||
*
|
||||
* @param flags Any provided command line flags.
|
||||
*/
|
||||
@Inject
|
||||
LexicographicalAnnotationAttributeListing(ErrorProneFlags flags) {
|
||||
public LexicographicalAnnotationAttributeListingCheck(ErrorProneFlags flags) {
|
||||
matcher = createAnnotationAttributeMatcher(flags);
|
||||
}
|
||||
|
||||
@@ -125,9 +102,13 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
|
||||
}
|
||||
|
||||
private static Optional<NewArrayTree> extractArray(ExpressionTree expr) {
|
||||
return expr instanceof AssignmentTree assignment
|
||||
? extractArray(assignment.getExpression())
|
||||
: Optional.of(expr).filter(NewArrayTree.class::isInstance).map(NewArrayTree.class::cast);
|
||||
if (expr.getKind() == Kind.ASSIGNMENT) {
|
||||
return extractArray(((AssignmentTree) expr).getExpression());
|
||||
}
|
||||
|
||||
return Optional.of(expr)
|
||||
.filter(e -> e.getKind() == Kind.NEW_ARRAY)
|
||||
.map(NewArrayTree.class::cast);
|
||||
}
|
||||
|
||||
private static Optional<SuggestedFix.Builder> suggestSorting(
|
||||
@@ -138,7 +119,7 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
|
||||
}
|
||||
|
||||
List<? extends ExpressionTree> actualOrdering = array.getInitializers();
|
||||
ImmutableList<? extends ExpressionTree> desiredOrdering = doSort(actualOrdering);
|
||||
ImmutableList<? extends ExpressionTree> desiredOrdering = doSort(actualOrdering, state);
|
||||
if (actualOrdering.equals(desiredOrdering)) {
|
||||
/* In the (presumably) common case the elements are already sorted. */
|
||||
return Optional.empty();
|
||||
@@ -147,7 +128,7 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
|
||||
/* The elements aren't sorted. Suggest the sorted alternative. */
|
||||
String suggestion =
|
||||
desiredOrdering.stream()
|
||||
.map(expr -> SourceCode.treeToString(expr, state))
|
||||
.map(expr -> Util.treeToString(expr, state))
|
||||
.collect(joining(", ", "{", "}"));
|
||||
return Optional.of(SuggestedFix.builder().replace(array, suggestion));
|
||||
}
|
||||
@@ -168,12 +149,12 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
|
||||
}
|
||||
|
||||
private static ImmutableList<? extends ExpressionTree> doSort(
|
||||
Iterable<? extends ExpressionTree> elements) {
|
||||
Iterable<? extends ExpressionTree> elements, VisitorState state) {
|
||||
// XXX: Perhaps we should use `Collator` with `.setStrength(Collator.PRIMARY)` and
|
||||
// `getCollationKey`. Not clear whether that's worth the hassle at this point.
|
||||
return ImmutableList.sortedCopyOf(
|
||||
comparing(
|
||||
LexicographicalAnnotationAttributeListing::getStructure,
|
||||
Comparator.comparing(
|
||||
e -> getStructure(e, state),
|
||||
Comparators.lexicographical(
|
||||
Comparators.lexicographical(
|
||||
String.CASE_INSENSITIVE_ORDER.thenComparing(naturalOrder())))),
|
||||
@@ -185,31 +166,35 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
|
||||
* performed. This approach disregards e.g. irrelevant whitespace. It also allows special
|
||||
* structure within string literals to be respected.
|
||||
*/
|
||||
private static ImmutableList<ImmutableList<String>> getStructure(ExpressionTree array) {
|
||||
private static ImmutableList<ImmutableList<String>> getStructure(
|
||||
ExpressionTree array, VisitorState state) {
|
||||
ImmutableList.Builder<ImmutableList<String>> nodes = ImmutableList.builder();
|
||||
|
||||
new TreeScanner<@Nullable Void, @Nullable Void>() {
|
||||
new TreeScanner<Void, Void>() {
|
||||
@Override
|
||||
public @Nullable Void visitIdentifier(IdentifierTree node, @Nullable Void unused) {
|
||||
nodes.add(ImmutableList.of(node.getName().toString()));
|
||||
return super.visitIdentifier(node, null);
|
||||
public Void visitIdentifier(IdentifierTree node, Void ctx) {
|
||||
nodes.add(tokenize(node));
|
||||
return super.visitIdentifier(node, ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Void visitLiteral(LiteralTree node, @Nullable Void unused) {
|
||||
Object value = ASTHelpers.constValue(node);
|
||||
nodes.add(
|
||||
value instanceof String str
|
||||
? STRING_ARGUMENT_SPLITTER.splitToStream(str).collect(toImmutableList())
|
||||
: ImmutableList.of(String.valueOf(value)));
|
||||
|
||||
return super.visitLiteral(node, null);
|
||||
public Void visitLiteral(LiteralTree node, Void ctx) {
|
||||
nodes.add(tokenize(node));
|
||||
return super.visitLiteral(node, ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Void visitPrimitiveType(PrimitiveTypeTree node, @Nullable Void unused) {
|
||||
nodes.add(ImmutableList.of(node.getPrimitiveTypeKind().toString()));
|
||||
return super.visitPrimitiveType(node, null);
|
||||
public Void visitPrimitiveType(PrimitiveTypeTree node, Void ctx) {
|
||||
nodes.add(tokenize(node));
|
||||
return super.visitPrimitiveType(node, ctx);
|
||||
}
|
||||
|
||||
private ImmutableList<String> tokenize(Tree node) {
|
||||
/*
|
||||
* Tokens are split on `=` so that e.g. inline Spring property declarations are properly
|
||||
* sorted by key, then value.
|
||||
*/
|
||||
return ImmutableList.copyOf(Util.treeToString(node, state).split("=", -1));
|
||||
}
|
||||
}.scan(array, null);
|
||||
|
||||
@@ -219,15 +204,12 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
|
||||
private static AnnotationAttributeMatcher createAnnotationAttributeMatcher(
|
||||
ErrorProneFlags flags) {
|
||||
return AnnotationAttributeMatcher.create(
|
||||
flags.get(INCLUDED_ANNOTATIONS_FLAG).isPresent()
|
||||
? Optional.of(flags.getListOrEmpty(INCLUDED_ANNOTATIONS_FLAG))
|
||||
: Optional.empty(),
|
||||
excludedAnnotations(flags));
|
||||
flags.getList(INCLUDED_ANNOTATIONS_FLAG), excludedAnnotations(flags));
|
||||
}
|
||||
|
||||
private static ImmutableList<String> excludedAnnotations(ErrorProneFlags flags) {
|
||||
Set<String> exclusions = new HashSet<>();
|
||||
exclusions.addAll(Flags.getList(flags, EXCLUDED_ANNOTATIONS_FLAG));
|
||||
flags.getList(EXCLUDED_ANNOTATIONS_FLAG).ifPresent(exclusions::addAll);
|
||||
exclusions.addAll(BLACKLISTED_ANNOTATIONS);
|
||||
return ImmutableList.copyOf(exclusions);
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
|
||||
import static com.sun.tools.javac.code.TypeAnnotations.AnnotationType.DECLARATION;
|
||||
import static com.sun.tools.javac.code.TypeAnnotations.AnnotationType.TYPE;
|
||||
import static java.util.Comparator.comparing;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.VerifyException;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
import com.google.errorprone.fixes.Fix;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import com.sun.tools.javac.code.TypeAnnotations.AnnotationType;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags annotations that are not lexicographically sorted.
|
||||
*
|
||||
* <p>The idea behind this checker is that maintaining a sorted sequence simplifies conflict
|
||||
* resolution, and can even avoid it if two branches add the same annotation.
|
||||
*/
|
||||
// XXX: Currently this checker only flags method-level annotations. It should likely also flag
|
||||
// type-, field- and parameter-level annotations.
|
||||
// XXX: Duplicate entries are often a mistake. Consider introducing a similar `BugChecker` that
|
||||
// flags duplicates.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Sort annotations lexicographically where possible",
|
||||
link = BUG_PATTERNS_BASE_URL + "LexicographicalAnnotationListing",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
public final class LexicographicalAnnotationListing extends BugChecker
|
||||
implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* A comparator that minimally reorders {@link AnnotationType}s, such that declaration annotations
|
||||
* are placed before type annotations.
|
||||
*/
|
||||
@SuppressWarnings({
|
||||
"java:S1067",
|
||||
"java:S3358"
|
||||
} /* Avoiding the nested ternary operator hurts readability. */)
|
||||
private static final Comparator<@Nullable AnnotationType> BY_ANNOTATION_TYPE =
|
||||
(a, b) ->
|
||||
(a == null || a == DECLARATION) && b == TYPE
|
||||
? -1
|
||||
: (a == TYPE && b == DECLARATION ? 1 : 0);
|
||||
|
||||
/** Instantiates a new {@link LexicographicalAnnotationListing} instance. */
|
||||
public LexicographicalAnnotationListing() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
List<? extends AnnotationTree> originalOrdering = tree.getModifiers().getAnnotations();
|
||||
if (originalOrdering.size() < 2) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
ImmutableList<? extends AnnotationTree> sortedAnnotations =
|
||||
sort(originalOrdering, ASTHelpers.getSymbol(tree), state);
|
||||
if (originalOrdering.equals(sortedAnnotations)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return describeMatch(
|
||||
originalOrdering.get(0), fixOrdering(originalOrdering, sortedAnnotations, state));
|
||||
}
|
||||
|
||||
private static ImmutableList<? extends AnnotationTree> sort(
|
||||
List<? extends AnnotationTree> annotations, Symbol symbol, VisitorState state) {
|
||||
return annotations.stream()
|
||||
.sorted(
|
||||
comparing(
|
||||
(AnnotationTree annotation) ->
|
||||
ASTHelpers.getAnnotationType(annotation, symbol, state),
|
||||
BY_ANNOTATION_TYPE)
|
||||
.thenComparing(annotation -> SourceCode.treeToString(annotation, state)))
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
private static Fix fixOrdering(
|
||||
List<? extends AnnotationTree> originalAnnotations,
|
||||
ImmutableList<? extends AnnotationTree> sortedAnnotations,
|
||||
VisitorState state) {
|
||||
return Streams.zip(
|
||||
originalAnnotations.stream(),
|
||||
sortedAnnotations.stream(),
|
||||
(original, replacement) ->
|
||||
SuggestedFix.builder()
|
||||
.replace(original, SourceCode.treeToString(replacement, state)))
|
||||
.reduce(SuggestedFix.Builder::merge)
|
||||
.map(SuggestedFix.Builder::build)
|
||||
.orElseThrow(() -> new VerifyException("No annotations were provided"));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.utils;
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
@@ -15,27 +15,13 @@ import java.util.regex.Pattern;
|
||||
/** A method invocation expression {@link Matcher} factory. */
|
||||
// XXX: Document better. The expressions accepted here could also be defined using `MethodMatchers`.
|
||||
// So explain why this class is still useful.
|
||||
public final class MethodMatcherFactory {
|
||||
final class MethodMatcherFactory {
|
||||
private static final Splitter ARGUMENT_TYPE_SPLITTER =
|
||||
Splitter.on(',').trimResults().omitEmptyStrings();
|
||||
|
||||
// XXX: Check whether we can use a parser for "standard" Java signatures here. Maybe
|
||||
// `sun.reflect.generics.parser.SignatureParser`?
|
||||
@SuppressWarnings("java:S5998" /* In practice there will be only modest recursion. */)
|
||||
private static final Pattern METHOD_SIGNATURE =
|
||||
Pattern.compile("([^\\s#(,)]+)#([^\\s#(,)]+)\\(((?:[^\\s#(,)]+(?:,[^\\s#(,)]+)*)?)\\)");
|
||||
|
||||
/** Instantiates a new {@link MethodMatcherFactory} instance. */
|
||||
public MethodMatcherFactory() {}
|
||||
|
||||
/**
|
||||
* Creates a {@link Matcher} of methods with any of the given signatures.
|
||||
*
|
||||
* @param signatures The method signatures of interest.
|
||||
* @return A new {@link Matcher} which accepts invocation expressions of any method identified by
|
||||
* the given signatures.
|
||||
*/
|
||||
public Matcher<ExpressionTree> create(Collection<String> signatures) {
|
||||
Matcher<ExpressionTree> create(Collection<String> signatures) {
|
||||
return anyOf(
|
||||
signatures.stream()
|
||||
.map(MethodMatcherFactory::createMethodMatcher)
|
||||
@@ -1,15 +1,14 @@
|
||||
package tech.picnic.errorprone.experimental.bugpatterns;
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.VerifyException;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.LambdaExpressionTreeMatcher;
|
||||
@@ -27,6 +26,7 @@ import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.ParenthesizedTree;
|
||||
import com.sun.source.tree.ReturnTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
@@ -35,7 +35,7 @@ import java.util.Optional;
|
||||
import javax.lang.model.element.Name;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags lambda expressions that can be replaced with method references.
|
||||
* A {@link BugChecker} which flags lambda expressions that can be replaced with method references.
|
||||
*/
|
||||
// XXX: Other custom expressions we could rewrite:
|
||||
// - `a -> "str" + a` to `"str"::concat`. But only if `str` is provably non-null.
|
||||
@@ -47,25 +47,17 @@ import javax.lang.model.element.Name;
|
||||
// black-and-white. Maybe we can more closely approximate it?
|
||||
// XXX: With Java 9's introduction of `Predicate.not`, we could write many lambda expressions to
|
||||
// `not(some::reference)`.
|
||||
// XXX: This check is extremely inefficient due to its reliance on `SuggestedFixes.compilesWithFix`.
|
||||
// Palantir's `LambdaMethodReference` check seems to suffer a similar issue at this time.
|
||||
// XXX: Expressions of the form `i -> SomeType.class.isInstance(i)` are not replaced; fix that using
|
||||
// a suitable generalization.
|
||||
// XXX: Consider folding the `IsInstanceLambdaUsage` check of the `error-prone-contrib` module into
|
||||
// this class.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "MethodReferenceUsage",
|
||||
summary = "Prefer method references over lambda expressions",
|
||||
link = BUG_PATTERNS_BASE_URL + "MethodReferenceUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
public final class MethodReferenceUsage extends BugChecker implements LambdaExpressionTreeMatcher {
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.STYLE)
|
||||
public final class MethodReferenceUsageCheck extends BugChecker
|
||||
implements LambdaExpressionTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** Instantiates a new {@link MethodReferenceUsage} instance. */
|
||||
public MethodReferenceUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) {
|
||||
/*
|
||||
@@ -75,27 +67,27 @@ public final class MethodReferenceUsage extends BugChecker implements LambdaExpr
|
||||
*/
|
||||
return constructMethodRef(tree, tree.getBody())
|
||||
.map(SuggestedFix.Builder::build)
|
||||
.filter(
|
||||
fix ->
|
||||
SuggestedFixes.compilesWithFix(
|
||||
fix, state, ImmutableList.of(), /* onlyInSameCompilationUnit= */ true))
|
||||
.filter(fix -> SuggestedFixes.compilesWithFix(fix, state))
|
||||
.map(fix -> describeMatch(tree, fix))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
// XXX: Use switch pattern matching once the targeted JDK supports this.
|
||||
private static Optional<SuggestedFix.Builder> constructMethodRef(
|
||||
LambdaExpressionTree lambdaExpr, Tree subTree) {
|
||||
return switch (subTree.getKind()) {
|
||||
case BLOCK -> constructMethodRef(lambdaExpr, (BlockTree) subTree);
|
||||
case EXPRESSION_STATEMENT ->
|
||||
constructMethodRef(lambdaExpr, ((ExpressionStatementTree) subTree).getExpression());
|
||||
case METHOD_INVOCATION -> constructMethodRef(lambdaExpr, (MethodInvocationTree) subTree);
|
||||
case PARENTHESIZED ->
|
||||
constructMethodRef(lambdaExpr, ((ParenthesizedTree) subTree).getExpression());
|
||||
case RETURN -> constructMethodRef(lambdaExpr, ((ReturnTree) subTree).getExpression());
|
||||
default -> Optional.empty();
|
||||
};
|
||||
switch (subTree.getKind()) {
|
||||
case BLOCK:
|
||||
return constructMethodRef(lambdaExpr, (BlockTree) subTree);
|
||||
case EXPRESSION_STATEMENT:
|
||||
return constructMethodRef(lambdaExpr, ((ExpressionStatementTree) subTree).getExpression());
|
||||
case METHOD_INVOCATION:
|
||||
return constructMethodRef(lambdaExpr, (MethodInvocationTree) subTree);
|
||||
case PARENTHESIZED:
|
||||
return constructMethodRef(lambdaExpr, ((ParenthesizedTree) subTree).getExpression());
|
||||
case RETURN:
|
||||
return constructMethodRef(lambdaExpr, ((ReturnTree) subTree).getExpression());
|
||||
default:
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<SuggestedFix.Builder> constructMethodRef(
|
||||
@@ -105,43 +97,38 @@ public final class MethodReferenceUsage extends BugChecker implements LambdaExpr
|
||||
.flatMap(statements -> constructMethodRef(lambdaExpr, statements.get(0)));
|
||||
}
|
||||
|
||||
// XXX: Replace nested `Optional` usage.
|
||||
@SuppressWarnings("NestedOptionals")
|
||||
private static Optional<SuggestedFix.Builder> constructMethodRef(
|
||||
LambdaExpressionTree lambdaExpr, MethodInvocationTree subTree) {
|
||||
return matchArguments(lambdaExpr, subTree)
|
||||
.flatMap(expectedInstance -> constructMethodRef(lambdaExpr, subTree, expectedInstance));
|
||||
}
|
||||
|
||||
// XXX: Review whether to use switch pattern matching once the targeted JDK supports this.
|
||||
private static Optional<SuggestedFix.Builder> constructMethodRef(
|
||||
LambdaExpressionTree lambdaExpr,
|
||||
MethodInvocationTree subTree,
|
||||
Optional<Name> expectedInstance) {
|
||||
ExpressionTree methodSelect = subTree.getMethodSelect();
|
||||
|
||||
if (methodSelect instanceof IdentifierTree) {
|
||||
if (expectedInstance.isPresent()) {
|
||||
/* Direct method call; there is no matching "implicit parameter". */
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Symbol sym = ASTHelpers.getSymbol(methodSelect);
|
||||
return ASTHelpers.isStatic(sym)
|
||||
? constructFix(lambdaExpr, sym.owner, methodSelect)
|
||||
: constructFix(lambdaExpr, "this", methodSelect);
|
||||
switch (methodSelect.getKind()) {
|
||||
case IDENTIFIER:
|
||||
if (expectedInstance.isPresent()) {
|
||||
/* Direct method call; there is no matching "implicit parameter". */
|
||||
return Optional.empty();
|
||||
}
|
||||
Symbol sym = ASTHelpers.getSymbol(methodSelect);
|
||||
if (!sym.isStatic()) {
|
||||
return constructFix(lambdaExpr, "this", methodSelect);
|
||||
}
|
||||
return constructFix(lambdaExpr, sym.owner, methodSelect);
|
||||
case MEMBER_SELECT:
|
||||
return constructMethodRef(lambdaExpr, (MemberSelectTree) methodSelect, expectedInstance);
|
||||
default:
|
||||
throw new VerifyException("Unexpected type of expression: " + methodSelect.getKind());
|
||||
}
|
||||
|
||||
if (methodSelect instanceof MemberSelectTree memberSelect) {
|
||||
return constructMethodRef(lambdaExpr, memberSelect, expectedInstance);
|
||||
}
|
||||
|
||||
throw new VerifyException("Unexpected type of expression: " + methodSelect.getKind());
|
||||
}
|
||||
|
||||
private static Optional<SuggestedFix.Builder> constructMethodRef(
|
||||
LambdaExpressionTree lambdaExpr, MemberSelectTree subTree, Optional<Name> expectedInstance) {
|
||||
if (!(subTree.getExpression() instanceof IdentifierTree identifier)) {
|
||||
if (subTree.getExpression().getKind() != Kind.IDENTIFIER) {
|
||||
// XXX: Could be parenthesized. Handle. Also in other classes.
|
||||
/*
|
||||
* Only suggest a replacement if the method select's expression provably doesn't have
|
||||
@@ -150,12 +137,12 @@ public final class MethodReferenceUsage extends BugChecker implements LambdaExpr
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Name lhs = identifier.getName();
|
||||
Name lhs = ((IdentifierTree) subTree.getExpression()).getName();
|
||||
if (expectedInstance.isEmpty()) {
|
||||
return constructFix(lambdaExpr, lhs, subTree.getIdentifier());
|
||||
}
|
||||
|
||||
Type lhsType = ASTHelpers.getType(identifier);
|
||||
Type lhsType = ASTHelpers.getType(subTree.getExpression());
|
||||
if (lhsType == null || !expectedInstance.orElseThrow().equals(lhs)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
@@ -166,8 +153,6 @@ public final class MethodReferenceUsage extends BugChecker implements LambdaExpr
|
||||
return constructFix(lambdaExpr, lhsType.tsym, subTree.getIdentifier());
|
||||
}
|
||||
|
||||
// XXX: Refactor or replace inner `Optional` with a custom type.
|
||||
@SuppressWarnings("NestedOptionals")
|
||||
private static Optional<Optional<Name>> matchArguments(
|
||||
LambdaExpressionTree lambdaExpr, MethodInvocationTree subTree) {
|
||||
ImmutableList<Name> expectedArguments = getVariables(lambdaExpr);
|
||||
@@ -180,8 +165,8 @@ public final class MethodReferenceUsage extends BugChecker implements LambdaExpr
|
||||
|
||||
for (int i = 0; i < args.size(); i++) {
|
||||
ExpressionTree arg = args.get(i);
|
||||
if (!(arg instanceof IdentifierTree identifier)
|
||||
|| !identifier.getName().equals(expectedArguments.get(i + diff))) {
|
||||
if (arg.getKind() != Kind.IDENTIFIER
|
||||
|| !((IdentifierTree) arg).getName().equals(expectedArguments.get(i + diff))) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -193,14 +178,12 @@ public final class MethodReferenceUsage extends BugChecker implements LambdaExpr
|
||||
return tree.getParameters().stream().map(VariableTree::getName).collect(toImmutableList());
|
||||
}
|
||||
|
||||
// XXX: Resolve this suppression.
|
||||
@SuppressWarnings("UnqualifiedSuggestedFixImport")
|
||||
private static Optional<SuggestedFix.Builder> constructFix(
|
||||
LambdaExpressionTree lambdaExpr, Symbol target, Object methodName) {
|
||||
Name sName = target.getSimpleName();
|
||||
Optional<SuggestedFix.Builder> fix = constructFix(lambdaExpr, sName, methodName);
|
||||
|
||||
if (!"java.lang".equals(ASTHelpers.enclosingPackage(target).toString())) {
|
||||
if (!"java.lang".equals(target.packge().toString())) {
|
||||
Name fqName = target.getQualifiedName();
|
||||
if (!sName.equals(fqName)) {
|
||||
return fix.map(b -> b.addImport(fqName.toString()));
|
||||
@@ -0,0 +1,53 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.MultiMatcher;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
|
||||
/** A {@link BugChecker} that flags likely missing Refaster annotations. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "MissingRefasterAnnotation",
|
||||
summary = "The Refaster template contains a method without any Refaster annotations",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.WARNING,
|
||||
tags = StandardTags.LIKELY_ERROR)
|
||||
public final class MissingRefasterAnnotationCheck extends BugChecker implements ClassTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final MultiMatcher<Tree, AnnotationTree> HAS_REFASTER_ANNOTATION =
|
||||
annotations(
|
||||
AT_LEAST_ONE,
|
||||
anyOf(
|
||||
isType("com.google.errorprone.refaster.annotation.Placeholder"),
|
||||
isType("com.google.errorprone.refaster.annotation.BeforeTemplate"),
|
||||
isType("com.google.errorprone.refaster.annotation.AfterTemplate")));
|
||||
|
||||
@Override
|
||||
public Description matchClass(ClassTree tree, VisitorState state) {
|
||||
long methodTypes =
|
||||
tree.getMembers().stream()
|
||||
.filter(member -> member.getKind() == Tree.Kind.METHOD)
|
||||
.map(MethodTree.class::cast)
|
||||
.map(method -> HAS_REFASTER_ANNOTATION.matches(method, state))
|
||||
.distinct()
|
||||
.count();
|
||||
|
||||
return methodTypes < 2 ? Description.NO_MATCH : buildDescription(tree).build();
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
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.utils.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.utils.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.getCanonicalName()), 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));
|
||||
}
|
||||
|
||||
// XXX: Use switch pattern matching once the targeted JDK supports this.
|
||||
private static boolean isTypeDerivableFromContext(MethodInvocationTree tree, VisitorState state) {
|
||||
Tree parent = state.getPath().getParentPath().getLeaf();
|
||||
return switch (parent.getKind()) {
|
||||
case VARIABLE ->
|
||||
!ASTHelpers.hasImplicitType((VariableTree) parent, state)
|
||||
&& MoreASTHelpers.areSameType(tree, parent, state);
|
||||
case ASSIGNMENT -> MoreASTHelpers.areSameType(tree, parent, state);
|
||||
case RETURN ->
|
||||
MoreASTHelpers.findMethodExitedOnReturn(state)
|
||||
.filter(m -> MoreASTHelpers.areSameType(tree, m.getReturnType(), state))
|
||||
.isPresent();
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
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.staticMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import java.util.List;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags method invocations for which all arguments are wrapped using
|
||||
* {@link org.mockito.Mockito#eq}; this is redundant.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Don't unnecessarily use Mockito's `eq(...)`",
|
||||
link = BUG_PATTERNS_BASE_URL + "MockitoStubbing",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class MockitoStubbing extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> MOCKITO_EQ_METHOD =
|
||||
staticMethod().onClass("org.mockito.ArgumentMatchers").named("eq");
|
||||
|
||||
/** Instantiates a new {@link MockitoStubbing} instance. */
|
||||
public MockitoStubbing() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
List<? extends ExpressionTree> arguments = tree.getArguments();
|
||||
if (arguments.isEmpty() || !arguments.stream().allMatch(arg -> isEqInvocation(arg, state))) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix.Builder suggestedFix = SuggestedFix.builder();
|
||||
for (ExpressionTree arg : arguments) {
|
||||
suggestedFix.replace(
|
||||
arg,
|
||||
SourceCode.treeToString(
|
||||
Iterables.getOnlyElement(((MethodInvocationTree) arg).getArguments()), state));
|
||||
}
|
||||
|
||||
return describeMatch(tree, suggestedFix.build());
|
||||
}
|
||||
|
||||
private static boolean isEqInvocation(ExpressionTree tree, VisitorState state) {
|
||||
return tree instanceof MethodInvocationTree && MOCKITO_EQ_METHOD.matches(tree, state);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
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.PERFORMANCE;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static tech.picnic.errorprone.utils.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.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags usages of MongoDB {@code $text} filter usages.
|
||||
*
|
||||
* @see <a href="https://www.mongodb.com/docs/manual/text-search/">MongoDB Text Search</a>
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"""
|
||||
Avoid MongoDB's `$text` filter operator, as it can trigger heavy queries and even cause \
|
||||
the server to run out of memory""",
|
||||
link = BUG_PATTERNS_BASE_URL + "MongoDBTextFilterUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = PERFORMANCE)
|
||||
public final class MongoDBTextFilterUsage extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> MONGO_FILTERS_TEXT_METHOD =
|
||||
staticMethod().onClass("com.mongodb.client.model.Filters").named("text");
|
||||
|
||||
/** Instantiates a new {@link MongoDBTextFilterUsage} instance. */
|
||||
public MongoDBTextFilterUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
return MONGO_FILTERS_TEXT_METHOD.matches(tree, state)
|
||||
? describeMatch(tree)
|
||||
: Description.NO_MATCH;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreMatchers.isSubTypeOf;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.generic;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.raw;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.subOf;
|
||||
|
||||
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.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Supplier;
|
||||
import com.google.errorprone.suppliers.Suppliers;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.util.Optional;
|
||||
|
||||
/** A {@link BugChecker} that flags nesting of {@link Optional Optionals}. */
|
||||
// XXX: Extend this checker to also flag method return types and variable/field types.
|
||||
// XXX: Consider generalizing this checker and `NestedPublishers` to a single `NestedMonad` check,
|
||||
// which e.g. also flags nested `Stream`s. Alternatively, combine these and other checkers into an
|
||||
// even more generic `ConfusingType` checker.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"Avoid nesting `Optional`s inside `Optional`s; the resultant code is hard to reason about",
|
||||
link = BUG_PATTERNS_BASE_URL + "NestedOptionals",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class NestedOptionals extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Supplier<Type> OPTIONAL = Suppliers.typeFromClass(Optional.class);
|
||||
private static final Matcher<Tree> IS_OPTIONAL_OF_OPTIONAL =
|
||||
isSubTypeOf(VisitorState.memoize(generic(OPTIONAL, subOf(raw(OPTIONAL)))));
|
||||
|
||||
/** Instantiates a new {@link NestedOptionals} instance. */
|
||||
public NestedOptionals() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
return IS_OPTIONAL_OF_OPTIONAL.matches(tree, state)
|
||||
? describeMatch(tree)
|
||||
: Description.NO_MATCH;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static com.google.errorprone.matchers.Matchers.typePredicateMatcher;
|
||||
import static com.google.errorprone.predicates.TypePredicates.allOf;
|
||||
import static com.google.errorprone.predicates.TypePredicates.not;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreTypePredicates.hasTypeParameter;
|
||||
import static tech.picnic.errorprone.utils.MoreTypePredicates.isSubTypeOf;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.generic;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.raw;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.subOf;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.type;
|
||||
|
||||
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.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Supplier;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@code Publisher<? extends Publisher>} instances, unless the
|
||||
* nested {@link org.reactivestreams.Publisher} is a {@link reactor.core.publisher.GroupedFlux}.
|
||||
*/
|
||||
// XXX: See the `NestedOptionals` check for some ideas on how to generalize this kind of checker.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"""
|
||||
Avoid `Publisher`s that emit other `Publishers`s; the resultant code is hard to reason \
|
||||
about""",
|
||||
link = BUG_PATTERNS_BASE_URL + "NestedPublishers",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class NestedPublishers extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Supplier<Type> PUBLISHER = type("org.reactivestreams.Publisher");
|
||||
private static final Matcher<ExpressionTree> IS_NON_GROUPED_PUBLISHER_OF_PUBLISHERS =
|
||||
typePredicateMatcher(
|
||||
allOf(
|
||||
isSubTypeOf(generic(PUBLISHER, subOf(raw(PUBLISHER)))),
|
||||
not(
|
||||
hasTypeParameter(
|
||||
0, isSubTypeOf(raw(type("reactor.core.publisher.GroupedFlux")))))));
|
||||
|
||||
/** Instantiates a new {@link NestedPublishers} instance. */
|
||||
public NestedPublishers() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
return IS_NON_GROUPED_PUBLISHER_OF_PUBLISHERS.matches(tree, state)
|
||||
? describeMatch(tree)
|
||||
: Description.NO_MATCH;
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static tech.picnic.errorprone.utils.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.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 java.util.function.BiFunction;
|
||||
import reactor.core.publisher.Mono;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@link Mono} operations that are known to be vacuous, given that
|
||||
* they are invoked on a {@link Mono} that is known not to complete empty.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Avoid vacuous operations on known non-empty `Mono`s",
|
||||
link = BUG_PATTERNS_BASE_URL + "NonEmptyMono",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = SIMPLIFICATION)
|
||||
// XXX: This check does not simplify `someFlux.defaultIfEmpty(T).{defaultIfEmpty(T),hasElements()}`,
|
||||
// as `someFlux.defaultIfEmpty(T)` yields a `Flux` rather than a `Mono`. Consider adding support for
|
||||
// these cases.
|
||||
// XXX: Given more advanced analysis many more expressions could be flagged. Consider
|
||||
// `Mono.just(someValue)`, `Flux.just(someNonEmptySequence)`,
|
||||
// `someMono.switchIfEmpty(someProvablyNonEmptyMono)` and many other variants.
|
||||
// XXX: Consider implementing a similar check for `Publisher`s that are known to complete without
|
||||
// emitting a value (e.g. `Mono.empty()`, `someFlux.then()`, ...), or known not to complete normally
|
||||
// (`Mono.never()`, `someFlux.repeat()`, `Mono.error(...)`, ...). The latter category could
|
||||
// potentially be split out further.
|
||||
@SuppressWarnings("java:S1192" /* Factoring out repeated method names impacts readability. */)
|
||||
public final class NonEmptyMono extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> MONO_SIZE_CHECK =
|
||||
instanceMethod()
|
||||
.onDescendantOf("reactor.core.publisher.Mono")
|
||||
.namedAnyOf("defaultIfEmpty", "single", "switchIfEmpty");
|
||||
private static final Matcher<ExpressionTree> NON_EMPTY_MONO =
|
||||
anyOf(
|
||||
instanceMethod()
|
||||
.onDescendantOf("reactor.core.publisher.Flux")
|
||||
.namedAnyOf(
|
||||
"all",
|
||||
"any",
|
||||
"collect",
|
||||
"collectList",
|
||||
"collectMap",
|
||||
"collectMultimap",
|
||||
"collectSortedList",
|
||||
"count",
|
||||
"elementAt",
|
||||
"hasElement",
|
||||
"hasElements",
|
||||
"last",
|
||||
"reduceWith",
|
||||
"single"),
|
||||
instanceMethod()
|
||||
.onDescendantOf("reactor.core.publisher.Flux")
|
||||
.named("reduce")
|
||||
.withParameters(Object.class.getCanonicalName(), BiFunction.class.getCanonicalName()),
|
||||
instanceMethod()
|
||||
.onDescendantOf("reactor.core.publisher.Mono")
|
||||
.namedAnyOf("defaultIfEmpty", "hasElement", "single"));
|
||||
|
||||
/** Instantiates a new {@link NonEmptyMono} instance. */
|
||||
public NonEmptyMono() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!MONO_SIZE_CHECK.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
ExpressionTree receiver = ASTHelpers.getReceiver(tree);
|
||||
if (!NON_EMPTY_MONO.matches(receiver, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return describeMatch(
|
||||
tree, SuggestedFix.replace(tree, SourceCode.treeToString(receiver, state)));
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
|
||||
import static tech.picnic.errorprone.bugpatterns.StaticImport.STATIC_IMPORT_CANDIDATE_MEMBERS;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSetMultimap;
|
||||
import com.google.common.collect.ImmutableTable;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.CompilationUnitTree;
|
||||
import com.sun.source.tree.IdentifierTree;
|
||||
import com.sun.source.tree.ImportTree;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.util.TreeScanner;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import java.time.Clock;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags static imports of type members that should *not* be statically
|
||||
* imported.
|
||||
*/
|
||||
// XXX: This check is closely linked to `StaticImport`. Consider merging the two.
|
||||
// XXX: Add suppression support. If qualification of one more more identifiers is suppressed, then
|
||||
// the associated static import should *not* be removed.
|
||||
// XXX: Also introduce logic that disallows statically importing `ZoneOffset.ofHours` and other
|
||||
// `ofXXX`-style methods.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Member should not be statically imported",
|
||||
link = BUG_PATTERNS_BASE_URL + "NonStaticImport",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
public final class NonStaticImport extends BugChecker implements CompilationUnitTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Types whose members should not be statically imported, unless exempted by {@link
|
||||
* StaticImport#STATIC_IMPORT_CANDIDATE_MEMBERS}.
|
||||
*
|
||||
* <p>Types listed here should be mutually exclusive with {@link
|
||||
* StaticImport#STATIC_IMPORT_CANDIDATE_TYPES}.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final ImmutableSet<String> NON_STATIC_IMPORT_CANDIDATE_TYPES =
|
||||
ImmutableSet.of(
|
||||
ASTHelpers.class.getCanonicalName(),
|
||||
Clock.class.getCanonicalName(),
|
||||
Strings.class.getCanonicalName(),
|
||||
VisitorState.class.getCanonicalName(),
|
||||
ZoneOffset.class.getCanonicalName(),
|
||||
"com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode",
|
||||
"reactor.core.publisher.Flux",
|
||||
"reactor.core.publisher.Mono");
|
||||
|
||||
/**
|
||||
* Type members that should never be statically imported.
|
||||
*
|
||||
* <p>Please note that:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Types listed by {@link #NON_STATIC_IMPORT_CANDIDATE_TYPES} and members listed by {@link
|
||||
* #NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS} should be omitted from this collection.
|
||||
* <li>This collection should be mutually exclusive with {@link
|
||||
* StaticImport#STATIC_IMPORT_CANDIDATE_MEMBERS}.
|
||||
* </ul>
|
||||
*/
|
||||
// XXX: Perhaps the set of exempted `java.util.Collections` methods is too strict. For now any
|
||||
// method name that could be considered "too vague" or could conceivably mean something else in a
|
||||
// specific context is left out.
|
||||
static final ImmutableSetMultimap<String, String> NON_STATIC_IMPORT_CANDIDATE_MEMBERS =
|
||||
ImmutableSetMultimap.<String, String>builder()
|
||||
.putAll(
|
||||
Collections.class.getCanonicalName(),
|
||||
"addAll",
|
||||
"copy",
|
||||
"fill",
|
||||
"list",
|
||||
"max",
|
||||
"min",
|
||||
"nCopies",
|
||||
"rotate",
|
||||
"sort",
|
||||
"swap")
|
||||
.put(Locale.class.getCanonicalName(), "ROOT")
|
||||
.put(Optional.class.getCanonicalName(), "empty")
|
||||
.putAll(Pattern.class.getCanonicalName(), "compile", "matches", "quote")
|
||||
.put(Predicates.class.getCanonicalName(), "contains")
|
||||
.put("org.springframework.http.MediaType", "ALL")
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Identifiers that should never be statically imported.
|
||||
*
|
||||
* <p>Please note that:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Identifiers listed by {@link StaticImport#STATIC_IMPORT_CANDIDATE_MEMBERS} should be
|
||||
* mutually exclusive with identifiers listed here.
|
||||
* <li>This list should contain a superset of the identifiers flagged by {@link
|
||||
* com.google.errorprone.bugpatterns.BadImport}.
|
||||
* </ul>
|
||||
*/
|
||||
static final ImmutableSet<String> NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS =
|
||||
ImmutableSet.of(
|
||||
"builder",
|
||||
"copyOf",
|
||||
"create",
|
||||
"from",
|
||||
"getDefaultInstance",
|
||||
"INSTANCE",
|
||||
"MAX",
|
||||
"MAX_VALUE",
|
||||
"MIN",
|
||||
"MIN_VALUE",
|
||||
"newBuilder",
|
||||
"newInstance",
|
||||
"of",
|
||||
"parse",
|
||||
"valueOf");
|
||||
|
||||
/** Instantiates a new {@link NonStaticImport} instance. */
|
||||
public NonStaticImport() {}
|
||||
|
||||
@Override
|
||||
public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) {
|
||||
ImmutableTable<String, String, UndesiredStaticImport> undesiredStaticImports =
|
||||
getUndesiredStaticImports(tree, state);
|
||||
|
||||
if (!undesiredStaticImports.isEmpty()) {
|
||||
replaceUndesiredStaticImportUsages(tree, undesiredStaticImports, state);
|
||||
|
||||
for (UndesiredStaticImport staticImport : undesiredStaticImports.values()) {
|
||||
state.reportMatch(
|
||||
describeMatch(staticImport.importTree(), staticImport.fixBuilder().build()));
|
||||
}
|
||||
}
|
||||
|
||||
/* Any violations have been flagged against the offending static import statement. */
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
private static ImmutableTable<String, String, UndesiredStaticImport> getUndesiredStaticImports(
|
||||
CompilationUnitTree tree, VisitorState state) {
|
||||
ImmutableTable.Builder<String, String, UndesiredStaticImport> imports =
|
||||
ImmutableTable.builder();
|
||||
for (ImportTree importTree : tree.getImports()) {
|
||||
Tree qualifiedIdentifier = importTree.getQualifiedIdentifier();
|
||||
if (importTree.isStatic() && qualifiedIdentifier instanceof MemberSelectTree memberSelect) {
|
||||
String type = SourceCode.treeToString(memberSelect.getExpression(), state);
|
||||
String member = memberSelect.getIdentifier().toString();
|
||||
if (shouldNotBeStaticallyImported(type, member)) {
|
||||
imports.put(
|
||||
type,
|
||||
member,
|
||||
new AutoValue_NonStaticImport_UndesiredStaticImport(
|
||||
importTree, SuggestedFix.builder().removeStaticImport(type + '.' + member)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imports.build();
|
||||
}
|
||||
|
||||
private static boolean shouldNotBeStaticallyImported(String type, String member) {
|
||||
return (NON_STATIC_IMPORT_CANDIDATE_TYPES.contains(type)
|
||||
&& !STATIC_IMPORT_CANDIDATE_MEMBERS.containsEntry(type, member))
|
||||
|| NON_STATIC_IMPORT_CANDIDATE_MEMBERS.containsEntry(type, member)
|
||||
|| NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS.contains(member);
|
||||
}
|
||||
|
||||
private static void replaceUndesiredStaticImportUsages(
|
||||
CompilationUnitTree tree,
|
||||
ImmutableTable<String, String, UndesiredStaticImport> undesiredStaticImports,
|
||||
VisitorState state) {
|
||||
new TreeScanner<@Nullable Void, @Nullable Void>() {
|
||||
@Override
|
||||
public @Nullable Void visitIdentifier(IdentifierTree node, @Nullable Void unused) {
|
||||
Symbol symbol = ASTHelpers.getSymbol(node);
|
||||
if (symbol != null) {
|
||||
UndesiredStaticImport staticImport =
|
||||
undesiredStaticImports.get(
|
||||
symbol.owner.getQualifiedName().toString(), symbol.name.toString());
|
||||
if (staticImport != null) {
|
||||
SuggestedFix.Builder fix = staticImport.fixBuilder();
|
||||
fix.prefixWith(node, SuggestedFixes.qualifyType(state, fix, symbol.owner) + '.');
|
||||
}
|
||||
}
|
||||
|
||||
return super.visitIdentifier(node, null);
|
||||
}
|
||||
}.scan(tree, null);
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class UndesiredStaticImport {
|
||||
abstract ImportTree importTree();
|
||||
|
||||
abstract SuggestedFix.Builder fixBuilder();
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.PERFORMANCE;
|
||||
import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
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.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.refaster.Refaster;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import tech.picnic.errorprone.refaster.matchers.RequiresComputation;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags arguments to {@link Optional#orElse(Object)} that should be
|
||||
* deferred using {@link Optional#orElseGet(Supplier)}.
|
||||
*
|
||||
* <p>The suggested fix assumes that the argument to {@code orElse} does not have side effects. If
|
||||
* it does, the suggested fix changes the program's semantics. Such fragile code must instead be
|
||||
* refactored such that the side-effectful code does not appear accidental.
|
||||
*/
|
||||
// XXX: This rule may introduce a compilation error: the `value` expression may reference a
|
||||
// non-effectively final variable, which is not allowed in the replacement lambda expression.
|
||||
// Review whether a `@Matcher` can be used to avoid this.
|
||||
// XXX: Once the `MethodReferenceUsageCheck` bug checker becomes generally usable, consider leaving
|
||||
// the method reference cleanup to that check, and express the remainder of the logic in this class
|
||||
// using a Refaster template, i.c.w. a `@NotMatches(RequiresComputation.class)` constraint.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"""
|
||||
Prefer `Optional#orElseGet` over `Optional#orElse` if the fallback requires additional \
|
||||
computation""",
|
||||
linkType = NONE,
|
||||
severity = WARNING,
|
||||
tags = PERFORMANCE)
|
||||
public final class OptionalOrElseGet extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> REQUIRES_COMPUTATION = new RequiresComputation();
|
||||
private static final Matcher<ExpressionTree> OPTIONAL_OR_ELSE_METHOD =
|
||||
instanceMethod().onExactClass(Optional.class.getCanonicalName()).namedAnyOf("orElse");
|
||||
// XXX: Also exclude invocations of `@Placeholder`-annotated methods.
|
||||
private static final Matcher<ExpressionTree> REFASTER_METHOD =
|
||||
staticMethod().onClass(Refaster.class.getCanonicalName());
|
||||
|
||||
/** Instantiates a new {@link OptionalOrElseGet} instance. */
|
||||
public OptionalOrElseGet() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!OPTIONAL_OR_ELSE_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
ExpressionTree argument = Iterables.getOnlyElement(tree.getArguments());
|
||||
if (!REQUIRES_COMPUTATION.matches(argument, state)
|
||||
|| REFASTER_METHOD.matches(argument, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
/*
|
||||
* We have a match. Construct the method reference or lambda expression to be passed to the
|
||||
* replacement `#orElseGet` invocation.
|
||||
*/
|
||||
String newArgument =
|
||||
tryMethodReferenceConversion(argument, state)
|
||||
.orElseGet(() -> "() -> " + SourceCode.treeToString(argument, state));
|
||||
|
||||
/* Construct the suggested fix, replacing the method invocation and its argument. */
|
||||
SuggestedFix fix =
|
||||
SuggestedFix.builder()
|
||||
.merge(SuggestedFixes.renameMethodInvocation(tree, "orElseGet", state))
|
||||
.replace(argument, newArgument)
|
||||
.build();
|
||||
|
||||
return describeMatch(tree, fix);
|
||||
}
|
||||
|
||||
/** Returns the nullary method reference matching the given expression, if any. */
|
||||
private static Optional<String> tryMethodReferenceConversion(
|
||||
ExpressionTree tree, VisitorState state) {
|
||||
if (!(tree instanceof MethodInvocationTree methodInvocation)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (!methodInvocation.getArguments().isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (!(methodInvocation.getMethodSelect() instanceof MemberSelectTree memberSelect)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (REQUIRES_COMPUTATION.matches(memberSelect.getExpression(), state)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(
|
||||
SourceCode.treeToString(memberSelect.getExpression(), state)
|
||||
+ "::"
|
||||
+ (methodInvocation.getTypeArguments().isEmpty()
|
||||
? ""
|
||||
: methodInvocation.getTypeArguments().stream()
|
||||
.map(arg -> SourceCode.treeToString(arg, state))
|
||||
.collect(joining(",", "<", ">")))
|
||||
+ memberSelect.getIdentifier());
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.PERFORMANCE;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.VerifyException;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.Fix;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.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.IdentifierTree;
|
||||
import com.sun.source.tree.LambdaExpressionTree;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Symtab;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import com.sun.tools.javac.code.Types;
|
||||
import com.sun.tools.javac.tree.JCTree.JCMemberReference;
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@code Comparator#comparing*} invocations that can be replaced
|
||||
* with an equivalent alternative so as to avoid unnecessary (un)boxing.
|
||||
*/
|
||||
// XXX: Add more documentation. Explain how this is useful in the face of refactoring to more
|
||||
// specific types.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"""
|
||||
Ensure invocations of `Comparator#comparing{,Double,Int,Long}` match the return type of \
|
||||
the provided function""",
|
||||
link = BUG_PATTERNS_BASE_URL + "PrimitiveComparison",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = PERFORMANCE)
|
||||
@SuppressWarnings("java:S1192" /* Factoring out repeated method names impacts readability. */)
|
||||
public final class PrimitiveComparison extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> STATIC_COMPARISON_METHOD =
|
||||
anyOf(
|
||||
staticMethod()
|
||||
.onClass(Comparator.class.getCanonicalName())
|
||||
.namedAnyOf("comparingInt", "comparingLong", "comparingDouble"),
|
||||
staticMethod()
|
||||
.onClass(Comparator.class.getCanonicalName())
|
||||
.named("comparing")
|
||||
.withParameters(Function.class.getCanonicalName()));
|
||||
private static final Matcher<ExpressionTree> INSTANCE_COMPARISON_METHOD =
|
||||
anyOf(
|
||||
instanceMethod()
|
||||
.onDescendantOf(Comparator.class.getCanonicalName())
|
||||
.namedAnyOf("thenComparingInt", "thenComparingLong", "thenComparingDouble"),
|
||||
instanceMethod()
|
||||
.onDescendantOf(Comparator.class.getCanonicalName())
|
||||
.named("thenComparing")
|
||||
.withParameters(Function.class.getCanonicalName()));
|
||||
|
||||
/** Instantiates a new {@link PrimitiveComparison} instance. */
|
||||
public PrimitiveComparison() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
boolean isStatic = STATIC_COMPARISON_METHOD.matches(tree, state);
|
||||
if (!isStatic && !INSTANCE_COMPARISON_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return getPotentiallyBoxedReturnType(tree.getArguments().get(0))
|
||||
.flatMap(cmpType -> attemptMethodInvocationReplacement(tree, cmpType, isStatic, state))
|
||||
.map(fix -> describeMatch(tree, fix))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
private static Optional<Fix> attemptMethodInvocationReplacement(
|
||||
MethodInvocationTree tree, Type cmpType, boolean isStatic, VisitorState state) {
|
||||
String actualMethodName = ASTHelpers.getSymbol(tree).getSimpleName().toString();
|
||||
String preferredMethodName = getPreferredMethod(cmpType, isStatic, state);
|
||||
if (actualMethodName.equals(preferredMethodName)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(
|
||||
suggestFix(
|
||||
tree, prefixTypeArgumentsIfRelevant(preferredMethodName, tree, cmpType, state), state));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefixes the given method name with generic type parameters if it replaces a {@code
|
||||
* Comparator#comparing{,Double,Long,Int}} method which also has generic type parameters.
|
||||
*
|
||||
* <p>Such type parameters are retained as they are likely required.
|
||||
*
|
||||
* <p>Note that any type parameter to {@code Comparator#thenComparing} is likely redundant, and in
|
||||
* any case becomes obsolete once that method is replaced with {@code
|
||||
* Comparator#thenComparing{Double,Long,Int}}. Conversion in the opposite direction does not
|
||||
* require the introduction of a generic type parameter.
|
||||
*/
|
||||
private static String prefixTypeArgumentsIfRelevant(
|
||||
String preferredMethodName, MethodInvocationTree tree, Type cmpType, VisitorState state) {
|
||||
if (tree.getTypeArguments().isEmpty() || preferredMethodName.startsWith("then")) {
|
||||
return preferredMethodName;
|
||||
}
|
||||
|
||||
String typeArguments =
|
||||
Stream.concat(
|
||||
Stream.of(SourceCode.treeToString(tree.getTypeArguments().get(0), state)),
|
||||
Stream.of(cmpType.tsym.getSimpleName())
|
||||
.filter(u -> "comparing".equals(preferredMethodName)))
|
||||
.collect(joining(", ", "<", ">"));
|
||||
|
||||
return typeArguments + preferredMethodName;
|
||||
}
|
||||
|
||||
private static String getPreferredMethod(Type cmpType, boolean isStatic, VisitorState state) {
|
||||
Types types = state.getTypes();
|
||||
Symtab symtab = state.getSymtab();
|
||||
|
||||
if (types.isSubtype(cmpType, symtab.intType)) {
|
||||
return isStatic ? "comparingInt" : "thenComparingInt";
|
||||
}
|
||||
|
||||
if (types.isSubtype(cmpType, symtab.longType)) {
|
||||
return isStatic ? "comparingLong" : "thenComparingLong";
|
||||
}
|
||||
|
||||
if (types.isSubtype(cmpType, symtab.doubleType)) {
|
||||
return isStatic ? "comparingDouble" : "thenComparingDouble";
|
||||
}
|
||||
|
||||
return isStatic ? "comparing" : "thenComparing";
|
||||
}
|
||||
|
||||
// XXX: Use switch pattern matching once the targeted JDK supports this.
|
||||
private static Optional<Type> getPotentiallyBoxedReturnType(ExpressionTree tree) {
|
||||
if (tree instanceof LambdaExpressionTree lambdaExpression) {
|
||||
/* Return the lambda expression's actual return type. */
|
||||
return Optional.ofNullable(ASTHelpers.getType(lambdaExpression.getBody()));
|
||||
}
|
||||
|
||||
// XXX: The match against a concrete type and reference to one of its fields is fragile. Do
|
||||
// better.
|
||||
if (tree instanceof JCMemberReference memberReference) {
|
||||
/* Return the method's declared return type. */
|
||||
Type subType = memberReference.referentType;
|
||||
return Optional.of(subType.getReturnType());
|
||||
}
|
||||
|
||||
/* This appears to be a genuine `{,ToInt,ToLong,ToDouble}Function`. */
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// XXX: Use switch pattern matching once the targeted JDK supports this.
|
||||
private static Fix suggestFix(
|
||||
MethodInvocationTree tree, String preferredMethodName, VisitorState state) {
|
||||
ExpressionTree expr = tree.getMethodSelect();
|
||||
|
||||
if (expr instanceof IdentifierTree) {
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder();
|
||||
String replacement =
|
||||
SuggestedFixes.qualifyStaticImport(
|
||||
Comparator.class.getCanonicalName() + '.' + preferredMethodName, fix, state);
|
||||
return fix.replace(expr, replacement).build();
|
||||
}
|
||||
|
||||
if (expr instanceof MemberSelectTree memberSelect) {
|
||||
return SuggestedFix.replace(
|
||||
memberSelect,
|
||||
SourceCode.treeToString(memberSelect.getExpression(), state) + '.' + preferredMethodName);
|
||||
}
|
||||
|
||||
throw new VerifyException("Unexpected type of expression: " + expr.getKind());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static java.util.function.Predicate.not;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.VerifyException;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.Fix;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.LambdaExpressionTree;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Symtab;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import com.sun.tools.javac.code.Types;
|
||||
import com.sun.tools.javac.tree.JCTree.JCMemberReference;
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} which flags {@code Comparator#comparing*} invocations that can be replaced
|
||||
* with an equivalent alternative so as to avoid unnecessary (un)boxing.
|
||||
*/
|
||||
// XXX: Add more documentation. Explain how this is useful in the face of refactoring to more
|
||||
// specific types.
|
||||
// XXX: Change this checker's name?
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "PrimitiveComparison",
|
||||
summary =
|
||||
"Ensure invocations of `Comparator#comparing{,Double,Int,Long}` match the return type"
|
||||
+ " of the provided function",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.WARNING,
|
||||
tags = StandardTags.PERFORMANCE)
|
||||
public final class PrimitiveComparisonCheck extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> STATIC_COMPARISON_METHOD =
|
||||
anyOf(
|
||||
staticMethod()
|
||||
.onClass(Comparator.class.getName())
|
||||
.namedAnyOf("comparingInt", "comparingLong", "comparingDouble"),
|
||||
staticMethod()
|
||||
.onClass(Comparator.class.getName())
|
||||
.named("comparing")
|
||||
.withParameters(Function.class.getName()));
|
||||
private static final Matcher<ExpressionTree> INSTANCE_COMPARISON_METHOD =
|
||||
anyOf(
|
||||
instanceMethod()
|
||||
.onDescendantOf(Comparator.class.getName())
|
||||
.namedAnyOf("thenComparingInt", "thenComparingLong", "thenComparingDouble"),
|
||||
instanceMethod()
|
||||
.onDescendantOf(Comparator.class.getName())
|
||||
.named("thenComparing")
|
||||
.withParameters(Function.class.getName()));
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
boolean isStatic = STATIC_COMPARISON_METHOD.matches(tree, state);
|
||||
if (!isStatic && !INSTANCE_COMPARISON_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return getPotentiallyBoxedReturnType(tree.getArguments().get(0))
|
||||
.flatMap(cmpType -> tryFix(tree, state, cmpType, isStatic))
|
||||
.map(fix -> describeMatch(tree, fix))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
private static Optional<Fix> tryFix(
|
||||
MethodInvocationTree tree, VisitorState state, Type cmpType, boolean isStatic) {
|
||||
return Optional.ofNullable(ASTHelpers.getSymbol(tree))
|
||||
.map(methodSymbol -> methodSymbol.getSimpleName().toString())
|
||||
.flatMap(
|
||||
actualMethodName ->
|
||||
Optional.of(getPreferredMethod(state, cmpType, isStatic))
|
||||
.filter(not(actualMethodName::equals)))
|
||||
.map(preferredMethodName -> suggestFix(tree, preferredMethodName, state));
|
||||
}
|
||||
|
||||
private static String getPreferredMethod(VisitorState state, Type cmpType, boolean isStatic) {
|
||||
Types types = state.getTypes();
|
||||
Symtab symtab = state.getSymtab();
|
||||
|
||||
if (types.isSubtype(cmpType, symtab.intType)) {
|
||||
return isStatic ? "comparingInt" : "thenComparingInt";
|
||||
}
|
||||
|
||||
if (types.isSubtype(cmpType, symtab.longType)) {
|
||||
return isStatic ? "comparingLong" : "thenComparingLong";
|
||||
}
|
||||
|
||||
if (types.isSubtype(cmpType, symtab.doubleType)) {
|
||||
return isStatic ? "comparingDouble" : "thenComparingDouble";
|
||||
}
|
||||
|
||||
return isStatic ? "comparing" : "thenComparing";
|
||||
}
|
||||
|
||||
private static Optional<Type> getPotentiallyBoxedReturnType(ExpressionTree tree) {
|
||||
switch (tree.getKind()) {
|
||||
case LAMBDA_EXPRESSION:
|
||||
/* Return the lambda expression's actual return type. */
|
||||
return Optional.ofNullable(ASTHelpers.getType(((LambdaExpressionTree) tree).getBody()));
|
||||
case MEMBER_REFERENCE:
|
||||
/* Return the method's declared return type. */
|
||||
// XXX: Very fragile. Do better.
|
||||
Type subType2 = ((JCMemberReference) tree).referentType;
|
||||
return Optional.of(subType2.getReturnType());
|
||||
default:
|
||||
/* This appears to be a genuine `{,ToInt,ToLong,ToDouble}Function`. */
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: We drop explicitly specified generic type information. In case the number of type
|
||||
// arguments before and after doesn't match, that's for the better. But if we e.g. replace
|
||||
// `comparingLong` with `comparingInt`, then we should retain it.
|
||||
private static Fix suggestFix(
|
||||
MethodInvocationTree tree, String preferredMethodName, VisitorState state) {
|
||||
ExpressionTree expr = tree.getMethodSelect();
|
||||
switch (expr.getKind()) {
|
||||
case IDENTIFIER:
|
||||
return SuggestedFix.builder()
|
||||
.addStaticImport(Comparator.class.getName() + '.' + preferredMethodName)
|
||||
.replace(expr, preferredMethodName)
|
||||
.build();
|
||||
case MEMBER_SELECT:
|
||||
MemberSelectTree ms = (MemberSelectTree) tree.getMethodSelect();
|
||||
return SuggestedFix.replace(
|
||||
ms, Util.treeToString(ms.getExpression(), state) + '.' + preferredMethodName);
|
||||
default:
|
||||
throw new VerifyException("Unexpected type of expression: " + expr.getKind());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,20 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
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.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.isNonNull;
|
||||
import static com.google.errorprone.matchers.Matchers.isSameType;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Verify;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.primitives.Primitives;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.ErrorProneFlags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
@@ -33,7 +24,6 @@ import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Suppliers;
|
||||
import com.sun.source.tree.BinaryTree;
|
||||
import com.sun.source.tree.CompoundAssignmentTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
@@ -51,117 +41,132 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import javax.inject.Inject;
|
||||
import tech.picnic.errorprone.utils.Flags;
|
||||
import tech.picnic.errorprone.utils.MethodMatcherFactory;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/** A {@link BugChecker} that flags redundant explicit string conversions. */
|
||||
/** A {@link BugChecker} which flags redundant explicit string conversions. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "RedundantStringConversion",
|
||||
summary = "Avoid redundant string conversions when possible",
|
||||
link = BUG_PATTERNS_BASE_URL + "RedundantStringConversion",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
@SuppressWarnings({
|
||||
"java:S1192" /* Factoring out repeated method names impacts readability. */,
|
||||
"java:S2160" /* Super class equality definition suffices. */,
|
||||
"key-to-resolve-AnnotationUseStyle-and-TrailingComment-check-conflict"
|
||||
})
|
||||
public final class RedundantStringConversion extends BugChecker
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class RedundantStringConversionCheck extends BugChecker
|
||||
implements BinaryTreeMatcher, CompoundAssignmentTreeMatcher, MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String FLAG_PREFIX = "RedundantStringConversion:";
|
||||
private static final String EXTRA_STRING_CONVERSION_METHODS_FLAG =
|
||||
"RedundantStringConversion:ExtraConversionMethods";
|
||||
FLAG_PREFIX + "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);
|
||||
private static final Matcher<ExpressionTree> THROWABLE = isSubtypeOf(Throwable.class);
|
||||
private static final Matcher<ExpressionTree> NON_NULL_STRING =
|
||||
allOf(STRING, isNonNullUsingDataflow());
|
||||
private static final Matcher<ExpressionTree> NON_NULL_STRING = allOf(STRING, isNonNull());
|
||||
private static final Matcher<ExpressionTree> NOT_FORMATTABLE =
|
||||
not(isSubtypeOf(Formattable.class));
|
||||
private static final Matcher<MethodInvocationTree> WELL_KNOWN_STRING_CONVERSION_METHODS =
|
||||
private static final Matcher<ExpressionTree> WELL_KNOWN_STRING_CONVERSION_METHODS =
|
||||
anyOf(
|
||||
instanceMethod()
|
||||
.onDescendantOfAny(Object.class.getCanonicalName())
|
||||
instanceMethod().onDescendantOfAny(Object.class.getName()).named("toString"),
|
||||
staticMethod()
|
||||
.onClass(Objects.class.getName())
|
||||
.named("toString")
|
||||
.withNoParameters(),
|
||||
allOf(
|
||||
argumentCount(1),
|
||||
anyOf(
|
||||
staticMethod()
|
||||
.onClassAny(
|
||||
Stream.concat(
|
||||
Primitives.allWrapperTypes().stream(), Stream.of(Objects.class))
|
||||
.map(Class::getName)
|
||||
.collect(toImmutableSet()))
|
||||
.named("toString"),
|
||||
allOf(
|
||||
staticMethod().onClass(String.class.getCanonicalName()).named("valueOf"),
|
||||
not(
|
||||
anyMethod()
|
||||
.anyClass()
|
||||
.withAnyName()
|
||||
.withParametersOfType(
|
||||
ImmutableList.of(Suppliers.arrayOf(Suppliers.CHAR_TYPE))))))));
|
||||
.withParameters(Object.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(String.class.getName())
|
||||
.named("valueOf")
|
||||
.withParameters(Object.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(String.class.getName())
|
||||
.named("valueOf")
|
||||
.withParameters(String.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Byte.class.getName())
|
||||
.named("toString")
|
||||
.withParameters(byte.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Character.class.getName())
|
||||
.named("toString")
|
||||
.withParameters(char.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Short.class.getName())
|
||||
.named("toString")
|
||||
.withParameters(short.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Integer.class.getName())
|
||||
.named("toString")
|
||||
.withParameters(int.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Long.class.getName())
|
||||
.named("toString")
|
||||
.withParameters(long.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Float.class.getName())
|
||||
.named("toString")
|
||||
.withParameters(float.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Double.class.getName())
|
||||
.named("toString")
|
||||
.withParameters(double.class.getName()));
|
||||
private static final Matcher<ExpressionTree> STRINGBUILDER_APPEND_INVOCATION =
|
||||
instanceMethod()
|
||||
.onDescendantOf(StringBuilder.class.getCanonicalName())
|
||||
.onDescendantOf(StringBuilder.class.getName())
|
||||
.named("append")
|
||||
.withParameters(String.class.getCanonicalName());
|
||||
.withParameters(String.class.getName());
|
||||
private static final Matcher<ExpressionTree> STRINGBUILDER_INSERT_INVOCATION =
|
||||
instanceMethod()
|
||||
.onDescendantOf(StringBuilder.class.getCanonicalName())
|
||||
.onDescendantOf(StringBuilder.class.getName())
|
||||
.named("insert")
|
||||
.withParameters(int.class.getCanonicalName(), String.class.getCanonicalName());
|
||||
.withParameters(int.class.getName(), String.class.getName());
|
||||
private static final Matcher<ExpressionTree> FORMATTER_INVOCATION =
|
||||
anyOf(
|
||||
staticMethod().onClass(String.class.getCanonicalName()).named("format"),
|
||||
instanceMethod().onDescendantOf(Formatter.class.getCanonicalName()).named("format"),
|
||||
staticMethod().onClass(String.class.getName()).named("format"),
|
||||
instanceMethod().onDescendantOf(Formatter.class.getName()).named("format"),
|
||||
instanceMethod()
|
||||
.onDescendantOfAny(
|
||||
PrintStream.class.getCanonicalName(), PrintWriter.class.getCanonicalName())
|
||||
.onDescendantOf(PrintStream.class.getName())
|
||||
.namedAnyOf("format", "printf"),
|
||||
instanceMethod()
|
||||
.onDescendantOfAny(
|
||||
PrintStream.class.getCanonicalName(), PrintWriter.class.getCanonicalName())
|
||||
.onDescendantOf(PrintStream.class.getName())
|
||||
.namedAnyOf("print", "println")
|
||||
.withParameters(Object.class.getCanonicalName()),
|
||||
.withParameters(Object.class.getName()),
|
||||
instanceMethod()
|
||||
.onDescendantOf(PrintWriter.class.getName())
|
||||
.namedAnyOf("format", "printf"),
|
||||
instanceMethod()
|
||||
.onDescendantOf(PrintWriter.class.getName())
|
||||
.namedAnyOf("print", "println")
|
||||
.withParameters(Object.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Console.class.getCanonicalName())
|
||||
.onClass(Console.class.getName())
|
||||
.namedAnyOf("format", "printf", "readline", "readPassword"));
|
||||
private static final Matcher<ExpressionTree> GUAVA_GUARD_INVOCATION =
|
||||
anyOf(
|
||||
staticMethod()
|
||||
.onClass(Preconditions.class.getCanonicalName())
|
||||
.onClass("com.google.common.base.Preconditions")
|
||||
.namedAnyOf("checkArgument", "checkState", "checkNotNull"),
|
||||
staticMethod()
|
||||
.onClass(Verify.class.getCanonicalName())
|
||||
.onClass("com.google.common.base.Verify")
|
||||
.namedAnyOf("verify", "verifyNotNull"));
|
||||
private static final Matcher<ExpressionTree> SLF4J_LOGGER_INVOCATION =
|
||||
instanceMethod()
|
||||
.onDescendantOf("org.slf4j.Logger")
|
||||
.namedAnyOf("trace", "debug", "info", "warn", "error");
|
||||
|
||||
private final Matcher<MethodInvocationTree> conversionMethodMatcher;
|
||||
private final Matcher<ExpressionTree> conversionMethodMatcher;
|
||||
|
||||
/** Instantiates a default {@link RedundantStringConversion} instance. */
|
||||
public RedundantStringConversion() {
|
||||
/** Instantiates the default {@link RedundantStringConversionCheck}. */
|
||||
public RedundantStringConversionCheck() {
|
||||
this(ErrorProneFlags.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a customized {@link RedundantStringConversion}.
|
||||
* Instantiates a customized {@link RedundantStringConversionCheck}.
|
||||
*
|
||||
* @param flags Any provided command line flags.
|
||||
*/
|
||||
@Inject
|
||||
RedundantStringConversion(ErrorProneFlags flags) {
|
||||
public RedundantStringConversionCheck(ErrorProneFlags flags) {
|
||||
conversionMethodMatcher = createConversionMethodMatcher(flags);
|
||||
}
|
||||
|
||||
@@ -174,12 +179,12 @@ public final class RedundantStringConversion extends BugChecker
|
||||
ExpressionTree lhs = tree.getLeftOperand();
|
||||
ExpressionTree rhs = tree.getRightOperand();
|
||||
if (!STRING.matches(lhs, state)) {
|
||||
return createDescription(tree, tryFix(rhs, state, STRING));
|
||||
return finalize(tree, tryFix(rhs, state, STRING));
|
||||
}
|
||||
|
||||
List<SuggestedFix.Builder> fixes = new ArrayList<>();
|
||||
|
||||
// XXX: Avoid trying to simplify the RHS twice.
|
||||
// XXX: Not so nice: we try to simplify the RHS twice.
|
||||
ExpressionTree preferredRhs = trySimplify(rhs, state).orElse(rhs);
|
||||
if (STRING.matches(preferredRhs, state)) {
|
||||
tryFix(lhs, state, ANY_EXPR).ifPresent(fixes::add);
|
||||
@@ -188,7 +193,7 @@ public final class RedundantStringConversion extends BugChecker
|
||||
}
|
||||
tryFix(rhs, state, ANY_EXPR).ifPresent(fixes::add);
|
||||
|
||||
return createDescription(tree, fixes.stream().reduce(SuggestedFix.Builder::merge));
|
||||
return finalize(tree, fixes.stream().reduce(SuggestedFix.Builder::merge));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -197,36 +202,36 @@ public final class RedundantStringConversion extends BugChecker
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return createDescription(tree, tryFix(tree.getExpression(), state, ANY_EXPR));
|
||||
return finalize(tree, tryFix(tree.getExpression(), state, ANY_EXPR));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (STRINGBUILDER_APPEND_INVOCATION.matches(tree, state)) {
|
||||
return createDescription(tree, tryFixPositionalConverter(tree.getArguments(), state, 0));
|
||||
return finalize(tree, tryFixPositionalConverter(tree.getArguments(), state, 0));
|
||||
}
|
||||
|
||||
if (STRINGBUILDER_INSERT_INVOCATION.matches(tree, state)) {
|
||||
return createDescription(tree, tryFixPositionalConverter(tree.getArguments(), state, 1));
|
||||
return finalize(tree, tryFixPositionalConverter(tree.getArguments(), state, 1));
|
||||
}
|
||||
|
||||
if (FORMATTER_INVOCATION.matches(tree, state)) {
|
||||
return createDescription(tree, tryFixFormatter(tree.getArguments(), state));
|
||||
return finalize(tree, tryFixFormatter(tree.getArguments(), state));
|
||||
}
|
||||
|
||||
if (GUAVA_GUARD_INVOCATION.matches(tree, state)) {
|
||||
return createDescription(tree, tryFixGuavaGuard(tree.getArguments(), state));
|
||||
return finalize(tree, tryFixGuavaGuard(tree.getArguments(), state));
|
||||
}
|
||||
|
||||
if (SLF4J_LOGGER_INVOCATION.matches(tree, state)) {
|
||||
return createDescription(tree, tryFixSlf4jLogger(tree.getArguments(), state));
|
||||
return finalize(tree, tryFixSlf4jLogger(tree.getArguments(), state));
|
||||
}
|
||||
|
||||
if (instanceMethod().matches(tree, state)) {
|
||||
return createDescription(tree, tryFix(tree, state, STRING));
|
||||
return finalize(tree, tryFix(tree, state, STRING));
|
||||
}
|
||||
|
||||
return createDescription(tree, tryFix(tree, state, NON_NULL_STRING));
|
||||
return finalize(tree, tryFix(tree, state, NON_NULL_STRING));
|
||||
}
|
||||
|
||||
private Optional<SuggestedFix.Builder> tryFixPositionalConverter(
|
||||
@@ -236,7 +241,7 @@ public final class RedundantStringConversion extends BugChecker
|
||||
.flatMap(args -> tryFix(args.get(index), state, ANY_EXPR));
|
||||
}
|
||||
|
||||
// XXX: Write another check that checks that Formatter patterns don't use `{}` and have a
|
||||
// XXX: Write another check which checks that Formatter patterns don't use `{}` and have a
|
||||
// matching number of arguments of the appropriate type. Also flag explicit conversions from
|
||||
// `Formattable` to string.
|
||||
private Optional<SuggestedFix.Builder> tryFixFormatter(
|
||||
@@ -267,17 +272,17 @@ public final class RedundantStringConversion extends BugChecker
|
||||
return tryFixFormatterArguments(arguments, state, ANY_EXPR, ANY_EXPR);
|
||||
}
|
||||
|
||||
// XXX: Write another check that checks that SLF4J patterns don't use `%s` and have a matching
|
||||
// XXX: Write another check which checks that SLF4J patterns don't use `%s` and have a matching
|
||||
// number of arguments of the appropriate type. Also flag explicit conversions from `Throwable` to
|
||||
// string as the last logger argument. Suggests either dropping the conversion or going with
|
||||
// string as the last logger argument. Suggests either dropping the converison or going with
|
||||
// `Throwable#getMessage()` instead.
|
||||
private Optional<SuggestedFix.Builder> tryFixSlf4jLogger(
|
||||
List<? extends ExpressionTree> arguments, VisitorState state) {
|
||||
/*
|
||||
* SLF4J treats the final argument to a log statement specially if it is a `Throwable`: it
|
||||
* SLF4J treats the final argument to a log statement specially if it is a `Throwabe`: it
|
||||
* will always choose to render the associated stacktrace, even if the argument has a
|
||||
* matching `{}` placeholder. (In this case the `{}` will simply be logged verbatim.) So if
|
||||
* a log statement's final argument is the string representation of a `Throwable`, then we
|
||||
* a log statement's final argument is the string representation of a `Throwble`, then we
|
||||
* must not strip this explicit string conversion, as that would change the statement's
|
||||
* semantics.
|
||||
*/
|
||||
@@ -309,7 +314,7 @@ public final class RedundantStringConversion extends BugChecker
|
||||
|
||||
/* Simplify the values to be plugged into the format pattern, if possible. */
|
||||
return arguments.stream()
|
||||
.skip(patternIndex + 1L)
|
||||
.skip(patternIndex + 1)
|
||||
.map(arg -> tryFix(arg, state, remainingArgFilter))
|
||||
.flatMap(Optional::stream)
|
||||
.reduce(SuggestedFix.Builder::merge);
|
||||
@@ -320,7 +325,7 @@ public final class RedundantStringConversion extends BugChecker
|
||||
return trySimplify(tree, state, filter)
|
||||
.map(
|
||||
replacement ->
|
||||
SuggestedFix.builder().replace(tree, SourceCode.treeToString(replacement, state)));
|
||||
SuggestedFix.builder().replace(tree, Util.treeToString(replacement, state)));
|
||||
}
|
||||
|
||||
private Optional<ExpressionTree> trySimplify(
|
||||
@@ -331,33 +336,33 @@ public final class RedundantStringConversion extends BugChecker
|
||||
}
|
||||
|
||||
private Optional<ExpressionTree> trySimplify(ExpressionTree tree, VisitorState state) {
|
||||
if (!(tree instanceof MethodInvocationTree methodInvocation)) {
|
||||
if (tree.getKind() != Kind.METHOD_INVOCATION || !conversionMethodMatcher.matches(tree, state)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (!conversionMethodMatcher.matches(methodInvocation, state)) {
|
||||
return Optional.empty();
|
||||
MethodInvocationTree methodInvocation = (MethodInvocationTree) tree;
|
||||
switch (methodInvocation.getArguments().size()) {
|
||||
case 0:
|
||||
return trySimplifyNullaryMethod(methodInvocation, state);
|
||||
case 1:
|
||||
return trySimplifyUnaryMethod(methodInvocation, state);
|
||||
default:
|
||||
throw new IllegalStateException(
|
||||
"Cannot simplify method call with two or more arguments: "
|
||||
+ Util.treeToString(tree, state));
|
||||
}
|
||||
|
||||
return switch (methodInvocation.getArguments().size()) {
|
||||
case 0 -> trySimplifyNullaryMethod(methodInvocation, state);
|
||||
case 1 -> trySimplifyUnaryMethod(methodInvocation, state);
|
||||
default ->
|
||||
throw new IllegalStateException(
|
||||
"Cannot simplify method call with two or more arguments: "
|
||||
+ SourceCode.treeToString(tree, state));
|
||||
};
|
||||
}
|
||||
|
||||
private static Optional<ExpressionTree> trySimplifyNullaryMethod(
|
||||
MethodInvocationTree methodInvocation, VisitorState state) {
|
||||
if (!instanceMethod().matches(methodInvocation, state)
|
||||
|| !(methodInvocation.getMethodSelect() instanceof MemberSelectTree memberSelect)) {
|
||||
if (!instanceMethod().matches(methodInvocation, state)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(memberSelect.getExpression())
|
||||
.filter(expr -> !"super".equals(SourceCode.treeToString(expr, state)));
|
||||
return Optional.of(methodInvocation.getMethodSelect())
|
||||
.filter(methodSelect -> methodSelect.getKind() == Kind.MEMBER_SELECT)
|
||||
.map(methodSelect -> ((MemberSelectTree) methodSelect).getExpression())
|
||||
.filter(expr -> !"super".equals(Util.treeToString(expr, state)));
|
||||
}
|
||||
|
||||
private static Optional<ExpressionTree> trySimplifyUnaryMethod(
|
||||
@@ -369,20 +374,20 @@ public final class RedundantStringConversion extends BugChecker
|
||||
return Optional.of(Iterables.getOnlyElement(methodInvocation.getArguments()));
|
||||
}
|
||||
|
||||
private Description createDescription(Tree tree, Optional<SuggestedFix.Builder> fixes) {
|
||||
private Description finalize(Tree tree, Optional<SuggestedFix.Builder> fixes) {
|
||||
return fixes
|
||||
.map(SuggestedFix.Builder::build)
|
||||
.map(fix -> describeMatch(tree, fix))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
private static Matcher<MethodInvocationTree> createConversionMethodMatcher(
|
||||
ErrorProneFlags flags) {
|
||||
private static Matcher<ExpressionTree> createConversionMethodMatcher(ErrorProneFlags flags) {
|
||||
// XXX: ErrorProneFlags#getList splits by comma, but method signatures may also contain commas.
|
||||
// For this class methods accepting more than one argument are not valid, but still: not nice.
|
||||
return anyOf(
|
||||
WELL_KNOWN_STRING_CONVERSION_METHODS,
|
||||
new MethodMatcherFactory()
|
||||
.create(Flags.getList(flags, EXTRA_STRING_CONVERSION_METHODS_FLAG)));
|
||||
return flags
|
||||
.getList(EXTRA_STRING_CONVERSION_METHODS_FLAG)
|
||||
.map(new MethodMatcherFactory()::create)
|
||||
.map(m -> anyOf(WELL_KNOWN_STRING_CONVERSION_METHODS, m))
|
||||
.orElse(WELL_KNOWN_STRING_CONVERSION_METHODS);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
package tech.picnic.errorprone.refaster.runner;
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.ImmutableRangeSet.toImmutableRangeSet;
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static java.util.function.Predicate.not;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
@@ -15,18 +10,21 @@ import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.ImmutableRangeSet;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Range;
|
||||
import com.google.common.collect.RangeSet;
|
||||
import com.google.common.collect.TreeRangeSet;
|
||||
import com.google.common.reflect.ClassPath;
|
||||
import com.google.common.reflect.ClassPath.ResourceInfo;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.CodeTransformer;
|
||||
import com.google.errorprone.CompositeCodeTransformer;
|
||||
import com.google.errorprone.ErrorProneFlags;
|
||||
import com.google.errorprone.ErrorProneOptions.Severity;
|
||||
import com.google.errorprone.SubContext;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher;
|
||||
import com.google.errorprone.fixes.Replacement;
|
||||
@@ -34,62 +32,67 @@ import com.google.errorprone.matchers.Description;
|
||||
import com.sun.source.tree.CompilationUnitTree;
|
||||
import com.sun.tools.javac.tree.EndPosTable;
|
||||
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags code that can be simplified using Refaster rules located on the
|
||||
* classpath.
|
||||
* A {@link BugChecker} which flags code which can be simplified using Refaster templates located on
|
||||
* the classpath.
|
||||
*
|
||||
* <p>This checker locates all {@code *.refaster} classpath resources and assumes that they contain
|
||||
* a {@link CodeTransformer}. The set of loaded Refaster rules can be restricted by passing {@code
|
||||
* <p>This checker locates all {@code *.refaster} classpath resources and assumes they contain a
|
||||
* {@link CodeTransformer}. The set of loaded Refaster templates can be restricted by passing {@code
|
||||
* -XepOpt:Refaster:NamePattern=<someRegex>}.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "Refaster",
|
||||
summary = "Write idiomatic code when possible",
|
||||
linkType = NONE,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
@SuppressWarnings("java:S2160" /* Super class equality definition suffices. */)
|
||||
public final class Refaster extends BugChecker implements CompilationUnitTreeMatcher {
|
||||
/** Flag to pass a pattern that restricts which Refaster rules are loaded. */
|
||||
public static final String INCLUDED_RULES_PATTERN_FLAG = "Refaster:NamePattern";
|
||||
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class RefasterCheck extends BugChecker implements CompilationUnitTreeMatcher {
|
||||
private static final String REFASTER_TEMPLATE_SUFFIX = ".refaster";
|
||||
private static final String INCLUDED_TEMPLATES_PATTERN_FLAG = "Refaster:NamePattern";
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@SuppressWarnings({"java:S1948", "serial"} /* Concrete instance will be `Serializable`. */)
|
||||
private final CodeTransformer codeTransformer;
|
||||
|
||||
/** Instantiates a default {@link Refaster} instance. */
|
||||
public Refaster() {
|
||||
/** Instantiates the default {@link RefasterCheck}. */
|
||||
public RefasterCheck() {
|
||||
this(ErrorProneFlags.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a customized {@link Refaster}.
|
||||
* Instantiates a customized {@link RefasterCheck}.
|
||||
*
|
||||
* @param flags Any provided command line flags.
|
||||
*/
|
||||
@Inject
|
||||
@VisibleForTesting
|
||||
public Refaster(ErrorProneFlags flags) {
|
||||
codeTransformer = createCompositeCodeTransformer(flags);
|
||||
public RefasterCheck(ErrorProneFlags flags) {
|
||||
codeTransformer = createCompositeCodeTransformer(flags, loadAllCodeTransformers());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
RefasterCheck(
|
||||
ErrorProneFlags flags, ImmutableListMultimap<String, CodeTransformer> allTransformers) {
|
||||
codeTransformer = createCompositeCodeTransformer(flags, allTransformers);
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
@Override
|
||||
public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) {
|
||||
/* First, collect all matches. */
|
||||
List<Description> matches = new ArrayList<>();
|
||||
codeTransformer.apply(state.getPath(), new SubContext(state.context), matches::add);
|
||||
|
||||
/* Then apply them. */
|
||||
applyMatches(matches, ((JCCompilationUnit) tree).endPositions, state);
|
||||
|
||||
@@ -101,13 +104,13 @@ public final class Refaster extends BugChecker implements CompilationUnitTreeMat
|
||||
* Reports a subset of the given matches, such that no two reported matches suggest a replacement
|
||||
* of the same part of the source code.
|
||||
*
|
||||
* <p>In the common case all matches will be reported. In case of overlap the match that replaces
|
||||
* <p>In the common case all matches will be reported. In case of overlap the match which replaces
|
||||
* the largest piece of source code is preferred. In case two matches wish to replace exactly the
|
||||
* same piece of code, preference is given to the match that suggests the shortest replacement.
|
||||
* same piece of code, preference is given to the match which suggests the shortest replacement.
|
||||
*/
|
||||
// XXX: This selection logic solves an issue described in
|
||||
// https://github.com/google/error-prone/issues/559. Consider contributing it back upstream.
|
||||
private void applyMatches(
|
||||
private static void applyMatches(
|
||||
Iterable<Description> allMatches, EndPosTable endPositions, VisitorState state) {
|
||||
ImmutableList<Description> byReplacementSize =
|
||||
ImmutableList.sortedCopyOf(
|
||||
@@ -120,57 +123,13 @@ public final class Refaster extends BugChecker implements CompilationUnitTreeMat
|
||||
for (Description description : byReplacementSize) {
|
||||
ImmutableRangeSet<Integer> ranges = getReplacementRanges(description, endPositions);
|
||||
if (ranges.asRanges().stream().noneMatch(replacedSections::intersects)) {
|
||||
/*
|
||||
* This suggested fix does not overlap with any ("larger") replacement seen until now, so
|
||||
* apply it.
|
||||
*/
|
||||
state.reportMatch(augmentDescription(description, getSeverityOverride(state)));
|
||||
/* This suggested fix does not overlap with any ("larger") replacement seen until now. Apply it. */
|
||||
state.reportMatch(description);
|
||||
replacedSections.addAll(ranges);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<SeverityLevel> getSeverityOverride(VisitorState state) {
|
||||
return Optional.ofNullable(state.errorProneOptions().getSeverityMap().get(canonicalName()))
|
||||
.flatMap(Refaster::toSeverityLevel);
|
||||
}
|
||||
|
||||
private static Optional<SeverityLevel> toSeverityLevel(Severity severity) {
|
||||
return switch (severity) {
|
||||
case DEFAULT -> Optional.empty();
|
||||
case WARN -> Optional.of(WARNING);
|
||||
case ERROR -> Optional.of(ERROR);
|
||||
default ->
|
||||
throw new IllegalStateException(String.format("Unsupported severity='%s'", severity));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given {@link Description}'s details by standardizing the reported check name,
|
||||
* updating the associated message, and optionally overriding its severity.
|
||||
*
|
||||
* <p>The assigned severity is overridden only if this bug checker's severity was explicitly
|
||||
* configured.
|
||||
*
|
||||
* <p>The original check name (i.e. the Refaster rule name) is prepended to the {@link
|
||||
* Description}'s message. The replacement check name ("Refaster Rule", a name which includes a
|
||||
* space) is chosen such that it is guaranteed not to match any canonical bug checker name (as
|
||||
* that could cause {@link VisitorState#reportMatch(Description)}} to override the reported
|
||||
* severity).
|
||||
*/
|
||||
@SuppressWarnings("RestrictedApi" /* We create a heavily customized `Description` here. */)
|
||||
private static Description augmentDescription(
|
||||
Description description, Optional<SeverityLevel> severityOverride) {
|
||||
return Description.builder(
|
||||
description.position,
|
||||
"Refaster Rule",
|
||||
description.getLink(),
|
||||
String.join(": ", description.checkName, description.getRawMessage()))
|
||||
.overrideSeverity(severityOverride.orElseGet(description::severity))
|
||||
.addAllFixes(description.fixes)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static int getReplacedCodeSize(Description description, EndPosTable endPositions) {
|
||||
return getReplacements(description, endPositions).mapToInt(Replacement::length).sum();
|
||||
}
|
||||
@@ -193,23 +152,73 @@ public final class Refaster extends BugChecker implements CompilationUnitTreeMat
|
||||
return description.fixes.stream().flatMap(fix -> fix.getReplacements(endPositions).stream());
|
||||
}
|
||||
|
||||
private static CodeTransformer createCompositeCodeTransformer(ErrorProneFlags flags) {
|
||||
ImmutableListMultimap<String, CodeTransformer> allTransformers =
|
||||
CodeTransformers.getAllCodeTransformers();
|
||||
private static CodeTransformer createCompositeCodeTransformer(
|
||||
ErrorProneFlags flags, ImmutableListMultimap<String, CodeTransformer> allTransformers) {
|
||||
return CompositeCodeTransformer.compose(
|
||||
flags
|
||||
.get(INCLUDED_RULES_PATTERN_FLAG)
|
||||
.get(INCLUDED_TEMPLATES_PATTERN_FLAG)
|
||||
.map(Pattern::compile)
|
||||
.<ImmutableCollection<CodeTransformer>>map(
|
||||
nameFilter -> filterCodeTransformers(allTransformers, nameFilter))
|
||||
.map(nameFilter -> filterCodeTransformers(allTransformers, nameFilter))
|
||||
.orElseGet(allTransformers::values));
|
||||
}
|
||||
|
||||
private static ImmutableList<CodeTransformer> filterCodeTransformers(
|
||||
private static ImmutableCollection<CodeTransformer> filterCodeTransformers(
|
||||
ImmutableListMultimap<String, CodeTransformer> transformers, Pattern nameFilter) {
|
||||
return transformers.entries().stream()
|
||||
.filter(e -> nameFilter.matcher(e.getKey()).matches())
|
||||
.map(Map.Entry::getValue)
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static ImmutableListMultimap<String, CodeTransformer> loadAllCodeTransformers() {
|
||||
ImmutableListMultimap.Builder<String, CodeTransformer> transformers =
|
||||
ImmutableListMultimap.builder();
|
||||
|
||||
for (ResourceInfo resource : getClassPathResources()) {
|
||||
getRefasterTemplateName(resource)
|
||||
.ifPresent(
|
||||
templateName ->
|
||||
loadCodeTransformer(resource)
|
||||
.ifPresent(transformer -> transformers.put(templateName, transformer)));
|
||||
}
|
||||
|
||||
return transformers.build();
|
||||
}
|
||||
|
||||
private static ImmutableSet<ResourceInfo> getClassPathResources() {
|
||||
try {
|
||||
return ClassPath.from(
|
||||
Objects.requireNonNullElseGet(
|
||||
RefasterCheck.class.getClassLoader(), () -> ClassLoader.getSystemClassLoader()))
|
||||
.getResources();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Failed to scan classpath for resources", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<String> getRefasterTemplateName(ResourceInfo resource) {
|
||||
String resourceName = resource.getResourceName();
|
||||
if (!resourceName.endsWith(REFASTER_TEMPLATE_SUFFIX)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
int lastPathSeparator = resourceName.lastIndexOf('/');
|
||||
int beginIndex = lastPathSeparator < 0 ? 0 : lastPathSeparator + 1;
|
||||
int endIndex = resourceName.length() - REFASTER_TEMPLATE_SUFFIX.length();
|
||||
return Optional.of(resourceName.substring(beginIndex, endIndex));
|
||||
}
|
||||
|
||||
private static Optional<CodeTransformer> loadCodeTransformer(ResourceInfo resource) {
|
||||
try (InputStream in = resource.url().openStream();
|
||||
ObjectInputStream ois = new ObjectInputStream(in)) {
|
||||
return Optional.of((CodeTransformer) ois.readObject());
|
||||
} catch (NoSuchElementException e) {
|
||||
/* For some reason we can't load the resource. Skip it. */
|
||||
// XXX: Should we log this?
|
||||
return Optional.empty();
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
throw new IllegalStateException("Can't load `CodeTransformer` from " + resource, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
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.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isSameType;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static com.google.errorprone.matchers.Matchers.methodHasParameters;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.ProvidesFix;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
@@ -22,25 +23,22 @@ import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import java.io.InputStream;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@code @RequestMapping} methods that have one or more parameters
|
||||
* A {@link BugChecker} which flags {@code @RequestMapping} methods that have one or more parameters
|
||||
* that appear to lack a relevant annotation.
|
||||
*
|
||||
* <p>Matched mappings are {@code @{Delete,Get,Patch,Post,Put,Request}Mapping}.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "RequestMappingAnnotation",
|
||||
summary = "Make sure all `@RequestMapping` method parameters are annotated",
|
||||
link = BUG_PATTERNS_BASE_URL + "RequestMappingAnnotation",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class RequestMappingAnnotation extends BugChecker implements MethodTreeMatcher {
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.WARNING,
|
||||
tags = StandardTags.LIKELY_ERROR,
|
||||
providesFix = ProvidesFix.REQUIRES_HUMAN_ATTENTION)
|
||||
public final class RequestMappingAnnotationCheck extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String ANN_PACKAGE_PREFIX = "org.springframework.web.bind.annotation.";
|
||||
// XXX: Generalize this logic to fully support Spring meta-annotations, then update the class
|
||||
@@ -55,11 +53,8 @@ public final class RequestMappingAnnotation extends BugChecker implements Method
|
||||
isType(ANN_PACKAGE_PREFIX + "PostMapping"),
|
||||
isType(ANN_PACKAGE_PREFIX + "PutMapping"),
|
||||
isType(ANN_PACKAGE_PREFIX + "RequestMapping")));
|
||||
// XXX: Add other parameters as necessary. Also consider whether it makes sense to have WebMVC-
|
||||
// and WebFlux-specific logic. See
|
||||
// https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-arguments
|
||||
// and
|
||||
// https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-ann-arguments.
|
||||
// XXX: Add other parameters as necessary. See
|
||||
// https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-arguments.
|
||||
private static final Matcher<MethodTree> LACKS_PARAMETER_ANNOTATION =
|
||||
not(
|
||||
methodHasParameters(
|
||||
@@ -69,32 +64,14 @@ public final class RequestMappingAnnotation extends BugChecker implements Method
|
||||
AT_LEAST_ONE,
|
||||
anyOf(
|
||||
isType(ANN_PACKAGE_PREFIX + "PathVariable"),
|
||||
isType(ANN_PACKAGE_PREFIX + "RequestAttribute"),
|
||||
isType(ANN_PACKAGE_PREFIX + "RequestBody"),
|
||||
isType(ANN_PACKAGE_PREFIX + "RequestHeader"),
|
||||
isType(ANN_PACKAGE_PREFIX + "RequestParam"),
|
||||
isType(ANN_PACKAGE_PREFIX + "RequestPart"),
|
||||
isType(
|
||||
"org.springframework.security.core.annotation.CurrentSecurityContext"))),
|
||||
isSameType(InputStream.class.getCanonicalName()),
|
||||
isSameType(Locale.class.getCanonicalName()),
|
||||
isSameType(TimeZone.class.getCanonicalName()),
|
||||
isSameType(ZoneId.class.getCanonicalName()),
|
||||
isSameType("jakarta.servlet.http.HttpServletRequest"),
|
||||
isSameType("jakarta.servlet.http.HttpServletResponse"),
|
||||
isType(ANN_PACKAGE_PREFIX + "RequestParam"))),
|
||||
isSameType("java.io.InputStream"),
|
||||
isSameType("javax.servlet.http.HttpServletRequest"),
|
||||
isSameType("javax.servlet.http.HttpServletResponse"),
|
||||
isSameType("org.springframework.http.HttpMethod"),
|
||||
isSameType("org.springframework.ui.Model"),
|
||||
isSameType("org.springframework.validation.BindingResult"),
|
||||
isSameType("org.springframework.web.context.request.NativeWebRequest"),
|
||||
isSameType("org.springframework.web.context.request.WebRequest"),
|
||||
isSameType("org.springframework.web.server.ServerWebExchange"),
|
||||
isSameType("org.springframework.web.util.UriBuilder"),
|
||||
isSameType("org.springframework.web.util.UriComponentsBuilder"))));
|
||||
|
||||
/** Instantiates a new {@link RequestMappingAnnotation} instance. */
|
||||
public RequestMappingAnnotation() {}
|
||||
isSubtypeOf("org.springframework.web.context.request.WebRequest"))));
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
@@ -105,10 +82,8 @@ public final class RequestMappingAnnotation extends BugChecker implements Method
|
||||
&& LACKS_PARAMETER_ANNOTATION.matches(tree, state)
|
||||
? buildDescription(tree)
|
||||
.setMessage(
|
||||
"""
|
||||
Not all parameters of this request mapping method are annotated; this may be a \
|
||||
mistake. If the unannotated parameters represent query string parameters, annotate \
|
||||
them with `@RequestParam`.""")
|
||||
"Not all parameters of this request mapping method are annotated; this may be a mistake. "
|
||||
+ "If the unannotated parameters represent query string parameters, annotate them with `@RequestParam`.")
|
||||
.build()
|
||||
: Description.NO_MATCH;
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.ErrorProneFlags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Suppliers;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import javax.inject.Inject;
|
||||
import tech.picnic.errorprone.utils.Flags;
|
||||
|
||||
/** A {@link BugChecker} that flags {@code @RequestParam} parameters with an unsupported type. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"""
|
||||
By default, `@RequestParam` does not support `ImmutableCollection` and `ImmutableMap` \
|
||||
subtypes""",
|
||||
link = BUG_PATTERNS_BASE_URL + "RequestParamType",
|
||||
linkType = CUSTOM,
|
||||
severity = ERROR,
|
||||
tags = LIKELY_ERROR)
|
||||
@SuppressWarnings("java:S2160" /* Super class equality definition suffices. */)
|
||||
public final class RequestParamType extends BugChecker implements VariableTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String SUPPORTED_CUSTOM_TYPES_FLAG = "RequestParamType:SupportedCustomTypes";
|
||||
|
||||
private final Matcher<VariableTree> hasUnsupportedRequestParamType;
|
||||
|
||||
/** Instantiates a default {@link RequestParamType} instance. */
|
||||
public RequestParamType() {
|
||||
this(ErrorProneFlags.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a customized {@link RequestParamType} instance.
|
||||
*
|
||||
* @param flags Any provided command line flags.
|
||||
*/
|
||||
@Inject
|
||||
RequestParamType(ErrorProneFlags flags) {
|
||||
hasUnsupportedRequestParamType = hasUnsupportedRequestParamType(flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Description matchVariable(VariableTree tree, VisitorState state) {
|
||||
return hasUnsupportedRequestParamType.matches(tree, state)
|
||||
? describeMatch(tree)
|
||||
: Description.NO_MATCH;
|
||||
}
|
||||
|
||||
private static Matcher<VariableTree> hasUnsupportedRequestParamType(ErrorProneFlags flags) {
|
||||
return allOf(
|
||||
annotations(AT_LEAST_ONE, isType("org.springframework.web.bind.annotation.RequestParam")),
|
||||
anyOf(isSubtypeOf(ImmutableCollection.class), isSubtypeOf(ImmutableMap.class)),
|
||||
not(isSubtypeOfAny(Flags.getList(flags, SUPPORTED_CUSTOM_TYPES_FLAG))));
|
||||
}
|
||||
|
||||
private static Matcher<Tree> isSubtypeOfAny(ImmutableList<String> inclusions) {
|
||||
return anyOf(
|
||||
inclusions.stream()
|
||||
.map(inclusion -> isSubtypeOf(Suppliers.typeFromString(inclusion)))
|
||||
.collect(toImmutableList()));
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
@@ -23,23 +22,22 @@ import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/** A {@link BugChecker} that flags SLF4J usages that are likely to be in error. */
|
||||
/** A {@link BugChecker} which flags SLF4J usages that are likely to be in error. */
|
||||
// XXX: The special-casing of Throwable applies only to SLF4J 1.6.0+; see
|
||||
// https://www.slf4j.org/faq.html#paramException. That should be documented.
|
||||
// XXX: Also simplify `LOG.error(String.format("Something %s", arg), throwable)`.
|
||||
// XXX: Also simplify `LOG.error(String.join("sep", arg1, arg2), throwable)`? Perhaps too obscure.
|
||||
// XXX: Write a similar checker for Spring RestTemplates, String.format and friends, Guava
|
||||
// preconditions, ...
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "Slf4jLogStatement",
|
||||
summary = "Make sure SLF4J log statements contain proper placeholders with matching arguments",
|
||||
link = BUG_PATTERNS_BASE_URL + "Slf4jLogStatement",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class Slf4jLogStatement extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.WARNING,
|
||||
tags = StandardTags.LIKELY_ERROR)
|
||||
public final class Slf4jLogStatementCheck extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> MARKER = isSubtypeOf("org.slf4j.Marker");
|
||||
private static final Matcher<ExpressionTree> THROWABLE = isSubtypeOf(Throwable.class);
|
||||
@@ -48,9 +46,6 @@ public final class Slf4jLogStatement extends BugChecker implements MethodInvocat
|
||||
.onDescendantOf("org.slf4j.Logger")
|
||||
.namedAnyOf("trace", "debug", "info", "warn", "error");
|
||||
|
||||
/** Instantiates a new {@link Slf4jLogStatement} instance. */
|
||||
public Slf4jLogStatement() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!SLF4J_LOGGER_INVOCATION.matches(tree, state)) {
|
||||
@@ -119,7 +114,7 @@ public final class Slf4jLogStatement extends BugChecker implements MethodInvocat
|
||||
* replaced at this usage site.
|
||||
*/
|
||||
description.addFix(
|
||||
SuggestedFix.replace(tree, SourceCode.treeToString(tree, state).replace("%s", "{}")));
|
||||
SuggestedFix.replace(tree, Util.treeToString(tree, state).replace("%s", "{}")));
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -1,46 +1,44 @@
|
||||
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.common.base.Verify.verify;
|
||||
import static java.util.function.Predicate.not;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.VerifyException;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.AnnotationTreeMatcher;
|
||||
import com.google.errorprone.fixes.Fix;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.AssignmentTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.NewArrayTree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import java.util.Optional;
|
||||
import tech.picnic.errorprone.utils.AnnotationAttributeMatcher;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@code @RequestMapping} annotations that can be written more
|
||||
* A {@link BugChecker} which flags {@code @RequestMapping} annotations that can be written more
|
||||
* concisely.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "SpringMvcAnnotation",
|
||||
summary =
|
||||
"Prefer the conciseness of `@{Get,Put,Post,Delete,Patch}Mapping` over `@RequestMapping`",
|
||||
link = BUG_PATTERNS_BASE_URL + "SpringMvcAnnotation",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class SpringMvcAnnotation extends BugChecker implements AnnotationTreeMatcher {
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class SpringMvcAnnotationCheck extends BugChecker implements AnnotationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String ANN_PACKAGE_PREFIX = "org.springframework.web.bind.annotation.";
|
||||
private static final AnnotationAttributeMatcher ARGUMENT_SELECTOR =
|
||||
@@ -56,9 +54,6 @@ public final class SpringMvcAnnotation extends BugChecker implements AnnotationT
|
||||
.put("PUT", "PutMapping")
|
||||
.build();
|
||||
|
||||
/** Instantiates a new {@link SpringMvcAnnotation} instance. */
|
||||
public SpringMvcAnnotation() {}
|
||||
|
||||
@Override
|
||||
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
|
||||
// XXX: We could remove the `@RequestMapping` import if not other usages remain.
|
||||
@@ -78,25 +73,31 @@ public final class SpringMvcAnnotation extends BugChecker implements AnnotationT
|
||||
}
|
||||
|
||||
private static Optional<String> extractUniqueMethod(ExpressionTree arg, VisitorState state) {
|
||||
if (!(arg instanceof AssignmentTree assignment)) {
|
||||
throw new VerifyException("Annotation attribute is not an assignment:" + arg.getKind());
|
||||
verify(
|
||||
arg.getKind() == Kind.ASSIGNMENT,
|
||||
"Annotation attribute is not an assignment: %s",
|
||||
arg.getKind());
|
||||
|
||||
ExpressionTree expr = ((AssignmentTree) arg).getExpression();
|
||||
if (expr.getKind() != Kind.NEW_ARRAY) {
|
||||
return Optional.of(extractMethod(expr, state));
|
||||
}
|
||||
|
||||
ExpressionTree expr = assignment.getExpression();
|
||||
return expr instanceof NewArrayTree newArray
|
||||
? Optional.of(newArray.getInitializers())
|
||||
.filter(args -> args.size() == 1)
|
||||
.map(args -> extractMethod(args.get(0), state))
|
||||
: Optional.of(extractMethod(expr, state));
|
||||
NewArrayTree newArray = (NewArrayTree) expr;
|
||||
return Optional.of(newArray.getInitializers())
|
||||
.filter(args -> args.size() == 1)
|
||||
.map(args -> extractMethod(args.get(0), state));
|
||||
}
|
||||
|
||||
// XXX: Use switch pattern matching once the targeted JDK supports this.
|
||||
private static String extractMethod(ExpressionTree expr, VisitorState state) {
|
||||
return switch (expr.getKind()) {
|
||||
case IDENTIFIER -> SourceCode.treeToString(expr, state);
|
||||
case MEMBER_SELECT -> ((MemberSelectTree) expr).getIdentifier().toString();
|
||||
default -> throw new VerifyException("Unexpected type of expression: " + expr.getKind());
|
||||
};
|
||||
switch (expr.getKind()) {
|
||||
case IDENTIFIER:
|
||||
return Util.treeToString(expr, state);
|
||||
case MEMBER_SELECT:
|
||||
return ((MemberSelectTree) expr).getIdentifier().toString();
|
||||
default:
|
||||
throw new VerifyException("Unexpected type of expression: " + expr.getKind());
|
||||
}
|
||||
}
|
||||
|
||||
private static Fix replaceAnnotation(
|
||||
@@ -104,11 +105,12 @@ public final class SpringMvcAnnotation extends BugChecker implements AnnotationT
|
||||
String newArguments =
|
||||
tree.getArguments().stream()
|
||||
.filter(not(argToRemove::equals))
|
||||
.map(arg -> SourceCode.treeToString(arg, state))
|
||||
.map(arg -> Util.treeToString(arg, state))
|
||||
.collect(joining(", "));
|
||||
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder();
|
||||
String annotation = SuggestedFixes.qualifyType(state, fix, ANN_PACKAGE_PREFIX + newAnnotation);
|
||||
return fix.replace(tree, String.format("@%s(%s)", annotation, newArguments)).build();
|
||||
return SuggestedFix.builder()
|
||||
.addImport(ANN_PACKAGE_PREFIX + newAnnotation)
|
||||
.replace(tree, String.format("@%s(%s)", newAnnotation, newArguments))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static tech.picnic.errorprone.bugpatterns.NonStaticImport.NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS;
|
||||
import static tech.picnic.errorprone.bugpatterns.NonStaticImport.NON_STATIC_IMPORT_CANDIDATE_MEMBERS;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Functions;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.base.Verify;
|
||||
import com.google.common.collect.Comparators;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultiset;
|
||||
import com.google.common.collect.ImmutableRangeSet;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSetMultimap;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.ImmutableSortedMultiset;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.collect.ImmutableTable;
|
||||
import com.google.common.collect.MoreCollectors;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MemberSelectTreeMatcher;
|
||||
import com.google.errorprone.bugpatterns.StaticImports;
|
||||
import com.google.errorprone.bugpatterns.StaticImports.StaticImportInfo;
|
||||
import com.google.errorprone.fixes.Fix;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matchers;
|
||||
import com.google.errorprone.predicates.TypePredicates;
|
||||
import com.google.errorprone.refaster.ImportPolicy;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** A {@link BugChecker} that flags type members that can and should be statically imported. */
|
||||
// XXX: This check is closely linked to `NonStaticImport`. Consider merging the two.
|
||||
// XXX: Tricky cases:
|
||||
// - `org.springframework.http.HttpStatus` (not always an improvement, and `valueOf` must
|
||||
// certainly be excluded)
|
||||
// - `com.google.common.collect.Tables`
|
||||
// - `ch.qos.logback.classic.Level.{DEBUG, ERROR, INFO, TRACE, WARN}`
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Identifier should be statically imported",
|
||||
link = BUG_PATTERNS_BASE_URL + "StaticImport",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
public final class StaticImport extends BugChecker implements MemberSelectTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Types whose members should be statically imported, unless exempted by {@link
|
||||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_MEMBERS} or {@link
|
||||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS}.
|
||||
*
|
||||
* <p>Types listed here should be mutually exclusive with {@link
|
||||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_TYPES}.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final ImmutableSet<String> STATIC_IMPORT_CANDIDATE_TYPES =
|
||||
ImmutableSet.of(
|
||||
BugPattern.LinkType.class.getCanonicalName(),
|
||||
BugPattern.SeverityLevel.class.getCanonicalName(),
|
||||
BugPattern.StandardTags.class.getCanonicalName(),
|
||||
Collections.class.getCanonicalName(),
|
||||
Collectors.class.getCanonicalName(),
|
||||
Comparator.class.getCanonicalName(),
|
||||
ImportPolicy.class.getCanonicalName(),
|
||||
Map.Entry.class.getCanonicalName(),
|
||||
Matchers.class.getCanonicalName(),
|
||||
MoreCollectors.class.getCanonicalName(),
|
||||
Pattern.class.getCanonicalName(),
|
||||
Preconditions.class.getCanonicalName(),
|
||||
Predicates.class.getCanonicalName(),
|
||||
StandardCharsets.class.getCanonicalName(),
|
||||
TypePredicates.class.getCanonicalName(),
|
||||
Verify.class.getCanonicalName(),
|
||||
"com.fasterxml.jackson.annotation.JsonCreator.Mode",
|
||||
"com.fasterxml.jackson.annotation.JsonFormat.Shape",
|
||||
"com.fasterxml.jackson.annotation.JsonInclude.Include",
|
||||
"com.fasterxml.jackson.annotation.JsonProperty.Access",
|
||||
"com.mongodb.client.model.Accumulators",
|
||||
"com.mongodb.client.model.Aggregates",
|
||||
"com.mongodb.client.model.Filters",
|
||||
"com.mongodb.client.model.Indexes",
|
||||
"com.mongodb.client.model.Projections",
|
||||
"com.mongodb.client.model.Sorts",
|
||||
"com.mongodb.client.model.Updates",
|
||||
"org.assertj.core.api.Assertions",
|
||||
"org.assertj.core.api.InstanceOfAssertFactories",
|
||||
"org.assertj.core.api.SoftAssertions",
|
||||
"org.assertj.core.data.Offset",
|
||||
"org.assertj.core.groups.Tuple",
|
||||
"org.hamcrest.Matchers",
|
||||
"org.hamcrest.text.MatchesPattern",
|
||||
"org.hibernate.validator.testutil.ConstraintViolationAssert",
|
||||
"org.junit.jupiter.api.Assertions",
|
||||
"org.mockito.AdditionalAnswers",
|
||||
"org.mockito.Answers",
|
||||
"org.mockito.ArgumentMatchers",
|
||||
"org.mockito.Mockito",
|
||||
"org.springframework.boot.test.context.SpringBootTest.WebEnvironment",
|
||||
"org.springframework.format.annotation.DateTimeFormat.ISO",
|
||||
"org.springframework.http.HttpHeaders",
|
||||
"org.springframework.http.HttpMethod",
|
||||
"org.springframework.http.MediaType",
|
||||
"org.testng.Assert",
|
||||
"reactor.function.TupleUtils",
|
||||
"tech.picnic.errorprone.utils.MoreTypes");
|
||||
|
||||
/**
|
||||
* Type members that should be statically imported.
|
||||
*
|
||||
* <p>Please note that:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Types listed by {@link #STATIC_IMPORT_CANDIDATE_TYPES} should be omitted from this
|
||||
* collection.
|
||||
* <li>This collection should be mutually exclusive with {@link
|
||||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_MEMBERS}.
|
||||
* <li>This collection should not list members contained in {@link
|
||||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS}.
|
||||
* </ul>
|
||||
*/
|
||||
static final ImmutableSetMultimap<String, String> STATIC_IMPORT_CANDIDATE_MEMBERS =
|
||||
ImmutableSetMultimap.<String, String>builder()
|
||||
.putAll(Comparators.class.getCanonicalName(), "emptiesFirst", "emptiesLast")
|
||||
.put(Function.class.getCanonicalName(), "identity")
|
||||
.put(Functions.class.getCanonicalName(), "identity")
|
||||
.put(ImmutableList.class.getCanonicalName(), "toImmutableList")
|
||||
.putAll(
|
||||
ImmutableListMultimap.class.getCanonicalName(),
|
||||
"flatteningToImmutableListMultimap",
|
||||
"toImmutableListMultimap")
|
||||
.put(ImmutableMap.class.getCanonicalName(), "toImmutableMap")
|
||||
.put(ImmutableMultiset.class.getCanonicalName(), "toImmutableMultiset")
|
||||
.put(ImmutableRangeSet.class.getCanonicalName(), "toImmutableRangeSet")
|
||||
.put(ImmutableSet.class.getCanonicalName(), "toImmutableSet")
|
||||
.putAll(
|
||||
ImmutableSetMultimap.class.getCanonicalName(),
|
||||
"flatteningToImmutableSetMultimap",
|
||||
"toImmutableSetMultimap")
|
||||
.put(ImmutableSortedMap.class.getCanonicalName(), "toImmutableSortedMap")
|
||||
.put(ImmutableSortedMultiset.class.getCanonicalName(), "toImmutableSortedMultiset")
|
||||
.put(ImmutableSortedSet.class.getCanonicalName(), "toImmutableSortedSet")
|
||||
.put(ImmutableTable.class.getCanonicalName(), "toImmutableTable")
|
||||
.putAll(
|
||||
Objects.class.getCanonicalName(),
|
||||
"checkIndex",
|
||||
"checkFromIndexSize",
|
||||
"checkFromToIndex",
|
||||
"requireNonNull",
|
||||
"requireNonNullElse",
|
||||
"requireNonNullElseGet")
|
||||
.put(Predicate.class.getCanonicalName(), "not")
|
||||
.put(Sets.class.getCanonicalName(), "toImmutableEnumSet")
|
||||
.put(UUID.class.getCanonicalName(), "randomUUID")
|
||||
.put(ZoneOffset.class.getCanonicalName(), "UTC")
|
||||
.put("org.junit.jupiter.params.provider.Arguments", "arguments")
|
||||
.build();
|
||||
|
||||
/** Instantiates a new {@link StaticImport} instance. */
|
||||
public StaticImport() {}
|
||||
|
||||
@Override
|
||||
public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) {
|
||||
if (!isCandidateContext(state) || !isCandidate(tree)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
StaticImportInfo importInfo = StaticImports.tryCreate(tree, state);
|
||||
if (importInfo == null) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return getCandidateSimpleName(importInfo)
|
||||
.flatMap(n -> tryStaticImport(tree, importInfo.canonicalName() + '.' + n, n, state))
|
||||
.map(fix -> describeMatch(tree, fix))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
private static boolean isCandidateContext(VisitorState state) {
|
||||
Tree parentTree =
|
||||
requireNonNull(state.getPath().getParentPath(), "MemberSelectTree lacks enclosing node")
|
||||
.getLeaf();
|
||||
|
||||
return parentTree instanceof MethodInvocationTree methodInvocation
|
||||
? methodInvocation.getTypeArguments().isEmpty()
|
||||
: (parentTree.getKind() != Kind.IMPORT && parentTree.getKind() != Kind.MEMBER_SELECT);
|
||||
}
|
||||
|
||||
private static boolean isCandidate(MemberSelectTree tree) {
|
||||
String identifier = tree.getIdentifier().toString();
|
||||
if (NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS.contains(identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Type type = ASTHelpers.getType(tree.getExpression());
|
||||
return type != null
|
||||
&& !NON_STATIC_IMPORT_CANDIDATE_MEMBERS.containsEntry(type.toString(), identifier);
|
||||
}
|
||||
|
||||
private static Optional<String> getCandidateSimpleName(StaticImportInfo importInfo) {
|
||||
String canonicalName = importInfo.canonicalName();
|
||||
return importInfo
|
||||
.simpleName()
|
||||
.toJavaUtil()
|
||||
.filter(
|
||||
name ->
|
||||
STATIC_IMPORT_CANDIDATE_TYPES.contains(canonicalName)
|
||||
|| STATIC_IMPORT_CANDIDATE_MEMBERS.containsEntry(canonicalName, name));
|
||||
}
|
||||
|
||||
private static Optional<Fix> tryStaticImport(
|
||||
MemberSelectTree tree, String fullyQualifiedName, String simpleName, VisitorState state) {
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder().replace(tree, simpleName);
|
||||
|
||||
if (!simpleName.equals(SuggestedFixes.qualifyStaticImport(fullyQualifiedName, fix, state))) {
|
||||
/* Statically importing this symbol would clash with an existing import. */
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(fix.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSetMultimap;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MemberSelectTreeMatcher;
|
||||
import com.google.errorprone.bugpatterns.StaticImports;
|
||||
import com.google.errorprone.bugpatterns.StaticImports.StaticImportInfo;
|
||||
import com.google.errorprone.fixes.Fix;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/** A {@link BugChecker} which flags methods that can and should be statically imported. */
|
||||
// XXX: Tricky cases:
|
||||
// - `org.springframework.http.MediaType` (do except for `ALL`?)
|
||||
// - `org.springframework.http.HttpStatus` (not always an improvement, and `valueOf` must
|
||||
// certainly be excluded)
|
||||
// - `com.google.common.collect.Tables`
|
||||
// - `ch.qos.logback.classic.Level.{DEBUG, ERROR, INFO, TRACE, WARN"}`
|
||||
// XXX: Also introduce a check which disallows static imports of certain methods. Candidates:
|
||||
// - `com.google.common.base.Strings`
|
||||
// - `java.util.Optional.empty`
|
||||
// - `java.util.Locale.ROOT`
|
||||
// - `ZoneOffset.ofHours` and other `ofXXX`-style methods.
|
||||
// - `java.time.Clock`.
|
||||
// - Several other `java.time` classes.
|
||||
// - Likely any of `*.{ZERO, ONE, MIX, MAX, MIN_VALUE, MAX_VALUE}`.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "StaticImport",
|
||||
summary = "Method should be statically imported",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class StaticImportCheck extends BugChecker implements MemberSelectTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@VisibleForTesting
|
||||
static final ImmutableSet<String> STATIC_IMPORT_CANDIDATE_CLASSES =
|
||||
ImmutableSet.of(
|
||||
"com.google.common.base.Preconditions",
|
||||
"com.google.common.base.Predicates",
|
||||
"com.google.common.base.Verify",
|
||||
"com.google.common.collect.MoreCollectors",
|
||||
"com.mongodb.client.model.Accumulators",
|
||||
"com.mongodb.client.model.Aggregates",
|
||||
"com.mongodb.client.model.Filters",
|
||||
"com.mongodb.client.model.Indexes",
|
||||
"com.mongodb.client.model.Projections",
|
||||
"com.mongodb.client.model.Sorts",
|
||||
"com.mongodb.client.model.Updates",
|
||||
"java.nio.charset.StandardCharsets",
|
||||
"java.util.Comparator",
|
||||
"java.util.Map.Entry",
|
||||
"java.util.stream.Collectors",
|
||||
"org.assertj.core.api.Assertions",
|
||||
"org.assertj.core.api.InstanceOfAssertFactories",
|
||||
"org.assertj.core.api.SoftAssertions",
|
||||
"org.assertj.core.data.Offset",
|
||||
"org.assertj.core.groups.Tuple",
|
||||
"org.hamcrest.Matchers",
|
||||
"org.hamcrest.text.MatchesPattern",
|
||||
"org.hibernate.validator.testutil.ConstraintViolationAssert",
|
||||
"org.junit.jupiter.api.Assertions",
|
||||
"org.mockito.AdditionalAnswers",
|
||||
"org.mockito.Answers",
|
||||
"org.mockito.ArgumentMatchers",
|
||||
"org.mockito.Mockito",
|
||||
"org.springframework.format.annotation.DateTimeFormat.ISO",
|
||||
"org.springframework.http.HttpHeaders",
|
||||
"org.springframework.http.HttpMethod",
|
||||
"org.testng.Assert",
|
||||
"reactor.function.TupleUtils");
|
||||
|
||||
@VisibleForTesting
|
||||
static final ImmutableSetMultimap<String, String> STATIC_IMPORT_CANDIDATE_METHODS =
|
||||
ImmutableSetMultimap.<String, String>builder()
|
||||
.putAll(
|
||||
"com.google.common.collect.ImmutableListMultimap",
|
||||
"flatteningToImmutableListMultimap",
|
||||
"toImmutableListMultimap")
|
||||
.put("com.google.common.collect.ImmutableList", "toImmutableList")
|
||||
.put("com.google.common.collect.ImmutableMap", "toImmutableMap")
|
||||
.put("com.google.common.collect.ImmutableMultiset", "toImmutableMultiset")
|
||||
.put("com.google.common.collect.ImmutableRangeSet", "toImmutableRangeSet")
|
||||
.putAll(
|
||||
"com.google.common.collect.ImmutableSetMultimap",
|
||||
"flatteningToImmutableSetMultimap",
|
||||
"toImmutableSetMultimap")
|
||||
.put("com.google.common.collect.ImmutableSet", "toImmutableSet")
|
||||
.put("com.google.common.collect.ImmutableSortedMap", "toImmutableSortedMap")
|
||||
.put("com.google.common.collect.ImmutableSortedMultiset", "toImmutableSortedMultiset")
|
||||
.put("com.google.common.collect.ImmutableSortedSet", "toImmutableSortedSet")
|
||||
.put("com.google.common.collect.ImmutableTable", "toImmutableTable")
|
||||
.put("com.google.common.collect.Sets", "toImmutableEnumSet")
|
||||
.put("com.google.common.base.Functions", "identity")
|
||||
.put("java.util.function.Function", "identity")
|
||||
.put("java.util.function.Predicate", "not")
|
||||
.put("org.junit.jupiter.params.provider.Arguments", "arguments")
|
||||
.putAll(
|
||||
"java.util.Objects",
|
||||
"checkIndex",
|
||||
"checkFromIndexSize",
|
||||
"checkFromToIndex",
|
||||
"requireNonNull",
|
||||
"requireNonNullElse",
|
||||
"requireNonNullElseGet")
|
||||
.putAll("com.google.common.collect.Comparators", "emptiesFirst", "emptiesLast")
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) {
|
||||
if (!isCandidate(state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
StaticImportInfo importInfo = StaticImports.tryCreate(tree, state);
|
||||
if (importInfo == null) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return getCandidateSimpleName(importInfo)
|
||||
.flatMap(n -> tryStaticImport(tree, importInfo.canonicalName() + '.' + n, n, state))
|
||||
.map(fix -> describeMatch(tree, fix))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
private static boolean isCandidate(VisitorState state) {
|
||||
Tree parentTree =
|
||||
Objects.requireNonNull(
|
||||
state.getPath().getParentPath(), "MemberSelectTree lacks enclosing node")
|
||||
.getLeaf();
|
||||
switch (parentTree.getKind()) {
|
||||
case IMPORT:
|
||||
case MEMBER_SELECT:
|
||||
return false;
|
||||
case METHOD_INVOCATION:
|
||||
return ((MethodInvocationTree) parentTree).getTypeArguments().isEmpty();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<String> getCandidateSimpleName(StaticImportInfo importInfo) {
|
||||
String canonicalName = importInfo.canonicalName();
|
||||
return importInfo
|
||||
.simpleName()
|
||||
.toJavaUtil()
|
||||
.filter(
|
||||
name ->
|
||||
STATIC_IMPORT_CANDIDATE_CLASSES.contains(canonicalName)
|
||||
|| STATIC_IMPORT_CANDIDATE_METHODS.containsEntry(canonicalName, name));
|
||||
}
|
||||
|
||||
private static Optional<Fix> tryStaticImport(
|
||||
MemberSelectTree tree, String fullyQualifiedName, String simpleName, VisitorState state) {
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder().replace(tree, simpleName);
|
||||
|
||||
if (!simpleName.equals(SuggestedFixes.qualifyStaticImport(fullyQualifiedName, fix, state))) {
|
||||
/* Statically importing this symbol would clash with an existing import. */
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(fix.build());
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
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.method.MethodMatchers.staticMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
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.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
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.Constants;
|
||||
import java.util.Formattable;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@link String#format(String, Object...)} invocations which can be
|
||||
* replaced with a {@link String#join(CharSequence, CharSequence...)} or even a {@link
|
||||
* String#valueOf} invocation.
|
||||
*/
|
||||
// XXX: What about `v1 + "sep" + v2` and similar expressions? Do we want to rewrite those to
|
||||
// `String.join`, or should some `String.join` invocations be rewritten to use the `+` operator?
|
||||
// (The latter suggestion would conflict with the `FormatStringConcatenation` check.)
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Prefer `String#join` over `String#format`",
|
||||
link = BUG_PATTERNS_BASE_URL + "StringJoin",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class StringJoin extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Splitter FORMAT_SPECIFIER_SPLITTER = Splitter.on("%s");
|
||||
private static final Matcher<ExpressionTree> STRING_FORMAT_INVOCATION =
|
||||
staticMethod().onClass(String.class.getCanonicalName()).named("format");
|
||||
private static final Supplier<Type> CHAR_SEQUENCE_TYPE =
|
||||
Suppliers.typeFromClass(CharSequence.class);
|
||||
private static final Supplier<Type> FORMATTABLE_TYPE = Suppliers.typeFromClass(Formattable.class);
|
||||
|
||||
/** Instantiates a new {@link StringJoin} instance. */
|
||||
public StringJoin() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!STRING_FORMAT_INVOCATION.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
// XXX: This check assumes that if the first argument to `String#format` is a `Locale`, that
|
||||
// this argument is not vacuous, and that as a result the expression cannot be simplified using
|
||||
// `#valueOf` or `#join`. Implement a separate check that identifies and drops redundant
|
||||
// `Locale` arguments. See also a related comment in `FormatStringConcatenation`.
|
||||
String formatString = ASTHelpers.constValue(tree.getArguments().get(0), String.class);
|
||||
if (formatString == null) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
List<String> separators = FORMAT_SPECIFIER_SPLITTER.splitToList(formatString);
|
||||
if (separators.size() < 2) {
|
||||
/* The format string does not contain `%s` format specifiers. */
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (separators.size() != tree.getArguments().size()) {
|
||||
/* The number of arguments does not match the number of `%s` format specifiers. */
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
int lastIndex = separators.size() - 1;
|
||||
if (!separators.get(0).isEmpty() || !separators.get(lastIndex).isEmpty()) {
|
||||
/* The format string contains leading or trailing characters. */
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
ImmutableSet<String> innerSeparators = ImmutableSet.copyOf(separators.subList(1, lastIndex));
|
||||
if (innerSeparators.size() > 1) {
|
||||
/* The `%s` format specifiers are not uniformly separated. */
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (innerSeparators.isEmpty()) {
|
||||
/*
|
||||
* This `String#format` invocation performs a straightforward string conversion; use
|
||||
* `String#valueOf` instead.
|
||||
*/
|
||||
return trySuggestExplicitStringConversion(tree, state);
|
||||
}
|
||||
|
||||
String separator = Iterables.getOnlyElement(innerSeparators);
|
||||
if (separator.indexOf('%') >= 0) {
|
||||
/* The `%s` format specifiers are separated by another format specifier. */
|
||||
// XXX: Strictly speaking we could support `%%` by mapping it to a literal `%`, but that
|
||||
// doesn't seem worth the trouble.
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return trySuggestExplicitJoin(tree, separator, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* If guaranteed to be behavior preserving, suggests replacing {@code String.format("%s", arg)}
|
||||
* with {@code String.valueOf(arg)}.
|
||||
*
|
||||
* <p>If {@code arg} is already a string then the resultant conversion is vacuous. The {@link
|
||||
* IdentityConversion} check will subsequently drop it.
|
||||
*/
|
||||
private Description trySuggestExplicitStringConversion(
|
||||
MethodInvocationTree tree, VisitorState state) {
|
||||
ExpressionTree argument = tree.getArguments().get(1);
|
||||
if (isSubtype(ASTHelpers.getType(argument), FORMATTABLE_TYPE, state)) {
|
||||
/*
|
||||
* `Formattable` arguments are handled specially; `String#valueOf` is not a suitable
|
||||
* alternative.
|
||||
*/
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return buildDescription(tree)
|
||||
.setMessage("Prefer `String#valueOf` over `String#format`")
|
||||
.addFix(SuggestedFix.replace(tree, withStringConversionExpression(argument, state)))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unless the given {@code String.format} expression includes {@link Formattable} arguments,
|
||||
* suggests replacing it with a {@code String.join} expression using the specified argument
|
||||
* separator.
|
||||
*/
|
||||
private Description trySuggestExplicitJoin(
|
||||
MethodInvocationTree tree, String separator, VisitorState state) {
|
||||
Iterator<? extends ExpressionTree> arguments = tree.getArguments().iterator();
|
||||
|
||||
SuggestedFix.Builder fix =
|
||||
SuggestedFix.builder()
|
||||
.replace(tree.getMethodSelect(), "String.join")
|
||||
.replace(arguments.next(), Constants.format(separator));
|
||||
|
||||
while (arguments.hasNext()) {
|
||||
ExpressionTree argument = arguments.next();
|
||||
Type argumentType = ASTHelpers.getType(argument);
|
||||
if (isSubtype(argumentType, FORMATTABLE_TYPE, state)) {
|
||||
/*
|
||||
* `Formattable` arguments are handled specially; `String#join` is not a suitable
|
||||
* alternative.
|
||||
*/
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (!isSubtype(argumentType, CHAR_SEQUENCE_TYPE, state)) {
|
||||
/*
|
||||
* The argument was previously implicitly converted to a string; now this must happen
|
||||
* explicitly.
|
||||
*/
|
||||
fix.replace(argument, withStringConversionExpression(argument, state));
|
||||
}
|
||||
}
|
||||
|
||||
return describeMatch(tree, fix.build());
|
||||
}
|
||||
|
||||
private static boolean isSubtype(
|
||||
@Nullable Type subType, Supplier<Type> superType, VisitorState state) {
|
||||
return ASTHelpers.isSubtype(subType, superType.get(state), state);
|
||||
}
|
||||
|
||||
private static String withStringConversionExpression(
|
||||
ExpressionTree argument, VisitorState state) {
|
||||
return String.format("String.valueOf(%s)", SourceCode.treeToString(argument, state));
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.enclosingClass;
|
||||
import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static tech.picnic.errorprone.utils.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.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.OffsetTime;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
/** A {@link BugChecker} that flags illegal time-zone related operations. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"""
|
||||
Derive the current time from an existing `Clock` Spring bean, and don't rely on a \
|
||||
`Clock`'s time zone""",
|
||||
link = BUG_PATTERNS_BASE_URL + "TimeZoneUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class TimeZoneUsage extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> BANNED_TIME_METHOD =
|
||||
anyOf(
|
||||
allOf(
|
||||
instanceMethod()
|
||||
.onDescendantOf(Clock.class.getCanonicalName())
|
||||
.namedAnyOf("getZone", "withZone"),
|
||||
not(enclosingClass(isSubtypeOf(Clock.class)))),
|
||||
staticMethod()
|
||||
.onClass(Clock.class.getCanonicalName())
|
||||
.namedAnyOf(
|
||||
"system",
|
||||
"systemDefaultZone",
|
||||
"systemUTC",
|
||||
"tickMillis",
|
||||
"tickMinutes",
|
||||
"tickSeconds"),
|
||||
staticMethod()
|
||||
.onClassAny(
|
||||
LocalDate.class.getCanonicalName(),
|
||||
LocalDateTime.class.getCanonicalName(),
|
||||
LocalTime.class.getCanonicalName(),
|
||||
OffsetDateTime.class.getCanonicalName(),
|
||||
OffsetTime.class.getCanonicalName(),
|
||||
ZonedDateTime.class.getCanonicalName())
|
||||
.named("now"),
|
||||
staticMethod()
|
||||
.onClassAny(Instant.class.getCanonicalName())
|
||||
.named("now")
|
||||
.withNoParameters());
|
||||
|
||||
/** Instantiates a new {@link TimeZoneUsage} instance. */
|
||||
public TimeZoneUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
return BANNED_TIME_METHOD.matches(tree, state)
|
||||
? buildDescription(tree).build()
|
||||
: Description.NO_MATCH;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.sun.source.tree.Tree;
|
||||
|
||||
// XXX: Can we locate this code in a better place? Maybe contribute it upstream?
|
||||
final class Util {
|
||||
private Util() {}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the given {@link Tree}, preferring the original source code
|
||||
* (if available) over its prettified representation.
|
||||
*
|
||||
* @return A non-{@code null} string.
|
||||
*/
|
||||
static String treeToString(Tree tree, VisitorState state) {
|
||||
String src = state.getSourceForNode(tree);
|
||||
return src != null ? src : tree.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
/** Picnic Error Prone Contrib checks. */
|
||||
@com.google.errorprone.annotations.CheckReturnValue
|
||||
@org.jspecify.annotations.NullMarked
|
||||
@CheckReturnValue
|
||||
@ParametersAreNonnullByDefault
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import com.google.errorprone.annotations.CheckReturnValue;
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
import static org.assertj.core.data.Percentage.withPercentage;
|
||||
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import java.math.BigDecimal;
|
||||
import org.assertj.core.api.AbstractBigDecimalAssert;
|
||||
import org.assertj.core.api.BigDecimalAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/**
|
||||
* Refaster rules related to AssertJ assertions over {@link BigDecimal}s.
|
||||
*
|
||||
* <p>Note that, contrary to collections of Refaster rules for other {@link
|
||||
* org.assertj.core.api.NumberAssert} subtypes, these rules do not rewrite to/from {@link
|
||||
* BigDecimalAssert#isEqualTo(Object)} and {@link BigDecimalAssert#isNotEqualTo(Object)}. This is
|
||||
* because {@link BigDecimal#equals(Object)} considers not only the numeric value of compared
|
||||
* instances, but also their scale. As a result various seemingly straightforward transformations
|
||||
* would actually subtly change the assertion's semantics.
|
||||
*/
|
||||
@OnlineDocumentation
|
||||
final class AssertJBigDecimalRules {
|
||||
private AssertJBigDecimalRules() {}
|
||||
|
||||
static final class AbstractBigDecimalAssertIsEqualByComparingTo {
|
||||
@BeforeTemplate
|
||||
AbstractBigDecimalAssert<?> before(AbstractBigDecimalAssert<?> bigDecimalAssert, BigDecimal n) {
|
||||
return Refaster.anyOf(
|
||||
bigDecimalAssert.isCloseTo(n, offset(BigDecimal.ZERO)),
|
||||
bigDecimalAssert.isCloseTo(n, withPercentage(0)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractBigDecimalAssert<?> after(AbstractBigDecimalAssert<?> bigDecimalAssert, BigDecimal n) {
|
||||
return bigDecimalAssert.isEqualByComparingTo(n);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AbstractBigDecimalAssertIsNotEqualByComparingTo {
|
||||
@BeforeTemplate
|
||||
AbstractBigDecimalAssert<?> before(AbstractBigDecimalAssert<?> bigDecimalAssert, BigDecimal n) {
|
||||
return Refaster.anyOf(
|
||||
bigDecimalAssert.isNotCloseTo(n, offset(BigDecimal.ZERO)),
|
||||
bigDecimalAssert.isNotCloseTo(n, withPercentage(0)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractBigDecimalAssert<?> after(AbstractBigDecimalAssert<?> bigDecimalAssert, BigDecimal n) {
|
||||
return bigDecimalAssert.isNotEqualByComparingTo(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import org.assertj.core.api.AbstractComparableAssert;
|
||||
import org.assertj.core.api.AbstractIntegerAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
@OnlineDocumentation
|
||||
final class AssertJComparableRules {
|
||||
private AssertJComparableRules() {}
|
||||
|
||||
static final class AssertThatIsEqualByComparingTo<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(T actual, T expected) {
|
||||
return assertThat(actual.compareTo(expected)).isEqualTo(0);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractComparableAssert<?, ?> after(T actual, T expected) {
|
||||
return assertThat(actual).isEqualByComparingTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsNotEqualByComparingTo<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(T actual, T expected) {
|
||||
return assertThat(actual.compareTo(expected)).isNotEqualTo(0);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractComparableAssert<?, ?> after(T actual, T expected) {
|
||||
return assertThat(actual).isNotEqualByComparingTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsLessThan<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(T actual, T expected) {
|
||||
return assertThat(actual.compareTo(expected)).isNegative();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractComparableAssert<?, ?> after(T actual, T expected) {
|
||||
return assertThat(actual).isLessThan(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsLessThanOrEqualTo<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(T actual, T expected) {
|
||||
return assertThat(actual.compareTo(expected)).isNotPositive();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractComparableAssert<?, ?> after(T actual, T expected) {
|
||||
return assertThat(actual).isLessThanOrEqualTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsGreaterThan<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(T actual, T expected) {
|
||||
return assertThat(actual.compareTo(expected)).isPositive();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractComparableAssert<?, ?> after(T actual, T expected) {
|
||||
return assertThat(actual).isGreaterThan(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsGreaterThanOrEqualTo<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(T actual, T expected) {
|
||||
return assertThat(actual.compareTo(expected)).isNotNegative();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractComparableAssert<?, ?> after(T actual, T expected) {
|
||||
return assertThat(actual).isGreaterThanOrEqualTo(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
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.UseImportPolicy;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
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;
|
||||
import tech.picnic.errorprone.refaster.matchers.IsEmpty;
|
||||
|
||||
@OnlineDocumentation
|
||||
final class AssertJMapRules {
|
||||
private AssertJMapRules() {}
|
||||
|
||||
static final class AbstractMapAssertIsEmpty<K, V> {
|
||||
@BeforeTemplate
|
||||
void before(
|
||||
AbstractMapAssert<?, ?, K, V> mapAssert,
|
||||
@Matches(IsEmpty.class) Map<? extends K, ? extends V> wellTypedMap,
|
||||
@Matches(IsEmpty.class) Map<?, ?> arbitrarilyTypedMap,
|
||||
@Matches(IsEmpty.class) Iterable<? extends K> keys) {
|
||||
Refaster.anyOf(
|
||||
mapAssert.containsExactlyEntriesOf(wellTypedMap),
|
||||
mapAssert.containsExactlyInAnyOrderEntriesOf(wellTypedMap),
|
||||
mapAssert.hasSameSizeAs(arbitrarilyTypedMap),
|
||||
mapAssert.isEqualTo(arbitrarilyTypedMap),
|
||||
mapAssert.containsOnlyKeys(keys),
|
||||
mapAssert.containsExactly(),
|
||||
mapAssert.containsOnly(),
|
||||
mapAssert.containsOnlyKeys());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
void after(AbstractMapAssert<?, ?, K, V> mapAssert) {
|
||||
mapAssert.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapIsEmpty<K, V> {
|
||||
@BeforeTemplate
|
||||
void before(Map<K, V> map) {
|
||||
Refaster.anyOf(
|
||||
assertThat(map).hasSize(0),
|
||||
assertThat(map.isEmpty()).isTrue(),
|
||||
assertThat(map.size()).isEqualTo(0L),
|
||||
assertThat(map.size()).isNotPositive());
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
void before2(Map<K, V> map) {
|
||||
assertThat(Refaster.anyOf(map.keySet(), map.values(), map.entrySet())).isEmpty();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
void after(Map<K, V> map) {
|
||||
assertThat(map).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
static final class AbstractMapAssertIsNotEmpty<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractMapAssert<?, ?, K, V> before(
|
||||
AbstractMapAssert<?, ?, K, V> mapAssert, @Matches(IsEmpty.class) Map<?, ?> map) {
|
||||
return mapAssert.isNotEqualTo(map);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert) {
|
||||
return mapAssert.isNotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapIsNotEmpty<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractAssert<?, ?> before(Map<K, V> map) {
|
||||
return Refaster.anyOf(
|
||||
assertThat(map.isEmpty()).isFalse(),
|
||||
assertThat(map.size()).isNotEqualTo(0),
|
||||
assertThat(map.size()).isPositive(),
|
||||
assertThat(Refaster.anyOf(map.keySet(), map.values(), map.entrySet())).isNotEmpty());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map) {
|
||||
return assertThat(map).isNotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
static final class AbstractMapAssertContainsExactlyInAnyOrderEntriesOf<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractMapAssert<?, ?, K, V> before(
|
||||
AbstractMapAssert<?, ?, K, V> mapAssert, Map<? extends K, ? extends V> map) {
|
||||
return mapAssert.isEqualTo(map);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractMapAssert<?, ?, K, V> after(
|
||||
AbstractMapAssert<?, ?, K, V> mapAssert, Map<? extends K, ? extends V> map) {
|
||||
return mapAssert.containsExactlyInAnyOrderEntriesOf(map);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AbstractMapAssertContainsExactlyEntriesOf<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractMapAssert<?, ?, K, V> before(AbstractMapAssert<?, ?, K, V> mapAssert, K key, V value) {
|
||||
return mapAssert.containsExactlyInAnyOrderEntriesOf(ImmutableMap.of(key, value));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert, K key, V value) {
|
||||
return mapAssert.containsExactlyEntriesOf(ImmutableMap.of(key, value));
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapHasSize<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractAssert<?, ?> before(Map<K, V> map, int length) {
|
||||
return Refaster.anyOf(
|
||||
assertThat(map.size()).isEqualTo(length),
|
||||
assertThat(Refaster.anyOf(map.keySet(), map.values(), map.entrySet())).hasSize(length));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, int length) {
|
||||
return assertThat(map).hasSize(length);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AbstractMapAssertHasSameSizeAs<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractMapAssert<?, ?, K, V> before(AbstractMapAssert<?, ?, K, V> mapAssert, Map<?, ?> map) {
|
||||
return mapAssert.hasSize(map.size());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert, Map<?, ?> map) {
|
||||
return mapAssert.hasSameSizeAs(map);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapContainsKey<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(Map<K, V> map, K key) {
|
||||
return assertThat(map.containsKey(key)).isTrue();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, K key) {
|
||||
return assertThat(map).containsKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapDoesNotContainKey<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(Map<K, V> map, K key) {
|
||||
return assertThat(map.containsKey(key)).isFalse();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, K key) {
|
||||
return assertThat(map).doesNotContainKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapContainsOnlyKeys<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractCollectionAssert<?, Collection<? extends K>, K, ?> before(
|
||||
Map<K, V> map, Set<? extends K> keys) {
|
||||
return assertThat(map.keySet()).hasSameElementsAs(keys);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, Set<? extends K> keys) {
|
||||
return assertThat(map).containsOnlyKeys(keys);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapContainsValue<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(Map<K, V> map, V value) {
|
||||
return assertThat(map.containsValue(value)).isTrue();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, V value) {
|
||||
return assertThat(map).containsValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapDoesNotContainValue<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(Map<K, V> map, V value) {
|
||||
return assertThat(map.containsValue(value)).isFalse();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, V value) {
|
||||
return assertThat(map).doesNotContainValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import org.assertj.core.api.AbstractBooleanAssert;
|
||||
import org.assertj.core.api.AbstractDoubleAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
@OnlineDocumentation
|
||||
final class AssertJPrimitiveRules {
|
||||
private AssertJPrimitiveRules() {}
|
||||
|
||||
static final class AssertThatIsEqualTo {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(boolean actual, boolean expected) {
|
||||
return Refaster.anyOf(
|
||||
assertThat(actual == expected).isTrue(), assertThat(actual != expected).isFalse());
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"java:S1244" /* The (in)equality checks are fragile, but may be seen in the wild. */)
|
||||
AbstractBooleanAssert<?> before(double actual, double expected) {
|
||||
return Refaster.anyOf(
|
||||
assertThat(actual == expected).isTrue(), assertThat(actual != expected).isFalse());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractBooleanAssert<?> after(boolean actual, boolean expected) {
|
||||
return assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsNotEqualTo {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(boolean actual, boolean expected) {
|
||||
return Refaster.anyOf(
|
||||
assertThat(actual != expected).isTrue(), assertThat(actual == expected).isFalse());
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"java:S1244" /* The (in)equality checks are fragile, but may be seen in the wild. */)
|
||||
AbstractBooleanAssert<?> before(double actual, double expected) {
|
||||
return Refaster.anyOf(
|
||||
assertThat(actual != expected).isTrue(), assertThat(actual == expected).isFalse());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractBooleanAssert<?> after(boolean actual, boolean expected) {
|
||||
return assertThat(actual).isNotEqualTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsLessThan {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(double actual, double expected) {
|
||||
return Refaster.anyOf(
|
||||
assertThat(actual < expected).isTrue(), assertThat(actual >= expected).isFalse());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractDoubleAssert<?> after(double actual, double expected) {
|
||||
return assertThat(actual).isLessThan(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsLessThanOrEqualTo {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(double actual, double expected) {
|
||||
return Refaster.anyOf(
|
||||
assertThat(actual <= expected).isTrue(), assertThat(actual > expected).isFalse());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractDoubleAssert<?> after(double actual, double expected) {
|
||||
return assertThat(actual).isLessThanOrEqualTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsGreaterThan {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(double actual, double expected) {
|
||||
return Refaster.anyOf(
|
||||
assertThat(actual > expected).isTrue(), assertThat(actual <= expected).isFalse());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractDoubleAssert<?> after(double actual, double expected) {
|
||||
return assertThat(actual).isGreaterThan(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsGreaterThanOrEqualTo {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(double actual, double expected) {
|
||||
return Refaster.anyOf(
|
||||
assertThat(actual >= expected).isTrue(), assertThat(actual < expected).isFalse());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractDoubleAssert<?> after(double actual, double expected) {
|
||||
return assertThat(actual).isGreaterThanOrEqualTo(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
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;
|
||||
|
||||
@OnlineDocumentation
|
||||
final class AssertJStringRules {
|
||||
private AssertJStringRules() {}
|
||||
|
||||
static final class AbstractStringAssertStringIsEmpty {
|
||||
@BeforeTemplate
|
||||
void before(AbstractStringAssert<?> stringAssert) {
|
||||
stringAssert.isEqualTo("");
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
void after(AbstractStringAssert<?> stringAssert) {
|
||||
stringAssert.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatStringIsEmpty {
|
||||
@BeforeTemplate
|
||||
void before(String string) {
|
||||
assertThat(string.isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
void after(String string) {
|
||||
assertThat(string).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
static final class AbstractStringAssertStringIsNotEmpty {
|
||||
@BeforeTemplate
|
||||
AbstractStringAssert<?> before(AbstractStringAssert<?> stringAssert) {
|
||||
return stringAssert.isNotEqualTo("");
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractStringAssert<?> after(AbstractStringAssert<?> stringAssert) {
|
||||
return stringAssert.isNotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatStringIsNotEmpty {
|
||||
@BeforeTemplate
|
||||
AbstractAssert<?, ?> before(String string) {
|
||||
return assertThat(string.isEmpty()).isFalse();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractAssert<?, ?> after(String string) {
|
||||
return assertThat(string).isNotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMatches {
|
||||
@BeforeTemplate
|
||||
AbstractAssert<?, ?> before(String string, String regex) {
|
||||
return assertThat(string.matches(regex)).isTrue();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractAssert<?, ?> after(String string, String regex) {
|
||||
return assertThat(string).matches(regex);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatDoesNotMatch {
|
||||
@BeforeTemplate
|
||||
AbstractAssert<?, ?> before(String string, String regex) {
|
||||
return assertThat(string.matches(regex)).isFalse();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractAssert<?, ?> after(String string, String regex) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,629 +0,0 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIOException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.Repeated;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import java.io.IOException;
|
||||
import org.assertj.core.api.AbstractObjectAssert;
|
||||
import org.assertj.core.api.AbstractThrowableAssert;
|
||||
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/**
|
||||
* Refaster rules related to AssertJ assertions over expressions that may throw a {@link Throwable}
|
||||
* subtype.
|
||||
*
|
||||
* <p>For reasons of consistency we prefer {@link
|
||||
* org.assertj.core.api.Assertions#assertThatThrownBy} over static methods for specific exception
|
||||
* types. Note that only the most common assertion expressions are rewritten here; covering all
|
||||
* cases would require the implementation of an Error Prone check instead.
|
||||
*/
|
||||
@OnlineDocumentation
|
||||
final class AssertJThrowingCallableRules {
|
||||
private AssertJThrowingCallableRules() {}
|
||||
|
||||
static final class AssertThatThrownByIllegalArgumentException {
|
||||
@BeforeTemplate
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable) {
|
||||
return assertThatIllegalArgumentException().isThrownBy(throwingCallable);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable) {
|
||||
return assertThatThrownBy(throwingCallable).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIllegalArgumentExceptionHasMessage {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByIllegalArgumentException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatIllegalArgumentException().isThrownBy(throwingCallable).withMessage(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIllegalArgumentExceptionHasMessageParameters {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByIllegalArgumentException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(
|
||||
ThrowingCallable throwingCallable, String message, @Repeated Object parameters) {
|
||||
return assertThatIllegalArgumentException()
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessage(message, parameters);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(
|
||||
ThrowingCallable throwingCallable, String message, @Repeated Object parameters) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage(message, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIllegalArgumentExceptionHasMessageStartingWith {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByIllegalArgumentException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatIllegalArgumentException()
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessageStartingWith(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageStartingWith(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIllegalArgumentExceptionHasMessageContaining {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByIllegalArgumentException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatIllegalArgumentException()
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessageContaining(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIllegalArgumentExceptionHasMessageNotContainingAny {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByIllegalArgumentException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(
|
||||
ThrowingCallable throwingCallable, @Repeated CharSequence values) {
|
||||
return assertThatIllegalArgumentException()
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessageNotContainingAny(values);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(
|
||||
ThrowingCallable throwingCallable, @Repeated CharSequence values) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageNotContainingAny(values);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIllegalStateException {
|
||||
@BeforeTemplate
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable) {
|
||||
return assertThatIllegalStateException().isThrownBy(throwingCallable);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable) {
|
||||
return assertThatThrownBy(throwingCallable).isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIllegalStateExceptionHasMessage {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByIllegalStateException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatIllegalStateException().isThrownBy(throwingCallable).withMessage(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIllegalStateExceptionHasMessageParameters {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByIllegalStateException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(
|
||||
ThrowingCallable throwingCallable, String message, @Repeated Object parameters) {
|
||||
return assertThatIllegalStateException()
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessage(message, parameters);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(
|
||||
ThrowingCallable throwingCallable, String message, @Repeated Object parameters) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage(message, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIllegalStateExceptionHasMessageStartingWith {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByIllegalStateException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatIllegalStateException()
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessageStartingWith(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageStartingWith(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIllegalStateExceptionHasMessageContaining {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByIllegalStateException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatIllegalStateException()
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessageContaining(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIllegalStateExceptionHasMessageNotContaining {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByIllegalStateException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatIllegalStateException()
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessageNotContaining(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageNotContaining(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByNullPointerException {
|
||||
@BeforeTemplate
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable) {
|
||||
return assertThatNullPointerException().isThrownBy(throwingCallable);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable) {
|
||||
return assertThatThrownBy(throwingCallable).isInstanceOf(NullPointerException.class);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByNullPointerExceptionHasMessage {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByNullPointerException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatNullPointerException().isThrownBy(throwingCallable).withMessage(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(NullPointerException.class)
|
||||
.hasMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByNullPointerExceptionHasMessageParameters {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByNullPointerException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(
|
||||
ThrowingCallable throwingCallable, String message, @Repeated Object parameters) {
|
||||
return assertThatNullPointerException()
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessage(message, parameters);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(
|
||||
ThrowingCallable throwingCallable, String message, @Repeated Object parameters) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(NullPointerException.class)
|
||||
.hasMessage(message, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByNullPointerExceptionHasMessageStartingWith {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByNullPointerException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatNullPointerException()
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessageStartingWith(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(NullPointerException.class)
|
||||
.hasMessageStartingWith(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByNullPointerExceptionHasMessageContaining {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByNullPointerException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatNullPointerException()
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessageContaining(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(NullPointerException.class)
|
||||
.hasMessageContaining(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByNullPointerExceptionHasMessageNotContaining {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"AssertThatThrownByNullPointerException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatNullPointerException()
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessageNotContaining(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(NullPointerException.class)
|
||||
.hasMessageNotContaining(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIOException {
|
||||
@BeforeTemplate
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable) {
|
||||
return assertThatIOException().isThrownBy(throwingCallable);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable) {
|
||||
return assertThatThrownBy(throwingCallable).isInstanceOf(IOException.class);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIOExceptionHasMessage {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("AssertThatThrownByIOException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatIOException().isThrownBy(throwingCallable).withMessage(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IOException.class)
|
||||
.hasMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIOExceptionHasMessageParameters {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("AssertThatThrownByIOException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(
|
||||
ThrowingCallable throwingCallable, String message, @Repeated Object parameters) {
|
||||
return assertThatIOException().isThrownBy(throwingCallable).withMessage(message, parameters);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(
|
||||
ThrowingCallable throwingCallable, String message, @Repeated Object parameters) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IOException.class)
|
||||
.hasMessage(message, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIOExceptionHasMessageStartingWith {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("AssertThatThrownByIOException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatIOException().isThrownBy(throwingCallable).withMessageStartingWith(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IOException.class)
|
||||
.hasMessageStartingWith(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIOExceptionHasMessageContaining {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("AssertThatThrownByIOException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatIOException().isThrownBy(throwingCallable).withMessageContaining(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IOException.class)
|
||||
.hasMessageContaining(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByIOExceptionHasMessageNotContaining {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("AssertThatThrownByIOException" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatIOException().isThrownBy(throwingCallable).withMessageNotContaining(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(ThrowingCallable throwingCallable, String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(IOException.class)
|
||||
.hasMessageNotContaining(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownBy {
|
||||
@BeforeTemplate
|
||||
AbstractObjectAssert<?, ?> before(
|
||||
ThrowingCallable throwingCallable, Class<? extends Throwable> exceptionType) {
|
||||
return assertThatExceptionOfType(exceptionType).isThrownBy(throwingCallable);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(
|
||||
ThrowingCallable throwingCallable, Class<? extends Throwable> exceptionType) {
|
||||
return assertThatThrownBy(throwingCallable).isInstanceOf(exceptionType);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByHasMessage {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("AssertThatThrownBy" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(
|
||||
ThrowingCallable throwingCallable,
|
||||
Class<? extends Throwable> exceptionType,
|
||||
String message) {
|
||||
return assertThatExceptionOfType(exceptionType)
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessage(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(
|
||||
ThrowingCallable throwingCallable,
|
||||
Class<? extends Throwable> exceptionType,
|
||||
String message) {
|
||||
return assertThatThrownBy(throwingCallable).isInstanceOf(exceptionType).hasMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByHasMessageParameters {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("AssertThatThrownBy" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(
|
||||
ThrowingCallable throwingCallable,
|
||||
Class<? extends Throwable> exceptionType,
|
||||
String message,
|
||||
@Repeated Object parameters) {
|
||||
return assertThatExceptionOfType(exceptionType)
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessage(message, parameters);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(
|
||||
ThrowingCallable throwingCallable,
|
||||
Class<? extends Throwable> exceptionType,
|
||||
String message,
|
||||
@Repeated Object parameters) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(exceptionType)
|
||||
.hasMessage(message, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByHasMessageStartingWith {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("AssertThatThrownBy" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(
|
||||
ThrowingCallable throwingCallable,
|
||||
Class<? extends Throwable> exceptionType,
|
||||
String message) {
|
||||
return assertThatExceptionOfType(exceptionType)
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessageStartingWith(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(
|
||||
ThrowingCallable throwingCallable,
|
||||
Class<? extends Throwable> exceptionType,
|
||||
String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(exceptionType)
|
||||
.hasMessageStartingWith(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByHasMessageContaining {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("AssertThatThrownBy" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(
|
||||
ThrowingCallable throwingCallable,
|
||||
Class<? extends Throwable> exceptionType,
|
||||
String message) {
|
||||
return assertThatExceptionOfType(exceptionType)
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessageContaining(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(
|
||||
ThrowingCallable throwingCallable,
|
||||
Class<? extends Throwable> exceptionType,
|
||||
String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(exceptionType)
|
||||
.hasMessageContaining(message);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatThrownByHasMessageNotContaining {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("AssertThatThrownBy" /* This is a more specific template. */)
|
||||
AbstractObjectAssert<?, ?> before(
|
||||
ThrowingCallable throwingCallable,
|
||||
Class<? extends Throwable> exceptionType,
|
||||
String message) {
|
||||
return assertThatExceptionOfType(exceptionType)
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessageNotContaining(message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, ?> after(
|
||||
ThrowingCallable throwingCallable,
|
||||
Class<? extends Throwable> exceptionType,
|
||||
String message) {
|
||||
return assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(exceptionType)
|
||||
.hasMessageNotContaining(message);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Drop this rule in favour of a generic Error Prone check that flags `String.format(...)`
|
||||
// arguments to a wide range of format methods.
|
||||
static final class AbstractThrowableAssertHasMessage {
|
||||
@BeforeTemplate
|
||||
AbstractThrowableAssert<?, ? extends Throwable> before(
|
||||
AbstractThrowableAssert<?, ? extends Throwable> abstractThrowableAssert,
|
||||
String message,
|
||||
@Repeated Object parameters) {
|
||||
return abstractThrowableAssert.hasMessage(String.format(message, parameters));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractThrowableAssert<?, ? extends Throwable> after(
|
||||
AbstractThrowableAssert<?, ? extends Throwable> abstractThrowableAssert,
|
||||
String message,
|
||||
@Repeated Object parameters) {
|
||||
return abstractThrowableAssert.hasMessage(message, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Drop this rule in favour of a generic Error Prone check that flags `String.format(...)`
|
||||
// arguments to a wide range of format methods.
|
||||
static final class AbstractThrowableAssertWithFailMessage {
|
||||
@BeforeTemplate
|
||||
AbstractThrowableAssert<?, ? extends Throwable> before(
|
||||
AbstractThrowableAssert<?, ? extends Throwable> abstractThrowableAssert,
|
||||
String message,
|
||||
@Repeated Object args) {
|
||||
return abstractThrowableAssert.withFailMessage(String.format(message, args));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractThrowableAssert<?, ? extends Throwable> after(
|
||||
AbstractThrowableAssert<?, ? extends Throwable> abstractThrowableAssert,
|
||||
String message,
|
||||
@Repeated Object args) {
|
||||
return abstractThrowableAssert.withFailMessage(message, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
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 java.math.BigDecimal;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/** Refaster rules related to expressions dealing with {@link BigDecimal}s. */
|
||||
@OnlineDocumentation
|
||||
final class BigDecimalRules {
|
||||
private BigDecimalRules() {}
|
||||
|
||||
/** Prefer using the constant {@link BigDecimal#ZERO} when possible. */
|
||||
static final class BigDecimalZero {
|
||||
@BeforeTemplate
|
||||
BigDecimal before() {
|
||||
return Refaster.anyOf(BigDecimal.valueOf(0), new BigDecimal("0"));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
BigDecimal after() {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer using the constant {@link BigDecimal#ONE} when possible. */
|
||||
static final class BigDecimalOne {
|
||||
@BeforeTemplate
|
||||
BigDecimal before() {
|
||||
return Refaster.anyOf(BigDecimal.valueOf(1), new BigDecimal("1"));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
BigDecimal after() {
|
||||
return BigDecimal.ONE;
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer using the constant {@link BigDecimal#TEN} when possible. */
|
||||
static final class BigDecimalTen {
|
||||
@BeforeTemplate
|
||||
BigDecimal before() {
|
||||
return Refaster.anyOf(BigDecimal.valueOf(10), new BigDecimal("10"));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
BigDecimal after() {
|
||||
return BigDecimal.TEN;
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link BigDecimal#valueOf(double)} over the associated constructor. */
|
||||
// XXX: Ideally we also rewrite `new BigDecimal("<some-integer-value>")` in cases where the
|
||||
// specified number can be represented as an `int` or `long`, but that requires a custom
|
||||
// `BugChecker`.
|
||||
static final class BigDecimalValueOf {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("java:S2111" /* This violation will be rewritten. */)
|
||||
BigDecimal before(double value) {
|
||||
return new BigDecimal(value);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
BigDecimal after(double value) {
|
||||
return BigDecimal.valueOf(value);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer using {@link BigDecimal#signum()} over more contrived alternatives. */
|
||||
static final class BigDecimalSignumIsZero {
|
||||
@BeforeTemplate
|
||||
boolean before(BigDecimal value) {
|
||||
return Refaster.anyOf(
|
||||
value.compareTo(BigDecimal.ZERO) == 0, BigDecimal.ZERO.compareTo(value) == 0);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@AlsoNegation
|
||||
boolean after(BigDecimal value) {
|
||||
return value.signum() == 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer a {@link BigDecimal#signum()} comparison to 1 over more contrived or less clear
|
||||
* alternatives.
|
||||
*/
|
||||
static final class BigDecimalSignumIsPositive {
|
||||
@BeforeTemplate
|
||||
boolean before(BigDecimal value) {
|
||||
return Refaster.anyOf(
|
||||
value.compareTo(BigDecimal.ZERO) > 0,
|
||||
BigDecimal.ZERO.compareTo(value) < 0,
|
||||
value.signum() > 0,
|
||||
value.signum() >= 1);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@AlsoNegation
|
||||
boolean after(BigDecimal value) {
|
||||
return value.signum() == 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer a {@link BigDecimal#signum()} comparison to -1 over more contrived or less clear
|
||||
* alternatives.
|
||||
*/
|
||||
static final class BigDecimalSignumIsNegative {
|
||||
@BeforeTemplate
|
||||
boolean before(BigDecimal value) {
|
||||
return Refaster.anyOf(
|
||||
value.compareTo(BigDecimal.ZERO) < 0,
|
||||
BigDecimal.ZERO.compareTo(value) > 0,
|
||||
value.signum() < 0,
|
||||
value.signum() <= -1);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@AlsoNegation
|
||||
boolean after(BigDecimal value) {
|
||||
return value.signum() == -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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.annotations.CanIgnoreReturnValue;
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.sun.tools.javac.util.Constants;
|
||||
import com.sun.tools.javac.util.Convert;
|
||||
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
|
||||
@CanIgnoreReturnValue
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer using the {@link Constants} API over more verbose alternatives. */
|
||||
static final class ConstantsFormat {
|
||||
@BeforeTemplate
|
||||
String before(String value) {
|
||||
return String.format("\"%s\"", Convert.quote(value));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
String after(String value) {
|
||||
return Constants.format(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/** Refaster rules related to expressions dealing with classes. */
|
||||
@OnlineDocumentation
|
||||
final class ClassRules {
|
||||
private ClassRules() {}
|
||||
|
||||
/** Prefer {@link Class#isInstance(Object)} over more contrived alternatives. */
|
||||
static final class ClassIsInstance<T, S> {
|
||||
@BeforeTemplate
|
||||
boolean before(Class<T> clazz, S object) {
|
||||
return clazz.isAssignableFrom(object.getClass());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
boolean after(Class<T> clazz, S object) {
|
||||
return clazz.isInstance(object);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer using the {@code instanceof} keyword over less idiomatic alternatives. */
|
||||
static final class Instanceof<T, S> {
|
||||
@BeforeTemplate
|
||||
boolean before(S object) {
|
||||
return Refaster.<T>clazz().isInstance(object);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
boolean after(S object) {
|
||||
return Refaster.<T>isInstance(object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link Class#isInstance(Object)} method references over lambda expressions that require
|
||||
* naming a variable.
|
||||
*/
|
||||
// XXX: Once the `ClassReferenceIsInstancePredicate` rule is dropped, rename this rule to just
|
||||
// `ClassIsInstancePredicate`.
|
||||
static final class ClassLiteralIsInstancePredicate<T, S> {
|
||||
@BeforeTemplate
|
||||
Predicate<S> before() {
|
||||
return o -> Refaster.<T>isInstance(o);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Predicate<S> after() {
|
||||
return Refaster.<T>clazz()::isInstance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link Class#isInstance(Object)} method references over lambda expressions that require
|
||||
* naming a variable.
|
||||
*/
|
||||
// XXX: Drop this rule once the `MethodReferenceUsage` rule is enabled by default.
|
||||
static final class ClassReferenceIsInstancePredicate<T, S> {
|
||||
@BeforeTemplate
|
||||
Predicate<S> before(Class<T> clazz) {
|
||||
return o -> clazz.isInstance(o);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Predicate<S> after(Class<T> clazz) {
|
||||
return clazz::isInstance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link Class#cast(Object)} method references over lambda expressions that require naming
|
||||
* a variable.
|
||||
*/
|
||||
// XXX: Once the `ClassReferenceCast` rule is dropped, rename this rule to just `ClassCast`.
|
||||
static final class ClassLiteralCast<T, S> {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("unchecked")
|
||||
Function<T, S> before() {
|
||||
return t -> (S) t;
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Function<T, S> after() {
|
||||
return Refaster.<S>clazz()::cast;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link Class#cast(Object)} method references over lambda expressions that require naming
|
||||
* a variable.
|
||||
*/
|
||||
// XXX: Drop this rule once the `MethodReferenceUsage` rule is enabled by default.
|
||||
static final class ClassReferenceCast<T, S> {
|
||||
@BeforeTemplate
|
||||
Function<T, S> before(Class<? extends S> clazz) {
|
||||
return o -> clazz.cast(o);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Function<T, S> after(Class<? extends S> clazz) {
|
||||
return clazz::cast;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,580 +0,0 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static java.util.Comparator.comparing;
|
||||
import static java.util.Comparator.comparingDouble;
|
||||
import static java.util.Comparator.comparingInt;
|
||||
import static java.util.Comparator.comparingLong;
|
||||
import static java.util.Comparator.naturalOrder;
|
||||
import static java.util.Comparator.reverseOrder;
|
||||
import static java.util.stream.Collectors.maxBy;
|
||||
import static java.util.stream.Collectors.minBy;
|
||||
|
||||
import com.google.common.collect.Comparators;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
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.Matches;
|
||||
import com.google.errorprone.refaster.annotation.MayOptionallyUse;
|
||||
import com.google.errorprone.refaster.annotation.Placeholder;
|
||||
import com.google.errorprone.refaster.annotation.Repeated;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BinaryOperator;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.ToDoubleFunction;
|
||||
import java.util.function.ToIntFunction;
|
||||
import java.util.function.ToLongFunction;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Stream;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
import tech.picnic.errorprone.refaster.matchers.IsIdentityOperation;
|
||||
|
||||
/** Refaster rules related to expressions dealing with {@link Comparator}s. */
|
||||
@OnlineDocumentation
|
||||
final class ComparatorRules {
|
||||
private ComparatorRules() {}
|
||||
|
||||
/** Prefer {@link Comparator#naturalOrder()} over more complicated constructs. */
|
||||
static final class NaturalOrder<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
Comparator<T> before(
|
||||
@Matches(IsIdentityOperation.class) Function<? super T, ? extends T> keyExtractor) {
|
||||
return Refaster.anyOf(
|
||||
T::compareTo,
|
||||
comparing(keyExtractor),
|
||||
Collections.<T>reverseOrder(reverseOrder()),
|
||||
Comparator.<T>reverseOrder().reversed());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
Comparator<T> after() {
|
||||
return naturalOrder();
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Comparator#reverseOrder()} over more complicated constructs. */
|
||||
static final class ReverseOrder<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
Comparator<T> before() {
|
||||
return Refaster.anyOf(
|
||||
Collections.reverseOrder(),
|
||||
Collections.<T>reverseOrder(naturalOrder()),
|
||||
Comparator.<T>naturalOrder().reversed());
|
||||
}
|
||||
|
||||
// XXX: Add `@UseImportPolicy(STATIC_IMPORT_ALWAYS)` if/when
|
||||
// https://github.com/google/error-prone/pull/3584 is merged and released.
|
||||
@AfterTemplate
|
||||
Comparator<T> after() {
|
||||
return reverseOrder();
|
||||
}
|
||||
}
|
||||
|
||||
static final class CustomComparator<T> {
|
||||
@BeforeTemplate
|
||||
Comparator<T> before(
|
||||
Comparator<T> cmp,
|
||||
@Matches(IsIdentityOperation.class) Function<? super T, ? extends T> keyExtractor) {
|
||||
return comparing(keyExtractor, cmp);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@CanIgnoreReturnValue
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
Comparator<T> after(Comparator<T> cmp) {
|
||||
return cmp;
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't explicitly compare enums by their ordinal. */
|
||||
abstract static class ComparingEnum<E extends Enum<E>, T> {
|
||||
@Placeholder(allowsIdentity = true)
|
||||
abstract E toEnumFunction(@MayOptionallyUse T value);
|
||||
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("EnumOrdinal" /* This violation will be rewritten. */)
|
||||
Comparator<T> before() {
|
||||
return comparingInt(v -> toEnumFunction(v).ordinal());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
Comparator<T> after() {
|
||||
return comparing(v -> toEnumFunction(v));
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't explicitly create {@link Comparator}s unnecessarily. */
|
||||
static final class ThenComparing<S, T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
Comparator<S> before(Comparator<S> cmp, Function<? super S, ? extends T> function) {
|
||||
return cmp.thenComparing(comparing(function));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Comparator<S> after(Comparator<S> cmp, Function<? super S, ? extends T> function) {
|
||||
return cmp.thenComparing(function);
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't explicitly create {@link Comparator}s unnecessarily. */
|
||||
static final class ThenComparingReversed<S, T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
Comparator<S> before(Comparator<S> cmp, Function<? super S, ? extends T> function) {
|
||||
return cmp.thenComparing(comparing(function).reversed());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
Comparator<S> after(Comparator<S> cmp, Function<? super S, ? extends T> function) {
|
||||
return cmp.thenComparing(function, reverseOrder());
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't explicitly create {@link Comparator}s unnecessarily. */
|
||||
static final class ThenComparingCustom<S, T> {
|
||||
@BeforeTemplate
|
||||
Comparator<S> before(
|
||||
Comparator<S> cmp, Function<? super S, ? extends T> function, Comparator<? super T> cmp2) {
|
||||
return cmp.thenComparing(comparing(function, cmp2));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Comparator<S> after(
|
||||
Comparator<S> cmp, Function<? super S, ? extends T> function, Comparator<? super T> cmp2) {
|
||||
return cmp.thenComparing(function, cmp2);
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't explicitly create {@link Comparator}s unnecessarily. */
|
||||
static final class ThenComparingCustomReversed<S, T> {
|
||||
@BeforeTemplate
|
||||
Comparator<S> before(
|
||||
Comparator<S> cmp, Function<? super S, ? extends T> function, Comparator<? super T> cmp2) {
|
||||
return cmp.thenComparing(comparing(function, cmp2).reversed());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Comparator<S> after(
|
||||
Comparator<S> cmp, Function<? super S, ? extends T> function, Comparator<? super T> cmp2) {
|
||||
return cmp.thenComparing(function, cmp2.reversed());
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't explicitly create {@link Comparator}s unnecessarily. */
|
||||
static final class ThenComparingDouble<T> {
|
||||
@BeforeTemplate
|
||||
Comparator<T> before(Comparator<T> cmp, ToDoubleFunction<? super T> function) {
|
||||
return cmp.thenComparing(comparingDouble(function));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Comparator<T> after(Comparator<T> cmp, ToDoubleFunction<? super T> function) {
|
||||
return cmp.thenComparingDouble(function);
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't explicitly create {@link Comparator}s unnecessarily. */
|
||||
static final class ThenComparingInt<T> {
|
||||
@BeforeTemplate
|
||||
Comparator<T> before(Comparator<T> cmp, ToIntFunction<? super T> function) {
|
||||
return cmp.thenComparing(comparingInt(function));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Comparator<T> after(Comparator<T> cmp, ToIntFunction<? super T> function) {
|
||||
return cmp.thenComparingInt(function);
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't explicitly create {@link Comparator}s unnecessarily. */
|
||||
static final class ThenComparingLong<T> {
|
||||
@BeforeTemplate
|
||||
Comparator<T> before(Comparator<T> cmp, ToLongFunction<? super T> function) {
|
||||
return cmp.thenComparing(comparingLong(function));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Comparator<T> after(Comparator<T> cmp, ToLongFunction<? super T> function) {
|
||||
return cmp.thenComparingLong(function);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Where applicable, prefer {@link Comparator#naturalOrder()} over identity function-based
|
||||
* comparisons, as the former more clearly states intent.
|
||||
*/
|
||||
static final class ThenComparingNaturalOrder<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
Comparator<T> before(
|
||||
Comparator<T> cmp,
|
||||
@Matches(IsIdentityOperation.class) Function<? super T, ? extends T> keyExtractor) {
|
||||
return cmp.thenComparing(keyExtractor);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
Comparator<T> after(Comparator<T> cmp) {
|
||||
return cmp.thenComparing(naturalOrder());
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Comparable#compareTo(Object)}} over more verbose alternatives. */
|
||||
static final class CompareTo<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
int before(T value1, T value2) {
|
||||
return Refaster.anyOf(
|
||||
Comparator.<T>naturalOrder().compare(value1, value2),
|
||||
Comparator.<T>reverseOrder().compare(value2, value1));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
int after(T value1, T value2) {
|
||||
return value1.compareTo(value2);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Collections#sort(List)} over more verbose alternatives. */
|
||||
static final class CollectionsSort<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
void before(List<T> collection) {
|
||||
Collections.sort(collection, naturalOrder());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
void after(List<T> collection) {
|
||||
Collections.sort(collection);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Collections#min(Collection)} over more verbose alternatives. */
|
||||
static final class CollectionsMin<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
T before(Collection<T> collection) {
|
||||
return Refaster.anyOf(
|
||||
Collections.min(collection, naturalOrder()), Collections.max(collection, reverseOrder()));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
T after(Collection<T> collection) {
|
||||
return Collections.min(collection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid unnecessary creation of a {@link Stream} to determine the minimum of a known collection
|
||||
* of values.
|
||||
*/
|
||||
static final class MinOfArray<S, T extends S> {
|
||||
@BeforeTemplate
|
||||
T before(T[] array, Comparator<S> cmp) {
|
||||
return Arrays.stream(array).min(cmp).orElseThrow();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
T after(T[] array, Comparator<S> cmp) {
|
||||
return Collections.min(Arrays.asList(array), cmp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid unnecessary creation of a {@link Stream} to determine the minimum of a known collection
|
||||
* of values.
|
||||
*/
|
||||
static final class CollectionsMinWithComparator<S, T extends S> {
|
||||
@BeforeTemplate
|
||||
T before(Collection<T> collection, Comparator<S> cmp) {
|
||||
return collection.stream().min(cmp).orElseThrow();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
T after(Collection<T> collection, Comparator<S> cmp) {
|
||||
return Collections.min(collection, cmp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid unnecessary creation of a {@link Stream} to determine the minimum of a known collection
|
||||
* of values.
|
||||
*/
|
||||
static final class MinOfVarargs<S, T extends S> {
|
||||
@BeforeTemplate
|
||||
T before(@Repeated T value, Comparator<S> cmp) {
|
||||
return Stream.of(Refaster.asVarargs(value)).min(cmp).orElseThrow();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
T after(@Repeated T value, Comparator<S> cmp) {
|
||||
return Collections.min(Arrays.asList(value), cmp);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Comparators#min(Comparable, Comparable)}} over more verbose alternatives. */
|
||||
static final class MinOfPairNaturalOrder<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("java:S1067" /* The conditional operators are independent. */)
|
||||
T before(T value1, T value2) {
|
||||
return Refaster.anyOf(
|
||||
value1.compareTo(value2) <= 0 ? value1 : value2,
|
||||
value1.compareTo(value2) > 0 ? value2 : value1,
|
||||
value2.compareTo(value1) < 0 ? value2 : value1,
|
||||
value2.compareTo(value1) >= 0 ? value1 : value2,
|
||||
Comparators.min(value1, value2, naturalOrder()),
|
||||
Comparators.max(value1, value2, reverseOrder()),
|
||||
Collections.min(
|
||||
Refaster.anyOf(
|
||||
Arrays.asList(value1, value2),
|
||||
ImmutableList.of(value1, value2),
|
||||
ImmutableSet.of(value1, value2))));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
T after(T value1, T value2) {
|
||||
return Comparators.min(value1, value2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link Comparators#min(Object, Object, Comparator)}}} over more verbose alternatives.
|
||||
*/
|
||||
static final class MinOfPairCustomOrder<T> {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("java:S1067" /* The conditional operators are independent. */)
|
||||
T before(T value1, T value2, Comparator<? super T> cmp) {
|
||||
return Refaster.anyOf(
|
||||
cmp.compare(value1, value2) <= 0 ? value1 : value2,
|
||||
cmp.compare(value1, value2) > 0 ? value2 : value1,
|
||||
cmp.compare(value2, value1) < 0 ? value2 : value1,
|
||||
cmp.compare(value2, value1) >= 0 ? value1 : value2,
|
||||
Collections.min(
|
||||
Refaster.anyOf(
|
||||
Arrays.asList(value1, value2),
|
||||
ImmutableList.of(value1, value2),
|
||||
ImmutableSet.of(value1, value2)),
|
||||
cmp));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
T after(T value1, T value2, Comparator<? super T> cmp) {
|
||||
return Comparators.min(value1, value2, cmp);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Collections#max(Collection)} over more verbose alternatives. */
|
||||
static final class CollectionsMax<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
T before(Collection<T> collection) {
|
||||
return Refaster.anyOf(
|
||||
Collections.max(collection, naturalOrder()), Collections.min(collection, reverseOrder()));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
T after(Collection<T> collection) {
|
||||
return Collections.max(collection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid unnecessary creation of a {@link Stream} to determine the maximum of a known collection
|
||||
* of values.
|
||||
*/
|
||||
static final class MaxOfArray<S, T extends S> {
|
||||
@BeforeTemplate
|
||||
T before(T[] array, Comparator<S> cmp) {
|
||||
return Arrays.stream(array).max(cmp).orElseThrow();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
T after(T[] array, Comparator<S> cmp) {
|
||||
return Collections.max(Arrays.asList(array), cmp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid unnecessary creation of a {@link Stream} to determine the maximum of a known collection
|
||||
* of values.
|
||||
*/
|
||||
static final class CollectionsMaxWithComparator<S, T extends S> {
|
||||
@BeforeTemplate
|
||||
T before(Collection<T> collection, Comparator<S> cmp) {
|
||||
return collection.stream().max(cmp).orElseThrow();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
T after(Collection<T> collection, Comparator<S> cmp) {
|
||||
return Collections.max(collection, cmp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid unnecessary creation of a {@link Stream} to determine the maximum of a known collection
|
||||
* of values.
|
||||
*/
|
||||
static final class MaxOfVarargs<S, T extends S> {
|
||||
@BeforeTemplate
|
||||
T before(@Repeated T value, Comparator<S> cmp) {
|
||||
return Stream.of(Refaster.asVarargs(value)).max(cmp).orElseThrow();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
T after(@Repeated T value, Comparator<S> cmp) {
|
||||
return Collections.max(Arrays.asList(value), cmp);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Comparators#max(Comparable, Comparable)}} over more verbose alternatives. */
|
||||
static final class MaxOfPairNaturalOrder<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("java:S1067" /* The conditional operators are independent. */)
|
||||
T before(T value1, T value2) {
|
||||
return Refaster.anyOf(
|
||||
value1.compareTo(value2) >= 0 ? value1 : value2,
|
||||
value1.compareTo(value2) < 0 ? value2 : value1,
|
||||
value2.compareTo(value1) > 0 ? value2 : value1,
|
||||
value2.compareTo(value1) <= 0 ? value1 : value2,
|
||||
Comparators.max(value1, value2, naturalOrder()),
|
||||
Comparators.min(value1, value2, reverseOrder()),
|
||||
Collections.max(
|
||||
Refaster.anyOf(
|
||||
Arrays.asList(value1, value2),
|
||||
ImmutableList.of(value1, value2),
|
||||
ImmutableSet.of(value1, value2))));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
T after(T value1, T value2) {
|
||||
return Comparators.max(value1, value2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link Comparators#max(Object, Object, Comparator)}}} over more verbose alternatives.
|
||||
*/
|
||||
static final class MaxOfPairCustomOrder<T> {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("java:S1067" /* The conditional operators are independent. */)
|
||||
T before(T value1, T value2, Comparator<? super T> cmp) {
|
||||
return Refaster.anyOf(
|
||||
cmp.compare(value1, value2) >= 0 ? value1 : value2,
|
||||
cmp.compare(value1, value2) < 0 ? value2 : value1,
|
||||
cmp.compare(value2, value1) > 0 ? value2 : value1,
|
||||
cmp.compare(value2, value1) <= 0 ? value1 : value2,
|
||||
Collections.max(
|
||||
Refaster.anyOf(
|
||||
Arrays.asList(value1, value2),
|
||||
ImmutableList.of(value1, value2),
|
||||
ImmutableSet.of(value1, value2)),
|
||||
cmp));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
T after(T value1, T value2, Comparator<? super T> cmp) {
|
||||
return Comparators.max(value1, value2, cmp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer a method reference to {@link Comparators#min(Comparable, Comparable)} over calling
|
||||
* {@link BinaryOperator#minBy(Comparator)} with {@link Comparator#naturalOrder()}.
|
||||
*/
|
||||
static final class ComparatorsMin<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
BinaryOperator<T> before() {
|
||||
return BinaryOperator.minBy(naturalOrder());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
BinaryOperator<T> after() {
|
||||
return Comparators::min;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer a method reference to {@link Comparators#max(Comparable, Comparable)} over calling
|
||||
* {@link BinaryOperator#minBy(Comparator)} with {@link Comparator#naturalOrder()}.
|
||||
*/
|
||||
static final class ComparatorsMax<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
BinaryOperator<T> before() {
|
||||
return BinaryOperator.maxBy(naturalOrder());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
BinaryOperator<T> after() {
|
||||
return Comparators::max;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link Comparator#naturalOrder()} over {@link Comparator#reverseOrder()} where possible.
|
||||
*/
|
||||
static final class MinByNaturalOrder<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
Collector<T, ?, Optional<T>> before() {
|
||||
return maxBy(reverseOrder());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
Collector<T, ?, Optional<T>> after() {
|
||||
return minBy(naturalOrder());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link Comparator#naturalOrder()} over {@link Comparator#reverseOrder()} where possible.
|
||||
*/
|
||||
static final class MaxByNaturalOrder<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
Collector<T, ?, Optional<T>> before() {
|
||||
return minBy(reverseOrder());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
Collector<T, ?, Optional<T>> after() {
|
||||
return maxBy(naturalOrder());
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't explicitly compare enums by their ordinal. */
|
||||
static final class IsLessThan<E extends Enum<E>> {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("EnumOrdinal" /* This violation will be rewritten. */)
|
||||
boolean before(E value1, E value2) {
|
||||
return value1.ordinal() < value2.ordinal();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@AlsoNegation
|
||||
boolean after(E value1, E value2) {
|
||||
return value1.compareTo(value2) < 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't explicitly compare enums by their ordinal. */
|
||||
static final class IsLessThanOrEqualTo<E extends Enum<E>> {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("EnumOrdinal" /* This violation will be rewritten. */)
|
||||
boolean before(E value1, E value2) {
|
||||
return value1.ordinal() <= value2.ordinal();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@AlsoNegation
|
||||
boolean after(E value1, E value2) {
|
||||
return value1.compareTo(value2) <= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static java.util.function.Predicate.isEqual;
|
||||
import static java.util.function.Predicate.not;
|
||||
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
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.MayOptionallyUse;
|
||||
import com.google.errorprone.refaster.annotation.Placeholder;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/** Refaster rules related to expressions dealing with (in)equalities. */
|
||||
@OnlineDocumentation
|
||||
final class EqualityRules {
|
||||
private EqualityRules() {}
|
||||
|
||||
/** Prefer reference-based equality for enums. */
|
||||
// Primitive value comparisons are not matched, because Error Prone flags those out of the box.
|
||||
static final class EnumReferenceEquality<T extends Enum<T>> {
|
||||
/**
|
||||
* Enums can be compared by reference. It is safe to do so even in the face of refactorings,
|
||||
* because if the type is ever converted to a non-enum, then Error-Prone will complain about any
|
||||
* remaining reference-based equality checks.
|
||||
*/
|
||||
// XXX: This Refaster rule is the topic of https://github.com/google/error-prone/issues/559. We
|
||||
// work around the issue by selecting the "largest replacements". See the `Refaster` check.
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("EnumOrdinal" /* This violation will be rewritten. */)
|
||||
boolean before(T a, T b) {
|
||||
return Refaster.anyOf(a.equals(b), Objects.equals(a, b), a.ordinal() == b.ordinal());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@AlsoNegation
|
||||
@SuppressWarnings("java:S1698" /* Reference comparison is valid for enums. */)
|
||||
boolean after(T a, T b) {
|
||||
return a == b;
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer reference-based equality for enums. */
|
||||
static final class EnumReferenceEqualityLambda<T extends Enum<T>> {
|
||||
@BeforeTemplate
|
||||
Predicate<T> before(T e) {
|
||||
return Refaster.anyOf(isEqual(e), e::equals);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@SuppressWarnings("java:S1698" /* Reference comparison is valid for enums. */)
|
||||
Predicate<T> after(T e) {
|
||||
return v -> v == e;
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Object#equals(Object)} over the equivalent lambda function. */
|
||||
// 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.
|
||||
// XXX: Alternatively, the rule should be replaced with a plugin that also identifies cases where
|
||||
// the arguments are swapped but simplification is possible anyway, by virtue of `v` being
|
||||
// non-null.
|
||||
static final class EqualsPredicate<T> {
|
||||
@BeforeTemplate
|
||||
Predicate<T> before(T v) {
|
||||
return e -> v.equals(e);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Predicate<T> after(T v) {
|
||||
return v::equals;
|
||||
}
|
||||
}
|
||||
|
||||
/** Avoid double negations; this is not Javascript. */
|
||||
static final class DoubleNegation {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("java:S2761" /* This violation will be rewritten. */)
|
||||
boolean before(boolean b) {
|
||||
return !!b;
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@CanIgnoreReturnValue
|
||||
boolean after(boolean b) {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't negate an equality test or use the ternary operator to compare two booleans; directly
|
||||
* test for inequality instead.
|
||||
*/
|
||||
// XXX: Replacing `a ? !b : b` with `a != b` changes semantics if both `a` and `b` are boxed
|
||||
// booleans.
|
||||
@SuppressWarnings("java:S1940" /* This violation will be rewritten. */)
|
||||
static final class Negation {
|
||||
@BeforeTemplate
|
||||
boolean before(boolean a, boolean b) {
|
||||
return Refaster.anyOf(!(a == b), a ? !b : b);
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"java:S1244" /* The equality check is fragile, but may be seen in the wild. */)
|
||||
boolean before(double a, double b) {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
boolean before(Object a, Object b) {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
boolean after(boolean a, boolean b) {
|
||||
return a != b;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't negate an inequality test or use the ternary operator to compare two booleans; directly
|
||||
* test for equality instead.
|
||||
*/
|
||||
// XXX: Replacing `a ? b : !b` with `a == b` changes semantics if both `a` and `b` are boxed
|
||||
// booleans.
|
||||
@SuppressWarnings("java:S1940" /* This violation will be rewritten. */)
|
||||
static final class IndirectDoubleNegation {
|
||||
@BeforeTemplate
|
||||
boolean before(boolean a, boolean b) {
|
||||
return Refaster.anyOf(!(a != b), a ? b : !b);
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings(
|
||||
"java:S1244" /* The inequality check is fragile, but may be seen in the wild. */)
|
||||
boolean before(double a, double b) {
|
||||
return !(a != b);
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
boolean before(Object a, Object b) {
|
||||
return !(a != b);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
boolean after(boolean a, boolean b) {
|
||||
return a == b;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't pass a lambda expression to {@link Predicate#not(Predicate)}; instead push the negation
|
||||
* into the lambda expression.
|
||||
*/
|
||||
abstract static class PredicateLambda<T> {
|
||||
@Placeholder(allowsIdentity = true)
|
||||
abstract boolean predicate(@MayOptionallyUse T value);
|
||||
|
||||
@BeforeTemplate
|
||||
Predicate<T> before() {
|
||||
return not(v -> predicate(v));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Predicate<T> after() {
|
||||
return v -> !predicate(v);
|
||||
}
|
||||
}
|
||||
|
||||
/** Avoid contrived ways of handling {@code null} values during equality testing. */
|
||||
static final class Equals<T, S> {
|
||||
@BeforeTemplate
|
||||
boolean before(T value1, S value2) {
|
||||
return Refaster.anyOf(
|
||||
Optional.of(value1).equals(Optional.of(value2)),
|
||||
Optional.of(value1).equals(Optional.ofNullable(value2)),
|
||||
Optional.ofNullable(value2).equals(Optional.of(value1)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
boolean after(T value1, S value2) {
|
||||
return value1.equals(value2);
|
||||
}
|
||||
}
|
||||
|
||||
/** Avoid contrived ways of handling {@code null} values during equality testing. */
|
||||
static final class ObjectsEquals<T, S> {
|
||||
@BeforeTemplate
|
||||
boolean before(T value1, S value2) {
|
||||
return Optional.ofNullable(value1).equals(Optional.ofNullable(value2));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
boolean after(T value1, S value2) {
|
||||
return Objects.equals(value1, value2);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user