Compare commits

..

15 Commits

Author SHA1 Message Date
Stephan Schroevers
8ced020ba6 Post rebase fixes 2022-11-05 17:21:46 +01:00
Stephan Schroevers
389928ce43 Tweaks 2022-11-05 16:53:45 +01:00
Rick Ossendrijver
d88f226b30 Remove the TEXT_MATCH param from doTest 2022-11-05 16:52:02 +01:00
Rick Ossendrijver
ba91c6bed7 Suggestions 2022-11-05 16:52:02 +01:00
Nathan Kooij
85f402b089 Simplify by only working with longs 2022-11-05 16:52:02 +01:00
Nathan Kooij
ad8c0a472c Fix most warnings 2022-11-05 16:52:02 +01:00
Nathan Kooij
0af127652e Use a sorted set to derive the ceiling 2022-11-05 16:52:02 +01:00
Nathan Kooij
cede5e451b Assorted cleanup 2022-11-05 16:52:02 +01:00
Nathan Kooij
134895090f Support banned fields 2022-11-05 16:52:02 +01:00
Nathan Kooij
226bfd0cee Handle implicit value attribute case 2022-11-05 16:52:02 +01:00
Nathan Kooij
89a3c605fe Support multiple attributes per annotation 2022-11-05 16:52:02 +01:00
Nathan Kooij
63273a2609 Some cleanup of simplifying code 2022-11-05 16:52:02 +01:00
Nathan Kooij
79768a2428 Notes and random things 2022-11-05 16:52:02 +01:00
Nathan Kooij
dd8d094b5a Suggest a fix 2022-11-05 16:52:02 +01:00
Nathan Kooij
4830b5b2cd Extract TimeUnit, Number value, and determine a simplification 2022-11-05 16:52:02 +01:00
381 changed files with 3727 additions and 89859 deletions

View File

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

View File

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

8
.github/release.yml vendored
View File

@@ -3,16 +3,16 @@ changelog:
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: ":warning: Update considerations and deprecations"
labels:
- "breaking change"
- "deprecation"
- title: ":bug: Bug fixes"
labels:
- "bug"

55
.github/workflows/build.yaml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Build and verify
on:
pull_request:
push:
branches: [ master ]
permissions:
contents: read
jobs:
build:
strategy:
matrix:
os: [ ubuntu-22.04 ]
jdk: [ 11.0.16, 17.0.4, 19 ]
distribution: [ temurin ]
experimental: [ false ]
include:
- os: macos-12
jdk: 17.0.4
distribution: temurin
experimental: false
- os: windows-2022
jdk: 17.0.4
distribution: temurin
experimental: false
- os: ubuntu-22.04
jdk: 20-ea
distribution: zulu
experimental: true
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
steps:
# We run the build twice for each supported JDK: once against the
# original Error Prone release, using only Error Prone checks available
# on Maven Central, and once against the Picnic Error Prone fork,
# additionally enabling all checks defined in this project and any
# Error Prone checks available only from other artifact repositories.
- name: Check out code
uses: actions/checkout@v3.1.0
- name: Set up JDK
uses: actions/setup-java@v3.6.0
with:
java-version: ${{ matrix.jdk }}
distribution: ${{ matrix.distribution }}
cache: maven
- 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".
# XXX: Enable SonarCloud once we "go public".

View File

@@ -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 ]
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.6
- 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".

View File

@@ -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.6
- 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 }}

49
.github/workflows/deploy-website.yaml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Update `error-prone.picnic.tech` website content
on:
pull_request:
push:
branches: [ master, website ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
build:
permissions:
contents: read
runs-on: ubuntu-22.04
steps:
- name: Check out code
uses: actions/checkout@v3.1.0
- uses: ruby/setup-ruby@v1.120.0
with:
working-directory: ./website
bundler-cache: true
- name: Configure Github Pages
uses: actions/configure-pages@v2.1.2
- 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@v1.0.4
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: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1.2.2

View File

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

View File

@@ -1,52 +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
fulcio.sigstore.dev:443
github.com:443
oss-fuzz-build-logs.storage.googleapis.com:443
rekor.sigstore.dev:443
tuf-repo-cdn.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

View File

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

View File

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

View File

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

View File

@@ -1,49 +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: >
api.adoptium.net: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
scanner.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.6
- 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
View File

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

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

View File

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

View File

@@ -1,8 +1,5 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"helpers:pinGitHubActionDigests"
],
"packageRules": [
{
"matchPackagePatterns": [
@@ -12,12 +9,10 @@
"separateMinorPatch": true
},
{
"matchPackageNames": [
"dawidd6/action-download-artifact",
"github/codeql-action",
"ruby/setup-ruby"
"matchPackagePatterns": [
"^com\\.palantir\\.baseline:baseline-error-prone$"
],
"schedule": "every 4 weeks on Monday"
"schedule": "* * 1 * *"
}
],
"reviewers": [

View File

@@ -48,17 +48,6 @@ be accepted. When in doubt, make sure to first raise an
To the extent possible, the pull request process guards our coding guidelines.
Some pointers:
- Try to make sure that the
[`./run-full-build.sh`][error-prone-support-full-build] script completes
successfully, ideally before opening a pull request. See the [development
instructions][error-prone-support-developing] for details on how to
efficiently resolve many of the errors and warnings that may be reported. (In
particular, make sure to run `mvn fmt:format` and
[`./apply-error-prone-suggestions.sh`][error-prone-support-patch].) That
said, if you feel that the build fails for invalid or debatable reasons, or
if you're unsure how to best resolve an issue, don't let that discourage you
from opening a PR with a failing build; we can have a look at the issue
together!
- Checks should be _topical_: ideally they address a single concern.
- Where possible checks should provide _fixes_, and ideally these are
completely behavior-preserving. In order for a check to be adopted by users
@@ -77,9 +66,6 @@ Some pointers:
sneak in unrelated changes; instead just open more than one pull request 😉.
[error-prone-criteria]: https://errorprone.info/docs/criteria
[error-prone-support-developing]: https://github.com/PicnicSupermarket/error-prone-support/tree/master#-developing-error-prone-support
[error-prone-support-full-build]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-full-build.sh
[error-prone-support-issues]: https://github.com/PicnicSupermarket/error-prone-support/issues
[error-prone-support-mutation-tests]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-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-mutation-tests]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-mutation-tests.sh
[error-prone-support-pulls]: https://github.com/PicnicSupermarket/error-prone-support/pulls

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017-2024 Picnic Technologies BV
Copyright (c) 2017-2022 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

136
README.md
View File

@@ -15,26 +15,13 @@ 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].
Read more on how Picnic uses Error Prone (Support) in 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]
@@ -49,11 +36,7 @@ high-quality and consistent Java code_][picnic-blog-ep-post].
### 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
it:
1. First, follow Error Prone's [installation
guide][error-prone-installation-guide].
@@ -67,8 +50,6 @@ that your build is executed using JDK 17 or above, but supports builds that
<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. -->
@@ -98,6 +79,8 @@ that your build is executed using JDK 17 or above, but supports builds that
</arg>
<arg>-XDcompilePolicy=simple</arg>
</compilerArgs>
<!-- Some checks raise warnings rather than errors. -->
<showWarnings>true</showWarnings>
<!-- Enable this if you'd like to fail your build upon warnings. -->
<!-- <failOnWarning>true</failOnWarning> -->
</configuration>
@@ -111,32 +94,6 @@ that your build is executed using JDK 17 or above, but supports builds that
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:
@@ -187,15 +144,8 @@ 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:
full clean build and installs the library to your local Maven repository. Some
relevant flags:
- `-Dverification.warn` makes the warnings and errors emitted by various
plugins and the Java compiler non-fatal, where possible.
@@ -212,28 +162,19 @@ Relevant Maven build parameters:
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:
Some other commands one may find relevant:
- `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].
- `./run-mutation-tests.sh` 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].
- `./apply-error-prone-suggestions.sh` applies Error Prone and Error Prone
Support code suggestions to this project. Before running this command, make
sure to have installed the project (`mvn clean install`) and make sure that
the current working directory does not contain unstaged or uncommited
changes.
When running the project's tests in IntelliJ IDEA, you might see the following
error:
@@ -260,28 +201,17 @@ 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
[github-actions-build-badge]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/build.yaml/badge.svg
[github-actions-build-master]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/build.yaml?query=branch%3Amaster
[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
@@ -289,13 +219,8 @@ channel; please see our [security policy][security] for details.
[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
[picnic-blog-ep-post]: https://blog.picnic.nl/picnic-loves-error-prone-producing-high-quality-and-consistent-java-code-b8a566be6886
[pitest]: https://pitest.org
[pitest-maven]: https://pitest.org/quickstart/maven
[pr-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
@@ -304,22 +229,3 @@ channel; please see our [security policy][security] for details.
[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

View File

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

View File

@@ -9,14 +9,13 @@
set -e -u -o pipefail
if [ "${#}" -gt 1 ]; then
echo "Usage: ${0} [PatchChecks]"
echo "Usage: ./$(basename "${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 \

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,92 +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.nio.file.Paths;
import java.util.ServiceLoader;
import javax.tools.JavaFileObject;
/**
* A {@link TaskListener} that identifies and extracts relevant content for documentation generation
* and writes it to disk.
*/
// XXX: Find a better name for this class; it doesn't generate documentation per se.
final class DocumentationGeneratorTaskListener implements TaskListener {
@SuppressWarnings({"rawtypes", "unchecked"})
private static final ImmutableList<Extractor<?>> EXTRACTORS =
(ImmutableList)
ImmutableList.copyOf(
ServiceLoader.load(
Extractor.class, DocumentationGeneratorTaskListener.class.getClassLoader()));
private 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 Paths.get(path).getFileName().toString().replace(".java", "");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,6 +24,7 @@ project:
- Document how to apply patches.
- Document each of the checks.
- Add [SonarQube][sonarcloud] and [Codecov][codecov] integrations.
- Add non-Java file formatting support, like we have internally at Picnic.
(I.e., somehow open-source that stuff.)
- Auto-generate a website listing each of the checks, just like the Error Prone
@@ -125,6 +126,9 @@ The following is a list of checks we'd like to see implemented:
statement. Idem for other exception types.
- A Guava-specific check that replaces simple anonymous `CacheLoader` subclass
declarations with `CacheLoader.from(someLambda)`.
- A Spring-specific check that 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 that 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`
@@ -269,6 +273,7 @@ Refaster's expressiveness:
[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]: https://errorprone.info
[error-prone-repo]: https://github.com/google/error-prone
@@ -278,3 +283,4 @@ Refaster's expressiveness:
[main-contributing]: ../CONTRIBUTING.md
[main-readme]: ../README.md
[modernizer-maven-plugin]: https://github.com/gaul/modernizer-maven-plugin
[sonarcloud]: https://sonarcloud.io

View File

@@ -5,14 +5,13 @@
<parent>
<groupId>tech.picnic.error-prone-support</groupId>
<artifactId>error-prone-support</artifactId>
<version>0.16.2-SNAPSHOT</version>
<version>0.5.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,19 +37,7 @@
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_test_helpers</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>documentation-support</artifactId>
<!-- This dependency is declared only as a hint to Maven that
compilation depends on it; see the `maven-compiler-plugin`'s
`annotationProcessorPaths` configuration below. -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>error-prone-utils</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
@@ -65,6 +52,11 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.auto</groupId>
<artifactId>auto-common</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
@@ -73,15 +65,19 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value-annotations</artifactId>
<scope>provided</scope>
<groupId>com.google.googlejavaformat</groupId>
<artifactId>google-java-format</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.newrelic.agent.java</groupId>
<artifactId>newrelic-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
@@ -122,11 +118,6 @@
<artifactId>jakarta.servlet-api</artifactId>
<scope>test</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>
@@ -155,10 +146,8 @@
<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>
@@ -174,41 +163,6 @@
<artifactId>mockito-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>bson</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-core</artifactId>
<scope>provided</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>
@@ -244,11 +198,6 @@
<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>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
@@ -264,14 +213,6 @@
<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>
@@ -284,10 +225,21 @@
</path>
</annotationProcessorPaths>
<compilerArgs combine.children="append">
<arg>-Xplugin:DocumentationGenerator -XoutputDirectory=${project.build.directory}/docs</arg>
<arg>-Xplugin:RefasterRuleCompiler</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<configuration>
<ignoredUnusedDeclaredDependencies>
<!-- XXX: Figure out why the plugin thinks this
dependency is unused. -->
<ignoredUnusedDeclaredDependency>${project.groupId}:refaster-support</ignoredUnusedDeclaredDependency>
</ignoredUnusedDeclaredDependencies>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>

View File

@@ -4,7 +4,7 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
@@ -18,7 +18,7 @@ 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.source.tree.Tree;
import com.sun.tools.javac.code.Symbol;
import java.util.Map;
import javax.lang.model.element.AnnotationValue;
@@ -46,7 +46,7 @@ public final class AmbiguousJsonCreator extends BugChecker implements Annotation
}
ClassTree clazz = state.findEnclosing(ClassTree.class);
if (clazz == null || clazz.getKind() != Kind.ENUM) {
if (clazz == null || clazz.getKind() != Tree.Kind.ENUM) {
return Description.NO_MATCH;
}

View File

@@ -8,7 +8,7 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;

View File

@@ -6,24 +6,24 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableList;
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.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. */
@AutoService(BugChecker.class)
@@ -48,18 +48,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()) {
ImmutableList<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));
}
}

View File

@@ -3,7 +3,7 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableSet;
@@ -19,13 +19,14 @@ 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;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/** A {@link BugChecker} that flags annotations that could be written more concisely. */
@AutoService(BugChecker.class)
@@ -90,8 +91,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();
}
@@ -118,7 +119,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,10 +130,13 @@ 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. */

View File

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

View File

