Compare commits

..

1 Commits

Author SHA1 Message Date
Stephan Schroevers
7d9ffd8cb0 WIP 2022-08-20 11:11:38 +02:00
530 changed files with 7649 additions and 104555 deletions

View File

@@ -1,51 +0,0 @@
---
name: 🐛 Bug report
about: Create a report to help us improve.
title: ""
labels: bug
assignees: ""
---
### Describe the bug
<!-- Provide a clear and concise description of what the bug or issue is. -->
- [ ] I have verified that the issue is reproducible against the latest version
of the project.
- [ ] I have searched through existing issues to verify that this issue is not
already known.
### Minimal Reproducible Example
<!-- Provide a clear and concise description of what happened. Please include
steps on how to reproduce the issue. -->
```java
If applicable and possible, please replace this section with the code that
triggered the isue.
```
<details>
<summary>Logs</summary>
```sh
Please replace this sentence with log output, if applicable.
```
</details>
### Expected behavior
<!-- Provide a clear and concise description of what you expected to happen. -->
### Setup
<!-- Please complete the following information: -->
- Operating system (e.g. MacOS Monterey).
- Java version (i.e. `java --version`, e.g. `17.0.13`).
- Error Prone version (e.g. `2.35.1`).
- Error Prone Support version (e.g. `0.19.0`).
### Additional context
<!-- Provide any other context about the problem here. -->

View File

@@ -1,58 +0,0 @@
---
name: 🚀 Request a new feature
about: Suggest a new feature.
title: ""
labels: new feature
assignees: ""
---
### Problem
<!-- Here, describe the context of the problem that you're facing, and which
you'd like to be solved through Error Prone Support. -->
### Description of the proposed new feature
<!-- Please indicate the type of improvement. -->
- [ ] Support a stylistic preference.
- [ ] Avoid a common gotcha, or potential problem.
- [ ] Improve performance.
<!--
Here, provide a clear and concise description of the desired change.
If possible, provide a simple and minimal example using the following format:
-->
I would like to rewrite the following code:
```java
// XXX: Write the code to match here.
```
to:
```java
// XXX: Write the desired code here.
```
### Considerations
<!--
Here, mention any other aspects to consider. Relevant questions:
- If applicable, is the rewrite operation a clear improvement in all cases?
- Are there special cases to consider?
- Can we further generalize the proposed solution?
- Are there alternative solutions we should consider?
-->
### Participation
<!-- Pull requests are very welcome, and we happily review contributions. Are
you up for the challenge? :D -->
- [ ] I am willing to submit a pull request to implement this improvement.
### Additional context
<!-- Provide any other context about the request here. -->

29
.github/release.yml vendored
View File

@@ -1,29 +0,0 @@
changelog:
exclude:
labels:
- "ignore-changelog"
categories:
- title: ":warning: Update considerations and deprecations"
labels:
- "breaking change"
- "deprecation"
- title: ":rocket: New Error Prone checks and Refaster rules"
labels:
- "new feature"
- title: ":sparkles: Improvements"
labels:
- "improvement"
- title: ":bug: Bug fixes"
labels:
- "bug"
- "bug fix"
- title: ":books: Documentation, test and build improvements"
labels:
- "documentation"
- "chore"
- title: ":chart_with_upwards_trend: Dependency upgrades"
labels:
- "dependencies"
- title: "Other changes"
labels:
- "*"

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

@@ -0,0 +1,37 @@
name: Build and verify
on:
pull_request:
push:
branches:
- 'master'
jobs:
build:
runs-on: ubuntu-22.04
strategy:
matrix:
jdk: [ 11.0.16, 17.0.4 ]
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.0.2
- name: Set up JDK
uses: actions/setup-java@v3.4.1
with:
java-version: ${{ matrix.jdk }}
distribution: temurin
cache: maven
- name: Display build environment details
run: mvn --version
- name: Build project against vanilla Error Prone
run: mvn -T1C install
- 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-24.04 ]
jdk: [ 17.0.13, 21.0.5, 23.0.1 ]
distribution: [ temurin ]
experimental: [ false ]
include:
- os: macos-14
jdk: 17.0.13
distribution: temurin
experimental: false
- os: windows-2022
jdk: 17.0.13
distribution: temurin
experimental: false
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
steps:
- name: Install Harden-Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
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@382542f77617f34e56bf83868920a4d45b7451e7 # v1.16.0
with:
java-version: ${{ matrix.jdk }}
java-distribution: ${{ matrix.distribution }}
maven-version: 3.9.9
- name: Display build environment details
run: mvn --version
- name: Build project against vanilla Error Prone, compile Javadoc
run: mvn -T1C install javadoc:jar
- name: Build project with self-check against Error Prone fork
run: mvn -T1C clean verify -Perror-prone-fork -Pnon-maven-central -Pself-check -s settings.xml
- name: Remove installed project artifacts
run: mvn dependency:purge-local-repository -DmanualInclude='${project.groupId}' -DresolutionFuzziness=groupId
# 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-24.04
steps:
- name: Install Harden-Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
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@382542f77617f34e56bf83868920a4d45b7451e7 # v1.16.0
with:
java-version: 17.0.13
java-distribution: temurin
maven-version: 3.9.9
- name: Initialize CodeQL
uses: github/codeql-action/init@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
with:
languages: ${{ matrix.language }}
- name: Perform minimal build
if: matrix.language == 'java'
run: mvn -T1C clean package -DskipTests -Dverification.skip
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
with:
category: /language:${{ matrix.language }}

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-24.04
steps:
- name: Install Harden-Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: ruby/setup-ruby@7d3497fd78c07c0d84ebafa58d8dac60cd1f0763 # v1.199.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-24.04
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Install Harden-Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
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,53 +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-24.04
steps:
- name: Install Harden-Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
disable-sudo: true
egress-policy: block
allowed-endpoints: >
api.deps.dev:443
api.github.com:443
api.osv.dev:443
api.scorecard.dev:443
api.securityscorecards.dev:443
github.com:443
index.docker.io:443
oss-fuzz-build-logs.storage.googleapis.com:443
repo.maven.apache.org:443
*.sigstore.dev:443
www.bestpractices.dev:443
- name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Run OpenSSF Scorecard analysis
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
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-24.04
steps:
- name: Install Harden-Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
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@382542f77617f34e56bf83868920a4d45b7451e7 # v1.16.0
with:
checkout-fetch-depth: 2
java-version: 17.0.13
java-distribution: temurin
maven-version: 3.9.9
- name: Run Pitest
# By running with features `+GIT(from[HEAD~1]), +gitci`, Pitest only
# analyzes lines changed in the associated pull request, as GitHub
# exposes the changes unique to the PR as a single commit on top of the
# target branch. See https://blog.pitest.org/pitest-pr-setup for
# details.
run: mvn test pitest:mutationCoverage -DargLine.xmx=2048m -Dverification.skip -Dfeatures="+GIT(from[HEAD~1]), +gitci"
- name: Aggregate Pitest reports
run: mvn pitest-git:aggregate -DkilledEmoji=":tada:" -DmutantEmoji=":zombie:" -DtrailingText="Mutation testing report by [Pitest](https://pitest.org/). Review any surviving mutants by inspecting the line comments under [_Files changed_](${{ github.event.number }}/files)."
- name: Upload Pitest reports as artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.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-24.04
steps:
- name: Install Harden-Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
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@382542f77617f34e56bf83868920a4d45b7451e7 # v1.16.0
with:
java-version: 17.0.13
java-distribution: temurin
maven-version: 3.9.9
- name: Download Pitest analysis artifact
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
name: pitest-reports
path: ./target/pit-reports-ci
- name: Update PR
run: mvn -DrepoToken="${{ secrets.GITHUB_TOKEN }}" pitest-github:updatePR

View File

@@ -1,59 +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.
# 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:
pull_request:
branches: [ master ]
issue_comment:
types: [ created ]
permissions:
contents: read
jobs:
run-integration-tests:
runs-on: ubuntu-24.04
# if: github.event.issue.pull_request && contains(github.event.comment.body, '/integration-test')
# XXX: Configure permissions.
strategy:
matrix:
integration-test: [ "metrics", "checkstyle" ]
name: On-demand integration test
steps:
- name: Install Harden-Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
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@382542f77617f34e56bf83868920a4d45b7451e7 # v1.16.0
with:
# checkout-ref: "refs/pull/894/head"
checkout-ref: "refs/pull/${{ github.event.issue.number }}/head"
java-version: 17.0.13
java-distribution: temurin
maven-version: 3.9.9
- name: Install project to local Maven repository
run: mvn -T1C install -DskipTests -Dverification.skip
- name: Run integration test
run: xvfb-run "./integration-tests/${{ matrix.integration-test }}.sh" "${{ runner.temp }}/artifacts"
- name: Upload artifacts on failure
if: ${{ failure() }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: "integration-test-${{ matrix.integration-test }}"
path: "${{ runner.temp }}/artifacts"
- name: Remove installed project artifacts
run: mvn dependency:purge-local-repository -DmanualInclude='${project.groupId}' -DresolutionFuzziness=groupId

View File

@@ -1,51 +0,0 @@
# Analyzes the code base using SonarCloud. See
# https://sonarcloud.io/project/overview?id=PicnicSupermarket_error-prone-support.
name: SonarCloud analysis
on:
pull_request:
push:
branches: [ master ]
schedule:
- cron: '0 4 * * 1'
permissions:
contents: read
jobs:
analyze:
# Analysis of code in forked repositories is skipped, as such workflow runs
# do not have access to the requisite secrets.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
permissions:
contents: read
runs-on: ubuntu-24.04
steps:
- name: Install Harden-Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
disable-sudo: true
egress-policy: block
allowed-endpoints: >
analysis-sensorcache-eu-central-1-prod.s3.amazonaws.com:443
api.adoptium.net:443
api.nuget.org:443
ea6ne4j2sb.execute-api.eu-central-1.amazonaws.com:443
github.com:443
objects.githubusercontent.com:443
repo.maven.apache.org:443
sc-cleancode-sensorcache-eu-central-1-prod.s3.amazonaws.com:443
*.sonarcloud.io:443
sonarcloud.io:443
- name: Check out code and set up JDK and Maven
uses: s4u/setup-maven-action@382542f77617f34e56bf83868920a4d45b7451e7 # v1.16.0
with:
checkout-fetch-depth: 0
java-version: 17.0.13
java-distribution: temurin
maven-version: 3.9.9
- name: Create missing `test` directory
# XXX: Drop this step in favour of actually having a test.
run: mkdir refaster-compiler/src/test
- name: Perform SonarCloud analysis
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: mvn -T1C jacoco:prepare-agent verify jacoco:report sonar:sonar -Dverification.skip -Dsonar.projectKey=PicnicSupermarket_error-prone-support

4
.gitignore vendored
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

@@ -2,7 +2,6 @@
-XX:SoftRefLRUPolicyMSPerMB=10
-XX:+UseParallelGC
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
@@ -10,4 +9,5 @@
--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED

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

@@ -1,85 +0,0 @@
# Contributing
Thank you for checking this document! This project is free software, and we
(the maintainers) encourage and value any contribution.
Here are some guidelines to help you get started.
## 🐛 Reporting a bug
Like any non-trivial piece of software, this library is probably not bug-free.
If you found a bug, feel free to [report the issue][error-prone-support-issues]
on GitHub.
Before doing so, please:
- Verify that the issue is reproducible against the latest version of the
project.
- Search through the existing set of issues to see whether the problem is
already known. With some luck a solution is already in place, or a workaround
may have been provided.
When filing a bug report, please include the following:
- Any relevant information about your environment. This should generally
include the output of `java --version`, as well as the version of Error Prone
you're using.
- A description of what is going on (e.g. logging output, stacktraces).
- A minimum reproducible example, so that other developers can try to reproduce
(and optionally fix) the bug.
- Any additional information that may be relevant.
## 💡 Reporting an improvement
If you would like to see an improvement, you can file a [GitHub
issue][error-prone-support-issues]. This is also a good idea when you're
already working towards opening a pull request, as this allows for discussion
around the idea.
## 🚀 Opening a pull request
All submissions, including submissions by project members, require approval by
at least two reviewers. We use [GitHub pull
requests][error-prone-support-pulls] for this purpose.
Before opening a pull request, please check whether there are any existing
(open or closed) issues or pull requests addressing the same problem. This
avoids double work or lots of time spent on a solution that may ultimately not
be accepted. When in doubt, make sure to first raise an
[issue][error-prone-support-issues] to discuss the idea.
To the extent possible, the pull request process guards our coding guidelines.
Some pointers:
- Try to make sure that the
[`./run-full-build.sh`][error-prone-support-full-build] script completes
successfully, ideally before opening a pull request. See the [development
instructions][error-prone-support-developing] for details on how to
efficiently resolve many of the errors and warnings that may be reported. (In
particular, make sure to run `mvn fmt:format` and
[`./apply-error-prone-suggestions.sh`][error-prone-support-patch].) That
said, if you feel that the build fails for invalid or debatable reasons, or
if you're unsure how to best resolve an issue, don't let that discourage you
from opening a PR with a failing build; we can have a look at the issue
together!
- Checks should be _topical_: ideally they address a single concern.
- Where possible checks should provide _fixes_, and ideally these are
completely behavior-preserving. In order for a check to be adopted by users
it must not "get in the way". So for a check that addresses a relatively
trivial stylistic concern it is doubly important that the violations it
detects can be auto-patched.
- Make sure you have read Error Prone's [criteria for new
checks][error-prone-criteria]. Most guidelines described there apply to this
project as well, except that this project _does_ focus quite heavy on style
enforcement. But that just makes the previous point doubly important.
- Make sure that a check's [(mutation) test
coverage][error-prone-support-mutation-tests] is or remains about as high as
it can be. Not only does this lead to better tests, it also points out
opportunities to simplify the code.
- Please restrict the scope of a pull request to a single feature or fix. Don't
sneak in unrelated changes; instead just open more than one pull request 😉.
[error-prone-criteria]: https://errorprone.info/docs/criteria
[error-prone-support-developing]: https://github.com/PicnicSupermarket/error-prone-support/tree/master#-developing-error-prone-support
[error-prone-support-full-build]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-full-build.sh
[error-prone-support-issues]: https://github.com/PicnicSupermarket/error-prone-support/issues
[error-prone-support-mutation-tests]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-branch-mutation-tests.sh
[error-prone-support-patch]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/apply-error-prone-suggestions.sh
[error-prone-support-pulls]: https://github.com/PicnicSupermarket/error-prone-support/pulls

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2017-2024 Picnic Technologies BV
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

325
README.md
View File