@@ -4,12 +4,9 @@ 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 static tech.picnic.errorprone.bugpatterns.util.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;
@@ -20,12 +17,8 @@ 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;
import tech.picnic.errorprone.bugpatterns.util.ThirdPartyLibrary;
/**
* A {@link BugChecker} that flags {@link Collector Collectors} that don't clearly express
@@ -37,7 +30,7 @@ import tech.picnic.errorprone.utils.ThirdPartyLibrary;
@AutoService(BugChecker.class)
@BugPattern(
summary =
"Avoid `Collectors.to{List,Map,Set}` in favor of collectors that emphasize (im)mutability",
"Avoid `Collectors.to{List,Map,Set}` in favour of alternatives that emphasize (im)mutability",
link = BUG_PATTERNS_BASE_URL + "CollectorMutability",
linkType = CUSTOM,
severity = WARNING,
@@ -45,7 +38,7 @@ import tech.picnic.errorprone.utils.ThirdPartyLibrary;
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());
staticMethod().onClass("java.util.stream.Collectors");
private static final Matcher<ExpressionTree> LIST_COLLECTOR =
staticMethod().anyClass().named("toList");
private static final Matcher<ExpressionTree> MAP_COLLECTOR =
@@ -65,10 +58,7 @@ public final class CollectorMutability extends BugChecker implements MethodInvoc
if (LIST_COLLECTOR.matches(tree, state)) {
return suggestToCollectionAlternatives(
tree,
ImmutableList.class.getCanonicalName() + ".toImmutableList",
ArrayList.class.getCanonicalName(),
state);
tree, "com.google.common.collect.ImmutableList.toImmutableList", "ArrayList", state);
}
if (MAP_COLLECTOR.matches(tree, state)) {
@@ -77,10 +67,7 @@ public final class CollectorMutability extends BugChecker implements MethodInvoc
if (SET_COLLECTOR.matches(tree, state)) {
return suggestToCollectionAlternatives(
tree,
ImmutableSet.class.getCanonicalName() + ".toImmutableSet",
HashSet.class.getCanonicalName(),
state);
tree, "com.google.common.collect.ImmutableSet.toImmutableSet", "HashSet", state);
}
return Description.NO_MATCH;
@@ -88,20 +75,20 @@ public final class CollectorMutability extends BugChecker implements MethodInvoc
private Description suggestToCollectionAlternatives(
MethodInvocationTree tree,
String immutableReplacement,
String fullyQualifiedImmutableReplacement,
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);
"java.util.stream.Collectors.toCollection", mutableFix, state);
return buildDescription(tree)
.addFix(replaceMethodInvocation(tree, immutableReplacement, state))
.addFix(replaceMethodInvocation(tree, fullyQualifiedImmutableReplacement, state))
.addFix(
mutableFix
.replace(tree, String.format("%s(%s::new)", toCollectionSelect, mutableCollection))
.addImport(String.format("java.util.%s", mutableReplacement))
.replace(tree, String.format("%s(%s::new)", toCollectionSelect, mutableReplacement))
.build())
.build();
}
@@ -112,20 +99,17 @@ public final class CollectorMutability extends BugChecker implements MethodInvoc
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))
tree, "com.google.common.collect.ImmutableMap.toImmutableMap", state))
.addFix(
mutableFix
SuggestedFix.builder()
.addImport("java.util.HashMap")
.postfixWith(
tree.getArguments().get(argCount - 1),
(argCount == 2 ? ", (a, b) -> { throw new IllegalStateException(); }" : "")
+ String.format(", %s::new", hashMap))
+ ", HashMap::new")
.build())
.build();
}

View File

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

View File

@@ -7,13 +7,14 @@ import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAS
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.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.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;
@@ -21,7 +22,6 @@ 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)
@@ -36,15 +36,12 @@ public final class EmptyMethod extends BugChecker implements MethodTreeMatcher {
private static final Matcher<Tree> PERMITTED_ANNOTATION =
annotations(
AT_LEAST_ONE,
anyOf(
isType(Override.class.getCanonicalName()),
isType("org.aspectj.lang.annotation.Pointcut")));
anyOf(isType("java.lang.Override"), 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()
@@ -58,7 +55,7 @@ public final class EmptyMethod extends BugChecker implements MethodTreeMatcher {
return Description.NO_MATCH;
}
return describeMatch(tree, SourceCode.deleteWithTrailingWhitespace(tree, state));
return describeMatch(tree, SuggestedFix.delete(tree));
}
private static boolean isInPossibleTestHelperClass(VisitorState state) {

View File

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

View File

@@ -1,13 +1,12 @@
package tech.picnic.errorprone.guidelines.bugpatterns;
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.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.instanceMethod;
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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.Splitter;
@@ -67,12 +66,7 @@ public final class ErrorProneTestHelperSourceFormat extends BugChecker
.named("addSourceLines"),
instanceMethod()
.onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper")
.named("addInputLines"),
// XXX: Add tests for `Compilation.compileWithDocumentationGenerator`. Until done, make
// sure to update this matcher if that method's class or name is changed/moved.
staticMethod()
.onClass("tech.picnic.errorprone.documentation.Compilation")
.named("compileWithDocumentationGenerator"));
.named("addInputLines"));
private static final Matcher<ExpressionTree> OUTPUT_SOURCE_ACCEPTING_METHOD =
instanceMethod()
.onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper.ExpectOutput")
@@ -89,8 +83,7 @@ public final class ErrorProneTestHelperSourceFormat extends BugChecker
}
List<? extends ExpressionTree> sourceLines =
tree.getArguments()
.subList(ASTHelpers.getSymbol(tree).params().size() - 1, tree.getArguments().size());
tree.getArguments().subList(1, tree.getArguments().size());
if (sourceLines.isEmpty()) {
return buildDescription(tree).setMessage("No source code provided").build();
}
@@ -111,9 +104,7 @@ public final class ErrorProneTestHelperSourceFormat extends BugChecker
String formatted;
try {
formatted = formatSourceCode(source, retainUnusedImports).trim();
} catch (
@SuppressWarnings("java:S1166" /* Stack trace not relevant. */)
FormatterException e) {
} catch (FormatterException e) {
return buildDescription(methodInvocation)
.setMessage(String.format("Source code is malformed: %s", e.getMessage()))
.build();
@@ -140,7 +131,7 @@ public final class ErrorProneTestHelperSourceFormat extends BugChecker
SuggestedFix.replace(
startPos,
endPos,
Splitter.on(System.lineSeparator())
Splitter.on('\n')
.splitToStream(formatted)
.map(state::getConstantExpression)
.collect(joining(", "))));
@@ -156,18 +147,17 @@ public final class ErrorProneTestHelperSourceFormat extends BugChecker
return FORMATTER.formatSource(withOptionallyRemovedImports);
}
// XXX: This logic is duplicated in `BugPatternTestExtractor`. Can we do better?
private static Optional<String> getConstantSourceCode(
List<? extends ExpressionTree> sourceLines) {
StringBuilder source = new StringBuilder();
for (ExpressionTree sourceLine : sourceLines) {
String value = ASTHelpers.constValue(sourceLine, String.class);
Object value = ASTHelpers.constValue(sourceLine);
if (value == null) {
return Optional.empty();
}
source.append(value).append(System.lineSeparator());
source.append(value).append('\n');
}
return Optional.of(source.toString());

View File

@@ -7,7 +7,7 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableSet;
@@ -44,7 +44,7 @@ import java.util.stream.Stream;
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");
staticMethod().onClass(Ordering.class.getName()).named("explicit");
/** Instantiates a new {@link ExplicitEnumOrdering} instance. */
public ExplicitEnumOrdering() {}
@@ -72,7 +72,7 @@ public final class ExplicitEnumOrdering extends BugChecker implements MethodInvo
List<? extends ExpressionTree> expressions) {
return expressions.stream()
.map(ASTHelpers::getSymbol)
.filter(s -> s != null && s.isEnum())
.filter(Symbol::isEnum)
.collect(
collectingAndThen(
toImmutableSetMultimap(Symbol::asType, Symbol::toString),

View File

@@ -4,11 +4,11 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.generic;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.subOf;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.type;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.unbound;
import com.google.auto.service.AutoService;
import com.google.common.collect.Iterables;
@@ -50,9 +50,8 @@ import reactor.core.publisher.Flux;
@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""",
"`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,
@@ -67,7 +66,7 @@ public final class FluxFlatMapUsage extends BugChecker
instanceMethod()
.onDescendantOf(FLUX)
.namedAnyOf("flatMap", "flatMapSequential")
.withParameters(Function.class.getCanonicalName());
.withParameters(Function.class.getName());
private static final Supplier<Type> FLUX_OF_PUBLISHERS =
VisitorState.memoize(
generic(FLUX, subOf(generic(type("org.reactivestreams.Publisher"), unbound()))));
@@ -83,8 +82,10 @@ public final class FluxFlatMapUsage extends BugChecker
SuggestedFix serializationFix = SuggestedFixes.renameMethodInvocation(tree, "concatMap", state);
SuggestedFix concurrencyCapFix =
SuggestedFix.postfixWith(
Iterables.getOnlyElement(tree.getArguments()), ", " + MAX_CONCURRENCY_ARG_NAME);
SuggestedFix.builder()
.postfixWith(
Iterables.getOnlyElement(tree.getArguments()), ", " + MAX_CONCURRENCY_ARG_NAME)
.build();
Description.Builder description = buildDescription(tree);

View File

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

View File

@@ -10,11 +10,9 @@ 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 static tech.picnic.errorprone.bugpatterns.util.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;
@@ -32,11 +30,10 @@ 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;
import org.jspecify.nullness.Nullable;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags string concatenations that produce a format string; in such cases
@@ -62,7 +59,6 @@ import tech.picnic.errorprone.utils.SourceCode;
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.
@@ -71,8 +67,7 @@ public final class FormatStringConcatenation extends BugChecker
anyMethod()
.anyClass()
.withAnyName()
.withParameters(String.class.getCanonicalName(), Throwable.class.getCanonicalName());
.withParameters(String.class.getName(), Throwable.class.getName());
// XXX: Drop some of these methods if we use Refaster to replace some with others.
private static final Matcher<ExpressionTree> ASSERTJ_FORMAT_METHOD =
anyOf(
@@ -121,14 +116,14 @@ public final class FormatStringConcatenation extends BugChecker
private static final Matcher<ExpressionTree> GUAVA_FORMAT_METHOD =
anyOf(
staticMethod()
.onClass(Preconditions.class.getCanonicalName())
.onClass("com.google.common.base.Preconditions")
.namedAnyOf("checkArgument", "checkNotNull", "checkState"),
staticMethod().onClass(Verify.class.getCanonicalName()).named("verify"));
staticMethod().onClass("com.google.common.base.Verify").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"));
staticMethod().onClass("java.lang.String").named("format"),
instanceMethod().onExactClass("java.util.Formatter").named("format"));
private static final Matcher<ExpressionTree> SLF4J_FORMAT_METHOD =
instanceMethod()
.onDescendantOf("org.slf4j.Logger")
@@ -245,8 +240,8 @@ public final class FormatStringConcatenation extends BugChecker
}
private void appendExpression(Tree tree) {
if (tree instanceof LiteralTree literal) {
formatString.append(literal.getValue());
if (tree instanceof LiteralTree) {
formatString.append(((LiteralTree) tree).getValue());
} else {
formatString.append(formatSpecifier);
formatArguments.add(tree);

View File

@@ -7,20 +7,9 @@ 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 static tech.picnic.errorprone.bugpatterns.util.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;
@@ -31,26 +20,20 @@ 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;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/** A {@link BugChecker} that flags redundant identity conversions. */
// XXX: Consider detecting cases where a flagged expression is passed to a method, and where removal
// of the identity conversion would cause a different method overload to be selected. Depending on
// of the identify 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.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Avoid or clarify identity conversions",
@@ -62,28 +45,27 @@ public final class IdentityConversion extends BugChecker implements MethodInvoca
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> IS_CONVERSION_METHOD =
anyOf(
staticMethod()
.onClassAny(
"com.google.common.collect.ImmutableBiMap",
"com.google.common.collect.ImmutableList",
"com.google.common.collect.ImmutableListMultimap",
"com.google.common.collect.ImmutableMap",
"com.google.common.collect.ImmutableMultimap",
"com.google.common.collect.ImmutableMultiset",
"com.google.common.collect.ImmutableRangeMap",
"com.google.common.collect.ImmutableRangeSet",
"com.google.common.collect.ImmutableSet",
"com.google.common.collect.ImmutableSetMultimap",
"com.google.common.collect.ImmutableTable")
.named("copyOf"),
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(String.class.getName()).named("valueOf"),
staticMethod().onClass("reactor.adapter.rxjava.RxJava2Adapter"),
staticMethod()
.onClass("reactor.core.publisher.Flux")
@@ -113,20 +95,10 @@ public final class IdentityConversion extends BugChecker implements MethodInvoca
return Description.NO_MATCH;
}
if (sourceType.isPrimitive()
&& state.getPath().getParentPath().getLeaf() instanceof MemberSelectTree) {
/*
* The result of the conversion method is dereferenced, while the source type is a primitive:
* dropping the conversion would yield uncompilable code.
*/
return Description.NO_MATCH;
}
return buildDescription(tree)
.setMessage(
"""
This method invocation appears redundant; remove it or suppress this warning and add a \
comment explaining its purpose""")
"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();

View File

@@ -11,7 +11,7 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
@@ -43,9 +43,8 @@ import javax.lang.model.element.Modifier;
@AutoService(BugChecker.class)
@BugPattern(
summary =
"""
`SortedSet` properties of a `@Value.Immutable` or `@Value.Modifiable` type must be \
annotated with `@Value.NaturalOrder` or `@Value.ReverseOrder`""",
"`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,

View File

@@ -3,28 +3,27 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.LambdaExpressionTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.InstanceOfTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.VariableTree;
import tech.picnic.errorprone.utils.SourceCode;
import com.sun.source.tree.Tree.Kind;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags lambda expressions that can be replaced with a method reference
* of the form {@code T.class::isInstance}.
*
* @see MethodReferenceUsage
*/
// XXX: Consider folding this logic into the `MethodReferenceUsage` check of the
// `error-prone-experimental` module.
// XXX: Consider folding this logic into the `MethodReferenceUsage` check.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Prefer `Class::isInstance` method reference over equivalent lambda expression",
@@ -40,19 +39,15 @@ public final class IsInstanceLambdaUsage extends BugChecker implements LambdaExp
@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()))) {
if (tree.getKind() != Kind.LAMBDA_EXPRESSION || tree.getBody().getKind() != Kind.INSTANCE_OF) {
return Description.NO_MATCH;
}
return describeMatch(
tree,
SuggestedFix.replace(
tree, SourceCode.treeToString(instanceOf.getType(), state) + ".class::isInstance"));
tree,
SourceCode.treeToString(((InstanceOfTree) tree.getBody()).getType(), state)
+ ".class::isInstance"));
}
}

View File

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

View File

@@ -3,18 +3,19 @@ package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.annotations;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.enclosingClass;
import static com.google.errorprone.matchers.Matchers.hasModifier;
import static com.google.errorprone.matchers.Matchers.not;
import static com.google.errorprone.matchers.Matchers.isType;
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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import static tech.picnic.errorprone.bugpatterns.util.JavaKeywords.isReservedKeyword;
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;
@@ -23,12 +24,20 @@ import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
import com.google.errorprone.matchers.MultiMatcher;
import com.google.errorprone.predicates.TypePredicate;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.MethodTree;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Symbol;
import java.util.Optional;
import javax.lang.model.element.Modifier;
import tech.picnic.errorprone.utils.ConflictDetection;
import javax.lang.model.element.Name;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/** A {@link BugChecker} that flags non-canonical JUnit method declarations. */
// XXX: Consider introducing a class-level check that enforces that test classes:
@@ -47,19 +56,35 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
private static final long serialVersionUID = 1L;
private static final String TEST_PREFIX = "test";
private static final ImmutableSet<Modifier> ILLEGAL_MODIFIERS =
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)));
ImmutableSet.of(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC);
private static final Matcher<MethodTree> HAS_UNMODIFIABLE_SIGNATURE =
anyOf(
annotations(AT_LEAST_ONE, isType("java.lang.Override")),
allOf(
Matchers.not(hasModifier(Modifier.FINAL)),
Matchers.not(hasModifier(Modifier.PRIVATE)),
enclosingClass(hasModifier(Modifier.ABSTRACT))));
private static final MultiMatcher<MethodTree, AnnotationTree> TEST_METHOD =
annotations(
AT_LEAST_ONE,
anyOf(
isType("org.junit.jupiter.api.Test"),
hasMetaAnnotation("org.junit.jupiter.api.TestTemplate")));
private static final MultiMatcher<MethodTree, AnnotationTree> SETUP_OR_TEARDOWN_METHOD =
annotations(
AT_LEAST_ONE,
anyOf(
isType("org.junit.jupiter.api.AfterAll"),
isType("org.junit.jupiter.api.AfterEach"),
isType("org.junit.jupiter.api.BeforeAll"),
isType("org.junit.jupiter.api.BeforeEach")));
/** 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)) {
if (HAS_UNMODIFIABLE_SIGNATURE.matches(tree, state)) {
return Description.NO_MATCH;
}
@@ -81,11 +106,10 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
private void suggestTestMethodRenameIfApplicable(
MethodTree tree, SuggestedFix.Builder fixBuilder, VisitorState state) {
MethodSymbol symbol = ASTHelpers.getSymbol(tree);
tryCanonicalizeMethodName(symbol)
tryCanonicalizeMethodName(tree)
.ifPresent(
newName ->
ConflictDetection.findMethodRenameBlocker(symbol, newName, state)
findMethodRenameBlocker(newName, state)
.ifPresentOrElse(
blocker -> reportMethodRenameBlocker(tree, blocker, state),
() -> fixBuilder.merge(SuggestedFixes.renameMethod(tree, newName, state))));
@@ -101,8 +125,63 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
.build());
}
private static Optional<String> tryCanonicalizeMethodName(MethodSymbol symbol) {
return Optional.of(symbol.getQualifiedName().toString())
/**
* If applicable, returns a human-readable argument against assigning the given name to an
* existing method.
*
* <p>This method implements imperfect heuristics. Things it currently does not consider include
* the following:
*
* <ul>
* <li>Whether the rename would merely introduce a method overload, rather than clashing with an
* existing method declaration.
* <li>Whether the rename would cause a method in a superclass to be overridden.
* <li>Whether the rename would in fact clash with a static import. (It could be that a static
* import of the same name is only referenced from lexical scopes in which the method under
* consideration cannot be referenced directly.)
* </ul>
*/
private static Optional<String> findMethodRenameBlocker(String methodName, VisitorState state) {
if (isMethodInEnclosingClass(methodName, state)) {
return Optional.of(
String.format("a method named `%s` already exists in this class", methodName));
}
if (isSimpleNameStaticallyImported(methodName, state)) {
return Optional.of(String.format("`%s` is already statically imported", methodName));
}
if (isReservedKeyword(methodName)) {
return Optional.of(String.format("`%s` is a reserved keyword", methodName));
}
return Optional.empty();
}
private static boolean isMethodInEnclosingClass(String methodName, VisitorState state) {
return state.findEnclosing(ClassTree.class).getMembers().stream()
.filter(MethodTree.class::isInstance)
.map(MethodTree.class::cast)
.map(MethodTree::getName)
.map(Name::toString)
.anyMatch(methodName::equals);
}
private static boolean isSimpleNameStaticallyImported(String simpleName, VisitorState state) {
return state.getPath().getCompilationUnit().getImports().stream()
.filter(ImportTree::isStatic)
.map(ImportTree::getQualifiedIdentifier)
.map(tree -> getStaticImportSimpleName(tree, state))
.anyMatch(simpleName::contentEquals);
}
private static CharSequence getStaticImportSimpleName(Tree tree, VisitorState state) {
String source = SourceCode.treeToString(tree, state);
return source.subSequence(source.lastIndexOf('.') + 1, source.length());
}
private static Optional<String> tryCanonicalizeMethodName(MethodTree tree) {
return Optional.of(ASTHelpers.getSymbol(tree).getQualifiedName().toString())
.filter(name -> name.startsWith(TEST_PREFIX))
.map(name -> name.substring(TEST_PREFIX.length()))
.filter(not(String::isEmpty))
@@ -110,9 +189,17 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
.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();
// XXX: Move to a `MoreMatchers` utility class.
private static Matcher<AnnotationTree> hasMetaAnnotation(String annotationClassName) {
TypePredicate typePredicate = hasAnnotation(annotationClassName);
return (tree, state) -> {
Symbol sym = ASTHelpers.getSymbol(tree);
return sym != null && typePredicate.apply(sym.type, state);
};
}
// XXX: Move to a `MoreTypePredicates` utility class.
private static TypePredicate hasAnnotation(String annotationClassName) {
return (type, state) -> ASTHelpers.hasAnnotation(type.tsym, annotationClassName, state);
}
}

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.Splitter;
@@ -31,6 +31,7 @@ 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;
@@ -39,11 +40,9 @@ 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;
import org.jspecify.nullness.Nullable;
import tech.picnic.errorprone.bugpatterns.util.AnnotationAttributeMatcher;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags annotation array listings which aren't sorted lexicographically.
@@ -51,9 +50,6 @@ import tech.picnic.errorprone.utils.SourceCode;
* <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(
summary = "Where possible, sort annotation array attributes lexicographically",
@@ -61,25 +57,22 @@ import tech.picnic.errorprone.utils.SourceCode;
linkType = CUSTOM,
severity = SUGGESTION,
tags = STYLE)
@SuppressWarnings("java:S2160" /* Super class equality definition suffices. */)
public final class LexicographicalAnnotationAttributeListing 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");
"org.springframework.test.context.TestPropertySource#value");
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
@@ -99,8 +92,7 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
*
* @param flags Any provided command line flags.
*/
@Inject
LexicographicalAnnotationAttributeListing(ErrorProneFlags flags) {
public LexicographicalAnnotationAttributeListing(ErrorProneFlags flags) {
matcher = createAnnotationAttributeMatcher(flags);
}
@@ -125,9 +117,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(
@@ -192,24 +188,24 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
@Override
public @Nullable Void visitIdentifier(IdentifierTree node, @Nullable Void unused) {
nodes.add(ImmutableList.of(node.getName().toString()));
return super.visitIdentifier(node, null);
return super.visitIdentifier(node, unused);
}
@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())
value instanceof String
? STRING_ARGUMENT_SPLITTER.splitToStream((String) value).collect(toImmutableList())
: ImmutableList.of(String.valueOf(value)));
return super.visitLiteral(node, null);
return super.visitLiteral(node, unused);
}
@Override
public @Nullable Void visitPrimitiveType(PrimitiveTypeTree node, @Nullable Void unused) {
nodes.add(ImmutableList.of(node.getPrimitiveTypeKind().toString()));
return super.visitPrimitiveType(node, null);
return super.visitPrimitiveType(node, unused);
}
}.scan(array, null);
@@ -219,15 +215,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);
}

View File

@@ -7,7 +7,7 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.VerifyException;
@@ -27,8 +27,8 @@ 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;
import org.jspecify.nullness.Nullable;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags annotations that are not lexicographically sorted.
@@ -36,10 +36,6 @@ import tech.picnic.errorprone.utils.SourceCode;
* <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",
@@ -50,20 +46,9 @@ import tech.picnic.errorprone.utils.SourceCode;
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);
(a == null || a == DECLARATION) && b == TYPE ? -1 : a == TYPE && b == DECLARATION ? 1 : 0;
/** Instantiates a new {@link LexicographicalAnnotationListing} instance. */
public LexicographicalAnnotationListing() {}

View File

@@ -1,10 +1,10 @@
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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.VerifyException;
@@ -27,6 +27,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;
@@ -36,6 +37,8 @@ import javax.lang.model.element.Name;
/**
* A {@link BugChecker} that flags lambda expressions that can be replaced with method references.
*
* @see IsInstanceLambdaUsage
*/
// XXX: Other custom expressions we could rewrite:
// - `a -> "str" + a` to `"str"::concat`. But only if `str` is provably non-null.
@@ -51,8 +54,7 @@ import javax.lang.model.element.Name;
// 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.
// XXX: Consider folding the `IsInstanceLambdaUsage` check into this class.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Prefer method references over lambda expressions",
@@ -83,19 +85,22 @@ public final class MethodReferenceUsage extends BugChecker implements LambdaExpr
.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(
@@ -113,35 +118,32 @@ public final class MethodReferenceUsage extends BugChecker implements LambdaExpr
.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 (!ASTHelpers.isStatic(sym)) {
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 +152,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();
}
@@ -180,8 +182,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,8 +195,6 @@ 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();

View File

@@ -0,0 +1,59 @@
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.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.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.MultiMatcher;
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;
/** A {@link BugChecker} that flags likely missing Refaster annotations. */
@AutoService(BugChecker.class)
@BugPattern(
summary = "The Refaster rule contains a method without any Refaster annotations",
link = BUG_PATTERNS_BASE_URL + "MissingRefasterAnnotation",
linkType = CUSTOM,
severity = WARNING,
tags = LIKELY_ERROR)
public final class MissingRefasterAnnotation extends BugChecker implements ClassTreeMatcher {
private static final long serialVersionUID = 1L;
private static final MultiMatcher<Tree, AnnotationTree> 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")));
/** Instantiates a new {@link MissingRefasterAnnotation} instance. */
public MissingRefasterAnnotation() {}
@Override
public Description matchClass(ClassTree tree, VisitorState state) {
long methodTypes =
tree.getMembers().stream()
.filter(member -> member.getKind() == Tree.Kind.METHOD)
.map(MethodTree.class::cast)
.filter(method -> !ASTHelpers.isGeneratedConstructor(method))
.map(method -> REFASTER_ANNOTATION.matches(method, state))
.distinct()
.count();
return methodTypes < 2 ? Description.NO_MATCH : buildDescription(tree).build();
}
}

View File

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

View File

@@ -4,7 +4,7 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.Iterables;
@@ -18,7 +18,7 @@ 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;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags method invocations for which all arguments are wrapped using

View File

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

View File

@@ -3,11 +3,10 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.generic;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.raw;
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.subOf;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
@@ -15,19 +14,14 @@ 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.google.errorprone.util.ASTHelpers;
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 =
@@ -39,15 +33,15 @@ import java.util.Optional;
public final class NestedOptionals extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Supplier<Type> OPTIONAL = Suppliers.typeFromClass(Optional.class);
private static final Matcher<Tree> IS_OPTIONAL_OF_OPTIONAL =
isSubTypeOf(VisitorState.memoize(generic(OPTIONAL, subOf(raw(OPTIONAL)))));
private static final Supplier<Type> OPTIONAL_OF_OPTIONAL =
VisitorState.memoize(generic(OPTIONAL, subOf(raw(OPTIONAL))));
/** Instantiates a new {@link NestedOptionals} instance. */
public NestedOptionals() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
return IS_OPTIONAL_OF_OPTIONAL.matches(tree, state)
return state.getTypes().isSubtype(ASTHelpers.getType(tree), OPTIONAL_OF_OPTIONAL.get(state))
? describeMatch(tree)
: Description.NO_MATCH;
}

View File

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

View File

@@ -5,7 +5,7 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
@@ -20,7 +20,7 @@ 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;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags {@link Mono} operations that are known to be vacuous, given that
@@ -43,7 +43,6 @@ import tech.picnic.errorprone.utils.SourceCode;
// 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 =
@@ -72,7 +71,7 @@ public final class NonEmptyMono extends BugChecker implements MethodInvocationTr
instanceMethod()
.onDescendantOf("reactor.core.publisher.Flux")
.named("reduce")
.withParameters(Object.class.getCanonicalName(), BiFunction.class.getCanonicalName()),
.withParameters(Object.class.getName(), BiFunction.class.getName()),
instanceMethod()
.onDescendantOf("reactor.core.publisher.Mono")
.namedAnyOf("defaultIfEmpty", "hasElement", "single"));

View File

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

View File

@@ -1,135 +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.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.LiteralTree;
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.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: Consider also implementing the inverse, in which `.orElseGet(() -> someConstant)` is
// flagged.
// XXX: Once the `MethodReferenceUsageCheck` 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 `@Matches` constraint that implements the `requiresComputation`
// logic.
@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 OptionalOrElse extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
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 OptionalOrElse} instance. */
public OptionalOrElse() {}
@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 (!requiresComputation(argument) || 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);
}
/**
* Tells whether the given expression contains anything other than a literal or a (possibly
* dereferenced) variable or constant.
*/
private static boolean requiresComputation(ExpressionTree tree) {
return !(tree instanceof IdentifierTree
|| tree instanceof LiteralTree
|| (tree instanceof MemberSelectTree memberSelect
&& !requiresComputation(memberSelect.getExpression()))
|| ASTHelpers.constValue(tree) != null);
}
/** 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 (requiresComputation(memberSelect.getExpression())) {
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());
}
}

View File

@@ -7,7 +7,7 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.VerifyException;
@@ -17,12 +17,10 @@ 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;
@@ -34,7 +32,7 @@ import java.util.Comparator;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;
import tech.picnic.errorprone.utils.SourceCode;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags {@code Comparator#comparing*} invocations that can be replaced
@@ -45,34 +43,32 @@ import tech.picnic.errorprone.utils.SourceCode;
@AutoService(BugChecker.class)
@BugPattern(
summary =
"""
Ensure invocations of `Comparator#comparing{,Double,Int,Long}` match the return type of \
the provided function""",
"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())
.onClass(Comparator.class.getName())
.namedAnyOf("comparingInt", "comparingLong", "comparingDouble"),
staticMethod()
.onClass(Comparator.class.getCanonicalName())
.onClass(Comparator.class.getName())
.named("comparing")
.withParameters(Function.class.getCanonicalName()));
.withParameters(Function.class.getName()));
private static final Matcher<ExpressionTree> INSTANCE_COMPARISON_METHOD =
anyOf(
instanceMethod()
.onDescendantOf(Comparator.class.getCanonicalName())
.onDescendantOf(Comparator.class.getName())
.namedAnyOf("thenComparingInt", "thenComparingLong", "thenComparingDouble"),
instanceMethod()
.onDescendantOf(Comparator.class.getCanonicalName())
.onDescendantOf(Comparator.class.getName())
.named("thenComparing")
.withParameters(Function.class.getCanonicalName()));
.withParameters(Function.class.getName()));
/** Instantiates a new {@link PrimitiveComparison} instance. */
public PrimitiveComparison() {}
@@ -149,44 +145,37 @@ public final class PrimitiveComparison extends BugChecker implements MethodInvoc
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()));
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: 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();
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, SourceCode.treeToString(ms.getExpression(), state) + '.' + preferredMethodName);
default:
throw new VerifyException("Unexpected type of expression: " + expr.getKind());
}
if (expr instanceof MemberSelectTree memberSelect) {
return SuggestedFix.replace(
memberSelect,
SourceCode.treeToString(memberSelect.getExpression(), state) + '.' + preferredMethodName);
}
throw new VerifyException("Unexpected type of expression: " + expr.getKind());
}
}

View File

@@ -7,7 +7,6 @@ import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.anyMethod;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.anything;
import static com.google.errorprone.matchers.Matchers.argumentCount;
import static com.google.errorprone.matchers.Matchers.isNonNullUsingDataflow;
import static com.google.errorprone.matchers.Matchers.isSameType;
@@ -15,11 +14,9 @@ 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 static tech.picnic.errorprone.bugpatterns.util.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;
@@ -52,10 +49,8 @@ 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;
import tech.picnic.errorprone.bugpatterns.util.MethodMatcherFactory;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/** A {@link BugChecker} that flags redundant explicit string conversions. */
@AutoService(BugChecker.class)
@@ -65,18 +60,16 @@ import tech.picnic.errorprone.utils.SourceCode;
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
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);
@@ -88,7 +81,7 @@ public final class RedundantStringConversion extends BugChecker
private static final Matcher<MethodInvocationTree> WELL_KNOWN_STRING_CONVERSION_METHODS =
anyOf(
instanceMethod()
.onDescendantOfAny(Object.class.getCanonicalName())
.onDescendantOfAny(Object.class.getName())
.named("toString")
.withNoParameters(),
allOf(
@@ -102,7 +95,7 @@ public final class RedundantStringConversion extends BugChecker
.collect(toImmutableSet()))
.named("toString"),
allOf(
staticMethod().onClass(String.class.getCanonicalName()).named("valueOf"),
staticMethod().onClass(String.class.getName()).named("valueOf"),
not(
anyMethod()
.anyClass()
@@ -111,37 +104,35 @@ public final class RedundantStringConversion extends BugChecker
ImmutableList.of(Suppliers.arrayOf(Suppliers.CHAR_TYPE))))))));
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())
.onDescendantOfAny(PrintStream.class.getName(), PrintWriter.class.getName())
.namedAnyOf("format", "printf"),
instanceMethod()
.onDescendantOfAny(
PrintStream.class.getCanonicalName(), PrintWriter.class.getCanonicalName())
.onDescendantOfAny(PrintStream.class.getName(), PrintWriter.class.getName())
.namedAnyOf("print", "println")
.withParameters(Object.class.getCanonicalName()),
.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()
@@ -160,8 +151,7 @@ public final class RedundantStringConversion extends BugChecker
*
* @param flags Any provided command line flags.
*/
@Inject
RedundantStringConversion(ErrorProneFlags flags) {
public RedundantStringConversion(ErrorProneFlags flags) {
conversionMethodMatcher = createConversionMethodMatcher(flags);
}
@@ -174,7 +164,7 @@ 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<>();
@@ -188,7 +178,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 +187,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(
@@ -309,7 +299,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);
@@ -331,32 +321,36 @@ 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) {
return Optional.empty();
}
MethodInvocationTree methodInvocation = (MethodInvocationTree) tree;
if (!conversionMethodMatcher.matches(methodInvocation, state)) {
return Optional.empty();
}
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));
};
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: "
+ 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())
return Optional.of(methodInvocation.getMethodSelect())
.filter(methodSelect -> methodSelect.getKind() == Kind.MEMBER_SELECT)
.map(methodSelect -> ((MemberSelectTree) methodSelect).getExpression())
.filter(expr -> !"super".equals(SourceCode.treeToString(expr, state)));
}
@@ -369,7 +363,7 @@ 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))
@@ -380,9 +374,10 @@ public final class RedundantStringConversion extends BugChecker
ErrorProneFlags flags) {
// XXX: ErrorProneFlags#getList splits by comma, but method signatures may also contain commas.
// For this class methods accepting more than one argument are not valid, but still: not nice.
return 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);
}
}

View File

@@ -1,10 +1,10 @@
package tech.picnic.errorprone.guidelines.bugpatterns;
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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
@@ -17,7 +17,7 @@ import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.refaster.Refaster;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import tech.picnic.errorprone.utils.SourceCode;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags unnecessary {@link Refaster#anyOf(Object[])} usages.
@@ -35,25 +35,28 @@ import tech.picnic.errorprone.utils.SourceCode;
public final class RefasterAnyOfUsage extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> REFASTER_ANY_OF =
staticMethod().onClass(Refaster.class.getCanonicalName()).named("anyOf");
staticMethod().onClass(Refaster.class.getName()).named("anyOf");
/** Instantiates a new {@link RefasterAnyOfUsage} instance. */
public RefasterAnyOfUsage() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
int argumentCount = tree.getArguments().size();
if (argumentCount > 1 || !REFASTER_ANY_OF.matches(tree, state)) {
return Description.NO_MATCH;
if (REFASTER_ANY_OF.matches(tree, state)) {
switch (tree.getArguments().size()) {
case 0:
// We can't safely fix this case; dropping the expression may produce non-compilable code.
return describeMatch(tree);
case 1:
return describeMatch(
tree,
SuggestedFix.replace(
tree, SourceCode.treeToString(tree.getArguments().get(0), state)));
default:
/* Handled below. */
}
}
if (argumentCount == 0) {
/* We can't safely fix this case; dropping the expression may produce non-compilable code. */
return describeMatch(tree);
}
return describeMatch(
tree,
SuggestedFix.replace(tree, SourceCode.treeToString(tree.getArguments().get(0), state)));
return Description.NO_MATCH;
}
}

View File

@@ -1,11 +1,11 @@
package tech.picnic.errorprone.guidelines.bugpatterns;
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.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;

View File

@@ -11,7 +11,7 @@ import static com.google.errorprone.matchers.Matchers.isSameType;
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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
@@ -22,10 +22,6 @@ 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
@@ -73,20 +69,14 @@ public final class RequestMappingAnnotation extends BugChecker implements Method
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 + "RequestPart"))),
isSameType("java.io.InputStream"),
isSameType("java.time.ZoneId"),
isSameType("java.util.Locale"),
isSameType("java.util.TimeZone"),
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"),
@@ -105,10 +95,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;
}

View File

@@ -1,6 +1,5 @@
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;
@@ -10,77 +9,41 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.BugPattern;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.suppliers.Suppliers;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import 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""",
summary = "`@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 static final Matcher<VariableTree> HAS_UNSUPPORTED_REQUEST_PARAM =
allOf(
annotations(AT_LEAST_ONE, isType("org.springframework.web.bind.annotation.RequestParam")),
anyOf(isSubtypeOf(ImmutableCollection.class), isSubtypeOf(ImmutableMap.class)));
private 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);
}
/** Instantiates a new {@link RequestParamType} instance. */
public RequestParamType() {}
@Override
public Description matchVariable(VariableTree tree, VisitorState state) {
return hasUnsupportedRequestParamType.matches(tree, state)
return HAS_UNSUPPORTED_REQUEST_PARAM.matches(tree, state)
? describeMatch(tree)
: Description.NO_MATCH;
}
private static Matcher<VariableTree> hasUnsupportedRequestParamType(ErrorProneFlags flags) {
return allOf(
annotations(AT_LEAST_ONE, isType("org.springframework.web.bind.annotation.RequestParam")),
anyOf(isSubtypeOf(ImmutableCollection.class), isSubtypeOf(ImmutableMap.class)),
not(isSubtypeOfAny(Flags.getList(flags, SUPPORTED_CUSTOM_TYPES_FLAG))));
}
private static Matcher<Tree> isSubtypeOfAny(ImmutableList<String> inclusions) {
return anyOf(
inclusions.stream()
.map(inclusion -> isSubtypeOf(Suppliers.typeFromString(inclusion)))
.collect(toImmutableList()));
}
}

View File

@@ -0,0 +1,94 @@
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.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
import static com.google.errorprone.matchers.Matchers.annotations;
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
import static com.google.errorprone.matchers.Matchers.isType;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.common.AnnotationMirrors;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableList;
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.matchers.MultiMatcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import tech.picnic.errorprone.bugpatterns.util.ThirdPartyLibrary;
/**
* A {@link BugChecker} that flags methods with Spring's {@code @Scheduled} annotation that lack New
* Relic Agent's {@code @Trace(dispatcher = true)}.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Scheduled operation must start a new New Relic transaction",
link = BUG_PATTERNS_BASE_URL + "ScheduledTransactionTrace",
linkType = CUSTOM,
severity = ERROR,
tags = LIKELY_ERROR)
public final class ScheduledTransactionTrace extends BugChecker implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
private static final String TRACE_ANNOTATION_FQCN = "com.newrelic.api.agent.Trace";
private static final Matcher<Tree> IS_SCHEDULED =
hasAnnotation("org.springframework.scheduling.annotation.Scheduled");
private static final MultiMatcher<Tree, AnnotationTree> TRACE_ANNOTATION =
annotations(AT_LEAST_ONE, isType(TRACE_ANNOTATION_FQCN));
/** Instantiates a new {@link ScheduledTransactionTrace} instance. */
public ScheduledTransactionTrace() {}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (!ThirdPartyLibrary.NEW_RELIC_AGENT_API.isIntroductionAllowed(state)
|| !IS_SCHEDULED.matches(tree, state)) {
return Description.NO_MATCH;
}
ImmutableList<AnnotationTree> traceAnnotations =
TRACE_ANNOTATION.multiMatchResult(tree, state).matchingNodes();
if (traceAnnotations.isEmpty()) {
/* This method completely lacks the `@Trace` annotation; add it. */
return describeMatch(
tree,
SuggestedFix.builder()
.addImport(TRACE_ANNOTATION_FQCN)
.prefixWith(tree, "@Trace(dispatcher = true)")
.build());
}
AnnotationTree traceAnnotation = Iterables.getOnlyElement(traceAnnotations);
if (isCorrectAnnotation(traceAnnotation)) {
return Description.NO_MATCH;
}
/*
* The `@Trace` annotation is present but does not specify `dispatcher = true`. Add or update
* the `dispatcher` annotation element.
*/
return describeMatch(
traceAnnotation,
SuggestedFixes.updateAnnotationArgumentValues(
traceAnnotation, state, "dispatcher", ImmutableList.of("true"))
.build());
}
private static boolean isCorrectAnnotation(AnnotationTree traceAnnotation) {
return Boolean.TRUE.equals(
AnnotationMirrors.getAnnotationValue(
ASTHelpers.getAnnotationMirror(traceAnnotation), "dispatcher")
.getValue());
}
}