@@ -1,325 +0,0 @@
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="website/assets/images/logo-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="website/assets/images/logo.svg">
<img alt="Error Prone Support logo" src="website/assets/images/logo.svg" width="50%">
</picture>
</div>
# Error Prone Support
Error Prone Support is a [Picnic][picnic-blog]-opinionated extension of
Google's [Error Prone][error-prone-orig-repo]. It aims to improve code quality,
focussing on maintainability, consistency and avoidance of common pitfalls.
> Error Prone is a static analysis tool for Java that catches common
> programming mistakes at compile-time.
To learn more about Error Prone (Support), how you can start using Error Prone
in practice, and how we use it at Picnic, watch the conference talk
[_Automating away bugs with Error Prone in practice_][conference-talk]. Also
consider checking out the blog post [_Picnic loves Error Prone: producing
high-quality and consistent Java code_][picnic-blog-ep-post].
[![Maven Central][maven-central-badge]][maven-central-search]
[![Reproducible Builds][reproducible-builds-badge]][reproducible-builds-report]
[![OpenSSF Best Practices][openssf-best-practices-badge]][openssf-best-practices-checklist]
[![OpenSSF Scorecard][openssf-scorecard-badge]][openssf-scorecard-report]
[![CodeQL Analysis][codeql-badge]][codeql-master]
[![GitHub Actions][github-actions-build-badge]][github-actions-build-master]
[![Mutation tested with PIT][pitest-badge]][pitest]
[![Quality Gate Status][sonarcloud-quality-badge]][sonarcloud-quality-master]
[![Maintainability Rating][sonarcloud-maintainability-badge]][sonarcloud-maintainability-master]
[![Reliability Rating][sonarcloud-reliability-badge]][sonarcloud-reliability-master]
[![Security Rating][sonarcloud-security-badge]][sonarcloud-security-master]
[![Coverage][sonarcloud-coverage-badge]][sonarcloud-coverage-master]
[![Duplicated Lines (%)][sonarcloud-duplication-badge]][sonarcloud-duplication-master]
[![Technical Debt][sonarcloud-technical-debt-badge]][sonarcloud-technical-debt-master]
[![License][license-badge]][license]
[![PRs Welcome][pr-badge]][contributing]
[Getting started](#-getting-started) •
[Developing Error Prone Support](#-developing-error-prone-support) •
[How it works](#-how-it-works) • [Contributing](#%EF%B8%8F-contributing)
---
## ⚡ Getting started
### Installation
This library is built on top of [Error Prone][error-prone-orig-repo]. To use
it, read the installation guide for Maven or Gradle below. The library requires
that your build is executed using JDK 17 or above, but supports builds that
[target][baeldung-java-source-target-options] older versions of Java.
#### Maven
1. First, follow Error Prone's [installation
guide][error-prone-installation-guide].
2. Next, edit your `pom.xml` file to add one or more Error Prone Support
modules to the `annotationProcessorPaths` of the `maven-compiler-plugin`:
```xml
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<!-- Prefer using the latest release. -->
<version>3.12.0</version>
<configuration>
<annotationProcessorPaths>
<!-- Error Prone itself. -->
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>${error-prone.version}</version>
</path>
<!-- Error Prone Support's additional bug checkers. -->
<path>
<groupId>tech.picnic.error-prone-support</groupId>
<artifactId>error-prone-contrib</artifactId>
<version>${error-prone-support.version}</version>
</path>
<!-- Error Prone Support's Refaster rules. -->
<path>
<groupId>tech.picnic.error-prone-support</groupId>
<artifactId>refaster-runner</artifactId>
<version>${error-prone-support.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>
-Xplugin:ErrorProne
<!-- Add other Error Prone flags here. See
https://errorprone.info/docs/flags. -->
</arg>
<arg>-XDcompilePolicy=simple</arg>
</compilerArgs>
<!-- Enable this if you'd like to fail your build upon warnings. -->
<!-- <failOnWarning>true</failOnWarning> -->
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
```
<!-- XXX: Reference `oss-parent`'s `pom.xml` once that project also uses Error
Prone Support. Alternatively reference this project's `self-check` profile
definition. -->
#### Gradle
1. First, follow the [installation
guide][error-prone-gradle-installation-guide] of the
`gradle-errorprone-plugin`.
2. Next, edit your `build.gradle` file to add one or more Error Prone Support
modules:
```groovy
dependencies {
// Error Prone itself.
errorprone("com.google.errorprone:error_prone_core:${errorProneVersion}")
// Error Prone Support's additional bug checkers.
errorprone("tech.picnic.error-prone-support:error-prone-contrib:${errorProneSupportVersion}")
// Error Prone Support's Refaster rules.
errorprone("tech.picnic.error-prone-support:refaster-runner:${errorProneSupportVersion}")
}
tasks.withType(JavaCompile).configureEach {
options.errorprone.disableWarningsInGeneratedCode = true
// Add other Error Prone flags here. See:
// - https://github.com/tbroyer/gradle-errorprone-plugin#configuration
// - https://errorprone.info/docs/flags
}
```
### Seeing it in action
Consider the following example code:
```java
import com.google.common.collect.ImmutableSet;
import java.math.BigDecimal;
public class Example {
static BigDecimal getNumber() {
return BigDecimal.valueOf(0);
}
public ImmutableSet<Integer> getSet() {
ImmutableSet<Integer> set = ImmutableSet.of(1);
return ImmutableSet.copyOf(set);
}
}
```
If the [installation](#installation) was successful, then building the above
code with Maven should yield two compiler warnings:
```sh
$ mvn clean install
...
[INFO] Example.java:[9,34] [Refaster Rule] BigDecimalRules.BigDecimalZero: Refactoring opportunity
(see https://error-prone.picnic.tech/refasterrules/BigDecimalRules#BigDecimalZero)
Did you mean 'return BigDecimal.ZERO;'?
...
[WARNING] Example.java:[13,35] [IdentityConversion] This method invocation appears redundant; remove it or suppress this warning and add a comment explaining its purpose
(see https://error-prone.picnic.tech/bugpatterns/IdentityConversion)
Did you mean 'return set;' or '@SuppressWarnings("IdentityConversion") public ImmutableSet<Integer> getSet() {'?
...
```
Two things are kicking in here:
1. An Error Prone [`BugChecker`][error-prone-bugchecker] that flags unnecessary
[identity conversions][bug-checks-identity-conversion].
2. A [Refaster][refaster] rule capable of
[rewriting][refaster-rules-bigdecimal] expressions of the form
`BigDecimal.valueOf(0)` and `new BigDecimal(0)` to `BigDecimal.ZERO`.
Be sure to check out all [bug checks][bug-checks] and [refaster
rules][refaster-rules].
## 👷 Developing Error Prone Support
This is a [Maven][maven] project, so running `mvn clean install` performs a
full clean build and installs the library to your local Maven repository.
Once you've made changes, the build may fail due to a warning or error emitted
by static code analysis. The flags and commands listed below allow you to
suppress or (in a large subset of cases) automatically fix such cases. Make
sure to carefully check the available options, as this can save you significant
amounts of development time!
Relevant Maven build parameters:
- `-Dverification.warn` makes the warnings and errors emitted by various
plugins and the Java compiler non-fatal, where possible.
- `-Dverification.skip` disables various non-essential plugins and compiles the
code with minimal checks (i.e. without linting, Error Prone checks, etc.).
- `-Dversion.error-prone=some-version` runs the build using the specified
version of Error Prone. This is useful e.g. when testing a locally built
Error Prone SNAPSHOT.
- `-Perror-prone-fork` runs the build using Picnic's [Error Prone
fork][error-prone-fork-repo], hosted on [Jitpack][error-prone-fork-jitpack].
This fork generally contains a few changes on top of the latest Error Prone
release.
- `-Pself-check` runs the checks defined by this project against itself.
Pending a release of [google/error-prone#3301][error-prone-pull-3301], this
flag must currently be used in combination with `-Perror-prone-fork`.
Other highly relevant commands:
- `mvn fmt:format` formats the code using
[`google-java-format`][google-java-format].
- [`./run-full-build.sh`][script-run-full-build] builds the project twice,
where the second pass validates compatbility with Picnic's [Error Prone
fork][error-prone-fork-repo] and compliance of the code with any rules
defined within this project. (Consider running this before [opening a pull
request][contributing-pull-request], as the PR checks also perform this
validation.)
- [`./apply-error-prone-suggestions.sh`][script-apply-error-prone-suggestions]
applies Error Prone and Error Prone Support code suggestions to this project.
Before running this command, make sure to have installed the project (`mvn
clean install`) and make sure that the current working directory does not
contain unstaged or uncommited changes.
- [`./run-branch-mutation-tests.sh`][script-run-branch-mutation-tests] uses
[Pitest][pitest] to run mutation tests against code that is modified relative
to the upstream default branch. The results can be reviewed by opening the
respective `target/pit-reports/index.html` files. One can use
[`./run-mutation-tests.sh`][script-run-mutation-tests] to run mutation tests
against _all_ code in the current working directory. For more information
check the [PIT Maven plugin][pitest-maven].
When running the project's tests in IntelliJ IDEA, you might see the following
error:
```
java: exporting a package from system module jdk.compiler is not allowed with --release
```
If this happens, go to _Settings -> Build, Execution, Deployment -> Compiler ->
Java Compiler_ and deselect the option _Use '--release' option for
cross-compilation (Java 9 and later)_. See [IDEA-288052][idea-288052] for
details.
## 💡 How it works
This project provides additional [`BugChecker`][error-prone-bugchecker]
implementations.
<!-- XXX: Extend this section. -->
## ✍️ Contributing
Want to report or fix a bug, suggest or add a new feature, or improve the
documentation? That's awesome! Please read our [contribution
guidelines][contributing].
### Security
If you want to report a security vulnerability, please do so through a private
channel; please see our [security policy][security] for details.
[baeldung-java-source-target-options]: https://www.baeldung.com/java-source-target-options
[bug-checks]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/
[bug-checks-identity-conversion]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/IdentityConversion.java
[codeql-badge]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/codeql.yml/badge.svg?branch=master&event=push
[codeql-master]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/codeql.yml?query=branch:master+event:push
[conference-talk]: https://www.youtube.com/watch?v=-47WD-3wKBs
[contributing]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/CONTRIBUTING.md
[contributing-pull-request]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/CONTRIBUTING.md#-opening-a-pull-request
[error-prone-bugchecker]: https://github.com/google/error-prone/blob/master/check_api/src/main/java/com/google/errorprone/bugpatterns/BugChecker.java
[error-prone-fork-jitpack]: https://jitpack.io/#PicnicSupermarket/error-prone
[error-prone-fork-repo]: https://github.com/PicnicSupermarket/error-prone
[error-prone-gradle-installation-guide]: https://github.com/tbroyer/gradle-errorprone-plugin
[error-prone-installation-guide]: https://errorprone.info/docs/installation#maven
[error-prone-orig-repo]: https://github.com/google/error-prone
[error-prone-pull-3301]: https://github.com/google/error-prone/pull/3301
[github-actions-build-badge]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/build.yml/badge.svg
[github-actions-build-master]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/build.yml?query=branch:master&event=push
[google-java-format]: https://github.com/google/google-java-format
[idea-288052]: https://youtrack.jetbrains.com/issue/IDEA-288052
[license-badge]: https://img.shields.io/github/license/PicnicSupermarket/error-prone-support
[license]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/LICENSE.md
[maven-central-badge]: https://img.shields.io/maven-central/v/tech.picnic.error-prone-support/error-prone-support?color=blue
[maven-central-search]: https://search.maven.org/artifact/tech.picnic.error-prone-support/error-prone-support
[maven]: https://maven.apache.org
[openssf-best-practices-badge]: https://bestpractices.coreinfrastructure.org/projects/7199/badge
[openssf-best-practices-checklist]: https://bestpractices.coreinfrastructure.org/projects/7199
[openssf-scorecard-badge]: https://img.shields.io/ossf-scorecard/github.com/PicnicSupermarket/error-prone-support?label=openssf%20scorecard
[openssf-scorecard-report]: https://securityscorecards.dev/viewer/?uri=github.com/PicnicSupermarket/error-prone-support
[picnic-blog-ep-post]: https://blog.picnic.nl/picnic-loves-error-prone-producing-high-quality-and-consistent-java-code-b8a566be6886
[picnic-blog]: https://blog.picnic.nl
[pitest-badge]: https://img.shields.io/badge/-Mutation%20tested%20with%20PIT-blue.svg
[pitest]: https://pitest.org
[pitest-maven]: https://pitest.org/quickstart/maven
[pr-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
[refaster]: https://errorprone.info/docs/refaster
[refaster-rules-bigdecimal]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/BigDecimalRules.java
[refaster-rules]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/
[reproducible-builds-badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/jvm-repo-rebuild/reproducible-central/master/content/tech/picnic/error-prone-support/error-prone-support/badge.json
[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,107 +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.19.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>${project.groupId}</groupId>
<artifactId>error-prone-utils</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>refaster-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</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,287 +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.BugPatternTestCases;
/**
* 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<BugPatternTestCases> {
/** Instantiates a new {@link BugPatternTestExtractor} instance. */
public BugPatternTestExtractor() {}
@Override
public String identifier() {
return "bugpattern-test";
}
@Override
public Optional<BugPatternTestCases> 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_BugPatternTestCases(
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<BugPatternTestCase> collectedBugPatternTestCases = new ArrayList<>();
private ImmutableList<BugPatternTestCase> getCollectedTests() {
return ImmutableList.copyOf(collectedBugPatternTestCases);
}
@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) {
extractReplacementBugPatternTestCases(node, entries, state);
} else {
extractIdentificationBugPatternTestCases(node, entries, state);
}
if (!entries.isEmpty()) {
collectedBugPatternTestCases.add(
new AutoValue_BugPatternTestExtractor_BugPatternTestCase(
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 extractIdentificationBugPatternTestCases(
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) {
extractIdentificationBugPatternTestCases(methodInvocation, sink, state);
}
}
private static void extractReplacementBugPatternTestCases(
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) {
extractReplacementBugPatternTestCases(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_BugPatternTestCases.class)
abstract static class BugPatternTestCases {
static BugPatternTestCases create(
URI source, String testClass, ImmutableList<BugPatternTestCase> testCases) {
return new AutoValue_BugPatternTestExtractor_BugPatternTestCases(
source, testClass, testCases);
}
abstract URI source();
abstract String testClass();
abstract ImmutableList<BugPatternTestCase> testCases();
}
@AutoValue
@JsonDeserialize(as = AutoValue_BugPatternTestExtractor_BugPatternTestCase.class)
abstract static class BugPatternTestCase {
static BugPatternTestCase create(String classUnderTest, ImmutableList<TestEntry> entries) {
return new AutoValue_BugPatternTestExtractor_BugPatternTestCase(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,91 +0,0 @@
package tech.picnic.errorprone.documentation;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskEvent.Kind;
import com.sun.source.util.TaskListener;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.util.Context;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ServiceLoader;
import javax.tools.JavaFileObject;
/**
* A {@link TaskListener} that identifies and extracts relevant content for documentation generation
* and writes it to disk.
*/
// XXX: Find a better name for this class; it doesn't generate documentation per se.
final class DocumentationGeneratorTaskListener implements TaskListener {
@SuppressWarnings({"rawtypes", "unchecked"})
private static final ImmutableList<Extractor<?>> EXTRACTORS =
(ImmutableList)
ImmutableList.copyOf(
ServiceLoader.load(
Extractor.class, DocumentationGeneratorTaskListener.class.getClassLoader()));
private final Context context;
private final Path docsPath;
DocumentationGeneratorTaskListener(Context context, Path path) {
this.context = context;
this.docsPath = path;
}
@Override
public void started(TaskEvent taskEvent) {
if (taskEvent.getKind() == Kind.ANALYZE) {
createDocsDirectory();
}
}
@Override
public void finished(TaskEvent taskEvent) {
if (taskEvent.getKind() != Kind.ANALYZE) {
return;
}
JavaFileObject sourceFile = taskEvent.getSourceFile();
CompilationUnitTree compilationUnit = taskEvent.getCompilationUnit();
ClassTree classTree = JavacTrees.instance(context).getTree(taskEvent.getTypeElement());
if (sourceFile == null || compilationUnit == null || classTree == null) {
return;
}
VisitorState state =
VisitorState.createForUtilityPurposes(context)
.withPath(new TreePath(new TreePath(compilationUnit), classTree));
for (Extractor<?> extractor : EXTRACTORS) {
extractor
.tryExtract(classTree, state)
.ifPresent(
data ->
writeToFile(
extractor.identifier(), getSimpleClassName(sourceFile.toUri()), data));
}
}
private void createDocsDirectory() {
try {
Files.createDirectories(docsPath);
} catch (IOException e) {
throw new IllegalStateException(
String.format("Error while creating directory with path '%s'", docsPath), e);
}
}
private <T> void writeToFile(String identifier, String className, T data) {
Json.write(docsPath.resolve(String.format("%s-%s.json", identifier, className)), data);
}
private static String getSimpleClassName(URI path) {
return Path.of(path).getFileName().toString().replace(".java", "");
}
}

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,176 +0,0 @@
package tech.picnic.errorprone.documentation;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
import static java.util.stream.Collectors.joining;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.auto.service.AutoService;
import com.google.auto.value.AutoValue;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.FormatMethod;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.matchers.Matcher;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import java.net.URI;
import java.util.Optional;
import java.util.regex.Pattern;
import tech.picnic.errorprone.documentation.RefasterRuleCollectionTestExtractor.RefasterTestCases;
import tech.picnic.errorprone.utils.SourceCode;
/**
* An {@link Extractor} that describes how to extract data from Refaster rule input and output test
* classes.
*/
// XXX: Drop this extractor if/when the Refaster test framework is reimplemented such that tests can
// be located alongside rules, rather than in two additional resource files as currently required by
// `RefasterRuleCollection`.
@Immutable
@AutoService(Extractor.class)
@SuppressWarnings("rawtypes" /* See https://github.com/google/auto/issues/870. */)
public final class RefasterRuleCollectionTestExtractor implements Extractor<RefasterTestCases> {
private static final Matcher<ClassTree> IS_REFASTER_RULE_COLLECTION_TEST_CASE =
isSubtypeOf("tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase");
private static final Pattern TEST_CLASS_NAME_PATTERN = Pattern.compile("(.*)Test");
private static final Pattern TEST_CLASS_FILE_NAME_PATTERN =
Pattern.compile(".*(Input|Output)\\.java");
private static final Pattern TEST_METHOD_NAME_PATTERN = Pattern.compile("test(.*)");
private static final String LINE_SEPARATOR = "\n";
private static final Splitter LINE_SPLITTER = Splitter.on(LINE_SEPARATOR);
/** Instantiates a new {@link RefasterRuleCollectionTestExtractor} instance. */
public RefasterRuleCollectionTestExtractor() {}
@Override
public String identifier() {
return "refaster-rule-collection-test";
}
@Override
public Optional<RefasterTestCases> tryExtract(ClassTree tree, VisitorState state) {
if (!IS_REFASTER_RULE_COLLECTION_TEST_CASE.matches(tree, state)) {
return Optional.empty();
}
URI sourceFile = state.getPath().getCompilationUnit().getSourceFile().toUri();
return Optional.of(
RefasterTestCases.create(
sourceFile,
getRuleCollectionName(tree),
isInputFile(sourceFile),
getRefasterTestCases(tree, state)));
}
private static String getRuleCollectionName(ClassTree tree) {
String className = tree.getSimpleName().toString();
// XXX: Instead of throwing an error here, it'd be nicer to have a bug checker validate key
// aspects of `RefasterRuleCollectionTestCase` subtypes.
return tryExtractPatternGroup(className, TEST_CLASS_NAME_PATTERN)
.orElseThrow(
violation(
"Refaster rule collection test class name '%s' does not match '%s'",
className, TEST_CLASS_NAME_PATTERN));
}
private static boolean isInputFile(URI sourceFile) {
String path = sourceFile.getPath();
// XXX: Instead of throwing an error here, it'd be nicer to have a bug checker validate key
// aspects of `RefasterRuleCollectionTestCase` subtypes.
return "Input"
.equals(
tryExtractPatternGroup(path, TEST_CLASS_FILE_NAME_PATTERN)
.orElseThrow(
violation(
"Refaster rule collection test file name '%s' does not match '%s'",
path, TEST_CLASS_FILE_NAME_PATTERN)));
}
private static ImmutableList<RefasterTestCase> getRefasterTestCases(
ClassTree tree, VisitorState state) {
return tree.getMembers().stream()
.filter(MethodTree.class::isInstance)
.map(MethodTree.class::cast)
.flatMap(m -> tryExtractRefasterTestCase(m, state).stream())
.collect(toImmutableList());
}
private static Optional<RefasterTestCase> tryExtractRefasterTestCase(
MethodTree method, VisitorState state) {
return tryExtractPatternGroup(method.getName().toString(), TEST_METHOD_NAME_PATTERN)
.map(name -> RefasterTestCase.create(name, getFormattedSource(method, state)));
}
/**
* Returns the source code for the specified method.
*
* @implNote This operation attempts to trim leading whitespace, such that the start and end of
* the method declaration are aligned. The implemented heuristic assumes that the code is
* formatted using Google Java Format.
*/
// XXX: Leading Javadoc and other comments are currently not extracted. Consider fixing this.
private static String getFormattedSource(MethodTree method, VisitorState state) {
String source = SourceCode.treeToString(method, state);
int finalNewline = source.lastIndexOf(LINE_SEPARATOR);
if (finalNewline < 0) {
return source;
}
int indentation = Math.max(0, source.lastIndexOf(' ') - finalNewline);
String prefixToStrip = " ".repeat(indentation);
return LINE_SPLITTER
.splitToStream(source)
.map(line -> line.startsWith(prefixToStrip) ? line.substring(indentation) : line)
.collect(joining(LINE_SEPARATOR));
}
private static Optional<String> tryExtractPatternGroup(String input, Pattern pattern) {
java.util.regex.Matcher matcher = pattern.matcher(input);
return matcher.matches() ? Optional.of(matcher.group(1)) : Optional.empty();
}
@FormatMethod
private static Supplier<VerifyException> violation(String format, Object... args) {
return () -> new VerifyException(String.format(format, args));
}
@AutoValue
@JsonDeserialize(as = AutoValue_RefasterRuleCollectionTestExtractor_RefasterTestCases.class)
abstract static class RefasterTestCases {
static RefasterTestCases create(
URI source,
String ruleCollection,
boolean isInput,
ImmutableList<RefasterTestCase> testCases) {
return new AutoValue_RefasterRuleCollectionTestExtractor_RefasterTestCases(
source, ruleCollection, isInput, testCases);
}
abstract URI source();
abstract String ruleCollection();
abstract boolean isInput();
abstract ImmutableList<RefasterTestCase> testCases();
}
@AutoValue
@JsonDeserialize(as = AutoValue_RefasterRuleCollectionTestExtractor_RefasterTestCase.class)
abstract static class RefasterTestCase {
static RefasterTestCase create(String name, String content) {
return new AutoValue_RefasterRuleCollectionTestExtractor_RefasterTestCase(name, content);
}
abstract String name();
abstract String content();
}
}

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.BugPatternTestCase;
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.BugPatternTestCases;
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.IdentificationTestEntry;
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.ReplacementTestEntry;
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",
BugPatternTestCases.create(
URI.create("file:///SingleFileCompilationTestHelperTest.java"),
"SingleFileCompilationTestHelperTest",
ImmutableList.of(
BugPatternTestCase.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",
BugPatternTestCases.create(
URI.create("file:///SingleFileCompilationTestHelperWithSetArgsTest.java"),
"SingleFileCompilationTestHelperWithSetArgsTest",
ImmutableList.of(
BugPatternTestCase.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",
BugPatternTestCases.create(
URI.create("file:///MultiFileCompilationTestHelperTest.java"),
"MultiFileCompilationTestHelperTest",
ImmutableList.of(
BugPatternTestCase.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",
BugPatternTestCases.create(
URI.create("file:///SingleFileBugCheckerRefactoringTestHelperTest.java"),
"SingleFileBugCheckerRefactoringTestHelperTest",
ImmutableList.of(
BugPatternTestCase.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",
BugPatternTestCases.create(
URI.create(
"file:///SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest.java"),
"SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest",
ImmutableList.of(
BugPatternTestCase.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",
BugPatternTestCases.create(
URI.create("file:///MultiFileBugCheckerRefactoringTestHelperTest.java"),
"MultiFileBugCheckerRefactoringTestHelperTest",
ImmutableList.of(
BugPatternTestCase.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",
BugPatternTestCases.create(
URI.create("file:///CompilationAndBugCheckerRefactoringTestHelpersTest.java"),
"CompilationAndBugCheckerRefactoringTestHelpersTest",
ImmutableList.of(
BugPatternTestCase.create(
"CompilationAndBugCheckerRefactoringTestHelpersTest.TestChecker",
ImmutableList.of(
IdentificationTestEntry.create(
"A.java", "// BUG: Diagnostic contains:\nclass A {}\n"))),
BugPatternTestCase.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",
BugPatternTestCases.create(
URI.create(
"file:///CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest.java"),
"pkg.CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest",
ImmutableList.of(
BugPatternTestCase.create(
"pkg.CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest.CustomTestChecker",
ImmutableList.of(
IdentificationTestEntry.create(
"A.java", "// BUG: Diagnostic contains:\nclass A {}\n"))),
BugPatternTestCase.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, BugPatternTestCases expected) {
assertThat(outputDirectory.resolve(String.format("bugpattern-test-%s.json", testClass)))
.exists()
.returns(expected, path -> Json.read(path, BugPatternTestCases.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.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(), Sets.immutableEnumSet(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

@@ -1,166 +0,0 @@
package tech.picnic.errorprone.documentation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import com.google.common.base.VerifyException;
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.RefasterRuleCollectionTestExtractor.RefasterTestCase;
import tech.picnic.errorprone.documentation.RefasterRuleCollectionTestExtractor.RefasterTestCases;
final class RefasterRuleCollectionTestExtractorTest {
@Test
void noRefasterRuleTest(@TempDir Path outputDirectory) {
Compilation.compileWithDocumentationGenerator(
outputDirectory, "NoRefasterRuleTest.java", "public final class NoRefasterRuleTest {}");
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
}
@Test
void invalidTestClassName(@TempDir Path outputDirectory) {
assertThatThrownBy(
() ->
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"InvalidTestClassNameInput.java",
"import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;",
"",
"final class InvalidTestClassName implements RefasterRuleCollectionTestCase {}"))
.cause()
.isInstanceOf(VerifyException.class)
.hasMessage(
"Refaster rule collection test class name 'InvalidTestClassName' does not match '(.*)Test'");
}
@Test
void invalidFileName(@TempDir Path outputDirectory) {
assertThatThrownBy(
() ->
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"InvalidFileNameTest.java",
"import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;",
"",
"final class InvalidFileNameTest implements RefasterRuleCollectionTestCase {}"))
.cause()
.isInstanceOf(VerifyException.class)
.hasMessage(
"Refaster rule collection test file name '/InvalidFileNameTest.java' does not match '.*(Input|Output)\\.java'");
}
@Test
void emptyRefasterRuleCollectionTestInput(@TempDir Path outputDirectory) {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"EmptyRefasterRuleCollectionTestInput.java",
"import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;",
"",
"final class EmptyRefasterRuleCollectionTest implements RefasterRuleCollectionTestCase {}");
verifyGeneratedFileContent(
outputDirectory,
"EmptyRefasterRuleCollectionTestInput",
RefasterTestCases.create(
URI.create("file:///EmptyRefasterRuleCollectionTestInput.java"),
"EmptyRefasterRuleCollection",
/* isInput= */ true,
ImmutableList.of()));
}
@Test
void singletonRefasterRuleCollectionTestOutput(@TempDir Path outputDirectory) {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"SingletonRefasterRuleCollectionTestOutput.java",
"import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;",
"",
"final class SingletonRefasterRuleCollectionTest implements RefasterRuleCollectionTestCase {",
" int testMyRule() {",
" return 42;",
" }",
"}");
verifyGeneratedFileContent(
outputDirectory,
"SingletonRefasterRuleCollectionTestOutput",
RefasterTestCases.create(
URI.create("file:///SingletonRefasterRuleCollectionTestOutput.java"),
"SingletonRefasterRuleCollection",
/* isInput= */ false,
ImmutableList.of(
RefasterTestCase.create(
"MyRule",
"""
int testMyRule() {
return 42;
}"""))));
}
@Test
void complexRefasterRuleCollectionTestOutput(@TempDir Path outputDirectory) {
Compilation.compileWithDocumentationGenerator(
outputDirectory,
"pkg/ComplexRefasterRuleCollectionTestInput.java",
"package pkg;",
"",
"import com.google.common.collect.ImmutableSet;",
"import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;",
"",
"final class ComplexRefasterRuleCollectionTest implements RefasterRuleCollectionTestCase {",
" private static final String IGNORED_CONSTANT = \"constant\";",
"",
" @Override",
" public ImmutableSet<Object> elidedTypesAndStaticImports() {",
" return ImmutableSet.of();",
" }",
"",
" /** Javadoc. */",
" String testFirstRule() {",
" return \"Don't panic\";",
" }",
"",
" // Comment.",
" String testSecondRule() {",
" return \"Carry a towel\";",
" }",
"",
" void testEmptyRule() {}",
"}");
verifyGeneratedFileContent(
outputDirectory,
"ComplexRefasterRuleCollectionTestInput",
RefasterTestCases.create(
URI.create("file:///pkg/ComplexRefasterRuleCollectionTestInput.java"),
"ComplexRefasterRuleCollection",
/* isInput= */ true,
ImmutableList.of(
RefasterTestCase.create(
"FirstRule",
"""
String testFirstRule() {
return "Don't panic";
}"""),
RefasterTestCase.create(
"SecondRule",
"""
String testSecondRule() {
return "Carry a towel";
}"""),
RefasterTestCase.create("EmptyRule", "void testEmptyRule() {}"))));
}
private static void verifyGeneratedFileContent(
Path outputDirectory, String testIdentifier, RefasterTestCases expected) {
assertThat(
outputDirectory.resolve(
String.format("refaster-rule-collection-test-%s.json", testIdentifier)))
.exists()
.returns(expected, path -> Json.read(path, RefasterTestCases.class));
}
}

View File

@@ -11,21 +11,71 @@ request.
### Building
See the main [readme][main-readme].
This is a [Maven][maven] project, so running `mvn clean install` performs a
full clean build. Some relevant flags:
- `-Dverification.warn` makes the warnings and errors emitted by various
plugins and the Java compiler non-fatal, where possible.
- `-Dverification.skip` disables various non-essential plugins and compiles the
code with minimal checks (i.e. without linting, Error Prone checks, etc.)
- `-Dversion.error-prone=some-version` runs the build using the specified
version of Error Prone. This is useful e.g. when testing a locally built
Error Prone SNAPSHOT.
- `-Perror-prone-fork` run the build using Picnic's [Error Prone
fork][error-prone-fork-repo], hosted on [Jitpack][error-prone-fork-jitpack].
This fork generally contains a few changes on top of the latest Error Prone
release.
Two other goals that one may find relevant:
- `mvn fmt:format` formats the code using
[`google-java-format`][google-java-format].
- `mvn pitest:mutationCoverage` runs mutation tests using [PIT][pitest]. The
results can be reviewed by opening the respective
`target/pit-reports/index.html` files. For more information check the [PIT
Maven plugin][pitest-maven].
When loading the project in IntelliJ IDEA (and perhaps other IDEs) errors about
the inaccessibility of `com.sun.tools.javac.*` classes may be reported. If this
happens, configure your IDE to enable the `add-exports` profile.
### Contribution guidelines
See our [contributing guidelines][main-contributing].
To the extend possible, the pull request process guards our coding guidelines.
Some pointers:
- Checks should we _topical_: Ideally they address a single concern.
- Where possible checks should provide _fixes_, and ideally these are
completely behavior preserving. In order for a check to be adopted by users
it must not "get in the way". So for a check which addresses a relatively
trivial stylistic concern it is doubly important that the violations it
detects can be auto-patched.
- Make sure you have read Error Prone's [criteria for new
checks][error-prone-criteria]. Most guidelines described there apply to this
project as well, except that this project _does_ focus quite heavy on style
enforcement. But that just makes the previous point doubly important.
- Make sure that a check's (mutation) coverage is or remains about as high as
it can be. Not only does this lead to better tests, it also points out
opportunities to simplify the code.
- Please restrict the scope of a pull request to a single feature or fix. Don't
sneak in unrelated changes.
- When in doubt about whether a pull request will be accepted, please first
file an issue to discuss it.
### Our wishlist
We expect the following tasks to help improve the quality of this open source
project:
- Publish the artifact to Maven Central, then document the coordinates in this
`README.md`.
- Document how to enable the checks.
- Document how to apply patches.
- Document each of the checks.
- Add Travis CI, [SonarQube][sonarcloud] and [Codecov][codecov]
integrations.
- Investigate whether it makes sense to include license headers in each file.
If so, set that up and enforce it.
- Add non-Java file formatting support, like we have internally at Picnic.
(I.e., somehow open-source that stuff.)
- Add relevant "badges" at the top of this `README.md`.
- Auto-generate a website listing each of the checks, just like the Error Prone
[bug patterns page][error-prone-bug-patterns]. The [Error Prone
repository][error-prone-repo] contains code for this.
@@ -43,7 +93,7 @@ project:
- Improve an existing check (see `XXX`-marked comments in the code) or write a
new one (see the list of suggestions below).
### BugChecker extension ideas
### Ideas for new checks
The following is a list of checks we'd like to see implemented:
@@ -53,7 +103,7 @@ The following is a list of checks we'd like to see implemented:
signature groups. Using Error Prone's method matchers forbidden method calls
can easily be identified. But Error Prone can go one step further by
auto-patching violations. For each violation two fixes can be proposed: a
purely behavior-preserving fix, which makes the platform-dependent behavior
purely behavior-preserving fix which makes the platform-dependent behavior
explicit, and another which replaces the platform-dependent behavior with the
preferred alternative. (Such as using `UTF-8` instead of the system default
charset.)
@@ -63,125 +113,128 @@ The following is a list of checks we'd like to see implemented:
functionality.
- A subset of the refactor operations provided by the Eclipse-specific
[AutoRefactor][autorefactor] plugin.
- A check that replaces fully qualified types with simple types in contexts
- A check which replaces fully qualified types with simple types in contexts
where this does not introduce ambiguity. Should consider both actual Java
code and Javadoc `@link` references.
- A check that simplifies array expressions. It would replace empty array
- A check which simplifies array expressions. It would replace empty array
expressions of the form `new int[] {}` with `new int[0]`. Statements of the
form `byte[] arr = new byte[] {'c'};` would be shortened to `byte[] arr =
{'c'};`.
- A check that replaces expressions of the form `String.format("some prefix
- A check which replaces expressions of the form `String.format("some prefix
%s", arg)` with `"some prefix " + arg`, and similar for simple suffixes. Can
perhaps be generalized further, though it's unclear how far. (Well, a
`String.format` call without arguments can certainly be simplified, too.)
- A check that replaces single-character strings with `char`s where possible.
- A check which replaces single-character strings with `char`s where possible.
For example as argument to `StringBuilder.append` and in string
concatenations.
- A check that adds or removes the first `Locale` argument to `String.format`
- A check which adds or removes the first `Locale` argument to `String.format`
and similar calls as necessary. (For example, a format string containing only
`%s` placeholders is locale-insensitive unless any of the arguments is a
`Formattable`, while `%f` placeholders _are_ locale-sensitive.)
- A check that replaces `String.replaceAll` with `String.replace` if the first
- A check which replaces `String.replaceAll` with `String.replace` if the first
argument is certainly not a regular expression. And if both arguments are
single-character strings then the `(char, char)` overload can be invoked
instead.
- A check that flags (and ideally, replaces) `try-finally` constructs with
- A check which flags (and ideally, replaces) `try-finally` constructs with
equivalent `try-with-resources` constructs.
- A check that drops exceptions declared in `throws` clauses if they are (a)
- A check which drops exceptions declared in `throws` clauses if they are (a)
not actually thrown and (b) the associated method cannot be overridden.
- A check that tries to statically import certain methods whenever used, if
- A check which tries to statically import certain methods whenever used, if
possible. The set of targeted methods should be configurable, but may default
to e.g. `java.util.Function.identity()`, the static methods exposed by
`java.util.stream.Collectors` and the various Guava collector factory
methods.
- A check that replaces `new Random().someMethod()` calls with
`ThreadLocalRandom.current().someMethod()` calls, to avoid unnecessary
- A check which replaces `new Random().someMethod()` calls with
`ThreadLocalRandom.current().someMethod()` calls, so as to avoid unnecessary
synchronization.
- A check that drops `this.` from `this.someMethod()` calls and which
- A check which drops `this.` from `this.someMethod()` calls and which
optionally does the same for fields, if no ambiguity arises.
- A check that replaces `Integer.valueOf` calls with `Integer.parseInt` or vice
versa in order to prevent auto (un)boxing, and likewise for other number
- A check which replaces `Integer.valueOf` calls with `Integer.parseInt` or
vice versa in order to prevent auto (un)boxing, and likewise for other number
types.
- A check that flags nullable collections.
- A check that flags `AutoCloseable` resources not managed by a
- A check which flags nullable collections.
- A check which flags `AutoCloseable` resources not managed by a
`try-with-resources` construct. Certain subtypes, such as jOOQ's `DSLContext`
should be excluded.
- A check that flags `java.time` methods which implicitly consult the system
- A check which flags `java.time` methods which implicitly consult the system
clock, suggesting that a passed-in `Clock` is used instead.
- A check that flags public methods on public classes which reference
- A check which flags public methods on public classes which reference
non-public types. This can cause `IllegalAccessError`s and
`BootstrapMethodError`s at runtime.
- A check that swaps the LHS and RHS in expressions of the form
- A check which swaps the LHS and RHS in expressions of the form
`nonConstant.equals(someNonNullConstant)`.
- A check that annotates methods which only throw an exception with
- A check which annotates methods which only throw an exception with
`@Deprecated` or ` @DoNotCall`.
- A check that flags imports from other test classes.
- A Guava-specific check that replaces `Joiner.join` calls with `String.join`
- A check which flags imports from other test classes.
- A Guava-specific check which replaces `Joiner.join` calls with `String.join`
calls in those cases where the latter is a proper substitute for the former.
- A Guava-specific check that flags `{Immutable,}Multimap` type usages where
`{Immutable,}{List,Set}Multimap` would be more appropriate.
- A Guava-specific check that rewrites `if (conditional) { throw new
- A Guava-specific check which flags `{Immutable,}Multimap` type usages
where `{Immutable,}{List,Set}Multimap` would be more appropriate.
- A Guava-specific check which rewrites `if (conditional) { throw new
IllegalArgumentException(); }` and variants to an equivalent `checkArgument`
statement. Idem for other exception types.
- A Guava-specific check that replaces simple anonymous `CacheLoader` subclass
- A Guava-specific check which replaces simple anonymous `CacheLoader` subclass
declarations with `CacheLoader.from(someLambda)`.
- A Spring-specific check that enforces that `@RequestMapping` annotations,
- A Spring-specific check which enforces that methods with the `@Scheduled`
annotation are also annotated with New Relic's `@Trace` annotation. Such
methods should ideally not also represent Spring MVC endpoints.
- A Spring-specific check which enforces that `@RequestMapping` annotations,
when applied to a method, explicitly specify one or more target HTTP methods.
- A Spring-specific check that looks for classes in which all `@RequestMapping`
annotations (and the various aliases) specify the same `path`/`value`
property and then moves that path to the class level.
- A Spring-specific check that flags `@Value("some.property")` annotations, as
- A Spring-specific check which looks for classes in which all
`@RequestMapping` annotations (and the various aliases) specify the same
`path`/`value` property and then moves that path to the class level.
- A Spring-specific check which flags `@Value("some.property")` annotations, as
these almost certainly should be `@Value("${some.property}")`.
- A Spring-specific check that drops the `required` attribute from
- A Spring-specific check which drops the `required` attribute from
`@RequestParam` annotations when the `defaultValue` attribute is also
specified.
- A Spring-specific check that rewrites a class which uses field injection to
- A Spring-specific check which rewrites a class which uses field injection to
one which uses constructor injection. This check wouldn't be strictly
behavior preserving, but could be used for a one-off code base migration.
- A Spring-specific check that disallows field injection, except in
- A Spring-specific check which disallows field injection, except in
`AbstractTestNGSpringContextTests` subclasses. (One known edge case:
self-injections so that a bean can call itself through an implicit proxy.)
- A Spring-specific check that verifies that public methods on all classes
- A Spring-specific check which verifies that public methods on all classes
whose name matches a certain pattern, e.g. `.*Service`, are annotated
`@Secured`.
- A Spring-specific check that verifies that annotations such as
- A Spring-specific check which verifies that annotations such as
`@RequestParam` are only present in `@RestController` classes.
- A Spring-specific check that disallows `@ResponseStatus` on MVC endpoint
- A Spring-specific check which disallows `@ResponseStatus` on MVC endpoint
methods, as this prevents communication of error status codes.
- A Hibernate Validator-specific check that looks for `@UnwrapValidatedValue`
- A Hibernate Validator-specific check which looks for `@UnwrapValidatedValue`
usages and migrates the associated constraint annotations to the generic type
argument to which they (are presumed to) apply.
- A TestNG-specific check that drops method-level `@Test` annotations if a
- A TestNG-specific check which drops method-level `@Test` annotations if a
matching/more specific annotation is already present at the class level.
- A TestNG-specific check that enforces that all tests are in a group.
- A TestNG-specific check that flags field assignments in
- A TestNG-specific check which enforces that all tests are in a group.
- A TestNG-specific check which flags field assignments in
`@BeforeMethod`-annotated methods unless the class is annotated
`@Test(singleThreaded = true)`.
- A TestNG-specific check that flags usages of the `expectedExceptions`
- A TestNG-specific check which flags usages of the `expectedExceptions`
attribute of the `@Test` annotation, pointing to `assertThrows`.
- A Jongo-specific check that disallows the creation of sparse indices, in
- A Jongo-specific check which disallows the creation of sparse indices, in
favour of partial indices.
- An Immutables-specific check that replaces
- An Immutables-specific check which replaces
`checkState`/`IllegalStateException` usages inside a `@Value.Check`-annotated
method with `checkArgument`/`IllegalArgument`, since the method is invoked
when a caller attempts to create an immutable instance.
- An Immutables-specific check that disallows references to collection types
- An Immutables-specific check which disallows references to collection types
other than the Guava immutable collections, including inside generic type
arguments.
- An SLF4J-specific check that drops or adds a trailing dot from log messages,
- An SLF4J-specific check which drops or adds a trailing dot from log messages,
as applicable.
- A Mockito-specific check that identifies sequences of statements which mock a
significant number of methods on a single object with "default data"; such
- A Mockito-specific check which identifies sequences of statements which mock
a significant number of methods on a single object with "default data"; such
constructions can often benefit from a different type of default answer, such
as `Answers.RETURNS_MOCKS`.
- An RxJava-specific check that flags `.toCompletable()` calls on expressions
- An RxJava-specific check which flags `.toCompletable()` calls on expressions
of type `Single<Completable>` etc., as most likely
`.flatMapCompletable(Functions.identity())` was meant instead. Idem for other
variations.
- An RxJava-specific check that flags `expr.firstOrError()` calls and suggests
- An RxJava-specific check which flags `expr.firstOrError()` calls and suggests
`expr.switchIfEmpty(Single.error(...))`, so that an application-specific
exception is thrown instead of `NoSuchElementException`.
- An RxJava-specific check that flags use of `#assertValueSet` without
- An RxJava-specific check which flags use of `#assertValueSet` without
`#assertValueCount`, as the former method doesn't do what one may intuitively
expect it to do. See ReactiveX/RxJava#6151.
@@ -194,7 +247,6 @@ but on the flip side Refaster is much less expressive. While this gap can never
be fully closed, there are some ways in which Refaster's scope of utility could
be extended. The following is a non-exhaustive list of ideas on how to extend
Refaster's expressiveness:
- Allow more control over _which_ methods are statically imported by
`@UseImportPolicy`. Sometimes the `@AfterTemplate` contains more than one
static method invocation, and only a subset should be statically imported.
@@ -209,9 +261,9 @@ Refaster's expressiveness:
pure expression. Introduce a way to express such a constraint. For example,
rewriting `optional1.map(Optional::of).orElse(optional2)` to `optional1.or(()
-> optional2)` is not behavior preserving if evaluation of `optional2` has
side effects.
side-effects.
- Similarly, certain refactoring operations are only valid if one of the
matched expressions is not `@Nullable`. It'd be nice to be able to express
matches expressions is not `@Nullable`. It'd be nice to be able to express
this.
- Generalize `@Placeholder` support such that rules can reference e.g. "any
concrete unary method". This would allow refactorings such as
@@ -232,49 +284,59 @@ Refaster's expressiveness:
- Provide a way to express transformations of compile-time constants. This
would allow one to e.g. rewrite single-character strings to chars or vice
versa, thereby accommodating a target API. Another example would be to
replace SLF4J's `{}` placeholders with `%s` or vice versa. Yet another
replace SLF4J's `{}` place holders with `%s` or vice versa. Yet another
example would be to rewrite `BigDecimal.valueOf("<some-long-value>")` to
`BigDecimal.valueOf(theParsedLongValue)`.
- More generally, investigate ways to plug in fully dynamic behavior, e.g. by
providing hooks which enable plugging in arbitrary
predicates/transformations. The result would be a Refaster/`BugChecker`
hybrid. A feature like this could form the basis for many other features
listed here. (As a concrete example, consider the ability to reference
- More generally, investigate ways to plug in in fully dynamic behavior, e.g.
by providing hooks using which arbitrary predicates/transformations can be
plugged in. The result would be a Refaster/`BugChecker` hybrid. A feature
such as this could form the basis for many other features listed here. (As a
concrete example, consider the ability to reference
`com.google.errorprone.matchers.Matcher` implementations.)
- Provide an extension API that enables defining methods or expressions based
on functional properties. A motivating example is the Java Collections
- Provide a way to match lambda expressions and method references which match a
specified functional interface. This would allow rewrites such as
`Mono.fromCallable(this::doesNotThrowCheckException)` ->
`Mono.fromSupplier(this::doesNotThrowCheckException)`.
- Provide an extension API using which methods or expressions can be defined
based on functional properties. A motivating example is the Java Collections
framework, which allows many ways to define (im)mutable (un)ordered
collections with(out) duplicates. One could then express things like "match
any method call that collects its inputs into an immutable ordered list". An
any method call with collects its inputs into an immutable ordered list". An
enum analogous to `java.util.stream.Collector.Characteristics` could be used.
Out of the box JDK and Guava collection factory methods could be classified,
with the user having the option to extend the classification.
- Refaster currently unconditionally ignores expressions containing comments.
Provide two additional modes: (a) match and drop the comments or (b)
transport the comments to before/after the replaced expression.
- Extend Refaster to drop imports that become unnecessary as a result of a
refactoring. This e.g. allows one to replace a statically imported TestNG
- Extend Refaster to drop imports that come become unnecessary as a result of a
refactoring. This e.g. allows one to replace a statically import TestNG
`fail(...)` invocation with a statically imported equivalent AssertJ
`fail(...)` invocation. (Observe that without an import cleanup this
`fail(...)` invocation. (Observe that without an impor cleanup this
replacement would cause a compilation error.)
- Extend the `@Repeated` match semantics such that it also covers non-varargs
methods. For a motivating example see google/error-prone#568.
- When matching explicit type references, also match super types. For a
motivating example, see the two subtly different loop definitions in
`CollectionRemoveAllFromCollectionExpression`.
motivating example, see the two subtly difference loop definitions in
`CollectionRemoveAllFromCollectionBlock`.
- Figure out why Refaster sometimes doesn't match the correct generic overload.
See the `AssertThatIterableHasOneComparableElementEqualTo` rule for an
See the `AssertThatIterableHasOneComparableElementEqualTo` template for an
example.
[autorefactor]: https://autorefactor.org
[bettercodehub]: https://bettercodehub.com
[checkstyle-external-project-tests]: https://github.com/checkstyle/checkstyle/blob/master/wercker.yml
[codecov]: https://codecov.io
[error-prone-bug-patterns]: https://errorprone.info/bugpatterns
[error-prone-criteria]: https://errorprone.info/docs/criteria
[error-prone-fork-jitpack]: https://jitpack.io/#PicnicSupermarket/error-prone
[error-prone-fork-repo]: https://github.com/PicnicSupermarket/error-prone
[error-prone]: https://errorprone.info
[error-prone-repo]: https://github.com/google/error-prone
[forbidden-apis]: https://github.com/policeman-tools/forbidden-apis
[fossa]: https://fossa.io
[google-java-format]: https://github.com/google/google-java-format
[main-contributing]: ../CONTRIBUTING.md
[main-readme]: ../README.md
[maven]: https://maven.apache.org
[modernizer-maven-plugin]: https://github.com/gaul/modernizer-maven-plugin
[sonarcloud]: https://sonarcloud.io
[pitest]: https://pitest.org
[pitest-maven]: https://pitest.org/quickstart/maven

View File

@@ -5,14 +5,13 @@
<parent>
<groupId>tech.picnic.error-prone-support</groupId>
<artifactId>error-prone-support</artifactId>
<version>0.19.2-SNAPSHOT</version>
<version>0.1.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,29 +37,12 @@
<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>
<artifactId>refaster-support</artifactId>
<!-- XXX: One would expect this to be a `provided` dependency (as
Refaster rules are interpreted by the `refaster-runner` module),
but the `OptionalOrElseGet` bug checker defined by this module
depends on the `RequiresComputation` matcher that
`refaster-support` primarily exposes for use by Refaster rules.
Review this setup. (Should the matchers be moved elsewhere?) -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
@@ -70,11 +52,11 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<scope>provided</scope>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<groupId>com.google.auto</groupId>
<artifactId>auto-common</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
@@ -83,19 +65,28 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value-annotations</artifactId>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>javac</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<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>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<scope>provided</scope>
<groupId>com.newrelic.agent.java</groupId>
<artifactId>newrelic-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
@@ -112,11 +103,6 @@
<artifactId>reactor-adapter</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.projectreactor.addons</groupId>
<artifactId>reactor-extra</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava2</groupId>
<artifactId>rxjava</artifactId>
@@ -137,21 +123,16 @@
<artifactId>jakarta.servlet-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
@@ -167,18 +148,11 @@
<artifactId>value-annotations</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>provided</scope>
<scope>test</scope>
</dependency>
<!-- XXX: Explicitly declared as a workaround for
https://github.com/pitest/pitest-junit5-plugin/issues/105. -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
@@ -194,36 +168,6 @@
<artifactId>mockito-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-java</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-java-17</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>
@@ -236,8 +180,8 @@
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<scope>test</scope>
<artifactId>spring-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
@@ -259,11 +203,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>
@@ -279,14 +218,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>
@@ -299,58 +230,20 @@
</path>
</annotationProcessorPaths>
<compilerArgs combine.children="append">
<arg>-Xplugin:DocumentationGenerator -XoutputDirectory=${project.build.directory}/docs</arg>
<arg>-Xplugin:RefasterRuleCompiler</arg>
</compilerArgs>
</configuration>
<executions>
<!-- The Refaster input/output test classes used by
`RefasterRuleCollection` are modelled as classpath
resources, and thus not subject to the default test
compilation step. These two custom compilation steps
serve two purposes:
- To provide early feedback in case of syntax errors.
- To enable the `DocumentationGenerator` compiler
plugin to extract documentation metadata from them.
Note that the input and output files must be compiled
separately and to distinct output directories, as they
define the same set of class names. -->
<!-- XXX: Drop these executions if/when the Refaster
test framework is reimplemented such that tests can be
located alongside rules, rather than in two additional
resource files. -->
<execution>
<id>compile-refaster-test-input</id>
<goals>
<goal>testCompile</goal>
</goals>
<phase>process-test-resources</phase>
<configuration>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/test/resources</compileSourceRoot>
</compileSourceRoots>
<testIncludes>
<testInclude>**/*Input.java</testInclude>
</testIncludes>
<outputDirectory>${project.build.directory}/refaster-test-input</outputDirectory>
</configuration>
</execution>
<execution>
<id>compile-refaster-test-output</id>
<goals>
<goal>testCompile</goal>
</goals>
<phase>process-test-resources</phase>
<configuration>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/test/resources</compileSourceRoot>
</compileSourceRoots>
<testIncludes>
<testInclude>**/*Output.java</testInclude>
</testIncludes>
<outputDirectory>${project.build.directory}/refaster-test-output</outputDirectory>
</configuration>
</execution>
</executions>
</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>

View File

@@ -1,10 +1,9 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
import static com.google.errorprone.matchers.Matchers.isType;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
@@ -18,17 +17,16 @@ 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;
/** A {@link BugChecker} that flags ambiguous {@code @JsonCreator}s in enums. */
/** A {@link BugChecker} which flags ambiguous {@code @JsonCreator}s in enums. */
@AutoService(BugChecker.class)
@BugPattern(
summary = "`JsonCreator.Mode` should be set for single-argument creators",
link = BUG_PATTERNS_BASE_URL + "AmbiguousJsonCreator",
linkType = CUSTOM,
linkType = NONE,
severity = WARNING,
tags = LIKELY_ERROR)
public final class AmbiguousJsonCreator extends BugChecker implements AnnotationTreeMatcher {
@@ -36,9 +34,6 @@ public final class AmbiguousJsonCreator extends BugChecker implements Annotation
private static final Matcher<AnnotationTree> IS_JSON_CREATOR_ANNOTATION =
isType("com.fasterxml.jackson.annotation.JsonCreator");
/** Instantiates a new {@link AmbiguousJsonCreator} instance. */
public AmbiguousJsonCreator() {}
@Override
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
if (!IS_JSON_CREATOR_ANNOTATION.matches(tree, state)) {
@@ -46,7 +41,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

@@ -1,6 +1,6 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.Matchers.allOf;
@@ -8,7 +8,6 @@ import static com.google.errorprone.matchers.Matchers.argument;
import static com.google.errorprone.matchers.Matchers.argumentCount;
import static com.google.errorprone.matchers.Matchers.instanceMethod;
import static com.google.errorprone.matchers.Matchers.nullLiteral;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
@@ -22,9 +21,9 @@ import com.google.errorprone.matchers.Matcher;
import com.sun.source.tree.MethodInvocationTree;
/**
* A {@link BugChecker} that flags AssertJ {@code isEqualTo(null)} checks for simplification.
* A {@link BugChecker} which flags AssertJ {@code isEqualTo(null)} checks for simplification.
*
* <p>This bug checker cannot be replaced with a simple Refaster rule, as the Refaster approach
* <p>This bug checker cannot be replaced with a simple Refaster template, as the Refaster approach
* would require that all overloads of {@link org.assertj.core.api.Assert#isEqualTo(Object)} (such
* as {@link org.assertj.core.api.AbstractStringAssert#isEqualTo(String)}) are explicitly
* enumerated. This bug checker generically matches all such current and future overloads.
@@ -32,8 +31,7 @@ import com.sun.source.tree.MethodInvocationTree;
@AutoService(BugChecker.class)
@BugPattern(
summary = "Prefer `.isNull()` over `.isEqualTo(null)`",
link = BUG_PATTERNS_BASE_URL + "AssertJIsNull",
linkType = CUSTOM,
linkType = NONE,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class AssertJIsNull extends BugChecker implements MethodInvocationTreeMatcher {
@@ -44,9 +42,6 @@ public final class AssertJIsNull extends BugChecker implements MethodInvocationT
argumentCount(1),
argument(0, nullLiteral()));
/** Instantiates a new {@link AssertJIsNull} instance. */
public AssertJIsNull() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!ASSERT_IS_EQUAL_TO_NULL.matches(tree, state)) {

View File

@@ -1,36 +1,34 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
import static com.google.errorprone.matchers.Matchers.annotations;
import static com.google.errorprone.matchers.Matchers.isType;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.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. */
/** A {@link BugChecker} which flags redundant {@code @Autowired} constructor annotations. */
@AutoService(BugChecker.class)
@BugPattern(
summary = "Omit `@Autowired` on a class' sole constructor, as it is redundant",
link = BUG_PATTERNS_BASE_URL + "AutowiredConstructor",
linkType = CUSTOM,
linkType = NONE,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class AutowiredConstructor extends BugChecker implements ClassTreeMatcher {
@@ -38,9 +36,6 @@ public final class AutowiredConstructor extends BugChecker implements ClassTreeM
private static final MultiMatcher<Tree, AnnotationTree> AUTOWIRED_ANNOTATION =
annotations(AT_LEAST_ONE, isType("org.springframework.beans.factory.annotation.Autowired"));
/** Instantiates a new {@link AutowiredConstructor} instance. */
public AutowiredConstructor() {}
@Override
public Description matchClass(ClassTree tree, VisitorState state) {
List<MethodTree> constructors = ASTHelpers.getConstructors(tree);
@@ -48,18 +43,20 @@ public final class AutowiredConstructor extends BugChecker implements ClassTreeM
return Description.NO_MATCH;
}
MultiMatchResult<AnnotationTree> hasAutowiredAnnotation =
AUTOWIRED_ANNOTATION.multiMatchResult(Iterables.getOnlyElement(constructors), state);
if (!hasAutowiredAnnotation.matches()) {
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

@@ -1,10 +1,11 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
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 java.util.function.Predicate.not;
import com.google.auto.common.AnnotationMirrors;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.BugPattern;
@@ -15,24 +16,31 @@ import com.google.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.AnnotationMatcherUtils;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.predicates.TypePredicate;
import com.google.errorprone.predicates.TypePredicates;
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.NewArrayTree;
import com.sun.source.tree.Tree.Kind;
import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.Symbol;
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 java.util.stream.Stream;
import javax.lang.model.element.AnnotationValue;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/** A {@link BugChecker} that flags annotations that could be written more concisely. */
/** A {@link BugChecker} which flags annotations that could be written more concisely. */
@AutoService(BugChecker.class)
@BugPattern(
summary = "Omit redundant syntax from annotation declarations",
link = BUG_PATTERNS_BASE_URL + "CanonicalAnnotationSyntax",
linkType = CUSTOM,
linkType = NONE,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class CanonicalAnnotationSyntax extends BugChecker implements AnnotationTreeMatcher {
@@ -41,13 +49,11 @@ public final class CanonicalAnnotationSyntax extends BugChecker implements Annot
private static final ImmutableSet<BiFunction<AnnotationTree, VisitorState, Optional<Fix>>>
FIX_FACTORIES =
ImmutableSet.of(
CanonicalAnnotationSyntax::useSpringAlias,
CanonicalAnnotationSyntax::dropRedundantParentheses,
CanonicalAnnotationSyntax::dropRedundantValueAttribute,
CanonicalAnnotationSyntax::dropRedundantCurlies);
/** Instantiates a new {@link CanonicalAnnotationSyntax} instance. */
public CanonicalAnnotationSyntax() {}
@Override
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
return FIX_FACTORIES.stream()
@@ -58,6 +64,69 @@ public final class CanonicalAnnotationSyntax extends BugChecker implements Annot
.orElse(Description.NO_MATCH);
}
private static Optional<Fix> useSpringAlias(AnnotationTree tree, VisitorState state) {
boolean canDropAttributeName = tree.getArguments().size() == 1;
// XXX: The reduce is repeated. Fix.
return tree.getArguments().stream()
.flatMap(arg -> useBetterAlias(arg, canDropAttributeName, state).stream())
.reduce(SuggestedFix.Builder::merge)
.map(SuggestedFix.Builder::build);
}
private static Optional<SuggestedFix.Builder> useBetterAlias(
ExpressionTree tree, boolean canDropAttributeName, VisitorState state) {
if (tree.getKind() != Kind.ASSIGNMENT) {
return Optional.empty();
}
AssignmentTree assignment = (AssignmentTree) tree;
ExpressionTree element = assignment.getVariable();
if (canDropAttributeName == "value".equals(SourceCode.treeToString(element, state))) {
return Optional.empty();
}
return getAlias(element, state)
.map(
alias ->
"value".equals(alias)
? SuggestedFix.builder()
.replace(
assignment, SourceCode.treeToString(assignment.getExpression(), state))
: SuggestedFix.builder().replace(element, alias));
}
// XXX: Also add support for non-default `AliasFor#annotation` values!
private static Optional<String> getAlias(ExpressionTree tree, VisitorState state) {
Symbol sym = ASTHelpers.getSymbol(tree);
if (sym == null) {
return Optional.empty();
}
// XXX: Extract.
TypePredicate isAliasFor =
TypePredicates.isExactType("org.springframework.core.annotation.AliasFor");
TypePredicate isAnnotation = TypePredicates.isExactType("java.lang.annotation.Annotation");
return sym.getAnnotationMirrors().stream()
.filter(m -> isAliasFor.apply(m.type, state))
.filter(
m ->
isAnnotation.apply(
((Attribute.Class) AnnotationMirrors.getAnnotationValue(m, "annotation"))
.classType,
state))
.flatMap(
m ->
Stream.of(
AnnotationMirrors.getAnnotationValue(m, "value"),
AnnotationMirrors.getAnnotationValue(m, "attribute")))
.map(AnnotationValue::getValue)
.map(Object::toString)
.filter(not(String::isEmpty))
.findFirst();
}
private static Optional<Fix> dropRedundantParentheses(AnnotationTree tree, VisitorState state) {
if (!tree.getArguments().isEmpty()) {
/* Parentheses are necessary. */
@@ -90,8 +159,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 +187,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 +198,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

@@ -1,72 +0,0 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.LambdaExpressionTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.TypeCastTree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Type;
import tech.picnic.errorprone.utils.SourceCode;
/**
* A {@link BugChecker} that flags lambda expressions that can be replaced with a method reference
* of the form {@code T.class::cast}.
*/
// XXX: Consider folding this logic into the `MethodReferenceUsage` check of the
// `error-prone-experimental` module.
// XXX: This check and its tests are structurally nearly identical to `IsInstanceLambdaUsage`.
// Unless folded into `MethodReferenceUsage`, consider merging the two.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Prefer `Class::cast` method reference over equivalent lambda expression",
link = BUG_PATTERNS_BASE_URL + "ClassCastLambdaUsage",
linkType = CUSTOM,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class ClassCastLambdaUsage extends BugChecker implements LambdaExpressionTreeMatcher {
private static final long serialVersionUID = 1L;
/** Instantiates a new {@link ClassCastLambdaUsage} instance. */
public ClassCastLambdaUsage() {}
@Override
public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) {
if (tree.getParameters().size() != 1 || !(tree.getBody() instanceof TypeCastTree typeCast)) {
return Description.NO_MATCH;
}
Type type = ASTHelpers.getType(typeCast);
if (type == null || type.isParameterized() || type.isPrimitive()) {
/*
* The method reference syntax does not support casting to parameterized types. Additionally,
* `Class#cast` does not support the same range of type conversions between (boxed) primitive
* types as the cast operator.
*/
// XXX: Depending on the declared type of the value being cast, in some cases we _can_ rewrite
// primitive casts. Add support for this.
return Description.NO_MATCH;
}
VariableTree param = Iterables.getOnlyElement(tree.getParameters());
if (!ASTHelpers.getSymbol(param).equals(ASTHelpers.getSymbol(typeCast.getExpression()))) {
return Description.NO_MATCH;
}
return describeMatch(
tree,
SuggestedFix.replace(
tree, SourceCode.treeToString(typeCast.getType(), state) + ".class::cast"));
}
}

View File

@@ -1,15 +1,11 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
@@ -20,15 +16,10 @@ import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import tech.picnic.errorprone.utils.ThirdPartyLibrary;
/**
* A {@link BugChecker} that flags {@link Collector Collectors} that don't clearly express
* A {@link BugChecker} which flags {@link Collector Collectors} that don't clearly express
* (im)mutability.
*
* <p>Replacing such collectors with alternatives that produce immutable collections is preferred.
@@ -37,15 +28,14 @@ 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",
link = BUG_PATTERNS_BASE_URL + "CollectorMutability",
linkType = CUSTOM,
"Avoid `Collectors.to{List,Map,Set}` in favour of alternatives that emphasize (im)mutability",
linkType = NONE,
severity = WARNING,
tags = FRAGILE_CODE)
public final class CollectorMutability extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> COLLECTOR_METHOD =
staticMethod().onClass(Collectors.class.getCanonicalName());
staticMethod().onClass("java.util.stream.Collectors");
private static final Matcher<ExpressionTree> LIST_COLLECTOR =
staticMethod().anyClass().named("toList");
private static final Matcher<ExpressionTree> MAP_COLLECTOR =
@@ -53,22 +43,15 @@ public final class CollectorMutability extends BugChecker implements MethodInvoc
private static final Matcher<ExpressionTree> SET_COLLECTOR =
staticMethod().anyClass().named("toSet");
/** Instantiates a new {@link CollectorMutability} instance. */
public CollectorMutability() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!ThirdPartyLibrary.GUAVA.isIntroductionAllowed(state)
|| !COLLECTOR_METHOD.matches(tree, state)) {
if (!COLLECTOR_METHOD.matches(tree, state)) {
return Description.NO_MATCH;
}
if (LIST_COLLECTOR.matches(tree, state)) {
return suggestToCollectionAlternatives(
tree,
ImmutableList.class.getCanonicalName() + ".toImmutableList",
ArrayList.class.getCanonicalName(),
state);
tree, "com.google.common.collect.ImmutableList.toImmutableList", "ArrayList", state);
}
if (MAP_COLLECTOR.matches(tree, state)) {
@@ -77,10 +60,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 +68,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 +92,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,125 +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.STYLE;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.hasModifier;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
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.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.VariableTree;
import com.sun.source.util.TreeScanner;
import java.util.Locale;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.lang.model.element.Modifier;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.utils.Flags;
/**
* A {@link BugChecker} that flags static constants that do not follow the upper snake case naming
* convention.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Constant variables should adhere to the `UPPER_SNAKE_CASE` naming convention",
link = BUG_PATTERNS_BASE_URL + "ConstantNaming",
linkType = CUSTOM,
severity = WARNING,
tags = STYLE)
@SuppressWarnings("java:S2160" /* Super class equality definition suffices. */)
public final class ConstantNaming extends BugChecker implements VariableTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<VariableTree> IS_CONSTANT =
allOf(hasModifier(Modifier.STATIC), hasModifier(Modifier.FINAL));
private static final Matcher<VariableTree> IS_PRIVATE = hasModifier(Modifier.PRIVATE);
private static final Pattern SNAKE_CASE = Pattern.compile("([a-z])([A-Z])");
private static final ImmutableSet<String> DEFAULT_EXEMPTED_NAMES =
ImmutableSet.of("serialVersionUID");
/**
* Flag using which constant names that must not be flagged (in addition to those defined by
* {@link #DEFAULT_EXEMPTED_NAMES}) can be specified.
*/
private static final String ADDITIONAL_EXEMPTED_NAMES_FLAG =
"CanonicalConstantNaming:ExemptedNames";
private final ImmutableSet<String> exemptedNames;
/** Instantiates a default {@link ConstantNaming} instance. */
public ConstantNaming() {
this(ErrorProneFlags.empty());
}
/**
* Instantiates a customized {@link ConstantNaming}.
*
* @param flags Any provided command line flags.
*/
@Inject
ConstantNaming(ErrorProneFlags flags) {
exemptedNames =
Sets.union(DEFAULT_EXEMPTED_NAMES, Flags.getSet(flags, ADDITIONAL_EXEMPTED_NAMES_FLAG))
.immutableCopy();
}
@Override
public Description matchVariable(VariableTree tree, VisitorState state) {
String variableName = tree.getName().toString();
if (!IS_CONSTANT.matches(tree, state) || exemptedNames.contains(variableName)) {
return Description.NO_MATCH;
}
String replacement = toUpperSnakeCase(variableName);
if (replacement.equals(variableName)) {
return Description.NO_MATCH;
}
Description.Builder description = buildDescription(tree);
if (!IS_PRIVATE.matches(tree, state)) {
description.setMessage(
"%s; consider renaming to '%s', though note that this is not a private constant"
.formatted(message(), replacement));
} else if (isVariableNameInUse(replacement, state)) {
description.setMessage(
"%s; consider renaming to '%s', though note that a variable with this name is already declared"
.formatted(message(), replacement));
} else {
description.addFix(SuggestedFixes.renameVariable(tree, replacement, state));
}
return description.build();
}
private static String toUpperSnakeCase(String variableName) {
return SNAKE_CASE.matcher(variableName).replaceAll("$1_$2").toUpperCase(Locale.ROOT);
}
private static boolean isVariableNameInUse(String name, VisitorState state) {
return Boolean.TRUE.equals(
new TreeScanner<Boolean, @Nullable Void>() {
@Override
public Boolean visitVariable(VariableTree tree, @Nullable Void unused) {
return ASTHelpers.getSymbol(tree).getSimpleName().contentEquals(name)
|| super.visitVariable(tree, null);
}
@Override
public Boolean reduce(Boolean r1, Boolean r2) {
return Boolean.TRUE.equals(r1) || Boolean.TRUE.equals(r2);
}
}.scan(state.getPath().getCompilationUnit(), null));
}
}

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

@@ -1,19 +1,19 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
import static com.google.errorprone.matchers.Matchers.annotations;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.isType;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
@@ -21,14 +21,12 @@ 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. */
/** A {@link BugChecker} which flags empty methods that seemingly can simply be deleted. */
@AutoService(BugChecker.class)
@BugPattern(
summary = "Empty method can likely be deleted",
link = BUG_PATTERNS_BASE_URL + "EmptyMethod",
linkType = CUSTOM,
linkType = NONE,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class EmptyMethod extends BugChecker implements MethodTreeMatcher {
@@ -36,15 +34,9 @@ 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")));
/** Instantiates a new {@link EmptyMethod} instance. */
public EmptyMethod() {}
anyOf(isType("java.lang.Override"), isType("org.aspectj.lang.annotation.Pointcut")));
@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,11 +50,11 @@ 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) {
return Optional.ofNullable(state.findEnclosing(ClassTree.class))
return Optional.ofNullable(ASTHelpers.findEnclosingNode(state.getPath(), ClassTree.class))
.map(ClassTree::getSimpleName)
.filter(name -> name.toString().contains("Test"))
.isPresent();

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,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.LinkType.NONE;
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 com.google.auto.service.AutoService;
import com.google.common.base.Splitter;
@@ -32,7 +30,7 @@ import java.util.List;
import java.util.Optional;
/**
* A {@link BugChecker} that flags improperly formatted Error Prone test code.
* A {@link BugChecker} which flags improperly formatted Error Prone test code.
*
* <p>All test code should be formatted in accordance with Google Java Format's {@link Formatter}
* output, and imports should be ordered according to the {@link Style#GOOGLE Google} style.
@@ -52,8 +50,7 @@ import java.util.Optional;
@AutoService(BugChecker.class)
@BugPattern(
summary = "Test code should follow the Google Java style",
link = BUG_PATTERNS_BASE_URL + "ErrorProneTestHelperSourceFormat",
linkType = CUSTOM,
linkType = NONE,
severity = SUGGESTION,
tags = STYLE)
public final class ErrorProneTestHelperSourceFormat extends BugChecker
@@ -67,20 +64,12 @@ 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")
.named("addOutputLines");
/** Instantiates a new {@link ErrorProneTestHelperSourceFormat} instance. */
public ErrorProneTestHelperSourceFormat() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
boolean isOutputSource = OUTPUT_SOURCE_ACCEPTING_METHOD.matches(tree, state);
@@ -89,8 +78,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 +99,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 +126,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 +142,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

@@ -2,12 +2,11 @@ package tech.picnic.errorprone.bugpatterns;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
import static java.util.stream.Collectors.collectingAndThen;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableSet;
@@ -31,23 +30,19 @@ import java.util.Set;
import java.util.stream.Stream;
/**
* A {@link BugChecker} that flags {@link Ordering#explicit(Object, Object[])}} invocations listing
* A {@link BugChecker} which flags {@link Ordering#explicit(Object, Object[])}} invocations listing
* a subset of an enum type's values.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Make sure `Ordering#explicit` lists all of an enum's values",
link = BUG_PATTERNS_BASE_URL + "ExplicitEnumOrdering",
linkType = CUSTOM,
linkType = NONE,
severity = WARNING,
tags = FRAGILE_CODE)
public final class ExplicitEnumOrdering extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> EXPLICIT_ORDERING =
staticMethod().onClass(Ordering.class.getCanonicalName()).named("explicit");
/** Instantiates a new {@link ExplicitEnumOrdering} instance. */
public ExplicitEnumOrdering() {}
staticMethod().onClass(Ordering.class.getName()).named("explicit");
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
@@ -72,7 +67,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),
@@ -89,6 +84,6 @@ public final class ExplicitEnumOrdering extends BugChecker implements MethodInvo
private static Stream<String> getMissingEnumValues(Type enumType, Set<String> values) {
Symbol.TypeSymbol typeSymbol = enumType.asElement();
return Sets.difference(ASTHelpers.enumValues(typeSymbol), values).stream()
.map(v -> String.join(".", typeSymbol.getSimpleName(), v));
.map(v -> String.format("%s.%s", typeSymbol.getSimpleName(), v));
}
}

View File

@@ -1,14 +1,9 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import static tech.picnic.errorprone.utils.MoreTypes.generic;
import static tech.picnic.errorprone.utils.MoreTypes.subOf;
import static tech.picnic.errorprone.utils.MoreTypes.type;
import static tech.picnic.errorprone.utils.MoreTypes.unbound;
import com.google.auto.service.AutoService;
import com.google.common.collect.Iterables;
@@ -21,18 +16,15 @@ import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.suppliers.Supplier;
import com.google.errorprone.suppliers.Suppliers;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MemberReferenceTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.tools.javac.code.Type;
import java.util.function.Function;
import java.util.function.Supplier;
import reactor.core.publisher.Flux;
/**
* A {@link BugChecker} that flags usages of {@link Flux#flatMap(Function)} and {@link
* A {@link BugChecker} which flags usages of {@link Flux#flatMap(Function)} and {@link
* Flux#flatMapSequential(Function)}.
*
* <p>{@link Flux#flatMap(Function)} and {@link Flux#flatMapSequential(Function)} eagerly perform up
@@ -40,40 +32,29 @@ import reactor.core.publisher.Flux;
* former interleaves values as they are emitted, yielding nondeterministic results. In most cases
* {@link Flux#concatMap(Function)} should be preferred, as it produces consistent results and
* avoids potentially saturating the thread pool on which subscription happens. If {@code
* concatMap}'s sequential-subscription semantics are undesirable one should invoke a {@code
* flatMap} or {@code flatMapSequential} overload with an explicit concurrency level.
* concatMap}'s single-subscription semantics are undesirable one should invoke a {@code flatMap} or
* {@code flatMapSequential} overload with an explicit concurrency level.
*
* <p>NB: The rarely-used overload {@link Flux#flatMap(Function, Function,
* java.util.function.Supplier)} is not flagged by this check because there is no clear alternative
* to point to.
* <p>NB: The rarely-used overload {@link Flux#flatMap(Function, Function, Supplier)} is not flagged
* by this check because there is no clear alternative to point to.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary =
"""
`Flux#flatMap` and `Flux#flatMapSequential` have subtle semantics; please use \
`Flux#concatMap` or explicitly specify the desired amount of concurrency""",
link = BUG_PATTERNS_BASE_URL + "FluxFlatMapUsage",
linkType = CUSTOM,
"`Flux#flatMap` and `Flux#flatMapSequential` have subtle semantics; "
+ "please use `Flux#concatMap` or explicitly specify the desired amount of concurrency",
linkType = NONE,
severity = ERROR,
tags = LIKELY_ERROR)
public final class FluxFlatMapUsage extends BugChecker
implements MethodInvocationTreeMatcher, MemberReferenceTreeMatcher {
private static final long serialVersionUID = 1L;
private static final String MAX_CONCURRENCY_ARG_NAME = "MAX_CONCURRENCY";
private static final Supplier<Type> FLUX =
Suppliers.typeFromString("reactor.core.publisher.Flux");
private static final Matcher<ExpressionTree> FLUX_FLATMAP =
instanceMethod()
.onDescendantOf(FLUX)
.onDescendantOf("reactor.core.publisher.Flux")
.namedAnyOf("flatMap", "flatMapSequential")
.withParameters(Function.class.getCanonicalName());
private static final Supplier<Type> FLUX_OF_PUBLISHERS =
VisitorState.memoize(
generic(FLUX, subOf(generic(type("org.reactivestreams.Publisher"), unbound()))));
/** Instantiates a new {@link FluxFlatMapUsage} instance. */
public FluxFlatMapUsage() {}
.withParameters(Function.class.getName());
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
@@ -81,25 +62,14 @@ public final class FluxFlatMapUsage extends BugChecker
return Description.NO_MATCH;
}
SuggestedFix serializationFix = SuggestedFixes.renameMethodInvocation(tree, "concatMap", state);
SuggestedFix concurrencyCapFix =
SuggestedFix.postfixWith(
Iterables.getOnlyElement(tree.getArguments()), ", " + MAX_CONCURRENCY_ARG_NAME);
Description.Builder description = buildDescription(tree);
if (state.getTypes().isSubtype(ASTHelpers.getType(tree), FLUX_OF_PUBLISHERS.get(state))) {
/*
* Nested publishers may need to be subscribed to eagerly in order to avoid a deadlock, e.g.
* if they are produced by `Flux#groupBy`. In this case we suggest specifying an explicit
* concurrently bound, in favour of sequential subscriptions using `Flux#concatMap`.
*/
description.addFix(concurrencyCapFix).addFix(serializationFix);
} else {
description.addFix(serializationFix).addFix(concurrencyCapFix);
}
return description.build();
return buildDescription(tree)
.addFix(SuggestedFixes.renameMethodInvocation(tree, "concatMap", state))
.addFix(
SuggestedFix.builder()
.postfixWith(
Iterables.getOnlyElement(tree.getArguments()), ", " + MAX_CONCURRENCY_ARG_NAME)
.build())
.build();
}
@Override

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

@@ -1,6 +1,6 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.Matchers.allOf;
@@ -10,11 +10,8 @@ import static com.google.errorprone.matchers.Matchers.instanceMethod;
import static com.google.errorprone.matchers.Matchers.not;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import static java.util.stream.Collectors.joining;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
@@ -32,16 +29,14 @@ 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.MoreASTHelpers;
import tech.picnic.errorprone.utils.SourceCode;
import javax.annotation.Nullable;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags string concatenations that produce a format string; in such cases
* the string concatenation should instead be deferred to the invoked method.
* A {@link BugChecker} which flags string concatenations that produce a format string; in such
* cases the string concatenation should instead be deferred to the invoked method.
*
* @implNote This checker is based on the implementation of {@link
* com.google.errorprone.bugpatterns.flogger.FloggerStringConcatenation}.
@@ -51,19 +46,17 @@ import tech.picnic.errorprone.utils.SourceCode;
// should introduce special handling of `Formattable` arguments, as this check would replace a
// `Formattable#toString` invocation with a `Formattable#formatTo` invocation. But likely that
// should be considered a bug fix, too.
// XXX: Introduce a separate check that adds/removes the `Locale` parameter to `String.format`
// invocations, as necessary. See also a comment in the `StringJoin` check.
// XXX: Introduce a separate check which adds/removes the `Locale` parameter to `String.format`
// invocations, as necessary.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Defer string concatenation to the invoked method",
link = BUG_PATTERNS_BASE_URL + "FormatStringConcatenation",
linkType = CUSTOM,
linkType = NONE,
severity = WARNING,
tags = SIMPLIFICATION)
public final class FormatStringConcatenation extends BugChecker
implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
/**
* AssertJ exposes varargs {@code fail} methods with a {@link Throwable}-accepting overload, the
* latter of which should not be flagged.
@@ -72,8 +65,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(
@@ -122,22 +114,19 @@ 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")
.namedAnyOf("debug", "error", "info", "trace", "warn");
/** Instantiates a new {@link FormatStringConcatenation} instance. */
public FormatStringConcatenation() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (hasNonConstantStringConcatenationArgument(tree, 0, state)) {
@@ -204,12 +193,16 @@ public final class FormatStringConcatenation extends BugChecker
ExpressionTree argument = ASTHelpers.stripParentheses(arguments.get(argPosition));
return argument instanceof BinaryTree
&& MoreASTHelpers.isStringTyped(argument, state)
&& isStringTyped(argument, state)
&& ASTHelpers.constValue(argument, String.class) == null;
}
private static boolean isStringTyped(ExpressionTree tree, VisitorState state) {
return ASTHelpers.isSameType(ASTHelpers.getType(tree), state.getSymtab().stringType, state);
}
private static class ReplacementArgumentsConstructor
extends SimpleTreeVisitor<@Nullable Void, VisitorState> {
extends SimpleTreeVisitor<Void, VisitorState> {
private final StringBuilder formatString = new StringBuilder();
private final List<Tree> formatArguments = new ArrayList<>();
private final String formatSpecifier;
@@ -218,9 +211,10 @@ public final class FormatStringConcatenation extends BugChecker
this.formatSpecifier = formatSpecifier;
}
@Nullable
@Override
public @Nullable Void visitBinary(BinaryTree tree, VisitorState state) {
if (tree.getKind() == Kind.PLUS && MoreASTHelpers.isStringTyped(tree, state)) {
public Void visitBinary(BinaryTree tree, VisitorState state) {
if (tree.getKind() == Kind.PLUS && isStringTyped(tree, state)) {
tree.getLeftOperand().accept(this, state);
tree.getRightOperand().accept(this, state);
} else {
@@ -230,20 +224,22 @@ public final class FormatStringConcatenation extends BugChecker
return null;
}
@Nullable
@Override
public @Nullable Void visitParenthesized(ParenthesizedTree tree, VisitorState state) {
public Void visitParenthesized(ParenthesizedTree tree, VisitorState state) {
return tree.getExpression().accept(this, state);
}
@Nullable
@Override
protected @Nullable Void defaultAction(Tree tree, VisitorState state) {
protected Void defaultAction(Tree tree, VisitorState state) {
appendExpression(tree);
return null;
}
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

@@ -1,26 +1,14 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import static com.google.errorprone.suppliers.Suppliers.OBJECT_TYPE;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.ImmutableRangeMap;
import com.google.common.collect.ImmutableRangeSet;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableTable;
import com.google.common.primitives.Primitives;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
@@ -31,71 +19,57 @@ 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.time.Instant;
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.
// XXX: Also flag redundant round-trip conversions such as `path.toFile().toPath()`.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Avoid or clarify identity conversions",
link = BUG_PATTERNS_BASE_URL + "IdentityConversion",
linkType = CUSTOM,
linkType = NONE,
severity = WARNING,
tags = SIMPLIFICATION)
public final class IdentityConversion extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> IS_CONVERSION_METHOD =
anyOf(
staticMethod()
.onClassAny(
"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(Instant.class.getCanonicalName()).namedAnyOf("from"),
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")
.namedAnyOf("concat", "firstWithSignal", "from", "merge"),
staticMethod().onClass("reactor.core.publisher.Mono").namedAnyOf("from", "fromDirect"));
/** Instantiates a new {@link IdentityConversion} instance. */
public IdentityConversion() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
List<? extends ExpressionTree> arguments = tree.getArguments();
@@ -116,20 +90,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

@@ -1,6 +1,6 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
import static com.google.errorprone.matchers.Matchers.allOf;
@@ -11,7 +11,6 @@ import static com.google.errorprone.matchers.Matchers.hasModifier;
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
import static com.google.errorprone.matchers.Matchers.methodReturns;
import static com.google.errorprone.matchers.Matchers.not;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
@@ -27,7 +26,7 @@ import java.util.SortedSet;
import javax.lang.model.element.Modifier;
/**
* A {@link BugChecker} that flags {@link SortedSet} property declarations inside
* A {@link BugChecker} which flags {@link SortedSet} property declarations inside
* {@code @Value.Immutable}- and {@code @Value.Modifiable}-annotated types that lack a
* {@code @Value.NaturalOrder} or {@code @Value.ReverseOrder} annotation.
*
@@ -43,11 +42,9 @@ 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`""",
link = BUG_PATTERNS_BASE_URL + "ImmutablesSortedSetComparator",
linkType = CUSTOM,
"`SortedSet` properties of a `@Value.Immutable` or `@Value.Modifiable` type must be "
+ "annotated with `@Value.NaturalOrder` or `@Value.ReverseOrder`",
linkType = NONE,
severity = ERROR,
tags = LIKELY_ERROR)
public final class ImmutablesSortedSetComparator extends BugChecker implements MethodTreeMatcher {
@@ -68,9 +65,6 @@ public final class ImmutablesSortedSetComparator extends BugChecker implements M
hasAnnotation("org.immutables.value.Value.NaturalOrder"),
hasAnnotation("org.immutables.value.Value.ReverseOrder"))));
/** Instantiates a new {@link ImmutablesSortedSetComparator} instance. */
public ImmutablesSortedSetComparator() {}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (!METHOD_LACKS_ANNOTATION.matches(tree, state)) {

View File

@@ -1,60 +0,0 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.LambdaExpressionTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.InstanceOfTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.VariableTree;
import tech.picnic.errorprone.utils.SourceCode;
/**
* A {@link BugChecker} that flags lambda expressions that can be replaced with a method reference
* of the form {@code T.class::isInstance}.
*/
// XXX: Consider folding this logic into the `MethodReferenceUsage` check of the
// `error-prone-experimental` module.
// XXX: This check and its tests are structurally nearly identical to `ClassCastLambdaUsage`. Unless
// folded into `MethodReferenceUsage`, consider merging the two.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Prefer `Class::isInstance` method reference over equivalent lambda expression",
link = BUG_PATTERNS_BASE_URL + "IsInstanceLambdaUsage",
linkType = CUSTOM,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class IsInstanceLambdaUsage extends BugChecker implements LambdaExpressionTreeMatcher {
private static final long serialVersionUID = 1L;
/** Instantiates a new {@link IsInstanceLambdaUsage} instance. */
public IsInstanceLambdaUsage() {}
@Override
public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) {
if (tree.getParameters().size() != 1
|| !(tree.getBody() instanceof InstanceOfTree instanceOf)) {
return Description.NO_MATCH;
}
VariableTree param = Iterables.getOnlyElement(tree.getParameters());
if (!ASTHelpers.getSymbol(param).equals(ASTHelpers.getSymbol(instanceOf.getExpression()))) {
return Description.NO_MATCH;
}
return describeMatch(
tree,
SuggestedFix.replace(
tree, SourceCode.treeToString(instanceOf.getType(), state) + ".class::isInstance"));
}
}

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.Sets;
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,
Sets.immutableEnumSet(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

@@ -1,20 +1,20 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
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.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,15 +23,23 @@ 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:
/** A {@link BugChecker} which flags non-canonical JUnit method declarations. */
// XXX: Consider introducing a class-level check which enforces that test classes:
// 1. Are named `*Test` or `Abstract*TestCase`.
// 2. If not `abstract`, are package-private and don't have public methods and subclasses.
// 3. Only have private fields.
@@ -39,27 +47,39 @@ import tech.picnic.errorprone.utils.ConflictDetection;
@AutoService(BugChecker.class)
@BugPattern(
summary = "JUnit method declaration can likely be improved",
link = BUG_PATTERNS_BASE_URL + "JUnitMethodDeclaration",
linkType = CUSTOM,
linkType = NONE,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class JUnitMethodDeclaration extends BugChecker implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
private static final String TEST_PREFIX = "test";
private static final ImmutableSet<Modifier> ILLEGAL_MODIFIERS =
Sets.immutableEnumSet(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC);
private static final Matcher<MethodTree> IS_LIKELY_OVERRIDDEN =
allOf(
not(hasModifier(Modifier.FINAL)),
not(hasModifier(Modifier.PRIVATE)),
enclosingClass(hasModifier(Modifier.ABSTRACT)));
/** Instantiates a new {@link JUnitMethodDeclaration} instance. */
public JUnitMethodDeclaration() {}
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")));
@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 +101,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 +120,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 +184,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

@@ -1,20 +1,16 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
import static java.util.Comparator.comparing;
import static java.util.Comparator.naturalOrder;
import static java.util.stream.Collectors.joining;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.Splitter;
import com.google.common.collect.Comparators;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.errorprone.BugPattern;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
@@ -32,63 +28,51 @@ import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.PrimitiveTypeTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Symtab;
import com.sun.tools.javac.code.Type;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import javax.inject.Inject;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.utils.AnnotationAttributeMatcher;
import tech.picnic.errorprone.utils.Flags;
import tech.picnic.errorprone.utils.SourceCode;
import javax.annotation.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.
* A {@link BugChecker} which flags annotation array listings which aren't sorted lexicographically.
*
* <p>The idea behind this checker is that maintaining a sorted sequence simplifies conflict
* resolution, and can even avoid it if two branches add the same entry.
*/
// XXX: In some places we declare a `@SuppressWarnings` annotation with a final value of
// `key-to-resolve-AnnotationUseStyle-and-TrailingComment-check-conflict`. That entry must stay
// last. Consider adding (generic?) support for such cases.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Where possible, sort annotation array attributes lexicographically",
link = BUG_PATTERNS_BASE_URL + "LexicographicalAnnotationAttributeListing",
linkType = CUSTOM,
linkType = NONE,
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
* properly sorted by key, then value.
*/
private static final Splitter STRING_ARGUMENT_SPLITTER = Splitter.on('=');
private final AnnotationAttributeMatcher matcher;
/** Instantiates a default {@link LexicographicalAnnotationAttributeListing} instance. */
/** Instantiates the default {@link LexicographicalAnnotationAttributeListing}. */
public LexicographicalAnnotationAttributeListing() {
this(ErrorProneFlags.empty());
}
@@ -98,8 +82,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);
}
@@ -124,9 +107,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(
@@ -137,7 +124,7 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
}
List<? extends ExpressionTree> actualOrdering = array.getInitializers();
ImmutableList<? extends ExpressionTree> desiredOrdering = doSort(actualOrdering);
ImmutableList<? extends ExpressionTree> desiredOrdering = doSort(actualOrdering, state);
if (actualOrdering.equals(desiredOrdering)) {
/* In the (presumably) common case the elements are already sorted. */
return Optional.empty();
@@ -162,22 +149,17 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
/* For now we don't force sorting on numeric types. */
return Stream.of(
symtab.annotationType,
symtab.booleanType,
symtab.charType,
symtab.classType,
symtab.enumSym.type,
symtab.stringType)
symtab.annotationType, symtab.classType, symtab.enumSym.type, symtab.stringType)
.anyMatch(t -> ASTHelpers.isSubtype(elemType, t, state));
}
private static ImmutableList<? extends ExpressionTree> doSort(
Iterable<? extends ExpressionTree> elements) {
Iterable<? extends ExpressionTree> elements, VisitorState state) {
// XXX: Perhaps we should use `Collator` with `.setStrength(Collator.PRIMARY)` and
// `getCollationKey`. Not clear whether that's worth the hassle at this point.
return ImmutableList.sortedCopyOf(
comparing(
LexicographicalAnnotationAttributeListing::getStructure,
e -> getStructure(e, state),
Comparators.lexicographical(
Comparators.lexicographical(
String.CASE_INSENSITIVE_ORDER.thenComparing(naturalOrder())))),
@@ -189,31 +171,38 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
* performed. This approach disregards e.g. irrelevant whitespace. It also allows special
* structure within string literals to be respected.
*/
private static ImmutableList<ImmutableList<String>> getStructure(ExpressionTree array) {
private static ImmutableList<ImmutableList<String>> getStructure(
ExpressionTree array, VisitorState state) {
ImmutableList.Builder<ImmutableList<String>> nodes = ImmutableList.builder();
new TreeScanner<@Nullable Void, @Nullable Void>() {
new TreeScanner<Void, Void>() {
@Nullable
@Override
public @Nullable Void visitIdentifier(IdentifierTree node, @Nullable Void unused) {
nodes.add(ImmutableList.of(node.getName().toString()));
return super.visitIdentifier(node, null);
public Void visitIdentifier(IdentifierTree node, @Nullable Void ctx) {
nodes.add(tokenize(node));
return super.visitIdentifier(node, ctx);
}
@Nullable
@Override
public @Nullable Void visitLiteral(LiteralTree node, @Nullable Void unused) {
Object value = ASTHelpers.constValue(node);
nodes.add(
value instanceof String str
? STRING_ARGUMENT_SPLITTER.splitToStream(str).collect(toImmutableList())
: ImmutableList.of(String.valueOf(value)));
return super.visitLiteral(node, null);
public Void visitLiteral(LiteralTree node, @Nullable Void ctx) {
nodes.add(tokenize(node));
return super.visitLiteral(node, ctx);
}
@Nullable
@Override
public @Nullable Void visitPrimitiveType(PrimitiveTypeTree node, @Nullable Void unused) {
nodes.add(ImmutableList.of(node.getPrimitiveTypeKind().toString()));
return super.visitPrimitiveType(node, null);
public Void visitPrimitiveType(PrimitiveTypeTree node, @Nullable Void ctx) {
nodes.add(tokenize(node));
return super.visitPrimitiveType(node, ctx);
}
private ImmutableList<String> tokenize(Tree node) {
/*
* Tokens are split on `=` so that e.g. inline Spring property declarations are properly
* sorted by key, then value.
*/
return ImmutableList.copyOf(SourceCode.treeToString(node, state).split("=", -1));
}
}.scan(array, null);
@@ -223,14 +212,13 @@ 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 ImmutableSet<String> excludedAnnotations(ErrorProneFlags flags) {
return Sets.union(BLACKLISTED_ANNOTATIONS, Flags.getSet(flags, EXCLUDED_ANNOTATIONS_FLAG))
.immutableCopy();
private static ImmutableList<String> excludedAnnotations(ErrorProneFlags flags) {
Set<String> exclusions = new HashSet<>();
flags.getList(EXCLUDED_ANNOTATIONS_FLAG).ifPresent(exclusions::addAll);
exclusions.addAll(BLACKLISTED_ANNOTATIONS);
return ImmutableList.copyOf(exclusions);
}
}

View File

@@ -1,16 +1,12 @@
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.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
import static com.sun.tools.javac.code.TypeAnnotations.AnnotationType.DECLARATION;
import static com.sun.tools.javac.code.TypeAnnotations.AnnotationType.TYPE;
import static java.util.Comparator.comparing;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import com.google.errorprone.BugPattern;
@@ -20,15 +16,11 @@ import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.MethodTree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.TypeAnnotations.AnnotationType;
import java.util.Comparator;
import java.util.List;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.utils.SourceCode;
import java.util.Optional;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags annotations that are not lexicographically sorted.
@@ -36,38 +28,16 @@ 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",
link = BUG_PATTERNS_BASE_URL + "LexicographicalAnnotationListing",
linkType = CUSTOM,
linkType = NONE,
severity = SUGGESTION,
tags = STYLE)
public final class LexicographicalAnnotationListing extends BugChecker
implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
/**
* A comparator that minimally reorders {@link AnnotationType}s, such that declaration annotations
* are placed before type annotations.
*/
@SuppressWarnings({
"java:S1067",
"java:S3358"
} /* Avoiding the nested ternary operator hurts readability. */)
private static final Comparator<@Nullable AnnotationType> BY_ANNOTATION_TYPE =
(a, b) ->
(a == null || a == DECLARATION) && b == TYPE
? -1
: (a == TYPE && b == DECLARATION ? 1 : 0);
/** Instantiates a new {@link LexicographicalAnnotationListing} instance. */
public LexicographicalAnnotationListing() {}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
List<? extends AnnotationTree> originalOrdering = tree.getModifiers().getAnnotations();
@@ -75,29 +45,26 @@ public final class LexicographicalAnnotationListing extends BugChecker
return Description.NO_MATCH;
}
ImmutableList<? extends AnnotationTree> sortedAnnotations =
sort(originalOrdering, ASTHelpers.getSymbol(tree), state);
ImmutableList<? extends AnnotationTree> sortedAnnotations = sort(originalOrdering, state);
if (originalOrdering.equals(sortedAnnotations)) {
return Description.NO_MATCH;
}
return describeMatch(
originalOrdering.get(0), fixOrdering(originalOrdering, sortedAnnotations, state));
Optional<Fix> fix = tryFixOrdering(originalOrdering, sortedAnnotations, state);
Description.Builder description = buildDescription(originalOrdering.get(0));
fix.ifPresent(description::addFix);
return description.build();
}
private static ImmutableList<? extends AnnotationTree> sort(
List<? extends AnnotationTree> annotations, Symbol symbol, VisitorState state) {
List<? extends AnnotationTree> annotations, VisitorState state) {
return annotations.stream()
.sorted(
comparing(
(AnnotationTree annotation) ->
ASTHelpers.getAnnotationType(annotation, symbol, state),
BY_ANNOTATION_TYPE)
.thenComparing(annotation -> SourceCode.treeToString(annotation, state)))
.sorted(comparing(annotation -> SourceCode.treeToString(annotation, state)))
.collect(toImmutableList());
}
private static Fix fixOrdering(
private static Optional<Fix> tryFixOrdering(
List<? extends AnnotationTree> originalAnnotations,
ImmutableList<? extends AnnotationTree> sortedAnnotations,
VisitorState state) {
@@ -108,7 +75,6 @@ public final class LexicographicalAnnotationListing extends BugChecker
SuggestedFix.builder()
.replace(original, SourceCode.treeToString(replacement, state)))
.reduce(SuggestedFix.Builder::merge)
.map(SuggestedFix.Builder::build)
.orElseThrow(() -> new VerifyException("No annotations were provided"));
.map(SuggestedFix.Builder::build);
}
}

View File

@@ -1,10 +1,9 @@
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.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.VerifyException;
@@ -27,6 +26,7 @@ import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
@@ -35,7 +35,7 @@ import java.util.Optional;
import javax.lang.model.element.Name;
/**
* A {@link BugChecker} that flags lambda expressions that can be replaced with method references.
* A {@link BugChecker} which flags lambda expressions that can be replaced with method references.
*/
// XXX: Other custom expressions we could rewrite:
// - `a -> "str" + a` to `"str"::concat`. But only if `str` is provably non-null.
@@ -49,23 +49,15 @@ import javax.lang.model.element.Name;
// `not(some::reference)`.
// XXX: This check is extremely inefficient due to its reliance on `SuggestedFixes.compilesWithFix`.
// Palantir's `LambdaMethodReference` check seems to suffer a similar issue at this time.
// XXX: Expressions of the form `i -> SomeType.class.isInstance(i)` are not replaced; fix that using
// a suitable generalization.
// XXX: Consider folding the `IsInstanceLambdaUsage` check of the `error-prone-contrib` module into
// this class.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Prefer method references over lambda expressions",
link = BUG_PATTERNS_BASE_URL + "MethodReferenceUsage",
linkType = CUSTOM,
linkType = NONE,
severity = SUGGESTION,
tags = STYLE)
public final class MethodReferenceUsage extends BugChecker implements LambdaExpressionTreeMatcher {
private static final long serialVersionUID = 1L;
/** Instantiates a new {@link MethodReferenceUsage} instance. */
public MethodReferenceUsage() {}
@Override
public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) {
/*
@@ -83,19 +75,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 +108,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 (!sym.isStatic()) {
return constructFix(lambdaExpr, "this", methodSelect);
}
return constructFix(lambdaExpr, sym.owner, methodSelect);
case MEMBER_SELECT:
return constructMethodRef(lambdaExpr, (MemberSelectTree) methodSelect, expectedInstance);
default:
throw new VerifyException("Unexpected type of expression: " + methodSelect.getKind());
}
if (methodSelect instanceof MemberSelectTree memberSelect) {
return constructMethodRef(lambdaExpr, memberSelect, expectedInstance);
}
throw new VerifyException("Unexpected type of expression: " + methodSelect.getKind());
}
private static Optional<SuggestedFix.Builder> constructMethodRef(
LambdaExpressionTree lambdaExpr, MemberSelectTree subTree, Optional<Name> expectedInstance) {
if (!(subTree.getExpression() instanceof IdentifierTree identifier)) {
if (subTree.getExpression().getKind() != Kind.IDENTIFIER) {
// XXX: Could be parenthesized. Handle. Also in other classes.
/*
* Only suggest a replacement if the method select's expression provably doesn't have
@@ -150,12 +142,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 +172,8 @@ public final class MethodReferenceUsage extends BugChecker implements LambdaExpr
for (int i = 0; i < args.size(); i++) {
ExpressionTree arg = args.get(i);
if (!(arg instanceof IdentifierTree identifier)
|| !identifier.getName().equals(expectedArguments.get(i + diff))) {
if (arg.getKind() != Kind.IDENTIFIER
|| !((IdentifierTree) arg).getName().equals(expectedArguments.get(i + diff))) {
return Optional.empty();
}
}
@@ -193,14 +185,12 @@ public final class MethodReferenceUsage extends BugChecker implements LambdaExpr
return tree.getParameters().stream().map(VariableTree::getName).collect(toImmutableList());
}
// XXX: Resolve this suppression.
@SuppressWarnings("UnqualifiedSuggestedFixImport")
private static Optional<SuggestedFix.Builder> constructFix(
LambdaExpressionTree lambdaExpr, Symbol target, Object methodName) {
Name sName = target.getSimpleName();
Optional<SuggestedFix.Builder> fix = constructFix(lambdaExpr, sName, methodName);
if (!"java.lang".equals(ASTHelpers.enclosingPackage(target).toString())) {
if (!"java.lang".equals(target.packge().toString())) {
Name fqName = target.getQualifiedName();
if (!sName.equals(fqName)) {
return fix.map(b -> b.addImport(fqName.toString()));

View File

@@ -0,0 +1,54 @@
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.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 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 template contains a method without any Refaster annotations",
linkType = NONE,
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")));
@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

@@ -1,10 +1,9 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.Iterables;
@@ -18,17 +17,16 @@ 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
* A {@link BugChecker} which flags method invocations for which all arguments are wrapped using
* {@link org.mockito.Mockito#eq}; this is redundant.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Don't unnecessarily use Mockito's `eq(...)`",
link = BUG_PATTERNS_BASE_URL + "MockitoStubbing",
linkType = CUSTOM,
linkType = NONE,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class MockitoStubbing extends BugChecker implements MethodInvocationTreeMatcher {
@@ -36,9 +34,6 @@ public final class MockitoStubbing extends BugChecker implements MethodInvocatio
private static final Matcher<ExpressionTree> MOCKITO_EQ_METHOD =
staticMethod().onClass("org.mockito.ArgumentMatchers").named("eq");
/** Instantiates a new {@link MockitoStubbing} instance. */
public MockitoStubbing() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
List<? extends ExpressionTree> arguments = tree.getArguments();

View File

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

@@ -1,54 +1,51 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import static tech.picnic.errorprone.utils.MoreMatchers.isSubTypeOf;
import static tech.picnic.errorprone.utils.MoreTypes.generic;
import static tech.picnic.errorprone.utils.MoreTypes.raw;
import static tech.picnic.errorprone.utils.MoreTypes.subOf;
import com.google.auto.service.AutoService;
import com.google.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.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 com.sun.tools.javac.util.List;
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.
/** A {@link BugChecker} which flags nesting of {@link Optional Optionals}. */
@AutoService(BugChecker.class)
@BugPattern(
summary =
"Avoid nesting `Optional`s inside `Optional`s; the resultant code is hard to reason about",
link = BUG_PATTERNS_BASE_URL + "NestedOptionals",
linkType = CUSTOM,
linkType = NONE,
severity = WARNING,
tags = FRAGILE_CODE)
public final class NestedOptionals extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Supplier<Type> OPTIONAL = Suppliers.typeFromClass(Optional.class);
private static final Matcher<Tree> IS_OPTIONAL_OF_OPTIONAL =
isSubTypeOf(VisitorState.memoize(generic(OPTIONAL, subOf(raw(OPTIONAL)))));
/** Instantiates a new {@link NestedOptionals} instance. */
public NestedOptionals() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
return IS_OPTIONAL_OF_OPTIONAL.matches(tree, state)
? describeMatch(tree)
: Description.NO_MATCH;
return isOptionalOfOptional(tree, state) ? describeMatch(tree) : Description.NO_MATCH;
}
private static boolean isOptionalOfOptional(Tree tree, VisitorState state) {
Type optionalType = OPTIONAL.get(state);
Type type = ASTHelpers.getType(tree);
if (!ASTHelpers.isSubtype(type, optionalType, state)) {
return false;
}
List<Type> typeArguments = type.getTypeArguments();
return !typeArguments.isEmpty()
&& ASTHelpers.isSubtype(Iterables.getOnlyElement(typeArguments), optionalType, state);
}
}

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

@@ -1,97 +0,0 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.instanceMethod;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import java.util.function.BiFunction;
import reactor.core.publisher.Mono;
import tech.picnic.errorprone.utils.SourceCode;
/**
* A {@link BugChecker} that flags {@link Mono} operations that are known to be vacuous, given that
* they are invoked on a {@link Mono} that is known not to complete empty.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Avoid vacuous operations on known non-empty `Mono`s",
link = BUG_PATTERNS_BASE_URL + "NonEmptyMono",
linkType = CUSTOM,
severity = WARNING,
tags = SIMPLIFICATION)
// XXX: This check does not simplify `someFlux.defaultIfEmpty(T).{defaultIfEmpty(T),hasElements()}`,
// as `someFlux.defaultIfEmpty(T)` yields a `Flux` rather than a `Mono`. Consider adding support for
// these cases.
// XXX: Given more advanced analysis many more expressions could be flagged. Consider
// `Mono.just(someValue)`, `Flux.just(someNonEmptySequence)`,
// `someMono.switchIfEmpty(someProvablyNonEmptyMono)` and many other variants.
// XXX: Consider implementing a similar check for `Publisher`s that are known to complete without
// emitting a value (e.g. `Mono.empty()`, `someFlux.then()`, ...), or known not to complete normally
// (`Mono.never()`, `someFlux.repeat()`, `Mono.error(...)`, ...). The latter category could
// potentially be split out further.
@SuppressWarnings("java:S1192" /* Factoring out repeated method names impacts readability. */)
public final class NonEmptyMono extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> MONO_SIZE_CHECK =
instanceMethod()
.onDescendantOf("reactor.core.publisher.Mono")
.namedAnyOf("defaultIfEmpty", "single", "switchIfEmpty");
private static final Matcher<ExpressionTree> NON_EMPTY_MONO =
anyOf(
instanceMethod()
.onDescendantOf("reactor.core.publisher.Flux")
.namedAnyOf(
"all",
"any",
"collect",
"collectList",
"collectMap",
"collectMultimap",
"collectSortedList",
"count",
"elementAt",
"hasElement",
"hasElements",
"last",
"reduceWith",
"single"),
instanceMethod()
.onDescendantOf("reactor.core.publisher.Flux")
.named("reduce")
.withParameters(Object.class.getCanonicalName(), BiFunction.class.getCanonicalName()),
instanceMethod()
.onDescendantOf("reactor.core.publisher.Mono")
.namedAnyOf("defaultIfEmpty", "hasElement", "single"));
/** Instantiates a new {@link NonEmptyMono} instance. */
public NonEmptyMono() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!MONO_SIZE_CHECK.matches(tree, state)) {
return Description.NO_MATCH;
}
ExpressionTree receiver = ASTHelpers.getReceiver(tree);
if (!NON_EMPTY_MONO.matches(receiver, state)) {
return Description.NO_MATCH;
}
return describeMatch(
tree, SuggestedFix.replace(tree, SourceCode.treeToString(receiver, state)));
}
}

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,123 +0,0 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.PERFORMANCE;
import static com.google.errorprone.matchers.Matchers.instanceMethod;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import static java.util.stream.Collectors.joining;
import com.google.auto.service.AutoService;
import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.refaster.Refaster;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import java.util.Optional;
import java.util.function.Supplier;
import tech.picnic.errorprone.refaster.matchers.RequiresComputation;
import tech.picnic.errorprone.utils.SourceCode;
/**
* A {@link BugChecker} that flags arguments to {@link Optional#orElse(Object)} that should be
* deferred using {@link Optional#orElseGet(Supplier)}.
*
* <p>The suggested fix assumes that the argument to {@code orElse} does not have side effects. If
* it does, the suggested fix changes the program's semantics. Such fragile code must instead be
* refactored such that the side-effectful code does not appear accidental.
*/
// XXX: This rule may introduce a compilation error: the `value` expression may reference a
// non-effectively final variable, which is not allowed in the replacement lambda expression.
// Review whether a `@Matcher` can be used to avoid this.
// XXX: Once the `MethodReferenceUsageCheck` bug checker becomes generally usable, consider leaving
// the method reference cleanup to that check, and express the remainder of the logic in this class
// using a Refaster template, i.c.w. a `@NotMatches(RequiresComputation.class)` constraint.
@AutoService(BugChecker.class)
@BugPattern(
summary =
"""
Prefer `Optional#orElseGet` over `Optional#orElse` if the fallback requires additional \
computation""",
linkType = NONE,
severity = WARNING,
tags = PERFORMANCE)
public final class OptionalOrElseGet extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> REQUIRES_COMPUTATION = new RequiresComputation();
private static final Matcher<ExpressionTree> OPTIONAL_OR_ELSE_METHOD =
instanceMethod().onExactClass(Optional.class.getCanonicalName()).namedAnyOf("orElse");
// XXX: Also exclude invocations of `@Placeholder`-annotated methods.
private static final Matcher<ExpressionTree> REFASTER_METHOD =
staticMethod().onClass(Refaster.class.getCanonicalName());
/** Instantiates a new {@link OptionalOrElseGet} instance. */
public OptionalOrElseGet() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!OPTIONAL_OR_ELSE_METHOD.matches(tree, state)) {
return Description.NO_MATCH;
}
ExpressionTree argument = Iterables.getOnlyElement(tree.getArguments());
if (!REQUIRES_COMPUTATION.matches(argument, state)
|| REFASTER_METHOD.matches(argument, state)) {
return Description.NO_MATCH;
}
/*
* We have a match. Construct the method reference or lambda expression to be passed to the
* replacement `#orElseGet` invocation.
*/
String newArgument =
tryMethodReferenceConversion(argument, state)
.orElseGet(() -> "() -> " + SourceCode.treeToString(argument, state));
/* Construct the suggested fix, replacing the method invocation and its argument. */
SuggestedFix fix =
SuggestedFix.builder()
.merge(SuggestedFixes.renameMethodInvocation(tree, "orElseGet", state))
.replace(argument, newArgument)
.build();
return describeMatch(tree, fix);
}
/** Returns the nullary method reference matching the given expression, if any. */
private static Optional<String> tryMethodReferenceConversion(
ExpressionTree tree, VisitorState state) {
if (!(tree instanceof MethodInvocationTree methodInvocation)) {
return Optional.empty();
}
if (!methodInvocation.getArguments().isEmpty()) {
return Optional.empty();
}
if (!(methodInvocation.getMethodSelect() instanceof MemberSelectTree memberSelect)) {
return Optional.empty();
}
if (REQUIRES_COMPUTATION.matches(memberSelect.getExpression(), state)) {
return Optional.empty();
}
return Optional.of(
SourceCode.treeToString(memberSelect.getExpression(), state)
+ "::"
+ (methodInvocation.getTypeArguments().isEmpty()
? ""
: methodInvocation.getTypeArguments().stream()
.map(arg -> SourceCode.treeToString(arg, state))
.collect(joining(",", "<", ">")))
+ memberSelect.getIdentifier());
}
}

View File

@@ -1,13 +1,12 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
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.anyOf;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
import static java.util.stream.Collectors.joining;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.VerifyException;
@@ -17,12 +16,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,10 +31,10 @@ 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
* A {@link BugChecker} which flags {@code Comparator#comparing*} invocations that can be replaced
* with an equivalent alternative so as to avoid unnecessary (un)boxing.
*/
// XXX: Add more documentation. Explain how this is useful in the face of refactoring to more
@@ -45,37 +42,31 @@ 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""",
link = BUG_PATTERNS_BASE_URL + "PrimitiveComparison",
linkType = CUSTOM,
"Ensure invocations of `Comparator#comparing{,Double,Int,Long}` match the return type"
+ " of the provided function",
linkType = NONE,
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()));
/** Instantiates a new {@link PrimitiveComparison} instance. */
public PrimitiveComparison() {}
.withParameters(Function.class.getName()));
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
@@ -149,44 +140,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

@@ -1,28 +1,19 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.anyMethod;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.anything;
import static com.google.errorprone.matchers.Matchers.argumentCount;
import static com.google.errorprone.matchers.Matchers.isNonNullUsingDataflow;
import static com.google.errorprone.matchers.Matchers.isSameType;
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
import static com.google.errorprone.matchers.Matchers.not;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Primitives;
import com.google.errorprone.BugPattern;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
@@ -33,7 +24,6 @@ import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.suppliers.Suppliers;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.CompoundAssignmentTree;
import com.sun.source.tree.ExpressionTree;
@@ -51,32 +41,26 @@ import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import javax.inject.Inject;
import tech.picnic.errorprone.utils.Flags;
import tech.picnic.errorprone.utils.MethodMatcherFactory;
import tech.picnic.errorprone.utils.SourceCode;
import tech.picnic.errorprone.bugpatterns.util.MethodMatcherFactory;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/** A {@link BugChecker} that flags redundant explicit string conversions. */
/** A {@link BugChecker} which flags redundant explicit string conversions. */
@AutoService(BugChecker.class)
@BugPattern(
summary = "Avoid redundant string conversions when possible",
link = BUG_PATTERNS_BASE_URL + "RedundantStringConversion",
linkType = CUSTOM,
linkType = NONE,
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);
@@ -85,72 +69,96 @@ public final class RedundantStringConversion extends BugChecker
allOf(STRING, isNonNullUsingDataflow());
private static final Matcher<ExpressionTree> NOT_FORMATTABLE =
not(isSubtypeOf(Formattable.class));
private static final Matcher<MethodInvocationTree> WELL_KNOWN_STRING_CONVERSION_METHODS =
private static final Matcher<ExpressionTree> WELL_KNOWN_STRING_CONVERSION_METHODS =
anyOf(
instanceMethod()
.onDescendantOfAny(Object.class.getCanonicalName())
instanceMethod().onDescendantOfAny(Object.class.getName()).named("toString"),
staticMethod()
.onClass(Objects.class.getName())
.named("toString")
.withNoParameters(),
allOf(
argumentCount(1),
anyOf(
staticMethod()
.onClassAny(
Stream.concat(
Primitives.allWrapperTypes().stream(), Stream.of(Objects.class))
.map(Class::getName)
.collect(toImmutableSet()))
.named("toString"),
allOf(
staticMethod().onClass(String.class.getCanonicalName()).named("valueOf"),
not(
anyMethod()
.anyClass()
.withAnyName()
.withParametersOfType(
ImmutableList.of(Suppliers.arrayOf(Suppliers.CHAR_TYPE))))))));
.withParameters(Object.class.getName()),
staticMethod()
.onClass(String.class.getName())
.named("valueOf")
.withParameters(Object.class.getName()),
staticMethod()
.onClass(String.class.getName())
.named("valueOf")
.withParameters(String.class.getName()),
staticMethod()
.onClass(Byte.class.getName())
.named("toString")
.withParameters(byte.class.getName()),
staticMethod()
.onClass(Character.class.getName())
.named("toString")
.withParameters(char.class.getName()),
staticMethod()
.onClass(Short.class.getName())
.named("toString")
.withParameters(short.class.getName()),
staticMethod()
.onClass(Integer.class.getName())
.named("toString")
.withParameters(int.class.getName()),
staticMethod()
.onClass(Long.class.getName())
.named("toString")
.withParameters(long.class.getName()),
staticMethod()
.onClass(Float.class.getName())
.named("toString")
.withParameters(float.class.getName()),
staticMethod()
.onClass(Double.class.getName())
.named("toString")
.withParameters(double.class.getName()));
private static final Matcher<ExpressionTree> STRINGBUILDER_APPEND_INVOCATION =
instanceMethod()
.onDescendantOf(StringBuilder.class.getCanonicalName())
.onDescendantOf(StringBuilder.class.getName())
.named("append")
.withParameters(String.class.getCanonicalName());
.withParameters(String.class.getName());
private static final Matcher<ExpressionTree> STRINGBUILDER_INSERT_INVOCATION =
instanceMethod()
.onDescendantOf(StringBuilder.class.getCanonicalName())
.onDescendantOf(StringBuilder.class.getName())
.named("insert")
.withParameters(int.class.getCanonicalName(), String.class.getCanonicalName());
.withParameters(int.class.getName(), String.class.getName());
private static final Matcher<ExpressionTree> FORMATTER_INVOCATION =
anyOf(
staticMethod().onClass(String.class.getCanonicalName()).named("format"),
instanceMethod().onDescendantOf(Formatter.class.getCanonicalName()).named("format"),
staticMethod().onClass(String.class.getName()).named("format"),
instanceMethod().onDescendantOf(Formatter.class.getName()).named("format"),
instanceMethod()
.onDescendantOfAny(
PrintStream.class.getCanonicalName(), PrintWriter.class.getCanonicalName())
.onDescendantOf(PrintStream.class.getName())
.namedAnyOf("format", "printf"),
instanceMethod()
.onDescendantOfAny(
PrintStream.class.getCanonicalName(), PrintWriter.class.getCanonicalName())
.onDescendantOf(PrintStream.class.getName())
.namedAnyOf("print", "println")
.withParameters(Object.class.getCanonicalName()),
.withParameters(Object.class.getName()),
instanceMethod()
.onDescendantOf(PrintWriter.class.getName())
.namedAnyOf("format", "printf"),
instanceMethod()
.onDescendantOf(PrintWriter.class.getName())
.namedAnyOf("print", "println")
.withParameters(Object.class.getName()),
staticMethod()
.onClass(Console.class.getCanonicalName())
.onClass(Console.class.getName())
.namedAnyOf("format", "printf", "readline", "readPassword"));
private static final Matcher<ExpressionTree> GUAVA_GUARD_INVOCATION =
anyOf(
staticMethod()
.onClass(Preconditions.class.getCanonicalName())
.onClass("com.google.common.base.Preconditions")
.namedAnyOf("checkArgument", "checkState", "checkNotNull"),
staticMethod()
.onClass(Verify.class.getCanonicalName())
.onClass("com.google.common.base.Verify")
.namedAnyOf("verify", "verifyNotNull"));
private static final Matcher<ExpressionTree> SLF4J_LOGGER_INVOCATION =
instanceMethod()
.onDescendantOf("org.slf4j.Logger")
.namedAnyOf("trace", "debug", "info", "warn", "error");
private final Matcher<MethodInvocationTree> conversionMethodMatcher;
private final Matcher<ExpressionTree> conversionMethodMatcher;
/** Instantiates a default {@link RedundantStringConversion} instance. */
/** Instantiates the default {@link RedundantStringConversion}. */
public RedundantStringConversion() {
this(ErrorProneFlags.empty());
}
@@ -160,8 +168,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,12 +181,12 @@ public final class RedundantStringConversion extends BugChecker
ExpressionTree lhs = tree.getLeftOperand();
ExpressionTree rhs = tree.getRightOperand();
if (!STRING.matches(lhs, state)) {
return createDescription(tree, tryFix(rhs, state, STRING));
return finalize(tree, tryFix(rhs, state, STRING));
}
List<SuggestedFix.Builder> fixes = new ArrayList<>();
// XXX: Avoid trying to simplify the RHS twice.
// XXX: Not so nice: we try to simplify the RHS twice.
ExpressionTree preferredRhs = trySimplify(rhs, state).orElse(rhs);
if (STRING.matches(preferredRhs, state)) {
tryFix(lhs, state, ANY_EXPR).ifPresent(fixes::add);
@@ -188,7 +195,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 +204,36 @@ public final class RedundantStringConversion extends BugChecker
return Description.NO_MATCH;
}
return createDescription(tree, tryFix(tree.getExpression(), state, ANY_EXPR));
return finalize(tree, tryFix(tree.getExpression(), state, ANY_EXPR));
}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (STRINGBUILDER_APPEND_INVOCATION.matches(tree, state)) {
return createDescription(tree, tryFixPositionalConverter(tree.getArguments(), state, 0));
return finalize(tree, tryFixPositionalConverter(tree.getArguments(), state, 0));
}
if (STRINGBUILDER_INSERT_INVOCATION.matches(tree, state)) {
return createDescription(tree, tryFixPositionalConverter(tree.getArguments(), state, 1));
return finalize(tree, tryFixPositionalConverter(tree.getArguments(), state, 1));
}
if (FORMATTER_INVOCATION.matches(tree, state)) {
return createDescription(tree, tryFixFormatter(tree.getArguments(), state));
return finalize(tree, tryFixFormatter(tree.getArguments(), state));
}
if (GUAVA_GUARD_INVOCATION.matches(tree, state)) {
return createDescription(tree, tryFixGuavaGuard(tree.getArguments(), state));
return finalize(tree, tryFixGuavaGuard(tree.getArguments(), state));
}
if (SLF4J_LOGGER_INVOCATION.matches(tree, state)) {
return createDescription(tree, tryFixSlf4jLogger(tree.getArguments(), state));
return finalize(tree, tryFixSlf4jLogger(tree.getArguments(), state));
}
if (instanceMethod().matches(tree, state)) {
return createDescription(tree, tryFix(tree, state, STRING));
return finalize(tree, tryFix(tree, state, STRING));
}
return createDescription(tree, tryFix(tree, state, NON_NULL_STRING));
return finalize(tree, tryFix(tree, state, NON_NULL_STRING));
}
private Optional<SuggestedFix.Builder> tryFixPositionalConverter(
@@ -236,7 +243,7 @@ public final class RedundantStringConversion extends BugChecker
.flatMap(args -> tryFix(args.get(index), state, ANY_EXPR));
}
// XXX: Write another check that checks that Formatter patterns don't use `{}` and have a
// XXX: Write another check which checks that Formatter patterns don't use `{}` and have a
// matching number of arguments of the appropriate type. Also flag explicit conversions from
// `Formattable` to string.
private Optional<SuggestedFix.Builder> tryFixFormatter(
@@ -267,17 +274,17 @@ public final class RedundantStringConversion extends BugChecker
return tryFixFormatterArguments(arguments, state, ANY_EXPR, ANY_EXPR);
}
// XXX: Write another check that checks that SLF4J patterns don't use `%s` and have a matching
// XXX: Write another check which checks that SLF4J patterns don't use `%s` and have a matching
// number of arguments of the appropriate type. Also flag explicit conversions from `Throwable` to
// string as the last logger argument. Suggests either dropping the conversion or going with
// string as the last logger argument. Suggests either dropping the converison or going with
// `Throwable#getMessage()` instead.
private Optional<SuggestedFix.Builder> tryFixSlf4jLogger(
List<? extends ExpressionTree> arguments, VisitorState state) {
/*
* SLF4J treats the final argument to a log statement specially if it is a `Throwable`: it
* SLF4J treats the final argument to a log statement specially if it is a `Throwabe`: it
* will always choose to render the associated stacktrace, even if the argument has a
* matching `{}` placeholder. (In this case the `{}` will simply be logged verbatim.) So if
* a log statement's final argument is the string representation of a `Throwable`, then we
* a log statement's final argument is the string representation of a `Throwble`, then we
* must not strip this explicit string conversion, as that would change the statement's
* semantics.
*/
@@ -309,7 +316,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 +338,32 @@ public final class RedundantStringConversion extends BugChecker
}
private Optional<ExpressionTree> trySimplify(ExpressionTree tree, VisitorState state) {
if (!(tree instanceof MethodInvocationTree methodInvocation)) {
if (tree.getKind() != Kind.METHOD_INVOCATION || !conversionMethodMatcher.matches(tree, state)) {
return Optional.empty();
}
if (!conversionMethodMatcher.matches(methodInvocation, state)) {
return Optional.empty();
MethodInvocationTree methodInvocation = (MethodInvocationTree) tree;
switch (methodInvocation.getArguments().size()) {
case 0:
return trySimplifyNullaryMethod(methodInvocation, state);
case 1:
return trySimplifyUnaryMethod(methodInvocation, state);
default:
throw new IllegalStateException(
"Cannot simplify method call with two or more arguments: "
+ SourceCode.treeToString(tree, state));
}
return switch (methodInvocation.getArguments().size()) {
case 0 -> trySimplifyNullaryMethod(methodInvocation, state);
case 1 -> trySimplifyUnaryMethod(methodInvocation, state);
default ->
throw new IllegalStateException(
"Cannot simplify method call with two or more arguments: "
+ SourceCode.treeToString(tree, state));
};
}
private static Optional<ExpressionTree> trySimplifyNullaryMethod(
MethodInvocationTree methodInvocation, VisitorState state) {
if (!instanceMethod().matches(methodInvocation, state)
|| !(methodInvocation.getMethodSelect() instanceof MemberSelectTree memberSelect)) {
if (!instanceMethod().matches(methodInvocation, state)) {
return Optional.empty();
}
return Optional.of(memberSelect.getExpression())
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,20 +376,20 @@ public final class RedundantStringConversion extends BugChecker
return Optional.of(Iterables.getOnlyElement(methodInvocation.getArguments()));
}
private Description createDescription(Tree tree, Optional<SuggestedFix.Builder> fixes) {
private Description finalize(Tree tree, Optional<SuggestedFix.Builder> fixes) {
return fixes
.map(SuggestedFix.Builder::build)
.map(fix -> describeMatch(tree, fix))
.orElse(Description.NO_MATCH);
}
private static Matcher<MethodInvocationTree> createConversionMethodMatcher(
ErrorProneFlags flags) {
// XXX: `Flags#getSet` 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.getSet(flags, EXTRA_STRING_CONVERSION_METHODS_FLAG)));
private static Matcher<ExpressionTree> createConversionMethodMatcher(ErrorProneFlags flags) {
// XXX: ErrorProneFlags#getList splits by comma, but method signatures may also contain commas.
// For this class methods accepting more than one argument are not valid, but still: not nice.
return 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,9 @@
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.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
@@ -17,43 +16,42 @@ 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.
* A {@link BugChecker} which flags unnecessary {@link Refaster#anyOf(Object[])} usages.
*
* <p>Note that this logic can't be implemented as a Refaster rule, as the {@link Refaster} class is
* treated specially.
* <p>Note that this logic can't be implemented as a Refaster template, as the {@link Refaster}
* class is treated specially.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "`Refaster#anyOf` should be passed at least two parameters",
link = BUG_PATTERNS_BASE_URL + "RefasterAnyOfUsage",
linkType = CUSTOM,
linkType = NONE,
severity = SUGGESTION,
tags = SIMPLIFICATION)
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");
/** Instantiates a new {@link RefasterAnyOfUsage} instance. */
public RefasterAnyOfUsage() {}
staticMethod().onClass(Refaster.class.getName()).named("anyOf");
@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,6 +1,6 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.ALL;
@@ -11,7 +11,6 @@ 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 com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
@@ -22,13 +21,9 @@ import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import java.io.InputStream;
import java.time.ZoneId;
import java.util.Locale;
import java.util.TimeZone;
/**
* A {@link BugChecker} that flags {@code @RequestMapping} methods that have one or more parameters
* A {@link BugChecker} which flags {@code @RequestMapping} methods that have one or more parameters
* that appear to lack a relevant annotation.
*
* <p>Matched mappings are {@code @{Delete,Get,Patch,Post,Put,Request}Mapping}.
@@ -36,8 +31,7 @@ import java.util.TimeZone;
@AutoService(BugChecker.class)
@BugPattern(
summary = "Make sure all `@RequestMapping` method parameters are annotated",
link = BUG_PATTERNS_BASE_URL + "RequestMappingAnnotation",
linkType = CUSTOM,
linkType = NONE,
severity = WARNING,
tags = LIKELY_ERROR)
public final class RequestMappingAnnotation extends BugChecker implements MethodTreeMatcher {
@@ -72,30 +66,20 @@ public final class RequestMappingAnnotation extends BugChecker implements Method
isType(ANN_PACKAGE_PREFIX + "RequestAttribute"),
isType(ANN_PACKAGE_PREFIX + "RequestBody"),
isType(ANN_PACKAGE_PREFIX + "RequestHeader"),
isType(ANN_PACKAGE_PREFIX + "RequestParam"),
isType(ANN_PACKAGE_PREFIX + "RequestPart"),
isType(
"org.springframework.security.core.annotation.CurrentSecurityContext"))),
isSameType(InputStream.class.getCanonicalName()),
isSameType(Locale.class.getCanonicalName()),
isSameType(TimeZone.class.getCanonicalName()),
isSameType(ZoneId.class.getCanonicalName()),
isSameType("jakarta.servlet.http.HttpServletRequest"),
isSameType("jakarta.servlet.http.HttpServletResponse"),
isType(ANN_PACKAGE_PREFIX + "RequestParam"))),
isSameType("java.io.InputStream"),
isSameType("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"),
isSameType("org.springframework.web.util.UriBuilder"),
isSameType("org.springframework.web.util.UriComponentsBuilder"))));
/** Instantiates a new {@link RequestMappingAnnotation} instance. */
public RequestMappingAnnotation() {}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
// XXX: Auto-add `@RequestParam` where applicable.
@@ -105,10 +89,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,7 +1,6 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
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;
@@ -10,77 +9,36 @@ import static com.google.errorprone.matchers.Matchers.annotations;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
import static com.google.errorprone.matchers.Matchers.isType;
import static com.google.errorprone.matchers.Matchers.not;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
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. */
/** A {@link BugChecker} which flags {@code @RequestParam} parameters with an unsupported type. */
@AutoService(BugChecker.class)
@BugPattern(
summary =
"""
By default, `@RequestParam` does not support `ImmutableCollection` and `ImmutableMap` \
subtypes""",
link = BUG_PATTERNS_BASE_URL + "RequestParamType",
linkType = CUSTOM,
summary = "`@RequestParam` does not support `ImmutableCollection` and `ImmutableMap` subtypes",
linkType = NONE,
severity = ERROR,
tags = LIKELY_ERROR)
@SuppressWarnings("java:S2160" /* Super class equality definition suffices. */)
public final class RequestParamType extends BugChecker implements VariableTreeMatcher {
private static final long serialVersionUID = 1L;
private static final String SUPPORTED_CUSTOM_TYPES_FLAG = "RequestParamType:SupportedCustomTypes";
private final Matcher<VariableTree> hasUnsupportedRequestParamType;
/** Instantiates a default {@link RequestParamType} instance. */
public RequestParamType() {
this(ErrorProneFlags.empty());
}
/**
* Instantiates a customized {@link RequestParamType} instance.
*
* @param flags Any provided command line flags.
*/
@Inject
RequestParamType(ErrorProneFlags flags) {
hasUnsupportedRequestParamType = hasUnsupportedRequestParamType(flags);
}
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)));
@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.getSet(flags, SUPPORTED_CUSTOM_TYPES_FLAG))));
}
private static Matcher<Tree> isSubtypeOfAny(ImmutableSet<String> inclusions) {
return anyOf(
inclusions.stream()
.map(inclusion -> isSubtypeOf(Suppliers.typeFromString(inclusion)))
.collect(toImmutableList()));
}
}

View File

@@ -0,0 +1,87 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.NONE;
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 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;
/**
* A {@link BugChecker} which 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",
linkType = NONE,
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));
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (!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

@@ -1,12 +1,11 @@
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.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.Splitter;
@@ -23,20 +22,18 @@ 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. */
/** A {@link BugChecker} which flags SLF4J usages that are likely to be in error. */
// XXX: The special-casing of Throwable applies only to SLF4J 1.6.0+; see
// https://www.slf4j.org/faq.html#paramException. That should be documented.
// XXX: Also simplify `LOG.error(String.format("Something %s", arg), throwable)`.
// XXX: Also simplify `LOG.error(String.join("sep", arg1, arg2), throwable)`? Perhaps too obscure.
// XXX: Write a similar checker for Spring RestTemplates, String.format and friends, Guava
// preconditions, ...
@AutoService(BugChecker.class)
@BugPattern(
summary = "Make sure SLF4J log statements contain proper placeholders with matching arguments",
link = BUG_PATTERNS_BASE_URL + "Slf4jLogStatement",
linkType = CUSTOM,
linkType = NONE,
severity = WARNING,
tags = LIKELY_ERROR)
public final class Slf4jLogStatement extends BugChecker implements MethodInvocationTreeMatcher {
@@ -48,9 +45,6 @@ public final class Slf4jLogStatement extends BugChecker implements MethodInvocat
.onDescendantOf("org.slf4j.Logger")
.namedAnyOf("trace", "debug", "info", "warn", "error");
/** Instantiates a new {@link Slf4jLogStatement} instance. */
public Slf4jLogStatement() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!SLF4J_LOGGER_INVOCATION.matches(tree, state)) {

View File

@@ -1,185 +0,0 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.common.base.Verify.verify;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.classLiteral;
import static com.google.errorprone.matchers.Matchers.instanceMethod;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import static com.google.errorprone.matchers.Matchers.toType;
import static java.util.Objects.requireNonNull;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.CaseFormat;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
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.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.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Symbol;
import java.util.EnumSet;
import javax.inject.Inject;
import javax.lang.model.element.Modifier;
import tech.picnic.errorprone.utils.MoreASTHelpers;
/** A {@link BugChecker} that flags non-canonical SLF4J logger declarations. */
@AutoService(BugChecker.class)
@BugPattern(
summary = "SLF4J logger declarations should follow established best-practices",
link = BUG_PATTERNS_BASE_URL + "Slf4jLoggerDeclaration",
linkType = CUSTOM,
severity = WARNING,
tags = STYLE)
@SuppressWarnings("java:S2160" /* Super class equality definition suffices. */)
public final class Slf4jLoggerDeclaration extends BugChecker implements VariableTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> IS_GET_LOGGER =
staticMethod().onDescendantOf("org.slf4j.LoggerFactory").named("getLogger");
private static final String CANONICAL_STATIC_LOGGER_NAME_FLAG =
"Slf4jLogDeclaration:CanonicalStaticLoggerName";
private static final String DEFAULT_CANONICAL_LOGGER_NAME = "LOG";
private static final Matcher<ExpressionTree> IS_STATIC_ENCLOSING_CLASS_REFERENCE =
classLiteral(Slf4jLoggerDeclaration::isEnclosingClassReference);
private static final Matcher<ExpressionTree> IS_DYNAMIC_ENCLOSING_CLASS_REFERENCE =
toType(
MethodInvocationTree.class,
allOf(
instanceMethod().anyClass().named("getClass").withNoParameters(),
Slf4jLoggerDeclaration::getClassReceiverIsEnclosingClassInstance));
private static final ImmutableSet<Modifier> INSTANCE_DECLARATION_MODIFIERS =
Sets.immutableEnumSet(Modifier.PRIVATE, Modifier.FINAL);
private static final ImmutableSet<Modifier> STATIC_DECLARATION_MODIFIERS =
Sets.immutableEnumSet(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL);
private final String canonicalStaticFieldName;
private final String canonicalInstanceFieldName;
/** Instantiates a default {@link Slf4jLoggerDeclaration} instance. */
public Slf4jLoggerDeclaration() {
this(ErrorProneFlags.empty());
}
/**
* Instantiates a customized {@link Slf4jLoggerDeclaration}.
*
* @param flags Any provided command line flags.
*/
@Inject
Slf4jLoggerDeclaration(ErrorProneFlags flags) {
canonicalStaticFieldName =
flags.get(CANONICAL_STATIC_LOGGER_NAME_FLAG).orElse(DEFAULT_CANONICAL_LOGGER_NAME);
canonicalInstanceFieldName =
CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, canonicalStaticFieldName);
}
@Override
public Description matchVariable(VariableTree tree, VisitorState state) {
ExpressionTree initializer = tree.getInitializer();
if (!IS_GET_LOGGER.matches(initializer, state)) {
return Description.NO_MATCH;
}
ClassTree clazz = getEnclosingClass(state);
ExpressionTree factoryArg =
Iterables.getOnlyElement(((MethodInvocationTree) initializer).getArguments());
SuggestedFix.Builder fix = SuggestedFix.builder();
if (clazz.getModifiers().getFlags().contains(Modifier.ABSTRACT)
&& IS_DYNAMIC_ENCLOSING_CLASS_REFERENCE.matches(factoryArg, state)) {
/*
* While generally we prefer `Logger` declarations to be static and named after their
* enclosing class, we allow one exception: loggers in abstract classes with a name derived
* from `getClass()`.
*/
suggestModifiers(tree, INSTANCE_DECLARATION_MODIFIERS, fix, state);
suggestRename(tree, canonicalInstanceFieldName, fix, state);
} else {
suggestModifiers(
tree,
clazz.getKind() == Kind.INTERFACE ? ImmutableSet.of() : STATIC_DECLARATION_MODIFIERS,
fix,
state);
suggestRename(tree, canonicalStaticFieldName, fix, state);
if (!MoreASTHelpers.isStringTyped(factoryArg, state)
&& !IS_STATIC_ENCLOSING_CLASS_REFERENCE.matches(factoryArg, state)) {
/*
* Loggers with a custom string name are generally "special", but those with a name derived
* from a class other than the one that encloses it are likely in error.
*/
fix.merge(SuggestedFix.replace(factoryArg, clazz.getSimpleName() + ".class"));
}
}
return fix.isEmpty() ? Description.NO_MATCH : describeMatch(tree, fix.build());
}
private static void suggestModifiers(
VariableTree tree,
ImmutableSet<Modifier> modifiers,
SuggestedFix.Builder fixBuilder,
VisitorState state) {
ModifiersTree modifiersTree =
requireNonNull(ASTHelpers.getModifiers(tree), "`VariableTree` must have modifiers");
SuggestedFixes.addModifiers(tree, modifiersTree, state, modifiers).ifPresent(fixBuilder::merge);
SuggestedFixes.removeModifiers(
modifiersTree, state, Sets.difference(EnumSet.allOf(Modifier.class), modifiers))
.ifPresent(fixBuilder::merge);
}
private static void suggestRename(
VariableTree variableTree, String name, SuggestedFix.Builder fixBuilder, VisitorState state) {
if (!variableTree.getName().contentEquals(name)) {
fixBuilder.merge(SuggestedFixes.renameVariable(variableTree, name, state));
}
}
private static boolean isEnclosingClassReference(ExpressionTree tree, VisitorState state) {
return ASTHelpers.getSymbol(getEnclosingClass(state)).equals(ASTHelpers.getSymbol(tree));
}
private static boolean getClassReceiverIsEnclosingClassInstance(
MethodInvocationTree getClassInvocationTree, VisitorState state) {
ExpressionTree receiver = ASTHelpers.getReceiver(getClassInvocationTree);
if (receiver == null) {
/*
* Method invocations without an explicit receiver either involve static methods (possibly
* statically imported), or instance methods invoked on the enclosing class. As the given
* `getClassInvocationTree` is guaranteed to be a nullary `#getClass()` invocation, the latter
* must be the case.
*/
return true;
}
Symbol symbol = ASTHelpers.getSymbol(receiver);
return symbol != null
&& symbol.asType().tsym.equals(ASTHelpers.getSymbol(getEnclosingClass(state)));
}
private static ClassTree getEnclosingClass(VisitorState state) {
ClassTree clazz = state.findEnclosing(ClassTree.class);
// XXX: Review whether we should relax this constraint in the face of so-called anonymous
// classes. See
// https://docs.oracle.com/en/java/javase/23/language/implicitly-declared-classes-and-instance-main-methods.html
verify(clazz != null, "Variable not defined inside class");
return clazz;
}
}

View File

@@ -1,11 +1,11 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.common.base.Verify.verify;
import static com.google.errorprone.BugPattern.LinkType.NONE;
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 com.google.auto.service.AutoService;
import com.google.common.base.VerifyException;
@@ -17,27 +17,26 @@ 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
* A {@link BugChecker} which flags {@code @RequestMapping} annotations that can be written more
* concisely.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary =
"Prefer the conciseness of `@{Get,Put,Post,Delete,Patch}Mapping` over `@RequestMapping`",
link = BUG_PATTERNS_BASE_URL + "SpringMvcAnnotation",
linkType = CUSTOM,
linkType = NONE,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class SpringMvcAnnotation extends BugChecker implements AnnotationTreeMatcher {
@@ -56,9 +55,6 @@ public final class SpringMvcAnnotation extends BugChecker implements AnnotationT
.put("PUT", "PutMapping")
.build();
/** Instantiates a new {@link SpringMvcAnnotation} instance. */
public SpringMvcAnnotation() {}
@Override
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
// XXX: We could remove the `@RequestMapping` import if not other usages remain.
@@ -78,25 +74,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 +109,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

@@ -1,33 +1,14 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
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 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 +19,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} which 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 which disallows static imports of certain methods. Candidates:
// - `com.google.common.base.Strings`
// - `java.util.Optional.empty`
// - `java.util.Locale.ROOT`
// - `ZoneOffset.ofHours` and other `ofXXX`-style methods.
// - `java.time.Clock`.
// - Several other `java.time` classes.
// - Likely any of `*.{ZERO, ONE, MIX, MAX, MIN_VALUE, MAX_VALUE}`.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Identifier should be statically imported",
link = BUG_PATTERNS_BASE_URL + "StaticImport",
linkType = CUSTOM,
linkType = NONE,
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 +75,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",
@@ -134,62 +100,94 @@ public final class StaticImport extends BugChecker implements MemberSelectTreeMa
"org.springframework.http.HttpMethod",
"org.springframework.http.MediaType",
"org.testng.Assert",
"reactor.function.TupleUtils",
"tech.picnic.errorprone.utils.MoreTypes");
"reactor.function.TupleUtils");
/**
* 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();
/** Instantiates a new {@link StaticImport} instance. */
public StaticImport() {}
/**
* 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");
@Override
public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) {
@@ -212,21 +210,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

@@ -1,187 +0,0 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.suppliers.Supplier;
import com.google.errorprone.suppliers.Suppliers;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.util.Constants;
import java.util.Formattable;
import java.util.Iterator;
import java.util.List;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.utils.SourceCode;
/**
* A {@link BugChecker} that flags {@link String#format(String, Object...)} invocations which can be
* replaced with a {@link String#join(CharSequence, CharSequence...)} or even a {@link
* String#valueOf} invocation.
*/
// XXX: What about `v1 + "sep" + v2` and similar expressions? Do we want to rewrite those to
// `String.join`, or should some `String.join` invocations be rewritten to use the `+` operator?
// (The latter suggestion would conflict with the `FormatStringConcatenation` check.)
@AutoService(BugChecker.class)
@BugPattern(
summary = "Prefer `String#join` over `String#format`",
link = BUG_PATTERNS_BASE_URL + "StringJoin",
linkType = CUSTOM,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class StringJoin extends BugChecker implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Splitter FORMAT_SPECIFIER_SPLITTER = Splitter.on("%s");
private static final Matcher<ExpressionTree> STRING_FORMAT_INVOCATION =
staticMethod().onClass(String.class.getCanonicalName()).named("format");
private static final Supplier<Type> CHAR_SEQUENCE_TYPE =
Suppliers.typeFromClass(CharSequence.class);
private static final Supplier<Type> FORMATTABLE_TYPE = Suppliers.typeFromClass(Formattable.class);
/** Instantiates a new {@link StringJoin} instance. */
public StringJoin() {}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!STRING_FORMAT_INVOCATION.matches(tree, state)) {
return Description.NO_MATCH;
}
// XXX: This check assumes that if the first argument to `String#format` is a `Locale`, that
// this argument is not vacuous, and that as a result the expression cannot be simplified using
// `#valueOf` or `#join`. Implement a separate check that identifies and drops redundant
// `Locale` arguments. See also a related comment in `FormatStringConcatenation`.
String formatString = ASTHelpers.constValue(tree.getArguments().get(0), String.class);
if (formatString == null) {
return Description.NO_MATCH;
}
List<String> separators = FORMAT_SPECIFIER_SPLITTER.splitToList(formatString);
if (separators.size() < 2) {
/* The format string does not contain `%s` format specifiers. */
return Description.NO_MATCH;
}
if (separators.size() != tree.getArguments().size()) {
/* The number of arguments does not match the number of `%s` format specifiers. */
return Description.NO_MATCH;
}
int lastIndex = separators.size() - 1;
if (!separators.get(0).isEmpty() || !separators.get(lastIndex).isEmpty()) {
/* The format string contains leading or trailing characters. */
return Description.NO_MATCH;
}
ImmutableSet<String> innerSeparators = ImmutableSet.copyOf(separators.subList(1, lastIndex));
if (innerSeparators.size() > 1) {
/* The `%s` format specifiers are not uniformly separated. */
return Description.NO_MATCH;
}
if (innerSeparators.isEmpty()) {
/*
* This `String#format` invocation performs a straightforward string conversion; use
* `String#valueOf` instead.
*/
return trySuggestExplicitStringConversion(tree, state);
}
String separator = Iterables.getOnlyElement(innerSeparators);
if (separator.indexOf('%') >= 0) {
/* The `%s` format specifiers are separated by another format specifier. */
// XXX: Strictly speaking we could support `%%` by mapping it to a literal `%`, but that
// doesn't seem worth the trouble.
return Description.NO_MATCH;
}
return trySuggestExplicitJoin(tree, separator, state);
}
/**
* If guaranteed to be behavior preserving, suggests replacing {@code String.format("%s", arg)}
* with {@code String.valueOf(arg)}.
*
* <p>If {@code arg} is already a string then the resultant conversion is vacuous. The {@link
* IdentityConversion} check will subsequently drop it.
*/
private Description trySuggestExplicitStringConversion(
MethodInvocationTree tree, VisitorState state) {
ExpressionTree argument = tree.getArguments().get(1);
if (isSubtype(ASTHelpers.getType(argument), FORMATTABLE_TYPE, state)) {
/*
* `Formattable` arguments are handled specially; `String#valueOf` is not a suitable
* alternative.
*/
return Description.NO_MATCH;
}
return buildDescription(tree)
.setMessage("Prefer `String#valueOf` over `String#format`")
.addFix(SuggestedFix.replace(tree, withStringConversionExpression(argument, state)))
.build();
}
/**
* Unless the given {@code String.format} expression includes {@link Formattable} arguments,
* suggests replacing it with a {@code String.join} expression using the specified argument
* separator.
*/
private Description trySuggestExplicitJoin(
MethodInvocationTree tree, String separator, VisitorState state) {
Iterator<? extends ExpressionTree> arguments = tree.getArguments().iterator();
SuggestedFix.Builder fix =
SuggestedFix.builder()
.replace(tree.getMethodSelect(), "String.join")
.replace(arguments.next(), Constants.format(separator));
while (arguments.hasNext()) {
ExpressionTree argument = arguments.next();
Type argumentType = ASTHelpers.getType(argument);
if (isSubtype(argumentType, FORMATTABLE_TYPE, state)) {
/*
* `Formattable` arguments are handled specially; `String#join` is not a suitable
* alternative.
*/
return Description.NO_MATCH;
}
if (!isSubtype(argumentType, CHAR_SEQUENCE_TYPE, state)) {
/*
* The argument was previously implicitly converted to a string; now this must happen
* explicitly.
*/
fix.replace(argument, withStringConversionExpression(argument, state));
}
}
return describeMatch(tree, fix.build());
}
private static boolean isSubtype(
@Nullable Type subType, Supplier<Type> superType, VisitorState state) {
return ASTHelpers.isSubtype(subType, superType.get(state), state);
}
private static String withStringConversionExpression(
ExpressionTree argument, VisitorState state) {
return String.format("String.valueOf(%s)", SourceCode.treeToString(argument, state));
}
}

View File

@@ -1,6 +1,6 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.LinkType.NONE;
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;
@@ -10,7 +10,6 @@ import static com.google.errorprone.matchers.Matchers.instanceMethod;
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
import static com.google.errorprone.matchers.Matchers.not;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
@@ -26,19 +25,13 @@ import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
/** A {@link BugChecker} that flags illegal time-zone related operations. */
/** A {@link BugChecker} which flags illegal time-zone related operations. */
@AutoService(BugChecker.class)
@BugPattern(
summary =
"""
Derive the current time from an existing `Clock` Spring bean, and don't rely on a \
`Clock`'s time zone""",
link = BUG_PATTERNS_BASE_URL + "TimeZoneUsage",
linkType = CUSTOM,
"Derive the current time from an existing `Clock` Spring bean, and don't rely on a `Clock`'s time zone",
linkType = NONE,
severity = WARNING,
tags = FRAGILE_CODE)
public final class TimeZoneUsage extends BugChecker implements MethodInvocationTreeMatcher {
@@ -47,11 +40,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,20 +54,11 @@ 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())
.named("now"),
staticMethod()
.onClassAny(Instant.class.getCanonicalName())
.named("now")
.withNoParameters());
/** Instantiates a new {@link TimeZoneUsage} instance. */
public TimeZoneUsage() {}
staticMethod().onClassAny(Instant.class.getName()).named("now").withNoParameters());
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {

View File

@@ -1,4 +1,4 @@
/** Picnic Error Prone Contrib checks. */
@com.google.errorprone.annotations.CheckReturnValue
@org.jspecify.annotations.NullMarked
@javax.annotation.ParametersAreNonnullByDefault
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,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,16 +18,9 @@ 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#(,)]+)*)?)\\)");
/** Instantiates a new {@link MethodMatcherFactory} instance. */
public MethodMatcherFactory() {}
/**
* Creates a {@link Matcher} of methods with any of the given signatures.
*

View File

@@ -0,0 +1,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,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;
@javax.annotation.ParametersAreNonnullByDefault
package tech.picnic.errorprone.bugpatterns.util;

View File

@@ -1,94 +0,0 @@
package tech.picnic.errorprone.refasterrules;
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
import org.assertj.core.api.AbstractComparableAssert;
import org.assertj.core.api.AbstractIntegerAssert;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
@OnlineDocumentation
final class AssertJComparableRules {
private AssertJComparableRules() {}
static final class AssertThatIsEqualByComparingTo<T extends Comparable<? super T>> {
@BeforeTemplate
AbstractIntegerAssert<?> before(T actual, T expected) {
return assertThat(actual.compareTo(expected)).isEqualTo(0);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractComparableAssert<?, ?> after(T actual, T expected) {
return assertThat(actual).isEqualByComparingTo(expected);
}
}
static final class AssertThatIsNotEqualByComparingTo<T extends Comparable<? super T>> {
@BeforeTemplate
AbstractIntegerAssert<?> before(T actual, T expected) {
return assertThat(actual.compareTo(expected)).isNotEqualTo(0);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractComparableAssert<?, ?> after(T actual, T expected) {
return assertThat(actual).isNotEqualByComparingTo(expected);
}
}
static final class AssertThatIsLessThan<T extends Comparable<? super T>> {
@BeforeTemplate
AbstractIntegerAssert<?> before(T actual, T expected) {
return assertThat(actual.compareTo(expected)).isNegative();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractComparableAssert<?, ?> after(T actual, T expected) {
return assertThat(actual).isLessThan(expected);
}
}
static final class AssertThatIsLessThanOrEqualTo<T extends Comparable<? super T>> {
@BeforeTemplate
AbstractIntegerAssert<?> before(T actual, T expected) {
return assertThat(actual.compareTo(expected)).isNotPositive();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractComparableAssert<?, ?> after(T actual, T expected) {
return assertThat(actual).isLessThanOrEqualTo(expected);
}
}
static final class AssertThatIsGreaterThan<T extends Comparable<? super T>> {
@BeforeTemplate
AbstractIntegerAssert<?> before(T actual, T expected) {
return assertThat(actual.compareTo(expected)).isPositive();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractComparableAssert<?, ?> after(T actual, T expected) {
return assertThat(actual).isGreaterThan(expected);
}
}
static final class AssertThatIsGreaterThanOrEqualTo<T extends Comparable<? super T>> {
@BeforeTemplate
AbstractIntegerAssert<?> before(T actual, T expected) {
return assertThat(actual.compareTo(expected)).isNotNegative();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractComparableAssert<?, ?> after(T actual, T expected) {
return assertThat(actual).isGreaterThanOrEqualTo(expected);
}
}
}

View File

@@ -1,221 +0,0 @@
package tech.picnic.errorprone.refasterrules;
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import com.google.errorprone.refaster.annotation.Matches;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.AbstractBooleanAssert;
import org.assertj.core.api.AbstractCollectionAssert;
import org.assertj.core.api.AbstractMapAssert;
import org.assertj.core.api.MapAssert;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
import tech.picnic.errorprone.refaster.matchers.IsEmpty;
@OnlineDocumentation
final class AssertJMapRules {
private AssertJMapRules() {}
static final class AbstractMapAssertIsEmpty<K, V> {
@BeforeTemplate
void before(
AbstractMapAssert<?, ?, K, V> mapAssert,
@Matches(IsEmpty.class) Map<? extends K, ? extends V> wellTypedMap,
@Matches(IsEmpty.class) Map<?, ?> arbitrarilyTypedMap,
@Matches(IsEmpty.class) Iterable<? extends K> keys) {
Refaster.anyOf(
mapAssert.containsExactlyEntriesOf(wellTypedMap),
mapAssert.containsExactlyInAnyOrderEntriesOf(wellTypedMap),
mapAssert.hasSameSizeAs(arbitrarilyTypedMap),
mapAssert.isEqualTo(arbitrarilyTypedMap),
mapAssert.containsOnlyKeys(keys),
mapAssert.containsExactly(),
mapAssert.containsOnly(),
mapAssert.containsOnlyKeys());
}
@AfterTemplate
void after(AbstractMapAssert<?, ?, K, V> mapAssert) {
mapAssert.isEmpty();
}
}
static final class AssertThatMapIsEmpty<K, V> {
@BeforeTemplate
void before(Map<K, V> map) {
Refaster.anyOf(
assertThat(map).hasSize(0),
assertThat(map.isEmpty()).isTrue(),
assertThat(map.size()).isEqualTo(0L),
assertThat(map.size()).isNotPositive());
}
@BeforeTemplate
void before2(Map<K, V> map) {
assertThat(Refaster.anyOf(map.keySet(), map.values(), map.entrySet())).isEmpty();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Map<K, V> map) {
assertThat(map).isEmpty();
}
}
static final class AbstractMapAssertIsNotEmpty<K, V> {
@BeforeTemplate
AbstractMapAssert<?, ?, K, V> before(
AbstractMapAssert<?, ?, K, V> mapAssert, @Matches(IsEmpty.class) Map<?, ?> map) {
return mapAssert.isNotEqualTo(map);
}
@AfterTemplate
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert) {
return mapAssert.isNotEmpty();
}
}
static final class AssertThatMapIsNotEmpty<K, V> {
@BeforeTemplate
AbstractAssert<?, ?> before(Map<K, V> map) {
return Refaster.anyOf(
assertThat(map.isEmpty()).isFalse(),
assertThat(map.size()).isNotEqualTo(0),
assertThat(map.size()).isPositive(),
assertThat(Refaster.anyOf(map.keySet(), map.values(), map.entrySet())).isNotEmpty());
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map) {
return assertThat(map).isNotEmpty();
}
}
static final class AbstractMapAssertContainsExactlyInAnyOrderEntriesOf<K, V> {
@BeforeTemplate
AbstractMapAssert<?, ?, K, V> before(
AbstractMapAssert<?, ?, K, V> mapAssert, Map<? extends K, ? extends V> map) {
return mapAssert.isEqualTo(map);
}
@AfterTemplate
AbstractMapAssert<?, ?, K, V> after(
AbstractMapAssert<?, ?, K, V> mapAssert, Map<? extends K, ? extends V> map) {
return mapAssert.containsExactlyInAnyOrderEntriesOf(map);
}
}
static final class AbstractMapAssertContainsExactlyEntriesOf<K, V> {
@BeforeTemplate
AbstractMapAssert<?, ?, K, V> before(AbstractMapAssert<?, ?, K, V> mapAssert, K key, V value) {
return mapAssert.containsExactlyInAnyOrderEntriesOf(ImmutableMap.of(key, value));
}
@AfterTemplate
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert, K key, V value) {
return mapAssert.containsExactlyEntriesOf(ImmutableMap.of(key, value));
}
}
static final class AssertThatMapHasSize<K, V> {
@BeforeTemplate
AbstractAssert<?, ?> before(Map<K, V> map, int length) {
return Refaster.anyOf(
assertThat(map.size()).isEqualTo(length),
assertThat(Refaster.anyOf(map.keySet(), map.values(), map.entrySet())).hasSize(length));
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, int length) {
return assertThat(map).hasSize(length);
}
}
static final class AbstractMapAssertHasSameSizeAs<K, V> {
@BeforeTemplate
AbstractMapAssert<?, ?, K, V> before(AbstractMapAssert<?, ?, K, V> mapAssert, Map<?, ?> map) {
return mapAssert.hasSize(map.size());
}
@AfterTemplate
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert, Map<?, ?> map) {
return mapAssert.hasSameSizeAs(map);
}
}
static final class AssertThatMapContainsKey<K, V> {
@BeforeTemplate
AbstractBooleanAssert<?> before(Map<K, V> map, K key) {
return assertThat(map.containsKey(key)).isTrue();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, K key) {
return assertThat(map).containsKey(key);
}
}
static final class AssertThatMapDoesNotContainKey<K, V> {
@BeforeTemplate
AbstractBooleanAssert<?> before(Map<K, V> map, K key) {
return assertThat(map.containsKey(key)).isFalse();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, K key) {
return assertThat(map).doesNotContainKey(key);
}
}
static final class AssertThatMapContainsOnlyKeys<K, V> {
@BeforeTemplate
AbstractCollectionAssert<?, Collection<? extends K>, K, ?> before(
Map<K, V> map, Set<? extends K> keys) {
return assertThat(map.keySet()).hasSameElementsAs(keys);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, Set<? extends K> keys) {
return assertThat(map).containsOnlyKeys(keys);
}
}
static final class AssertThatMapContainsValue<K, V> {
@BeforeTemplate
AbstractBooleanAssert<?> before(Map<K, V> map, V value) {
return assertThat(map.containsValue(value)).isTrue();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, V value) {
return assertThat(map).containsValue(value);
}
}
static final class AssertThatMapDoesNotContainValue<K, V> {
@BeforeTemplate
AbstractBooleanAssert<?> before(Map<K, V> map, V value) {
return assertThat(map.containsValue(value)).isFalse();
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
MapAssert<K, V> after(Map<K, V> map, V value) {
return assertThat(map).doesNotContainValue(value);
}
}
}

View File

@@ -1,117 +0,0 @@
package tech.picnic.errorprone.refasterrules;
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
import org.assertj.core.api.AbstractBooleanAssert;
import org.assertj.core.api.AbstractDoubleAssert;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
@OnlineDocumentation
final class AssertJPrimitiveRules {
private AssertJPrimitiveRules() {}
static final class AssertThatIsEqualTo {
@BeforeTemplate
AbstractBooleanAssert<?> before(boolean actual, boolean expected) {
return Refaster.anyOf(
assertThat(actual == expected).isTrue(), assertThat(actual != expected).isFalse());
}
@BeforeTemplate
@SuppressWarnings(
"java:S1244" /* The (in)equality checks are fragile, but may be seen in the wild. */)
AbstractBooleanAssert<?> before(double actual, double expected) {
return Refaster.anyOf(
assertThat(actual == expected).isTrue(), assertThat(actual != expected).isFalse());
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractBooleanAssert<?> after(boolean actual, boolean expected) {
return assertThat(actual).isEqualTo(expected);
}
}
static final class AssertThatIsNotEqualTo {
@BeforeTemplate
AbstractBooleanAssert<?> before(boolean actual, boolean expected) {
return Refaster.anyOf(
assertThat(actual != expected).isTrue(), assertThat(actual == expected).isFalse());
}
@BeforeTemplate
@SuppressWarnings(
"java:S1244" /* The (in)equality checks are fragile, but may be seen in the wild. */)
AbstractBooleanAssert<?> before(double actual, double expected) {
return Refaster.anyOf(
assertThat(actual != expected).isTrue(), assertThat(actual == expected).isFalse());
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractBooleanAssert<?> after(boolean actual, boolean expected) {
return assertThat(actual).isNotEqualTo(expected);
}
}
static final class AssertThatIsLessThan {
@BeforeTemplate
AbstractBooleanAssert<?> before(double actual, double expected) {
return Refaster.anyOf(
assertThat(actual < expected).isTrue(), assertThat(actual >= expected).isFalse());
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractDoubleAssert<?> after(double actual, double expected) {
return assertThat(actual).isLessThan(expected);
}
}
static final class AssertThatIsLessThanOrEqualTo {
@BeforeTemplate
AbstractBooleanAssert<?> before(double actual, double expected) {
return Refaster.anyOf(
assertThat(actual <= expected).isTrue(), assertThat(actual > expected).isFalse());
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractDoubleAssert<?> after(double actual, double expected) {
return assertThat(actual).isLessThanOrEqualTo(expected);
}
}
static final class AssertThatIsGreaterThan {
@BeforeTemplate
AbstractBooleanAssert<?> before(double actual, double expected) {
return Refaster.anyOf(
assertThat(actual > expected).isTrue(), assertThat(actual <= expected).isFalse());
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractDoubleAssert<?> after(double actual, double expected) {
return assertThat(actual).isGreaterThan(expected);
}
}
static final class AssertThatIsGreaterThanOrEqualTo {
@BeforeTemplate
AbstractBooleanAssert<?> before(double actual, double expected) {
return Refaster.anyOf(
assertThat(actual >= expected).isTrue(), assertThat(actual < expected).isFalse());
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
AbstractDoubleAssert<?> after(double actual, double expected) {
return assertThat(actual).isGreaterThanOrEqualTo(expected);
}
}
}

View File

@@ -1,127 +0,0 @@
package tech.picnic.errorprone.refasterrules;
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.AlsoNegation;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import java.math.BigDecimal;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/** Refaster rules related to expressions dealing with {@link BigDecimal}s. */
@OnlineDocumentation
final class BigDecimalRules {
private BigDecimalRules() {}
/** Prefer using the constant {@link BigDecimal#ZERO} when possible. */
static final class BigDecimalZero {
@BeforeTemplate
BigDecimal before() {
return Refaster.anyOf(BigDecimal.valueOf(0), new BigDecimal("0"));
}
@AfterTemplate
BigDecimal after() {
return BigDecimal.ZERO;
}
}
/** Prefer using the constant {@link BigDecimal#ONE} when possible. */
static final class BigDecimalOne {
@BeforeTemplate
BigDecimal before() {
return Refaster.anyOf(BigDecimal.valueOf(1), new BigDecimal("1"));
}
@AfterTemplate
BigDecimal after() {
return BigDecimal.ONE;
}
}
/** Prefer using the constant {@link BigDecimal#TEN} when possible. */
static final class BigDecimalTen {
@BeforeTemplate
BigDecimal before() {
return Refaster.anyOf(BigDecimal.valueOf(10), new BigDecimal("10"));
}
@AfterTemplate
BigDecimal after() {
return BigDecimal.TEN;
}
}
/** Prefer {@link BigDecimal#valueOf(double)} over the associated constructor. */
// XXX: Ideally we also rewrite `new BigDecimal("<some-integer-value>")` in cases where the
// specified number can be represented as an `int` or `long`, but that requires a custom
// `BugChecker`.
static final class BigDecimalValueOf {
@BeforeTemplate
@SuppressWarnings("java:S2111" /* This violation will be rewritten. */)
BigDecimal before(double value) {
return new BigDecimal(value);
}
@AfterTemplate
BigDecimal after(double value) {
return BigDecimal.valueOf(value);
}
}
/** Prefer using {@link BigDecimal#signum()} over more contrived alternatives. */
static final class BigDecimalSignumIsZero {
@BeforeTemplate
boolean before(BigDecimal value) {
return Refaster.anyOf(
value.compareTo(BigDecimal.ZERO) == 0, BigDecimal.ZERO.compareTo(value) == 0);
}
@AfterTemplate
@AlsoNegation
boolean after(BigDecimal value) {
return value.signum() == 0;
}
}
/**
* Prefer a {@link BigDecimal#signum()} comparison to 1 over more contrived or less clear
* alternatives.
*/
static final class BigDecimalSignumIsPositive {
@BeforeTemplate
boolean before(BigDecimal value) {
return Refaster.anyOf(
value.compareTo(BigDecimal.ZERO) > 0,
BigDecimal.ZERO.compareTo(value) < 0,
value.signum() > 0,
value.signum() >= 1);
}
@AfterTemplate
@AlsoNegation
boolean after(BigDecimal value) {
return value.signum() == 1;
}
}
/**
* Prefer a {@link BigDecimal#signum()} comparison to -1 over more contrived or less clear
* alternatives.
*/
static final class BigDecimalSignumIsNegative {
@BeforeTemplate
boolean before(BigDecimal value) {
return Refaster.anyOf(
value.compareTo(BigDecimal.ZERO) < 0,
BigDecimal.ZERO.compareTo(value) > 0,
value.signum() < 0,
value.signum() <= -1);
}
@AfterTemplate
@AlsoNegation
boolean after(BigDecimal value) {
return value.signum() == -1;
}
}
}

View File

@@ -1,89 +0,0 @@
package tech.picnic.errorprone.refasterrules;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChooser;
import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import com.sun.tools.javac.util.Constants;
import com.sun.tools.javac.util.Convert;
import javax.lang.model.element.Name;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/** Refaster rules related to {@link com.google.errorprone.bugpatterns.BugChecker} classes. */
@OnlineDocumentation
final class BugCheckerRules {
private BugCheckerRules() {}
/**
* Avoid calling {@link BugCheckerRefactoringTestHelper#setFixChooser(FixChooser)} or {@link
* BugCheckerRefactoringTestHelper#setImportOrder(String)} with their respective default values.
*/
static final class BugCheckerRefactoringTestHelperIdentity {
@BeforeTemplate
BugCheckerRefactoringTestHelper before(BugCheckerRefactoringTestHelper helper) {
return Refaster.anyOf(
helper.setFixChooser(FixChoosers.FIRST), helper.setImportOrder("static-first"));
}
@AfterTemplate
@CanIgnoreReturnValue
BugCheckerRefactoringTestHelper after(BugCheckerRefactoringTestHelper helper) {
return helper;
}
}
/**
* Prefer {@link BugCheckerRefactoringTestHelper.ExpectOutput#expectUnchanged()} over repeating
* the input.
*/
// XXX: This rule assumes that the full source code is specified as a single string, e.g. using a
// text block. Support for multi-line source code input would require a `BugChecker`
// implementation instead.
static final class BugCheckerRefactoringTestHelperAddInputLinesExpectUnchanged {
@BeforeTemplate
BugCheckerRefactoringTestHelper before(
BugCheckerRefactoringTestHelper helper, String path, String source) {
return helper.addInputLines(path, source).addOutputLines(path, source);
}
@AfterTemplate
BugCheckerRefactoringTestHelper after(
BugCheckerRefactoringTestHelper helper, String path, String source) {
return helper.addInputLines(path, source).expectUnchanged();
}
}
/** Prefer using the {@link Constants} API over more verbose alternatives. */
static final class ConstantsFormat {
@BeforeTemplate
String before(String value) {
return String.format("\"%s\"", Convert.quote(value));
}
@AfterTemplate
String after(String value) {
return Constants.format(value);
}
}
/** Prefer {@link Name#contentEquals(CharSequence)} over more verbose alternatives. */
static final class NameContentEquals {
@BeforeTemplate
boolean before(Name name, CharSequence string) {
return name.toString().equals(string.toString());
}
@BeforeTemplate
boolean before(Name name, String string) {
return name.toString().equals(string);
}
@AfterTemplate
boolean after(Name name, CharSequence string) {
return name.contentEquals(string);
}
}
}

View File

@@ -1,92 +0,0 @@
package tech.picnic.errorprone.refasterrules;
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import java.util.function.Function;
import java.util.function.Predicate;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/** Refaster rules related to expressions dealing with classes. */
@OnlineDocumentation
final class ClassRules {
private ClassRules() {}
/** Prefer {@link Class#isInstance(Object)} over more contrived alternatives. */
static final class ClassIsInstance<T, S> {
@BeforeTemplate
boolean before(Class<T> clazz, S object) {
return clazz.isAssignableFrom(object.getClass());
}
@AfterTemplate
boolean after(Class<T> clazz, S object) {
return clazz.isInstance(object);
}
}
/** Prefer using the {@code instanceof} keyword over less idiomatic alternatives. */
static final class Instanceof<T, S> {
@BeforeTemplate
boolean before(S object) {
return Refaster.<T>clazz().isInstance(object);
}
@AfterTemplate
boolean after(S object) {
return Refaster.<T>isInstance(object);
}
}
/**
* Prefer {@link Class#isInstance(Object)} method references over lambda expressions that require
* naming a variable.
*/
// XXX: Once the `ClassReferenceIsInstancePredicate` rule is dropped, rename this rule to just
// `ClassIsInstancePredicate`.
static final class ClassLiteralIsInstancePredicate<T, S> {
@BeforeTemplate
Predicate<S> before() {
return o -> Refaster.<T>isInstance(o);
}
@AfterTemplate
Predicate<S> after() {
return Refaster.<T>clazz()::isInstance;
}
}
/**
* Prefer {@link Class#isInstance(Object)} method references over lambda expressions that require
* naming a variable.
*/
// XXX: Drop this rule once the `MethodReferenceUsage` rule is enabled by default.
static final class ClassReferenceIsInstancePredicate<T, S> {
@BeforeTemplate
Predicate<S> before(Class<T> clazz) {
return o -> clazz.isInstance(o);
}
@AfterTemplate
Predicate<S> after(Class<T> clazz) {
return clazz::isInstance;
}
}
/**
* Prefer {@link Class#cast(Object)} method references over lambda expressions that require naming
* a variable.
*/
// XXX: Drop this rule once the `MethodReferenceUsage` rule is enabled by default.
static final class ClassReferenceCast<T, S> {
@BeforeTemplate
Function<T, S> before(Class<? extends S> clazz) {
return o -> clazz.cast(o);
}
@AfterTemplate
Function<T, S> after(Class<? extends S> clazz) {
return clazz::cast;
}
}
}

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