View File

@@ -0,0 +1,350 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
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 java.util.Objects.requireNonNull;
import static tech.picnic.errorprone.bugpatterns.util.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.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
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.Fix;
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.AnnotationTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Scope;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import tech.picnic.errorprone.bugpatterns.util.AnnotationAttributeMatcher;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} which flags annotations with time attributes that can be written more
* concisely.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Simplifies annotations which express an amount of time using a `TimeUnit`",
link = BUG_PATTERNS_BASE_URL + "SimplifyTimeAnnotation",
linkType = CUSTOM,
severity = WARNING,
tags = SIMPLIFICATION)
public final class SimplifyTimeAnnotationCheck extends BugChecker implements AnnotationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final AnnotationAttributeMatcher ARGUMENT_SELECTOR =
createAnnotationAttributeMatcher();
/** Instantiates a new {@link SimplifyTimeAnnotationCheck} instance. */
public SimplifyTimeAnnotationCheck() {}
@Override
public Description matchAnnotation(AnnotationTree annotationTree, VisitorState state) {
ImmutableList<ExpressionTree> arguments =
ARGUMENT_SELECTOR.extractMatchingArguments(annotationTree).collect(toImmutableList());
if (arguments.isEmpty()) {
return Description.NO_MATCH;
}
return trySimplification(annotationTree, arguments, state)
.map(fix -> describeMatch(annotationTree, fix))
.orElse(Description.NO_MATCH);
}
private static Optional<Fix> trySimplification(
AnnotationTree annotation, ImmutableList<ExpressionTree> arguments, VisitorState state) {
checkArgument(!arguments.isEmpty());
AnnotationDescriptor annotationDescriptor =
AnnotationDescriptor.from(getAnnotationFqcn(annotation));
if (containsAnyAttributeOf(annotation, annotationDescriptor.bannedFields)) {
return Optional.empty();
}
ImmutableMap<String, ExpressionTree> indexedAttributes =
Maps.uniqueIndex(
arguments,
expr ->
ASTHelpers.getSymbol(((AssignmentTree) expr).getVariable())
.getSimpleName()
.toString());
TimeUnit currentTimeUnit =
getTimeUnit(annotation, annotationDescriptor.timeUnitField, indexedAttributes);
ImmutableMap<String, Number> timeValues =
annotationDescriptor.timeFields.stream()
.map(field -> Map.entry(field, getValue(field, indexedAttributes)))
.filter(entry -> entry.getValue().isPresent())
.collect(toImmutableMap(Map.Entry::getKey, entry -> entry.getValue().orElseThrow()));
Map<String, TimeSimplifier.Simplification> simplifications =
Maps.transformValues(
Maps.filterValues(
Maps.transformEntries(
timeValues, (field, value) -> trySimplify(value, currentTimeUnit)),
Optional::isPresent),
Optional::orElseThrow);
// Some could not be simplified, and since the unit is shared, the others can't either.
if (simplifications.size() != timeValues.size()) {
return Optional.empty();
}
// The annotation is of the form `@Annotation(v)` or `@Annotation(value = v)`. For the former we
// must synthesize the entire annotation, but this is OK for the latter, too.
if (indexedAttributes.size() == 1 && simplifications.containsKey("value")) {
TimeSimplifier.Simplification simplification = simplifications.get("value");
return Optional.of(
getImplicitValueAttributeFix(
annotation,
simplification.value,
annotationDescriptor.timeUnitField,
simplification.timeUnit,
state));
}
// Since each might have a different simplification possible, check the common unit.
// Since we only get simplifications iff it's possible, and we check that all can be simplified,
// we don't need to check if this equals `currentTimeUnit`.
TimeUnit commonUnit =
findCommonUnit(
ImmutableSet.copyOf(
Maps.transformValues(simplifications, simplification -> simplification.timeUnit)
.values()));
return getExplicitAttributesFix(
annotation, simplifications, annotationDescriptor.timeUnitField, commonUnit, state);
}
private static boolean containsAnyAttributeOf(
AnnotationTree annotation, ImmutableSet<String> attributes) {
return annotation.getArguments().stream()
.map(
expr ->
expr.getKind() == Tree.Kind.ASSIGNMENT
? ASTHelpers.getSymbol(((AssignmentTree) expr).getVariable())
.getSimpleName()
.toString()
: "value")
.anyMatch(attributes::contains);
}
private static Fix getImplicitValueAttributeFix(
AnnotationTree annotation,
long newValue,
String timeUnitField,
TimeUnit newTimeUnit,
VisitorState state) {
String synthesizedAnnotation =
SourceCode.treeToString(annotation, state)
.replaceFirst(
"\\(.+\\)",
String.format("(value=%s, %s=%s)", newValue, timeUnitField, newTimeUnit.name()));
return SuggestedFix.builder()
.replace(annotation, synthesizedAnnotation)
.addStaticImport(TimeUnit.class.getName() + '.' + newTimeUnit.name())
.build();
}
private static Optional<Fix> getExplicitAttributesFix(
AnnotationTree annotation,
Map<String, TimeSimplifier.Simplification> simplifications,
String timeUnitField,
TimeUnit newUnit,
VisitorState state) {
return simplifications.entrySet().stream()
.map(
simplificationEntry ->
SuggestedFixes.updateAnnotationArgumentValues(
annotation, state, timeUnitField, ImmutableList.of(newUnit.name()))
.merge(
SuggestedFixes.updateAnnotationArgumentValues(
annotation,
state,
simplificationEntry.getKey(),
ImmutableList.of(
String.valueOf(simplificationEntry.getValue().toUnit(newUnit))))))
.reduce(SuggestedFix.Builder::merge)
.map(builder -> builder.addStaticImport(TimeUnit.class.getName() + '.' + newUnit.name()))
.map(SuggestedFix.Builder::build);
}
private static String getAnnotationFqcn(AnnotationTree annotation) {
return ASTHelpers.getSymbol(annotation).getQualifiedName().toString();
}
private static Optional<Number> getValue(
String field, ImmutableMap<String, ExpressionTree> indexedArguments) {
return Optional.ofNullable(indexedArguments.get(field))
.filter(AssignmentTree.class::isInstance)
.map(AssignmentTree.class::cast)
.map(AssignmentTree::getExpression)
.map(expr -> ASTHelpers.constValue(expr, Number.class));
}
private static TimeUnit getTimeUnit(
AnnotationTree annotation,
String field,
ImmutableMap<String, ExpressionTree> indexedArguments) {
VarSymbol symbol =
Optional.ofNullable(indexedArguments.get(field))
.map(
argumentTree ->
(VarSymbol)
ASTHelpers.getSymbol(((AssignmentTree) argumentTree).getExpression()))
.orElseGet(() -> getDefaultTimeUnit(annotation, field));
return TimeUnit.valueOf(symbol.getQualifiedName().toString());
}
private static VarSymbol getDefaultTimeUnit(AnnotationTree annotation, String argument) {
Scope scope = ASTHelpers.getSymbol(annotation).members();
MethodSymbol argumentSymbol =
(MethodSymbol)
Iterables.getOnlyElement(
ASTHelpers.scope(scope)
.getSymbols(symbol -> symbol.getQualifiedName().contentEquals(argument)));
return (VarSymbol)
requireNonNull(argumentSymbol.getDefaultValue(), "Default value missing").getValue();
}
private static AnnotationAttributeMatcher createAnnotationAttributeMatcher() {
ImmutableList<String> toMatch =
Arrays.stream(AnnotationDescriptor.values())
.flatMap(
annotation ->
annotation.timeFields.stream().map(field -> annotation.fqcn + '#' + field))
.collect(toImmutableList());
return AnnotationAttributeMatcher.create(Optional.of(toMatch), ImmutableList.of());
}
private static Optional<TimeSimplifier.Simplification> trySimplify(Number value, TimeUnit unit) {
checkArgument(
value instanceof Integer || value instanceof Long,
"Only time expressed as an integer or long can be simplified");
return TimeSimplifier.simplify(value.longValue(), unit);
}
private static TimeUnit findCommonUnit(ImmutableSet<TimeUnit> units) {
return ImmutableSortedSet.copyOf(units).first();
}
private enum AnnotationDescriptor {
JUNIT_TIMEOUT("org.junit.jupiter.api.Timeout", ImmutableSet.of("value"), "unit"),
SPRING_SCHEDULED(
"org.springframework.scheduling.annotation.Scheduled",
ImmutableSet.of("fixedDelay", "fixedRate", "initialDelay"),
"timeUnit",
ImmutableSet.of("fixedDelayString", "fixedRateString", "initialDelayString"));
/** The fully-qualified class name of the annotation to simplify. */
private final String fqcn;
/** The attributes containing a value of time. */
private final ImmutableSet<String> timeFields;
/** The attribute containing the time unit. */
private final String timeUnitField;
/** The set of attributes that cause the check to back off. */
private final ImmutableSet<String> bannedFields;
AnnotationDescriptor(String fqcn, ImmutableSet<String> timeFields, String timeUnitField) {
this(fqcn, timeFields, timeUnitField, ImmutableSet.of());
}
AnnotationDescriptor(
String fqcn,
ImmutableSet<String> timeFields,
String timeUnitField,
ImmutableSet<String> bannedFields) {
this.fqcn = fqcn;
this.timeFields = timeFields;
this.timeUnitField = timeUnitField;
this.bannedFields = bannedFields;
}
public static AnnotationDescriptor from(String fqcn) {
return Arrays.stream(values())
.filter(annotation -> annotation.fqcn.equals(fqcn))
.findFirst()
.orElseThrow(
() ->
new IllegalArgumentException(
String.format(
"Unknown enum constant: %s.%s",
AnnotationDescriptor.class.getName(), fqcn)));
}
}
/** Utility class to help simplify time expressions. */
private static final class TimeSimplifier {
private static final ImmutableSortedSet<TimeUnit> TIME_UNITS =
ImmutableSortedSet.copyOf(TimeUnit.values());
/**
* Returns a {@link Simplification} (iff possible) that describes how the {@code originalValue}
* and {@code originalUnit} can be simplified using a larger {@link TimeUnit}.
*/
static Optional<Simplification> simplify(long originalValue, TimeUnit originalUnit) {
return descendingLargerUnits(originalUnit).stream()
.flatMap(unit -> trySimplify(originalValue, originalUnit, unit))
.findFirst();
}
private static Stream<Simplification> trySimplify(
long originalValue, TimeUnit originalUnit, TimeUnit unit) {
long converted = unit.convert(originalValue, originalUnit);
// Check whether we lose any precision by checking whether we can convert back.
return originalValue == originalUnit.convert(converted, unit)
? Stream.of(new Simplification(converted, unit))
: Stream.empty();
}
/**
* Returns all time units that represent a larger amount of time than {@code unit}, in
* descending order.
*/
private static ImmutableSortedSet<TimeUnit> descendingLargerUnits(TimeUnit unit) {
return TIME_UNITS.tailSet(unit, /* inclusive= */ false).descendingSet();
}
/** Represents a simplification in terms of the new value and new unit. */
private static final class Simplification {
private final long value;
private final TimeUnit timeUnit;
Simplification(long value, TimeUnit timeUnit) {
this.value = value;
this.timeUnit = timeUnit;
}
/**
* Converts the value with the unit represented by this simplification to an equivalent value
* in the given {@code unit}.
*/
public long toUnit(TimeUnit unit) {
return unit.convert(value, timeUnit);
}
}
}
}

View File

@@ -6,7 +6,7 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.Splitter;
@@ -23,7 +23,7 @@ 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;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/** A {@link BugChecker} that flags SLF4J usages that are likely to be in error. */
// XXX: The special-casing of Throwable applies only to SLF4J 1.6.0+; see

View File

@@ -1,11 +1,12 @@
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.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
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.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.VerifyException;
@@ -17,16 +18,16 @@ 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;
import tech.picnic.errorprone.bugpatterns.util.AnnotationAttributeMatcher;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags {@code @RequestMapping} annotations that can be written more
@@ -78,25 +79,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 SourceCode.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(
@@ -107,8 +114,9 @@ public final class SpringMvcAnnotation extends BugChecker implements AnnotationT
.map(arg -> SourceCode.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();
}
}

View File

@@ -2,32 +2,14 @@ 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.BugPattern.StandardTags.SIMPLIFICATION;
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 static tech.picnic.errorprone.bugpatterns.util.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;
@@ -38,76 +20,55 @@ 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.
/**
* A {@link BugChecker} that flags methods and constants that can and should be statically imported.
*/
// 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}`
// - `ch.qos.logback.classic.Level.{DEBUG, ERROR, INFO, TRACE, WARN"}`
// XXX: Also introduce a check that 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(
summary = "Identifier should be statically imported",
link = BUG_PATTERNS_BASE_URL + "StaticImport",
linkType = CUSTOM,
severity = SUGGESTION,
tags = STYLE)
tags = SIMPLIFICATION)
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}.
* #STATIC_IMPORT_EXEMPTED_MEMBERS} or {@link #STATIC_IMPORT_EXEMPTED_IDENTIFIERS}.
*/
@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.google.common.base.Preconditions",
"com.google.common.base.Predicates",
"com.google.common.base.Verify",
"com.google.common.collect.MoreCollectors",
"com.google.errorprone.BugPattern.LinkType",
"com.google.errorprone.BugPattern.SeverityLevel",
"com.google.errorprone.BugPattern.StandardTags",
"com.google.errorprone.matchers.Matchers",
"com.google.errorprone.refaster.ImportPolicy",
"com.mongodb.client.model.Accumulators",
"com.mongodb.client.model.Aggregates",
"com.mongodb.client.model.Filters",
@@ -115,6 +76,12 @@ public final class StaticImport extends BugChecker implements MemberSelectTreeMa
"com.mongodb.client.model.Projections",
"com.mongodb.client.model.Sorts",
"com.mongodb.client.model.Updates",
"java.nio.charset.StandardCharsets",
"java.util.Collections",
"java.util.Comparator",
"java.util.Map.Entry",
"java.util.regex.Pattern",
"java.util.stream.Collectors",
"org.assertj.core.api.Assertions",
"org.assertj.core.api.InstanceOfAssertFactories",
"org.assertj.core.api.SoftAssertions",
@@ -135,59 +102,95 @@ public final class StaticImport extends BugChecker implements MemberSelectTreeMa
"org.springframework.http.MediaType",
"org.testng.Assert",
"reactor.function.TupleUtils",
"tech.picnic.errorprone.utils.MoreTypes");
"tech.picnic.errorprone.bugpatterns.util.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>
*/
/** Type members that should be statically imported. */
@VisibleForTesting
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(),
"com.google.common.collect.ImmutableListMultimap",
"flatteningToImmutableListMultimap",
"toImmutableListMultimap")
.put(ImmutableMap.class.getCanonicalName(), "toImmutableMap")
.put(ImmutableMultiset.class.getCanonicalName(), "toImmutableMultiset")
.put(ImmutableRangeSet.class.getCanonicalName(), "toImmutableRangeSet")
.put(ImmutableSet.class.getCanonicalName(), "toImmutableSet")
.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(
ImmutableSetMultimap.class.getCanonicalName(),
"com.google.common.collect.ImmutableSetMultimap",
"flatteningToImmutableSetMultimap",
"toImmutableSetMultimap")
.put(ImmutableSortedMap.class.getCanonicalName(), "toImmutableSortedMap")
.put(ImmutableSortedMultiset.class.getCanonicalName(), "toImmutableSortedMultiset")
.put(ImmutableSortedSet.class.getCanonicalName(), "toImmutableSortedSet")
.put(ImmutableTable.class.getCanonicalName(), "toImmutableTable")
.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.time.ZoneOffset", "UTC")
.put("java.util.function.Function", "identity")
.put("java.util.function.Predicate", "not")
.put("java.util.UUID", "randomUUID")
.put("org.junit.jupiter.params.provider.Arguments", "arguments")
.putAll(
Objects.class.getCanonicalName(),
"java.util.Objects",
"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")
.putAll("com.google.common.collect.Comparators", "emptiesFirst", "emptiesLast")
.build();
/**
* Type members that should never be statically imported.
*
* <p>Identifiers listed by {@link #STATIC_IMPORT_EXEMPTED_IDENTIFIERS} should be omitted from
* this collection.
*/
// 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.
@VisibleForTesting
static final ImmutableSetMultimap<String, String> STATIC_IMPORT_EXEMPTED_MEMBERS =
ImmutableSetMultimap.<String, String>builder()
.put("com.mongodb.client.model.Filters", "empty")
.putAll(
"java.util.Collections",
"addAll",
"copy",
"fill",
"list",
"max",
"min",
"nCopies",
"rotate",
"sort",
"swap")
.putAll("java.util.regex.Pattern", "compile", "matches", "quote")
.put("org.springframework.http.MediaType", "ALL")
.build();
/**
* Identifiers that should never be statically imported.
*
* <p>This should be a superset of the identifiers flagged by {@link
* com.google.errorprone.bugpatterns.BadImport}.
*/
@VisibleForTesting
static final ImmutableSet<String> STATIC_IMPORT_EXEMPTED_IDENTIFIERS =
ImmutableSet.of(
"builder",
"create",
"copyOf",
"from",
"getDefaultInstance",
"INSTANCE",
"newBuilder",
"of",
"valueOf");
/** Instantiates a new {@link StaticImport} instance. */
public StaticImport() {}
@@ -212,21 +215,26 @@ public final class StaticImport extends BugChecker implements MemberSelectTreeMa
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);
switch (parentTree.getKind()) {
case IMPORT:
case MEMBER_SELECT:
return false;
case METHOD_INVOCATION:
return ((MethodInvocationTree) parentTree).getTypeArguments().isEmpty();
default:
return true;
}
}
private static boolean isCandidate(MemberSelectTree tree) {
String identifier = tree.getIdentifier().toString();
if (NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS.contains(identifier)) {
if (STATIC_IMPORT_EXEMPTED_IDENTIFIERS.contains(identifier)) {
return false;
}
Type type = ASTHelpers.getType(tree.getExpression());
return type != null
&& !NON_STATIC_IMPORT_CANDIDATE_MEMBERS.containsEntry(type.toString(), identifier);
&& !STATIC_IMPORT_EXEMPTED_MEMBERS.containsEntry(type.toString(), identifier);
}
private static Optional<String> getCandidateSimpleName(StaticImportInfo importInfo) {

View File

@@ -4,7 +4,7 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.Splitter;
@@ -23,12 +23,12 @@ 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 com.sun.tools.javac.util.Convert;
import java.util.Formattable;
import java.util.Iterator;
import java.util.List;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.utils.SourceCode;
import org.jspecify.nullness.Nullable;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags {@link String#format(String, Object...)} invocations which can be
@@ -49,7 +49,7 @@ public final class StringJoin extends BugChecker implements MethodInvocationTree
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");
staticMethod().onClass(String.class.getName()).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);
@@ -150,7 +150,7 @@ public final class StringJoin extends BugChecker implements MethodInvocationTree
SuggestedFix.Builder fix =
SuggestedFix.builder()
.replace(tree.getMethodSelect(), "String.join")
.replace(arguments.next(), Constants.format(separator));
.replace(arguments.next(), String.format("\"%s\"", Convert.quote(separator)));
while (arguments.hasNext()) {
ExpressionTree argument = arguments.next();

View File

@@ -10,7 +10,7 @@ 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 static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
@@ -34,9 +34,7 @@ import java.time.ZonedDateTime;
@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""",
"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,
@@ -47,11 +45,11 @@ public final class TimeZoneUsage extends BugChecker implements MethodInvocationT
anyOf(
allOf(
instanceMethod()
.onDescendantOf(Clock.class.getCanonicalName())
.onDescendantOf(Clock.class.getName())
.namedAnyOf("getZone", "withZone"),
not(enclosingClass(isSubtypeOf(Clock.class)))),
staticMethod()
.onClass(Clock.class.getCanonicalName())
.onClass(Clock.class.getName())
.namedAnyOf(
"system",
"systemDefaultZone",
@@ -61,17 +59,14 @@ public final class TimeZoneUsage extends BugChecker implements MethodInvocationT
"tickSeconds"),
staticMethod()
.onClassAny(
LocalDate.class.getCanonicalName(),
LocalDateTime.class.getCanonicalName(),
LocalTime.class.getCanonicalName(),
OffsetDateTime.class.getCanonicalName(),
OffsetTime.class.getCanonicalName(),
ZonedDateTime.class.getCanonicalName())
LocalDate.class.getName(),
LocalDateTime.class.getName(),
LocalTime.class.getName(),
OffsetDateTime.class.getName(),
OffsetTime.class.getName(),
ZonedDateTime.class.getName())
.named("now"),
staticMethod()
.onClassAny(Instant.class.getCanonicalName())
.named("now")
.withNoParameters());
staticMethod().onClassAny(Instant.class.getName()).named("now").withNoParameters());
/** Instantiates a new {@link TimeZoneUsage} instance. */
public TimeZoneUsage() {}

View File

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

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.utils;
package tech.picnic.errorprone.bugpatterns.util;
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;
@@ -102,7 +103,7 @@ public final class AnnotationAttributeMatcher implements Serializable {
* @param tree The annotation AST node to be inspected.
* @return Any matching annotation arguments.
*/
public Stream<ExpressionTree> extractMatchingArguments(AnnotationTree tree) {
public Stream<? extends ExpressionTree> extractMatchingArguments(AnnotationTree tree) {
Type type = ASTHelpers.getType(tree.getAnnotationType());
if (type == null) {
return Stream.empty();
@@ -110,13 +111,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";
}

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.utils;
package tech.picnic.errorprone.bugpatterns.util;
/** Utility class providing documentation-related code. */
public final class Documentation {

View File

@@ -1,23 +1,10 @@
package tech.picnic.errorprone.utils;
package tech.picnic.errorprone.bugpatterns.util;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
/** Utility class that can be used to identify reserved keywords of the Java language. */
// XXX: This class is no longer only about keywords. Consider changing its name and class-level
// documentation.
public final class JavaKeywords {
/**
* Enumeration of boolean and null literals.
*
* @see <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-3.html#jls-3.10.3">JDK 17
* JLS section 3.10.3: Boolean Literals</a>
* @see <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-3.html#jls-3.10.8">JDK 17
* JLS section 3.10.8: The Null Literal</a>
*/
private static final ImmutableSet<String> BOOLEAN_AND_NULL_LITERALS =
ImmutableSet.of("true", "false", "null");
/**
* List of all reserved keywords in the Java language.
*
@@ -109,23 +96,6 @@ public final class JavaKeywords {
private JavaKeywords() {}
/**
* Tells whether the given string is a valid identifier in the Java language.
*
* @param str The string of interest.
* @return {@code true} if the given string is a valid identifier in the Java language.
* @see <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-3.html#jls-3.8">JDK 17 JLS
* section 3.8: Identifiers</a>
*/
@SuppressWarnings("java:S1067" /* Chaining conjunctions like this does not impact readability. */)
public static boolean isValidIdentifier(String str) {
return !str.isEmpty()
&& !isReservedKeyword(str)
&& !BOOLEAN_AND_NULL_LITERALS.contains(str)
&& Character.isJavaIdentifierStart(str.codePointAt(0))
&& str.codePoints().skip(1).allMatch(Character::isUnicodeIdentifierPart);
}
/**
* Tells whether the given string is a reserved keyword in the Java language.
*

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.utils;
package tech.picnic.errorprone.bugpatterns.util;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
@@ -18,10 +18,6 @@ import java.util.regex.Pattern;
public 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#(,)]+)*)?)\\)");

View File

@@ -1,4 +1,4 @@
package tech.picnic.errorprone.utils;
package tech.picnic.errorprone.bugpatterns.util;
import static java.util.stream.Collectors.toCollection;

View File

@@ -0,0 +1,27 @@
package tech.picnic.errorprone.bugpatterns.util;
import com.google.errorprone.VisitorState;
import com.sun.source.tree.Tree;
/**
* A collection of Error Prone utility methods for dealing with the source code representation of
* AST nodes.
*/
// XXX: Can we locate this code in a better place? Maybe contribute it upstream?
public final class SourceCode {
private SourceCode() {}
/**
* Returns a string representation of the given {@link Tree}, preferring the original source code
* (if available) over its prettified representation.
*
* @param tree The AST node of interest.
* @param state A {@link VisitorState} describing the context in which the given {@link Tree} is
* found.
* @return A non-{@code null} string.
*/
public static String treeToString(Tree tree, VisitorState state) {
String src = state.getSourceForNode(tree);
return src != null ? src : tree.toString();
}
}

View File

@@ -1,18 +1,11 @@
package tech.picnic.errorprone.utils;
package tech.picnic.errorprone.bugpatterns.util;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.suppliers.Supplier;
import com.sun.tools.javac.code.ClassFinder;
import com.sun.tools.javac.code.Source;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.CompletionFailure;
import com.sun.tools.javac.code.Symbol.ModuleSymbol;
import com.sun.tools.javac.code.Symtab;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.util.Name;
import javax.lang.model.element.Modifier;
/**
* Utility class that helps decide whether it is appropriate to introduce references to (well-known)
@@ -35,7 +28,14 @@ public enum ThirdPartyLibrary {
*
* @see <a href="https://github.com/google/guava">Guava on GitHub</a>
*/
GUAVA(ImmutableList.class.getCanonicalName()),
GUAVA("com.google.common.collect.ImmutableList"),
/**
* New Relic's Java agent API.
*
* @see <a href="https://github.com/newrelic/newrelic-java-agent/tree/main/newrelic-api">New Relic
* Java agent API on GitHub</a>
*/
NEW_RELIC_AGENT_API("com.newrelic.api.agent.Agent"),
/**
* VMWare's Project Reactor.
*
@@ -70,15 +70,8 @@ public enum ThirdPartyLibrary {
return canUse.get(state);
}
/**
* Tells whether the given fully qualified type is available on the current class path.
*
* @param typeName The type of interest.
* @param state The context under consideration.
* @return {@code true} iff it is okay to assume or create a dependency on this type.
*/
public static boolean canIntroduceUsage(String typeName, VisitorState state) {
return shouldIgnoreClasspath(state) || isKnownClass(typeName, state);
private static boolean canIntroduceUsage(String className, VisitorState state) {
return shouldIgnoreClasspath(state) || isKnownClass(className, state);
}
/**
@@ -87,38 +80,21 @@ public enum ThirdPartyLibrary {
* <p>The {@link VisitorState}'s symbol table is consulted first. If the type has not yet been
* loaded, then an attempt is made to do so.
*/
private static boolean isKnownClass(String typeName, VisitorState state) {
return isPublicClassInSymbolTable(typeName, state) || canLoadPublicClass(typeName, state);
private static boolean isKnownClass(String className, VisitorState state) {
return state.getTypeFromString(className) != null || canLoadClass(className, state);
}
private static boolean isPublicClassInSymbolTable(String typeName, VisitorState state) {
Type type = state.getTypeFromString(typeName);
return type != null && isPublic(type.tsym);
}
private static boolean canLoadPublicClass(String typeName, VisitorState state) {
private static boolean canLoadClass(String className, VisitorState state) {
ClassFinder classFinder = ClassFinder.instance(state.context);
Symtab symtab = state.getSymtab();
// XXX: Drop support for targeting Java 8 once the oldest supported JDK drops such support.
ModuleSymbol module =
Source.instance(state.context).compareTo(Source.JDK9) < 0
? symtab.noModule
: symtab.unnamedModule;
Name binaryName = state.binaryNameFromClassname(typeName);
Name binaryName = state.binaryNameFromClassname(className);
try {
return isPublic(classFinder.loadClass(module, binaryName));
} catch (
@SuppressWarnings("java:S1166" /* Not exceptional. */)
CompletionFailure e) {
classFinder.loadClass(state.getSymtab().unnamedModule, binaryName);
return true;
} catch (CompletionFailure e) {
return false;
}
}
// XXX: Once we target JDK 14+, drop this method in favour of `Symbol#isPublic()`.
private static boolean isPublic(Symbol symbol) {
return symbol.getModifiers().contains(Modifier.PUBLIC);
}
private static boolean shouldIgnoreClasspath(VisitorState state) {
return state
.errorProneOptions()

View File

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

View File

@@ -1,115 +1,24 @@
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) {
AbstractMapAssert<?, ?, K, V> before(AbstractMapAssert<?, ?, K, V> mapAssert, Map<K, V> map) {
return mapAssert.isEqualTo(map);
}
@AfterTemplate
AbstractMapAssert<?, ?, K, V> after(
AbstractMapAssert<?, ?, K, V> mapAssert, Map<? extends K, ? extends V> map) {
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert, Map<K, V> map) {
return mapAssert.containsExactlyInAnyOrderEntriesOf(map);
}
}
@@ -125,97 +34,4 @@ final class AssertJMapRules {
return mapAssert.containsExactlyEntriesOf(ImmutableMap.of(key, value));
}
}
static final class AssertThatMapHasSize<K, V> {
@BeforeTemplate
AbstractAssert<?, ?> before(Map<K, V> map, int length) {
return Refaster.anyOf(
assertThat(map.size()).isEqualTo(length),
assertThat(Refaster.anyOf(map.keySet(), map.values(), map.entrySet())).hasSize(length));
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, int length) {
return assertThat(map).hasSize(length);
}
}
static final class AbstractMapAssertHasSameSizeAs<K, V> {
@BeforeTemplate
AbstractMapAssert<?, ?, K, V> before(AbstractMapAssert<?, ?, K, V> mapAssert, Map<?, ?> 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);
}
}
}

View File

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

View File

@@ -23,8 +23,6 @@ final class AssertJPrimitiveRules {
}
@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());
@@ -45,8 +43,6 @@ final class AssertJPrimitiveRules {
}
@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());

View File

@@ -3,35 +3,48 @@ package tech.picnic.errorprone.refasterrules;
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedMultiset;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multiset;
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.NotMatches;
import com.google.errorprone.refaster.annotation.Repeated;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Stream;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.AbstractBooleanAssert;
import org.assertj.core.api.AbstractCollectionAssert;
import org.assertj.core.api.AbstractComparableAssert;
import org.assertj.core.api.AbstractDoubleAssert;
import org.assertj.core.api.AbstractIntegerAssert;
import org.assertj.core.api.AbstractLongAssert;
import org.assertj.core.api.AbstractMapAssert;
import org.assertj.core.api.IterableAssert;
import org.assertj.core.api.ListAssert;
import org.assertj.core.api.MapAssert;
@@ -42,7 +55,6 @@ import org.assertj.core.api.OptionalIntAssert;
import org.assertj.core.api.OptionalLongAssert;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
import tech.picnic.errorprone.refaster.matchers.IsArray;
import tech.picnic.errorprone.refaster.matchers.IsEmpty;
/** Refaster rules related to AssertJ expressions and statements. */
// XXX: Most `AbstractIntegerAssert` rules can also be applied for other primitive types. Generate
@@ -177,16 +189,63 @@ final class AssertJRules {
static final class AssertThatObjectEnumerableIsEmpty<E> {
@BeforeTemplate
@SuppressWarnings("unchecked")
void before(
ObjectEnumerableAssert<?, E> enumAssert,
@Matches(IsEmpty.class) Iterable<? extends E> wellTypedIterable,
@Matches(IsEmpty.class) Iterable<?> arbitrarilyTypedIterable) {
void before(ObjectEnumerableAssert<?, E> enumAssert) {
Refaster.anyOf(
enumAssert.containsExactlyElementsOf(wellTypedIterable),
enumAssert.containsExactlyInAnyOrderElementsOf(wellTypedIterable),
enumAssert.hasSameElementsAs(wellTypedIterable),
enumAssert.hasSameSizeAs(arbitrarilyTypedIterable),
enumAssert.isSubsetOf(wellTypedIterable),
enumAssert.containsExactlyElementsOf(
Refaster.anyOf(
ImmutableList.of(),
new ArrayList<>(),
ImmutableSet.of(),
new HashSet<>(),
new LinkedHashSet<>(),
ImmutableSortedSet.of(),
new TreeSet<>(),
ImmutableMultiset.of(),
ImmutableSortedMultiset.of())),
enumAssert.containsExactlyInAnyOrderElementsOf(
Refaster.anyOf(
ImmutableList.of(),
new ArrayList<>(),
ImmutableSet.of(),
new HashSet<>(),
new LinkedHashSet<>(),
ImmutableSortedSet.of(),
new TreeSet<>(),
ImmutableMultiset.of(),
ImmutableSortedMultiset.of())),
enumAssert.hasSameElementsAs(
Refaster.anyOf(
ImmutableList.of(),
new ArrayList<>(),
ImmutableSet.of(),
new HashSet<>(),
new LinkedHashSet<>(),
ImmutableSortedSet.of(),
new TreeSet<>(),
ImmutableMultiset.of(),
ImmutableSortedMultiset.of())),
enumAssert.hasSameSizeAs(
Refaster.anyOf(
ImmutableList.of(),
new ArrayList<>(),
ImmutableSet.of(),
new HashSet<>(),
new LinkedHashSet<>(),
ImmutableSortedSet.of(),
new TreeSet<>(),
ImmutableMultiset.of(),
ImmutableSortedMultiset.of())),
enumAssert.isSubsetOf(
Refaster.anyOf(
ImmutableList.of(),
new ArrayList<>(),
ImmutableSet.of(),
new HashSet<>(),
new LinkedHashSet<>(),
ImmutableSortedSet.of(),
new TreeSet<>(),
ImmutableMultiset.of(),
ImmutableSortedMultiset.of())),
enumAssert.containsExactly(),
enumAssert.containsExactlyInAnyOrder(),
enumAssert.containsOnly(),
@@ -507,8 +566,173 @@ final class AssertJRules {
// Map
//
// XXX: To match in all cases there'll need to be a `@BeforeTemplate` variant for each
// `assertThat` overload. Consider defining a `BugChecker` instead.
static final class AssertThatMapIsEmpty<K, V> {
@BeforeTemplate
@SuppressWarnings("unchecked")
void before(AbstractMapAssert<?, ?, K, V> mapAssert) {
Refaster.anyOf(
mapAssert.containsExactlyEntriesOf(
Refaster.anyOf(
ImmutableMap.of(),
ImmutableBiMap.of(),
ImmutableSortedMap.of(),
new HashMap<>(),
new LinkedHashMap<>(),
new TreeMap<>())),
mapAssert.hasSameSizeAs(
Refaster.anyOf(
ImmutableMap.of(),
ImmutableBiMap.of(),
ImmutableSortedMap.of(),
new HashMap<>(),
new LinkedHashMap<>(),
new TreeMap<>())),
mapAssert.isEqualTo(
Refaster.anyOf(
ImmutableMap.of(),
ImmutableBiMap.of(),
ImmutableSortedMap.of(),
new HashMap<>(),
new LinkedHashMap<>(),
new TreeMap<>())),
mapAssert.containsOnlyKeys(
Refaster.anyOf(
ImmutableList.of(),
new ArrayList<>(),
ImmutableSet.of(),
new HashSet<>(),
new LinkedHashSet<>(),
ImmutableSortedSet.of(),
new TreeSet<>(),
ImmutableMultiset.of(),
ImmutableSortedMultiset.of())),
mapAssert.containsExactly(),
mapAssert.containsOnly(),
mapAssert.containsOnlyKeys());
}
@AfterTemplate
void after(AbstractMapAssert<?, ?, K, V> mapAssert) {
mapAssert.isEmpty();
}
}
// XXX: Find a better name.
static final class AssertThatMapIsEmpty2<K, V> {
@BeforeTemplate
void before(Map<K, V> map) {
Refaster.anyOf(
assertThat(map).hasSize(0),
assertThat(map.isEmpty()).isTrue(),
assertThat(map.size()).isEqualTo(0L),
assertThat(map.size()).isNotPositive());
}
@BeforeTemplate
void before2(Map<K, V> map) {
assertThat(Refaster.anyOf(map.keySet(), map.values())).isEmpty();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Map<K, V> map) {
assertThat(map).isEmpty();
}
}
static final class AssertThatMapIsNotEmpty<K, V> {
@BeforeTemplate
AbstractMapAssert<?, ?, K, V> before(AbstractMapAssert<?, ?, K, V> mapAssert) {
return mapAssert.isNotEqualTo(
Refaster.anyOf(
ImmutableMap.of(),
ImmutableBiMap.of(),
ImmutableSortedMap.of(),
new HashMap<>(),
new LinkedHashMap<>(),
new TreeMap<>()));
}
@AfterTemplate
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert) {
return mapAssert.isNotEmpty();
}
}
// XXX: Find a better name.
static final class AssertThatMapIsNotEmpty2<K, V> {
@BeforeTemplate
AbstractAssert<?, ?> before(Map<K, V> map) {
return Refaster.anyOf(
assertThat(map.isEmpty()).isFalse(),
assertThat(map.size()).isNotEqualTo(0),
assertThat(map.size()).isPositive(),
assertThat(Refaster.anyOf(map.keySet(), map.values())).isNotEmpty());
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map) {
return assertThat(map).isNotEmpty();
}
}
static final class AssertThatMapHasSize<K, V> {
@BeforeTemplate
AbstractAssert<?, ?> before(Map<K, V> map, int length) {
return Refaster.anyOf(
assertThat(map.size()).isEqualTo(length),
assertThat(Refaster.anyOf(map.keySet(), map.values())).hasSize(length));
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, int length) {
return assertThat(map).hasSize(length);
}
}
static final class AssertThatMapsHaveSameSize<K, V> {
@BeforeTemplate
AbstractAssert<?, ?> before(Map<K, V> map1, Map<K, V> map2) {
return assertThat(map1)
.hasSize(Refaster.anyOf(map2.size(), map2.keySet().size(), map2.values().size()));
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map1, Map<K, V> map2) {
return assertThat(map1).hasSameSizeAs(map2);
}
}
// XXX: Should also add a rule (elsewhere) to simplify `map.keySet().contains(key)`.
static final class AssertThatMapContainsKey<K, V> {
@BeforeTemplate
AbstractBooleanAssert<?> before(Map<K, V> map, K key) {
return assertThat(map.containsKey(key)).isTrue();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, K key) {
return assertThat(map).containsKey(key);
}
}
static final class AssertThatMapDoesNotContainKey<K, V> {
@BeforeTemplate
AbstractBooleanAssert<?> before(Map<K, V> map, K key) {
return assertThat(map.containsKey(key)).isFalse();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, K key) {
return assertThat(map).doesNotContainKey(key);
}
}
static final class AssertThatMapContainsEntry<K, V> {
@BeforeTemplate
ObjectAssert<?> before(Map<K, V> map, K key, V value) {
@@ -530,13 +754,13 @@ final class AssertJRules {
static final class AssertThatStreamContainsAnyElementsOf<S, T extends S, U extends T> {
@BeforeTemplate
IterableAssert<T> before(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).containsAnyElementsOf(iterable);
}
@BeforeTemplate
ListAssert<T> before2(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).containsAnyElementsOf(iterable);
}
@@ -551,13 +775,13 @@ final class AssertJRules {
static final class AssertThatStreamContainsAnyOf<S, T extends S, U extends T> {
@BeforeTemplate
IterableAssert<T> before(
Stream<S> stream, U[] array, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, U[] array) {
return assertThat(stream.collect(collector)).containsAnyOf(array);
}
@BeforeTemplate
ListAssert<T> before2(
Stream<S> stream, U[] array, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, U[] array) {
return assertThat(stream.collect(collector)).containsAnyOf(array);
}
@@ -573,14 +797,14 @@ final class AssertJRules {
@BeforeTemplate
@SuppressWarnings("AssertThatStreamContainsAnyOf" /* Varargs converted to array. */)
IterableAssert<T> before(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector)).containsAnyOf(Refaster.asVarargs(elements));
}
@BeforeTemplate
@SuppressWarnings("AssertThatStreamContainsAnyOf" /* Varargs converted to array. */)
ListAssert<T> before2(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector)).containsAnyOf(Refaster.asVarargs(elements));
}
@@ -596,13 +820,13 @@ final class AssertJRules {
static final class AssertThatStreamContainsAll<S, T extends S, U extends T> {
@BeforeTemplate
IterableAssert<T> before(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).containsAll(iterable);
}
@BeforeTemplate
ListAssert<T> before2(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).containsAll(iterable);
}
@@ -617,13 +841,13 @@ final class AssertJRules {
static final class AssertThatStreamContains<S, T extends S, U extends T> {
@BeforeTemplate
IterableAssert<T> before(
Stream<S> stream, U[] array, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, U[] array) {
return assertThat(stream.collect(collector)).contains(array);
}
@BeforeTemplate
ListAssert<T> before2(
Stream<S> stream, U[] array, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, U[] array) {
return assertThat(stream.collect(collector)).contains(array);
}
@@ -639,14 +863,14 @@ final class AssertJRules {
@BeforeTemplate
@SuppressWarnings("AssertThatStreamContains" /* Varargs converted to array. */)
IterableAssert<T> before(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector)).contains(Refaster.asVarargs(elements));
}
@BeforeTemplate
@SuppressWarnings("AssertThatStreamContains" /* Varargs converted to array. */)
ListAssert<T> before2(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector)).contains(Refaster.asVarargs(elements));
}
@@ -661,7 +885,7 @@ final class AssertJRules {
static final class AssertThatStreamContainsExactlyElementsOf<S, T extends S, U extends T> {
@BeforeTemplate
ListAssert<T> before(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).containsExactlyElementsOf(iterable);
}
@@ -676,7 +900,7 @@ final class AssertJRules {
static final class AssertThatStreamContainsExactly<S, T extends S, U extends T> {
@BeforeTemplate
ListAssert<T> before(
Stream<S> stream, U[] array, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, U[] array) {
return assertThat(stream.collect(collector)).containsExactly(array);
}
@@ -692,7 +916,7 @@ final class AssertJRules {
@BeforeTemplate
@SuppressWarnings("AssertThatStreamContainsExactly" /* Varargs converted to array. */)
ListAssert<T> before(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector)).containsExactly(Refaster.asVarargs(elements));
}
@@ -708,13 +932,13 @@ final class AssertJRules {
S, T extends S, U extends T> {
@BeforeTemplate
ListAssert<T> before(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).containsExactlyInAnyOrderElementsOf(iterable);
}
@BeforeTemplate
AbstractCollectionAssert<?, ?, T, ?> before2(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends Multiset<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Multiset<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).containsExactlyInAnyOrderElementsOf(iterable);
}
@@ -729,13 +953,13 @@ final class AssertJRules {
static final class AssertThatStreamContainsExactlyInAnyOrder<S, T extends S, U extends T> {
@BeforeTemplate
ListAssert<T> before(
Stream<S> stream, U[] array, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, U[] array) {
return assertThat(stream.collect(collector)).containsExactlyInAnyOrder(array);
}
@BeforeTemplate
AbstractCollectionAssert<?, ?, T, ?> before2(
Stream<S> stream, U[] array, Collector<S, ?, ? extends Multiset<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Multiset<T>> collector, U[] array) {
return assertThat(stream.collect(collector)).containsExactlyInAnyOrder(array);
}
@@ -751,7 +975,7 @@ final class AssertJRules {
@BeforeTemplate
@SuppressWarnings("AssertThatStreamContainsExactlyInAnyOrder" /* Varargs converted to array. */)
ListAssert<T> before(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector))
.containsExactlyInAnyOrder(Refaster.asVarargs(elements));
}
@@ -759,7 +983,7 @@ final class AssertJRules {
@BeforeTemplate
@SuppressWarnings("AssertThatStreamContainsExactlyInAnyOrder" /* Varargs converted to array. */)
AbstractCollectionAssert<?, ?, T, ?> before2(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends Multiset<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Multiset<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector))
.containsExactlyInAnyOrder(Refaster.asVarargs(elements));
}
@@ -776,13 +1000,13 @@ final class AssertJRules {
static final class AssertThatStreamContainsSequence<S, T extends S, U extends T> {
@BeforeTemplate
ListAssert<T> before(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).containsSequence(iterable);
}
@BeforeTemplate
ListAssert<T> before(
Stream<S> stream, U[] iterable, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, U[] iterable) {
return assertThat(stream.collect(collector)).containsSequence(iterable);
}
@@ -798,7 +1022,7 @@ final class AssertJRules {
@BeforeTemplate
@SuppressWarnings("AssertThatStreamContainsSequence" /* Varargs converted to array. */)
ListAssert<T> before(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector)).containsSequence(Refaster.asVarargs(elements));
}
@@ -814,13 +1038,13 @@ final class AssertJRules {
static final class AssertThatStreamContainsSubsequence<S, T extends S, U extends T> {
@BeforeTemplate
ListAssert<T> before(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).containsSubsequence(iterable);
}
@BeforeTemplate
ListAssert<T> before(
Stream<S> stream, U[] iterable, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, U[] iterable) {
return assertThat(stream.collect(collector)).containsSubsequence(iterable);
}
@@ -836,7 +1060,7 @@ final class AssertJRules {
@BeforeTemplate
@SuppressWarnings("AssertThatStreamContainsSubsequence" /* Varargs converted to array. */)
ListAssert<T> before(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector))
.containsSubsequence(Refaster.asVarargs(elements));
}
@@ -853,13 +1077,13 @@ final class AssertJRules {
static final class AssertThatStreamDoesNotContainAnyElementsOf<S, T extends S, U extends T> {
@BeforeTemplate
IterableAssert<T> before(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).doesNotContainAnyElementsOf(iterable);
}
@BeforeTemplate
ListAssert<T> before2(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).doesNotContainAnyElementsOf(iterable);
}
@@ -874,13 +1098,13 @@ final class AssertJRules {
static final class AssertThatStreamDoesNotContain<S, T extends S, U extends T> {
@BeforeTemplate
IterableAssert<T> before(
Stream<S> stream, U[] array, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, U[] array) {
return assertThat(stream.collect(collector)).doesNotContain(array);
}
@BeforeTemplate
ListAssert<T> before2(
Stream<S> stream, U[] array, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, U[] array) {
return assertThat(stream.collect(collector)).doesNotContain(array);
}
@@ -896,14 +1120,14 @@ final class AssertJRules {
@BeforeTemplate
@SuppressWarnings("AssertThatStreamDoesNotContain" /* Varargs converted to array. */)
IterableAssert<T> before(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector)).doesNotContain(Refaster.asVarargs(elements));
}
@BeforeTemplate
@SuppressWarnings("AssertThatStreamDoesNotContain" /* Varargs converted to array. */)
ListAssert<T> before2(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector)).doesNotContain(Refaster.asVarargs(elements));
}
@@ -918,13 +1142,13 @@ final class AssertJRules {
static final class AssertThatStreamDoesNotContainSequence<S, T extends S, U extends T> {
@BeforeTemplate
ListAssert<T> before(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).doesNotContainSequence(iterable);
}
@BeforeTemplate
ListAssert<T> before(
Stream<S> stream, U[] iterable, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, U[] iterable) {
return assertThat(stream.collect(collector)).doesNotContainSequence(iterable);
}
@@ -940,7 +1164,7 @@ final class AssertJRules {
@BeforeTemplate
@SuppressWarnings("AssertThatStreamDoesNotContainSequence" /* Varargs converted to array. */)
ListAssert<T> before(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector))
.doesNotContainSequence(Refaster.asVarargs(elements));
}
@@ -957,13 +1181,13 @@ final class AssertJRules {
static final class AssertThatStreamHasSameElementsAs<S, T extends S, U extends T> {
@BeforeTemplate
IterableAssert<T> before(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).hasSameElementsAs(iterable);
}
@BeforeTemplate
ListAssert<T> before2(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).hasSameElementsAs(iterable);
}
@@ -978,13 +1202,13 @@ final class AssertJRules {
static final class AssertThatStreamContainsOnly<S, T extends S, U extends T> {
@BeforeTemplate
IterableAssert<T> before(
Stream<S> stream, U[] array, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, U[] array) {
return assertThat(stream.collect(collector)).containsOnly(array);
}
@BeforeTemplate
ListAssert<T> before2(
Stream<S> stream, U[] array, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, U[] array) {
return assertThat(stream.collect(collector)).containsOnly(array);
}
@@ -1000,14 +1224,14 @@ final class AssertJRules {
@BeforeTemplate
@SuppressWarnings("AssertThatStreamContainsOnly" /* Varargs converted to array. */)
IterableAssert<T> before(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector)).containsOnly(Refaster.asVarargs(elements));
}
@BeforeTemplate
@SuppressWarnings("AssertThatStreamContainsOnly" /* Varargs converted to array. */)
ListAssert<T> before2(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector)).containsOnly(Refaster.asVarargs(elements));
}
@@ -1022,25 +1246,25 @@ final class AssertJRules {
static final class AssertThatStreamIsSubsetOf<S, T extends S, U extends T> {
@BeforeTemplate
IterableAssert<T> before(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).isSubsetOf(iterable);
}
@BeforeTemplate
IterableAssert<T> before(
Stream<S> stream, U[] iterable, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, U[] iterable) {
return assertThat(stream.collect(collector)).isSubsetOf(iterable);
}
@BeforeTemplate
ListAssert<T> before2(
Stream<S> stream, Iterable<U> iterable, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, Iterable<U> iterable) {
return assertThat(stream.collect(collector)).isSubsetOf(iterable);
}
@BeforeTemplate
ListAssert<T> before2(
Stream<S> stream, U[] iterable, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, U[] iterable) {
return assertThat(stream.collect(collector)).isSubsetOf(iterable);
}
@@ -1056,14 +1280,14 @@ final class AssertJRules {
@BeforeTemplate
@SuppressWarnings("AssertThatStreamIsSubsetOf" /* Varargs converted to array. */)
IterableAssert<T> before(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends Iterable<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends Iterable<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector)).isSubsetOf(Refaster.asVarargs(elements));
}
@BeforeTemplate
@SuppressWarnings("AssertThatStreamIsSubsetOf" /* Varargs converted to array. */)
ListAssert<T> before2(
Stream<S> stream, @Repeated U elements, Collector<S, ?, ? extends List<T>> collector) {
Stream<S> stream, Collector<S, ?, ? extends List<T>> collector, @Repeated U elements) {
return assertThat(stream.collect(collector)).isSubsetOf(Refaster.asVarargs(elements));
}
@@ -1973,8 +2197,8 @@ final class AssertJRules {
////////////////////////////////////////////////////////////////////////////
// Above: Generated code.
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////
// Organize the code below.
// XXX: Do the "single Comparable" match shown below.

View File

@@ -1,16 +1,11 @@
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;
@@ -94,30 +89,4 @@ final class AssertJStringRules {
return assertThat(string).doesNotMatch(regex);
}
}
static final class AssertThatPathContent {
@BeforeTemplate
AbstractStringAssert<?> before(Path path, Charset charset) throws IOException {
return assertThat(Files.readString(path, charset));
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractStringAssert<?> after(Path path, Charset charset) {
return assertThat(path).content(charset);
}
}
static final class AssertThatPathContentUtf8 {
@BeforeTemplate
AbstractStringAssert<?> before(Path path) throws IOException {
return assertThat(Files.readString(path));
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractStringAssert<?> after(Path path) {
return assertThat(path).content(UTF_8);
}
}
}

View File

@@ -47,7 +47,7 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByIllegalArgumentExceptionHasMessage {
@BeforeTemplate
@SuppressWarnings(
"AssertThatThrownByIllegalArgumentException" /* This is a more specific template. */)
"AssertThatThrownByIllegalArgumentException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
return assertThatIllegalArgumentException().isThrownBy(throwingCallable).withMessage(message);
}
@@ -64,7 +64,7 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByIllegalArgumentExceptionHasMessageParameters {
@BeforeTemplate
@SuppressWarnings(
"AssertThatThrownByIllegalArgumentException" /* This is a more specific template. */)
"AssertThatThrownByIllegalArgumentException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(
ThrowingCallable throwingCallable, String message, @Repeated Object parameters) {
return assertThatIllegalArgumentException()
@@ -85,7 +85,7 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByIllegalArgumentExceptionHasMessageStartingWith {
@BeforeTemplate
@SuppressWarnings(
"AssertThatThrownByIllegalArgumentException" /* This is a more specific template. */)
"AssertThatThrownByIllegalArgumentException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
return assertThatIllegalArgumentException()
.isThrownBy(throwingCallable)
@@ -104,7 +104,7 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByIllegalArgumentExceptionHasMessageContaining {
@BeforeTemplate
@SuppressWarnings(
"AssertThatThrownByIllegalArgumentException" /* This is a more specific template. */)
"AssertThatThrownByIllegalArgumentException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
return assertThatIllegalArgumentException()
.isThrownBy(throwingCallable)
@@ -123,7 +123,7 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByIllegalArgumentExceptionHasMessageNotContainingAny {
@BeforeTemplate
@SuppressWarnings(
"AssertThatThrownByIllegalArgumentException" /* This is a more specific template. */)
"AssertThatThrownByIllegalArgumentException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(
ThrowingCallable throwingCallable, @Repeated CharSequence values) {
return assertThatIllegalArgumentException()
@@ -157,7 +157,7 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByIllegalStateExceptionHasMessage {
@BeforeTemplate
@SuppressWarnings(
"AssertThatThrownByIllegalStateException" /* This is a more specific template. */)
"AssertThatThrownByIllegalStateException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
return assertThatIllegalStateException().isThrownBy(throwingCallable).withMessage(message);
}
@@ -174,7 +174,7 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByIllegalStateExceptionHasMessageParameters {
@BeforeTemplate
@SuppressWarnings(
"AssertThatThrownByIllegalStateException" /* This is a more specific template. */)
"AssertThatThrownByIllegalStateException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(
ThrowingCallable throwingCallable, String message, @Repeated Object parameters) {
return assertThatIllegalStateException()
@@ -195,7 +195,7 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByIllegalStateExceptionHasMessageStartingWith {
@BeforeTemplate
@SuppressWarnings(
"AssertThatThrownByIllegalStateException" /* This is a more specific template. */)
"AssertThatThrownByIllegalStateException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
return assertThatIllegalStateException()
.isThrownBy(throwingCallable)
@@ -214,7 +214,7 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByIllegalStateExceptionHasMessageContaining {
@BeforeTemplate
@SuppressWarnings(
"AssertThatThrownByIllegalStateException" /* This is a more specific template. */)
"AssertThatThrownByIllegalStateException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
return assertThatIllegalStateException()
.isThrownBy(throwingCallable)
@@ -233,7 +233,7 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByIllegalStateExceptionHasMessageNotContaining {
@BeforeTemplate
@SuppressWarnings(
"AssertThatThrownByIllegalStateException" /* This is a more specific template. */)
"AssertThatThrownByIllegalStateException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
return assertThatIllegalStateException()
.isThrownBy(throwingCallable)
@@ -265,7 +265,7 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByNullPointerExceptionHasMessage {
@BeforeTemplate
@SuppressWarnings(
"AssertThatThrownByNullPointerException" /* This is a more specific template. */)
"AssertThatThrownByNullPointerException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
return assertThatNullPointerException().isThrownBy(throwingCallable).withMessage(message);
}
@@ -282,7 +282,7 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByNullPointerExceptionHasMessageParameters {
@BeforeTemplate
@SuppressWarnings(
"AssertThatThrownByNullPointerException" /* This is a more specific template. */)
"AssertThatThrownByNullPointerException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(
ThrowingCallable throwingCallable, String message, @Repeated Object parameters) {
return assertThatNullPointerException()
@@ -303,7 +303,7 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByNullPointerExceptionHasMessageStartingWith {
@BeforeTemplate
@SuppressWarnings(
"AssertThatThrownByNullPointerException" /* This is a more specific template. */)
"AssertThatThrownByNullPointerException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
return assertThatNullPointerException()
.isThrownBy(throwingCallable)
@@ -319,44 +319,6 @@ final class AssertJThrowingCallableRules {
}
}
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) {
@@ -372,7 +334,8 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByIOExceptionHasMessage {
@BeforeTemplate
@SuppressWarnings("AssertThatThrownByIOException" /* This is a more specific template. */)
@SuppressWarnings(
"AssertThatThrownByIOException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(ThrowingCallable throwingCallable, String message) {
return assertThatIOException().isThrownBy(throwingCallable).withMessage(message);
}
@@ -388,7 +351,8 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByIOExceptionHasMessageParameters {
@BeforeTemplate
@SuppressWarnings("AssertThatThrownByIOException" /* This is a more specific template. */)
@SuppressWarnings(
"AssertThatThrownByIOException" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(
ThrowingCallable throwingCallable, String message, @Repeated Object parameters) {
return assertThatIOException().isThrownBy(throwingCallable).withMessage(message, parameters);
@@ -404,75 +368,27 @@ final class AssertJThrowingCallableRules {
}
}
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) {
Class<? extends Throwable> exceptionType, ThrowingCallable throwingCallable) {
return assertThatExceptionOfType(exceptionType).isThrownBy(throwingCallable);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractObjectAssert<?, ?> after(
ThrowingCallable throwingCallable, Class<? extends Throwable> exceptionType) {
Class<? extends Throwable> exceptionType, ThrowingCallable throwingCallable) {
return assertThatThrownBy(throwingCallable).isInstanceOf(exceptionType);
}
}
static final class AssertThatThrownByHasMessage {
@BeforeTemplate
@SuppressWarnings("AssertThatThrownBy" /* This is a more specific template. */)
@SuppressWarnings("AssertThatThrownBy" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(
ThrowingCallable throwingCallable,
Class<? extends Throwable> exceptionType,
ThrowingCallable throwingCallable,
String message) {
return assertThatExceptionOfType(exceptionType)
.isThrownBy(throwingCallable)
@@ -482,8 +398,8 @@ final class AssertJThrowingCallableRules {
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractObjectAssert<?, ?> after(
ThrowingCallable throwingCallable,
Class<? extends Throwable> exceptionType,
ThrowingCallable throwingCallable,
String message) {
return assertThatThrownBy(throwingCallable).isInstanceOf(exceptionType).hasMessage(message);
}
@@ -491,10 +407,10 @@ final class AssertJThrowingCallableRules {
static final class AssertThatThrownByHasMessageParameters {
@BeforeTemplate
@SuppressWarnings("AssertThatThrownBy" /* This is a more specific template. */)
@SuppressWarnings("AssertThatThrownBy" /* Matches strictly more specific expressions. */)
AbstractObjectAssert<?, ?> before(
ThrowingCallable throwingCallable,
Class<? extends Throwable> exceptionType,
ThrowingCallable throwingCallable,
String message,
@Repeated Object parameters) {
return assertThatExceptionOfType(exceptionType)
@@ -505,8 +421,8 @@ final class AssertJThrowingCallableRules {
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractObjectAssert<?, ?> after(
ThrowingCallable throwingCallable,
Class<? extends Throwable> exceptionType,
ThrowingCallable throwingCallable,
String message,
@Repeated Object parameters) {
return assertThatThrownBy(throwingCallable)
@@ -515,78 +431,6 @@ final class AssertJThrowingCallableRules {
}
}
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 {

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