mirror of
https://github.com/jlengrand/error-prone-support.git
synced 2026-03-10 15:49:33 +00:00
Compare commits
220 Commits
rossendrij
...
maxsumrall
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56ed83bf49 | ||
|
|
575d494303 | ||
|
|
844ef84d55 | ||
|
|
29469cbbfd | ||
|
|
da9a6dd270 | ||
|
|
0cb03aa132 | ||
|
|
0f15070883 | ||
|
|
14b5fa1feb | ||
|
|
d1f513373f | ||
|
|
cd1593009b | ||
|
|
0d52414c04 | ||
|
|
a55ed9cea9 | ||
|
|
9b191f46aa | ||
|
|
6ea756f3ce | ||
|
|
adbcc4a94f | ||
|
|
0ed2788dbd | ||
|
|
04749ffcf5 | ||
|
|
37077bd03c | ||
|
|
4798f7cf5f | ||
|
|
ac285f0c50 | ||
|
|
1f3fb08082 | ||
|
|
9a397aa047 | ||
|
|
60e74332de | ||
|
|
3a94aad3b0 | ||
|
|
a5b5f43974 | ||
|
|
c212b9a171 | ||
|
|
499f922328 | ||
|
|
5720732b48 | ||
|
|
81ffd04fe4 | ||
|
|
b2f514f0a5 | ||
|
|
dad92b5fa6 | ||
|
|
6d699f75ad | ||
|
|
cad0c74dc0 | ||
|
|
a3a1f495f1 | ||
|
|
b2a646fc21 | ||
|
|
1194b0c83c | ||
|
|
82a07fde25 | ||
|
|
dec3220b5b | ||
|
|
79356ac553 | ||
|
|
f079c53914 | ||
|
|
8e24da907d | ||
|
|
3c89a1c80d | ||
|
|
9bd4b16001 | ||
|
|
6370452803 | ||
|
|
feb9abfa91 | ||
|
|
560f52bad0 | ||
|
|
2356c61314 | ||
|
|
9a9ef3c59d | ||
|
|
e9a1d54035 | ||
|
|
e9733f7426 | ||
|
|
534ebb62a1 | ||
|
|
1ed1e6cd03 | ||
|
|
85e3db6f0a | ||
|
|
6f4db8fc4d | ||
|
|
9d08e8fd4d | ||
|
|
9992ff49ce | ||
|
|
190b47870b | ||
|
|
becfcb5374 | ||
|
|
d45682143d | ||
|
|
4237732c5b | ||
|
|
d7c86c4854 | ||
|
|
e6e50717d3 | ||
|
|
834f9ae49b | ||
|
|
601fcf2648 | ||
|
|
27c6c48e68 | ||
|
|
b22078657a | ||
|
|
165a003f6a | ||
|
|
ecb8820d80 | ||
|
|
6313bd56d8 | ||
|
|
5665470fe4 | ||
|
|
d0a89da24d | ||
|
|
7c40fdc033 | ||
|
|
8724701baf | ||
|
|
e9ae238c2b | ||
|
|
ff2ed6f82c | ||
|
|
17aeeb9ea9 | ||
|
|
fd2946a9c8 | ||
|
|
870d16a0b6 | ||
|
|
96114235c5 | ||
|
|
bfbf748d47 | ||
|
|
8d0f1d78e6 | ||
|
|
ec00a5522f | ||
|
|
465b16c471 | ||
|
|
2cbd48ec47 | ||
|
|
0153c1495f | ||
|
|
81450285be | ||
|
|
096acfb14f | ||
|
|
17bcdb6faa | ||
|
|
3ee527fda2 | ||
|
|
b1c815770b | ||
|
|
bc1f204877 | ||
|
|
cf995ece2b | ||
|
|
d427e298e2 | ||
|
|
ae327d8d64 | ||
|
|
a6f794de3d | ||
|
|
1794d36053 | ||
|
|
ee62af4a86 | ||
|
|
1afce12b52 | ||
|
|
f585306a1f | ||
|
|
4f9aba83ec | ||
|
|
066591c379 | ||
|
|
789f8c86f2 | ||
|
|
0ccebcc9c4 | ||
|
|
8803d23a8e | ||
|
|
5afa7e1878 | ||
|
|
8e3beb9d5c | ||
|
|
96ab66cdcf | ||
|
|
330328329f | ||
|
|
f46859ae3a | ||
|
|
6d15cfe7ff | ||
|
|
fa1bb8aa94 | ||
|
|
415ae35906 | ||
|
|
7cc8abc3de | ||
|
|
16c8bb0b27 | ||
|
|
609d80c9fa | ||
|
|
793bda50d2 | ||
|
|
5d86a66791 | ||
|
|
84d425e4ca | ||
|
|
4fead24e73 | ||
|
|
92df829801 | ||
|
|
98185b92ae | ||
|
|
1b6356a876 | ||
|
|
919a7c7ebe | ||
|
|
79b0123f41 | ||
|
|
3967542edf | ||
|
|
0f05d15dd2 | ||
|
|
5bdb90634a | ||
|
|
1808164c0d | ||
|
|
f451b4cb73 | ||
|
|
d4c95299eb | ||
|
|
1b93843d62 | ||
|
|
3929b39e62 | ||
|
|
81f701fd9e | ||
|
|
b040648400 | ||
|
|
79ca98d20f | ||
|
|
a6122270a9 | ||
|
|
4c0f2c966c | ||
|
|
778a6e7043 | ||
|
|
b6146efaf6 | ||
|
|
fcaa1f7068 | ||
|
|
ece33b0061 | ||
|
|
48772a044e | ||
|
|
281534aeca | ||
|
|
42e632e5db | ||
|
|
7febccb7ff | ||
|
|
d5c1c858d5 | ||
|
|
55d2622380 | ||
|
|
f10f2c9209 | ||
|
|
99f85614fd | ||
|
|
be24edadae | ||
|
|
068c03708b | ||
|
|
9c330981ea | ||
|
|
b780c05dc0 | ||
|
|
16955a9cfa | ||
|
|
8fa3ff3702 | ||
|
|
022a3d293e | ||
|
|
81227cdd94 | ||
|
|
04d886c031 | ||
|
|
afb2a28dcf | ||
|
|
dc0f90e981 | ||
|
|
2196bbd8f9 | ||
|
|
f3b81304b9 | ||
|
|
b0d374040a | ||
|
|
45dfc53d40 | ||
|
|
6cb10ffe2f | ||
|
|
92f2b0ab0f | ||
|
|
21388273c5 | ||
|
|
b2e15607c1 | ||
|
|
50e730fb40 | ||
|
|
a844b9e532 | ||
|
|
671ee1eedb | ||
|
|
7fe61c226a | ||
|
|
8fcc91febf | ||
|
|
e00aba12c3 | ||
|
|
0118cc6c10 | ||
|
|
91e009cab0 | ||
|
|
68dca3204e | ||
|
|
e6ccb523fd | ||
|
|
d0533f7190 | ||
|
|
759e7ea2ff | ||
|
|
2578857a37 | ||
|
|
1d28512f09 | ||
|
|
4efd79bfa6 | ||
|
|
d6b28733f6 | ||
|
|
b9884fa6d6 | ||
|
|
4fd8f57430 | ||
|
|
a2559bddda | ||
|
|
7de9bede19 | ||
|
|
85cb997f1b | ||
|
|
d5a78186db | ||
|
|
128e178d37 | ||
|
|
7aef2cfe51 | ||
|
|
5cec0dd508 | ||
|
|
3f1399c139 | ||
|
|
757d5b1d70 | ||
|
|
45cac6105e | ||
|
|
8f8f57fc5b | ||
|
|
902d4f7736 | ||
|
|
4ec349582c | ||
|
|
57836103ea | ||
|
|
bd73243c34 | ||
|
|
9af50d7e0b | ||
|
|
f5ae47fbac | ||
|
|
9ad8c275d3 | ||
|
|
f4e191e33b | ||
|
|
c4e476a731 | ||
|
|
5ca95eb36d | ||
|
|
9204ef0e84 | ||
|
|
ed45be4e15 | ||
|
|
0561c371de | ||
|
|
aa5ad4d25b | ||
|
|
8c0041a94e | ||
|
|
397f9c3df7 | ||
|
|
2ba7bf9f46 | ||
|
|
5b079eef84 | ||
|
|
a2ce053daf | ||
|
|
ba02bad9bf | ||
|
|
d97a20247f | ||
|
|
50970eb932 | ||
|
|
7e7318ad80 |
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -17,23 +17,24 @@ you'd like to be solved through Error Prone Support. -->
|
||||
|
||||
- [ ] Support a stylistic preference.
|
||||
- [ ] Avoid a common gotcha, or potential problem.
|
||||
- [ ] Improve performance.
|
||||
|
||||
<!--
|
||||
Here, provide a clear and concise description of the desired change.
|
||||
|
||||
If possible, provide a simple and minimal example using the following format:
|
||||
|
||||
I would like to rewrite the following code:
|
||||
```java
|
||||
// XXX: Write the code to match here.
|
||||
```
|
||||
|
||||
to:
|
||||
```java
|
||||
// XXX: Write the desired code here.
|
||||
```
|
||||
-->
|
||||
|
||||
I would like to rewrite the following code:
|
||||
```java
|
||||
// XXX: Write the code to match here.
|
||||
```
|
||||
|
||||
to:
|
||||
```java
|
||||
// XXX: Write the desired code here.
|
||||
```
|
||||
|
||||
### Considerations
|
||||
|
||||
<!--
|
||||
|
||||
2
.github/release.yml
vendored
2
.github/release.yml
vendored
@@ -3,7 +3,7 @@ changelog:
|
||||
labels:
|
||||
- "ignore-changelog"
|
||||
categories:
|
||||
- title: ":rocket: New Error Prone checks and Refaster templates"
|
||||
- title: ":rocket: New Error Prone checks and Refaster rules"
|
||||
labels:
|
||||
- "new feature"
|
||||
- title: ":sparkles: Improvements"
|
||||
|
||||
30
.github/workflows/build.yaml
vendored
30
.github/workflows/build.yaml
vendored
@@ -2,16 +2,32 @@ name: Build and verify
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
branches: [ master ]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
jdk: [ 11.0.16, 17.0.4 ]
|
||||
os: [ ubuntu-22.04 ]
|
||||
jdk: [ 11.0.16, 17.0.4, 19 ]
|
||||
distribution: [ temurin ]
|
||||
experimental: [ false ]
|
||||
include:
|
||||
- os: macos-12
|
||||
jdk: 17.0.4
|
||||
distribution: temurin
|
||||
experimental: false
|
||||
- os: windows-2022
|
||||
jdk: 17.0.4
|
||||
distribution: temurin
|
||||
experimental: false
|
||||
- os: ubuntu-22.04
|
||||
jdk: 20-ea
|
||||
distribution: zulu
|
||||
experimental: true
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
steps:
|
||||
# We run the build twice for each supported JDK: once against the
|
||||
# original Error Prone release, using only Error Prone checks available
|
||||
@@ -19,12 +35,12 @@ jobs:
|
||||
# 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
|
||||
uses: actions/checkout@v3.1.0
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3.5.1
|
||||
uses: actions/setup-java@v3.8.0
|
||||
with:
|
||||
java-version: ${{ matrix.jdk }}
|
||||
distribution: temurin
|
||||
distribution: ${{ matrix.distribution }}
|
||||
cache: maven
|
||||
- name: Display build environment details
|
||||
run: mvn --version
|
||||
|
||||
49
.github/workflows/deploy-website.yaml
vendored
Normal file
49
.github/workflows/deploy-website.yaml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Update `error-prone.picnic.tech` website content
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master, website ]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3.1.0
|
||||
- uses: ruby/setup-ruby@v1.126.0
|
||||
with:
|
||||
working-directory: ./website
|
||||
bundler-cache: true
|
||||
- name: Configure Github Pages
|
||||
uses: actions/configure-pages@v2.1.3
|
||||
- name: Generate documentation
|
||||
run: ./generate-docs.sh
|
||||
- name: Build website with Jekyll
|
||||
working-directory: ./website
|
||||
run: bundle exec jekyll build
|
||||
- name: Validate HTML output
|
||||
working-directory: ./website
|
||||
# XXX: Drop `--disable_external true` once we fully adopted the
|
||||
# "Refaster rules" terminology on our website and in the code.
|
||||
run: bundle exec htmlproofer --disable_external true --check-external-hash false ./_site
|
||||
- name: Upload website as artifact
|
||||
uses: actions/upload-pages-artifact@v1.0.5
|
||||
with:
|
||||
path: ./website/_site
|
||||
deploy:
|
||||
if: github.ref == 'refs/heads/website'
|
||||
needs: build
|
||||
permissions:
|
||||
id-token: write
|
||||
pages: write
|
||||
runs-on: ubuntu-22.04
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v1.2.3
|
||||
37
.github/workflows/pitest-analyze-pr.yml
vendored
Normal file
37
.github/workflows/pitest-analyze-pr.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# Performs mutation testing analysis on the files changed by a pull request and
|
||||
# uploads the results. The associated PR is subsequently updated by the
|
||||
# `pitest-update-pr.yml` workflow. See https://blog.pitest.org/oss-pitest-pr/
|
||||
# for details.
|
||||
name: "Mutation testing"
|
||||
on:
|
||||
pull_request:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
analyze-pr:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3.1.0
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3.8.0
|
||||
with:
|
||||
java-version: 17.0.4
|
||||
distribution: temurin
|
||||
cache: maven
|
||||
- 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@v3.1.1
|
||||
with:
|
||||
name: pitest-reports
|
||||
path: ./target/pit-reports-ci
|
||||
35
.github/workflows/pitest-update-pr.yml
vendored
Normal file
35
.github/workflows/pitest-update-pr.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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
|
||||
checks: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
jobs:
|
||||
update-pr:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3.1.0
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3.8.0
|
||||
with:
|
||||
java-version: 17.0.4
|
||||
distribution: temurin
|
||||
cache: maven
|
||||
- name: Download Pitest analysis artifact
|
||||
uses: dawidd6/action-download-artifact@v2.24.2
|
||||
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
|
||||
@@ -1 +1,3 @@
|
||||
--batch-mode --errors --strict-checksums
|
||||
--batch-mode
|
||||
--errors
|
||||
--strict-checksums
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"^com\\.palantir\\.baseline:baseline-error-prone$"
|
||||
"^ruby\\/setup-ruby$"
|
||||
],
|
||||
"schedule": "* * 1 * *"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2022 Picnic Technologies BV
|
||||
Copyright (c) 2017-2023 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
|
||||
|
||||
103
README.md
103
README.md
@@ -1,30 +1,34 @@
|
||||
<div align="center">
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="logo-dark.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="logo.svg">
|
||||
<img alt="Error Prone Support logo" src="logo.svg" width="50%">
|
||||
</picture>
|
||||
<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-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 gotchas.
|
||||
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.
|
||||
|
||||
Read more on how Picnic uses Error Prone (Support) in the blog post [_Picnic
|
||||
loves Error Prone: producing high-quality and consistent Java
|
||||
code_][picnic-blog-ep-post].
|
||||
|
||||
[![Maven Central][maven-central-badge]][maven-central-search]
|
||||
[![Reproducible Builds][reproducible-builds-badge]][reproducible-builds-report]
|
||||
[![GitHub Actions][github-actions-build-badge]][github-actions-build-master]
|
||||
[![License][license-badge]][license]
|
||||
[![PRs Welcome][pr-badge]][contributing]
|
||||
|
||||
[Getting started](#-getting-started) • [Building](#-building) •
|
||||
[Getting started](#-getting-started) •
|
||||
[Developing Error Prone Support](#-developing-error-prone-support) •
|
||||
[How it works](#-how-it-works) • [Contributing](#%EF%B8%8F-contributing)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Getting started
|
||||
@@ -32,7 +36,9 @@ maintainability, consistency and avoidance of common gotchas.
|
||||
### Installation
|
||||
|
||||
This library is built on top of [Error Prone][error-prone-orig-repo]. To use
|
||||
it:
|
||||
it, read the installation guide for Maven or Gradle below.
|
||||
|
||||
#### Maven
|
||||
|
||||
1. First, follow Error Prone's [installation
|
||||
guide][error-prone-installation-guide].
|
||||
@@ -60,7 +66,7 @@ it:
|
||||
<artifactId>error-prone-contrib</artifactId>
|
||||
<version>${error-prone-support.version}</version>
|
||||
</path>
|
||||
<!-- Error Prone Support's Refaster templates. -->
|
||||
<!-- Error Prone Support's Refaster rules. -->
|
||||
<path>
|
||||
<groupId>tech.picnic.error-prone-support</groupId>
|
||||
<artifactId>refaster-runner</artifactId>
|
||||
@@ -90,6 +96,31 @@ it:
|
||||
Prone Support. Alternatively reference this project's `self-check` profile
|
||||
definition. -->
|
||||
|
||||
#### Gradle
|
||||
|
||||
1. First, follow the [installation guide]
|
||||
[error-prone-gradle-installation-guide] of the `gradle-errorprone-plugin`.
|
||||
2. Next, edit your `build.gradle` file to add one or more Error Prone Support
|
||||
modules:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
// Error Prone itself.
|
||||
errorprone("com.google.errorprone:error_prone_core:${errorProneVersion}")
|
||||
// Error Prone Support's additional bug checkers.
|
||||
errorprone("tech.picnic.error-prone-support:error-prone-contrib:${errorProneSupportVersion}")
|
||||
// Error Prone Support's Refaster rules.
|
||||
errorprone("tech.picnic.error-prone-support:refaster-runner:${errorProneSupportVersion}")
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.errorprone.disableWarningsInGeneratedCode = true
|
||||
// Add other Error Prone flags here. See:
|
||||
// - https://github.com/tbroyer/gradle-errorprone-plugin#configuration
|
||||
// - https://errorprone.info/docs/flags
|
||||
}
|
||||
```
|
||||
|
||||
### Seeing it in action
|
||||
|
||||
Consider the following example code:
|
||||
@@ -116,15 +147,13 @@ code with Maven should yield two compiler warnings:
|
||||
```sh
|
||||
$ mvn clean install
|
||||
...
|
||||
[INFO] -------------------------------------------------------------
|
||||
[WARNING] COMPILATION WARNING :
|
||||
[INFO] -------------------------------------------------------------
|
||||
[WARNING] Example.java:[9,34] [tech.picnic.errorprone.refastertemplates.BigDecimalTemplates.BigDecimalZero]
|
||||
[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:[14,35] [IdentityConversion] This method invocation appears redundant; remove it or suppress this warning and add a comment explaining its purpose
|
||||
...
|
||||
[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() {'?
|
||||
[INFO] 2 warnings
|
||||
[INFO] -------------------------------------------------------------
|
||||
...
|
||||
```
|
||||
|
||||
@@ -132,17 +161,18 @@ 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] template capable of
|
||||
[rewriting][refaster-templates-bigdecimal] expressions of the form
|
||||
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
|
||||
templates][refaster-templates].
|
||||
rules][refaster-rules].
|
||||
|
||||
## 👷 Building
|
||||
## 👷 Developing Error Prone Support
|
||||
|
||||
This is a [Maven][maven] project, so running `mvn clean install`
|
||||
performs a full clean build. Some relevant flags:
|
||||
This is a [Maven][maven] project, so running `mvn clean install` performs a
|
||||
full clean build and installs the library to your local Maven repository. Some
|
||||
relevant flags:
|
||||
|
||||
- `-Dverification.warn` makes the warnings and errors emitted by various
|
||||
plugins and the Java compiler non-fatal, where possible.
|
||||
@@ -163,7 +193,7 @@ Some other commands one may find relevant:
|
||||
|
||||
- `mvn fmt:format` formats the code using
|
||||
[`google-java-format`][google-java-format].
|
||||
- `./run-mutation-tests.sh` runs mutation tests using [PIT][pitest]. The
|
||||
- `./run-mutation-tests.sh` runs mutation tests using [Pitest][pitest]. The
|
||||
results can be reviewed by opening the respective
|
||||
`target/pit-reports/index.html` files. For more information check the [PIT
|
||||
Maven plugin][pitest-maven].
|
||||
@@ -198,12 +228,13 @@ 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].
|
||||
|
||||
[bug-checks]: error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/
|
||||
[bug-checks-identity-conversion]: error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/IdentityConversion.java
|
||||
[contributing]: CONTRIBUTING.md
|
||||
[bug-checks]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/
|
||||
[bug-checks-identity-conversion]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/IdentityConversion.java
|
||||
[contributing]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/CONTRIBUTING.md
|
||||
[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
|
||||
@@ -212,13 +243,17 @@ guidelines][contributing].
|
||||
[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]: LICENSE.md
|
||||
[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
|
||||
[picnic-blog]: https://blog.picnic.nl
|
||||
[picnic-blog-ep-post]: https://blog.picnic.nl/picnic-loves-error-prone-producing-high-quality-and-consistent-java-code-b8a566be6886
|
||||
[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-templates-bigdecimal]: error-prone-contrib/src/main/java/tech/picnic/errorprone/refastertemplates/BigDecimalTemplates.java
|
||||
[refaster-templates]: error-prone-contrib/src/main/java/tech/picnic/errorprone/refastertemplates/
|
||||
[refaster-rules-bigdecimal]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/BigDecimalRules.java
|
||||
[refaster-rules]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/
|
||||
[reproducible-builds-badge]: https://img.shields.io/badge/Reproducible_Builds-ok-success?labelColor=1e5b96
|
||||
[reproducible-builds-report]: https://github.com/jvm-repo-rebuild/reproducible-central/blob/master/content/tech/picnic/error-prone-support/error-prone-support/README.md
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
set -e -u -o pipefail
|
||||
|
||||
if [ "${#}" -gt 1 ]; then
|
||||
echo "Usage: ./$(basename "${0}") [PatchChecks]"
|
||||
echo "Usage: ${0} [PatchChecks]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
patchChecks=${1:-}
|
||||
|
||||
mvn clean test-compile fmt:format \
|
||||
-s "$(dirname "${0}")/settings.xml" \
|
||||
-T 1.0C \
|
||||
-Perror-prone \
|
||||
-Perror-prone-fork \
|
||||
|
||||
7
cdg-pitest-licence.txt
Normal file
7
cdg-pitest-licence.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
# Arcmutate license for Error Prone Support, requested by sending an email to
|
||||
# support@arcmutate.com.
|
||||
expires=07/11/2023
|
||||
keyVersion=1
|
||||
signature=MhZxMbnO6UovNfllM0JuVWkZyvRT3/G5o/uT0Mm36c7200VpZNVu03gTAGivnl9W5RzvZhfpIHccuQ5ctjQkrqhsFSrl4fyqPqu3y5V2fsHIdFXP/G72EGj6Kay9ndLpaEHalqE0bEwxdnHMzEYq5y3O9vUPv8MhUl57xk+rvBo\=
|
||||
packages=tech.picnic.errorprone.*
|
||||
type=OSSS
|
||||
87
documentation-support/pom.xml
Normal file
87
documentation-support/pom.xml
Normal file
@@ -0,0 +1,87 @@
|
||||
<?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.8.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>documentation-support</artifactId>
|
||||
|
||||
<name>Picnic :: Error Prone Support :: Documentation Support</name>
|
||||
<description>Data extraction support for the purpose of documentation generation.</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_annotation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_check_api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_test_helpers</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto</groupId>
|
||||
<artifactId>auto-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.service</groupId>
|
||||
<artifactId>auto-service-annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.value</groupId>
|
||||
<artifactId>auto-value-annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jspecify</groupId>
|
||||
<artifactId>jspecify</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-params</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,103 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.google.auto.common.AnnotationMirrors;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.tools.javac.code.Attribute;
|
||||
import com.sun.tools.javac.code.Symbol.ClassSymbol;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
import 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
|
||||
final class BugPatternExtractor implements Extractor<BugPatternDocumentation> {
|
||||
@Override
|
||||
public BugPatternDocumentation extract(ClassTree tree, Context context) {
|
||||
ClassSymbol symbol = ASTHelpers.getSymbol(tree);
|
||||
BugPattern annotation = symbol.getAnnotation(BugPattern.class);
|
||||
requireNonNull(annotation, "BugPattern annotation must be present");
|
||||
|
||||
return new AutoValue_BugPatternExtractor_BugPatternDocumentation(
|
||||
symbol.getQualifiedName().toString(),
|
||||
annotation.name().isEmpty() ? tree.getSimpleName().toString() : annotation.name(),
|
||||
ImmutableList.copyOf(annotation.altNames()),
|
||||
annotation.link(),
|
||||
ImmutableList.copyOf(annotation.tags()),
|
||||
annotation.summary(),
|
||||
annotation.explanation(),
|
||||
annotation.severity(),
|
||||
annotation.disableable(),
|
||||
annotation.documentSuppression() ? getSuppressionAnnotations(tree) : ImmutableList.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canExtract(ClassTree tree) {
|
||||
return ASTHelpers.hasDirectAnnotationWithSimpleName(tree, BugPattern.class.getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fully-qualified class names of suppression annotations specified by the {@link
|
||||
* BugPattern} annotation located on the given tree.
|
||||
*
|
||||
* @implNote This method cannot simply invoke {@link BugPattern#suppressionAnnotations()}, as that
|
||||
* will yield an "Attempt to access Class objects for TypeMirrors" exception.
|
||||
*/
|
||||
private static ImmutableList<String> getSuppressionAnnotations(ClassTree tree) {
|
||||
AnnotationTree annotationTree =
|
||||
ASTHelpers.getAnnotationWithSimpleName(
|
||||
ASTHelpers.getAnnotations(tree), BugPattern.class.getSimpleName());
|
||||
requireNonNull(annotationTree, "BugPattern annotation must be present");
|
||||
|
||||
Attribute.Array types =
|
||||
doCast(
|
||||
AnnotationMirrors.getAnnotationValue(
|
||||
ASTHelpers.getAnnotationMirror(annotationTree), "suppressionAnnotations"),
|
||||
Attribute.Array.class);
|
||||
|
||||
return types.getValue().stream()
|
||||
.map(v -> doCast(v, Attribute.Class.class).classType.toString())
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T extends AnnotationValue> T doCast(AnnotationValue value, Class<T> target) {
|
||||
verify(target.isInstance(value), "Value '%s' is not of type '%s'", value, target);
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class BugPatternDocumentation {
|
||||
abstract String fullyQualifiedName();
|
||||
|
||||
abstract String name();
|
||||
|
||||
abstract ImmutableList<String> altNames();
|
||||
|
||||
abstract String link();
|
||||
|
||||
abstract ImmutableList<String> tags();
|
||||
|
||||
abstract String summary();
|
||||
|
||||
abstract String explanation();
|
||||
|
||||
abstract SeverityLevel severityLevel();
|
||||
|
||||
abstract boolean canDisable();
|
||||
|
||||
abstract ImmutableList<String> suppressionAnnotations();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.sun.source.util.JavacTask;
|
||||
import com.sun.source.util.Plugin;
|
||||
import com.sun.tools.javac.api.BasicJavacTask;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A compiler {@link Plugin} that analyzes and extracts relevant information for documentation
|
||||
* purposes from processed files.
|
||||
*/
|
||||
// XXX: Find a better name for this class; it doesn't generate documentation per se.
|
||||
@AutoService(Plugin.class)
|
||||
public final class DocumentationGenerator implements Plugin {
|
||||
@VisibleForTesting static final String OUTPUT_DIRECTORY_FLAG = "-XoutputDirectory";
|
||||
private static final Pattern OUTPUT_DIRECTORY_FLAG_PATTERN =
|
||||
Pattern.compile(Pattern.quote(OUTPUT_DIRECTORY_FLAG) + "=(.*)");
|
||||
|
||||
/** Instantiates a new {@link DocumentationGenerator} instance. */
|
||||
public DocumentationGenerator() {}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(JavacTask javacTask, String... args) {
|
||||
checkArgument(args.length == 1, "Precisely one path must be provided");
|
||||
|
||||
javacTask.addTaskListener(
|
||||
new DocumentationGeneratorTaskListener(
|
||||
((BasicJavacTask) javacTask).getContext(), getOutputPath(args[0])));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static Path getOutputPath(String pathArg) {
|
||||
Matcher matcher = OUTPUT_DIRECTORY_FLAG_PATTERN.matcher(pathArg);
|
||||
checkArgument(
|
||||
matcher.matches(), "'%s' must be of the form '%s=<value>'", pathArg, OUTPUT_DIRECTORY_FLAG);
|
||||
|
||||
String path = matcher.group(1);
|
||||
try {
|
||||
return Path.of(path);
|
||||
} catch (InvalidPathException e) {
|
||||
throw new IllegalArgumentException(String.format("Invalid path '%s'", path), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.util.TaskEvent;
|
||||
import com.sun.source.util.TaskEvent.Kind;
|
||||
import com.sun.source.util.TaskListener;
|
||||
import com.sun.tools.javac.api.JavacTrees;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import 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 {
|
||||
private static final ObjectMapper OBJECT_MAPPER =
|
||||
new ObjectMapper().setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
|
||||
|
||||
private final Context context;
|
||||
private final Path docsPath;
|
||||
|
||||
DocumentationGeneratorTaskListener(Context context, Path path) {
|
||||
this.context = context;
|
||||
this.docsPath = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void started(TaskEvent taskEvent) {
|
||||
if (taskEvent.getKind() == Kind.ANALYZE) {
|
||||
createDocsDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finished(TaskEvent taskEvent) {
|
||||
if (taskEvent.getKind() != Kind.ANALYZE) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClassTree classTree = JavacTrees.instance(context).getTree(taskEvent.getTypeElement());
|
||||
JavaFileObject sourceFile = taskEvent.getSourceFile();
|
||||
if (classTree == null || sourceFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExtractorType.findMatchingType(classTree)
|
||||
.ifPresent(
|
||||
extractorType ->
|
||||
writeToFile(
|
||||
extractorType.getIdentifier(),
|
||||
getSimpleClassName(sourceFile.toUri()),
|
||||
extractorType.getExtractor().extract(classTree, context)));
|
||||
}
|
||||
|
||||
private void createDocsDirectory() {
|
||||
try {
|
||||
Files.createDirectories(docsPath);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Error while creating directory with path '%s'", docsPath), e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> void writeToFile(String identifier, String className, T data) {
|
||||
File file = docsPath.resolve(String.format("%s-%s.json", identifier, className)).toFile();
|
||||
|
||||
try (FileWriter fileWriter = new FileWriter(file, UTF_8)) {
|
||||
OBJECT_MAPPER.writeValue(fileWriter, data);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(String.format("Cannot write to file '%s'", file.getPath()), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getSimpleClassName(URI path) {
|
||||
return Paths.get(path).getFileName().toString().replace(".java", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
/**
|
||||
* Extracts and returns an instance of {@link T} using the provided arguments.
|
||||
*
|
||||
* @param tree The {@link ClassTree} to analyze and from which to extract instances of {@link T}.
|
||||
* @param context The {@link Context} in which the current compilation takes place.
|
||||
* @return A non-null instance of {@link T}.
|
||||
*/
|
||||
// XXX: Drop `Context` parameter unless used.
|
||||
T extract(ClassTree tree, Context context);
|
||||
|
||||
/**
|
||||
* Tells whether this {@link Extractor} can extract documentation content from the given {@link
|
||||
* ClassTree}.
|
||||
*
|
||||
* @param tree The {@link ClassTree} of interest.
|
||||
* @return {@code true} iff data extraction is supported.
|
||||
*/
|
||||
boolean canExtract(ClassTree tree);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Optional;
|
||||
|
||||
/** An enumeration of {@link Extractor} types. */
|
||||
enum ExtractorType {
|
||||
BUG_PATTERN("bugpattern", new BugPatternExtractor());
|
||||
|
||||
private static final ImmutableSet<ExtractorType> TYPES =
|
||||
Sets.immutableEnumSet(EnumSet.allOf(ExtractorType.class));
|
||||
|
||||
private final String identifier;
|
||||
private final Extractor<?> extractor;
|
||||
|
||||
ExtractorType(String identifier, Extractor<?> extractor) {
|
||||
this.identifier = identifier;
|
||||
this.extractor = extractor;
|
||||
}
|
||||
|
||||
String getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
Extractor<?> getExtractor() {
|
||||
return extractor;
|
||||
}
|
||||
|
||||
static Optional<ExtractorType> findMatchingType(ClassTree tree) {
|
||||
return TYPES.stream().filter(type -> type.getExtractor().canExtract(tree)).findFirst();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* A Java compiler plugin that extracts data from compiled classes, in support of the Error Prone
|
||||
* Support documentation.
|
||||
*/
|
||||
@com.google.errorprone.annotations.CheckReturnValue
|
||||
@org.jspecify.annotations.NullMarked
|
||||
package tech.picnic.errorprone.documentation;
|
||||
@@ -0,0 +1,153 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.CompilationTestHelper;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
final class BugPatternExtractorTest {
|
||||
@Test
|
||||
void noBugPattern(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerWithoutAnnotation.java",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"public final class TestCheckerWithoutAnnotation extends BugChecker {}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void minimalBugPattern(@TempDir Path outputDirectory) throws IOException {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"MinimalBugChecker.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.BugPattern;",
|
||||
"import com.google.errorprone.BugPattern.SeverityLevel;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"@BugPattern(summary = \"MinimalBugChecker summary\", severity = SeverityLevel.ERROR)",
|
||||
"public final class MinimalBugChecker extends BugChecker {}");
|
||||
|
||||
verifyFileMatchesResource(
|
||||
outputDirectory,
|
||||
"bugpattern-MinimalBugChecker.json",
|
||||
"bugpattern-documentation-minimal.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void completeBugPattern(@TempDir Path outputDirectory) throws IOException {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"CompleteBugChecker.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.BugPattern;",
|
||||
"import com.google.errorprone.BugPattern.SeverityLevel;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import org.junit.jupiter.api.Test;",
|
||||
"",
|
||||
"@BugPattern(",
|
||||
" name = \"OtherName\",",
|
||||
" summary = \"CompleteBugChecker summary\",",
|
||||
" linkType = BugPattern.LinkType.CUSTOM,",
|
||||
" link = \"https://error-prone.picnic.tech\",",
|
||||
" explanation = \"Example explanation\",",
|
||||
" severity = SeverityLevel.SUGGESTION,",
|
||||
" altNames = \"Check\",",
|
||||
" tags = BugPattern.StandardTags.SIMPLIFICATION,",
|
||||
" disableable = false,",
|
||||
" suppressionAnnotations = {BugPattern.class, Test.class})",
|
||||
"public final class CompleteBugChecker extends BugChecker {}");
|
||||
|
||||
verifyFileMatchesResource(
|
||||
outputDirectory,
|
||||
"bugpattern-CompleteBugChecker.json",
|
||||
"bugpattern-documentation-complete.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void undocumentedSuppressionBugPattern(@TempDir Path outputDirectory) throws IOException {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"UndocumentedSuppressionBugPattern.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.BugPattern;",
|
||||
"import com.google.errorprone.BugPattern.SeverityLevel;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"@BugPattern(",
|
||||
" summary = \"UndocumentedSuppressionBugPattern summary\",",
|
||||
" severity = SeverityLevel.WARNING,",
|
||||
" documentSuppression = false)",
|
||||
"public final class UndocumentedSuppressionBugPattern extends BugChecker {}");
|
||||
|
||||
verifyFileMatchesResource(
|
||||
outputDirectory,
|
||||
"bugpattern-UndocumentedSuppressionBugPattern.json",
|
||||
"bugpattern-documentation-undocumented-suppression.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bugPatternAnnotationIsAbsent() {
|
||||
CompilationTestHelper.newInstance(TestChecker.class, getClass())
|
||||
.addSourceLines(
|
||||
"TestChecker.java",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"// BUG: Diagnostic contains: Can extract: false",
|
||||
"public final class TestChecker extends BugChecker {}")
|
||||
.doTest();
|
||||
}
|
||||
|
||||
private static void verifyFileMatchesResource(
|
||||
Path outputDirectory, String fileName, String resourceName) throws IOException {
|
||||
assertThat(Files.readString(outputDirectory.resolve(fileName)))
|
||||
.isEqualToIgnoringWhitespace(getResource(resourceName));
|
||||
}
|
||||
|
||||
// XXX: Once we support only JDK 15+, drop this method in favour of including the resources as
|
||||
// text blocks in this class. (This also requires renaming the `verifyFileMatchesResource`
|
||||
// method.)
|
||||
private static String getResource(String resourceName) throws IOException {
|
||||
return Resources.toString(
|
||||
Resources.getResource(BugPatternExtractorTest.class, resourceName), UTF_8);
|
||||
}
|
||||
|
||||
/** A {@link BugChecker} that validates the {@link BugPatternExtractor}. */
|
||||
@BugPattern(summary = "Validates `BugPatternExtractor` extraction", severity = ERROR)
|
||||
public static final class TestChecker extends BugChecker implements ClassTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public Description matchClass(ClassTree tree, VisitorState state) {
|
||||
BugPatternExtractor extractor = new BugPatternExtractor();
|
||||
|
||||
assertThatThrownBy(() -> extractor.extract(tree, state.context))
|
||||
.isInstanceOf(NullPointerException.class)
|
||||
.hasMessage("BugPattern annotation must be present");
|
||||
|
||||
return buildDescription(tree)
|
||||
.setMessage(String.format("Can extract: %s", extractor.canExtract(tree)))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.FileManagers;
|
||||
import com.google.errorprone.FileObjects;
|
||||
import com.sun.tools.javac.api.JavacTaskImpl;
|
||||
import com.sun.tools.javac.api.JavacTool;
|
||||
import com.sun.tools.javac.file.JavacFileManager;
|
||||
import java.nio.file.Path;
|
||||
import javax.tools.JavaCompiler;
|
||||
import javax.tools.JavaFileObject;
|
||||
|
||||
// XXX: Generalize and move this class so that it can also be used by `refaster-compiler`.
|
||||
// XXX: Add support for this class to the `ErrorProneTestHelperSourceFormat` check.
|
||||
public final class Compilation {
|
||||
private Compilation() {}
|
||||
|
||||
public static void compileWithDocumentationGenerator(
|
||||
Path outputDirectory, String fileName, String... lines) {
|
||||
compileWithDocumentationGenerator(outputDirectory.toAbsolutePath().toString(), fileName, lines);
|
||||
}
|
||||
|
||||
public static void compileWithDocumentationGenerator(
|
||||
String outputDirectory, String fileName, String... lines) {
|
||||
compile(
|
||||
ImmutableList.of("-Xplugin:DocumentationGenerator -XoutputDirectory=" + outputDirectory),
|
||||
FileObjects.forSourceLines(fileName, lines));
|
||||
}
|
||||
|
||||
private static void compile(ImmutableList<String> options, JavaFileObject javaFileObject) {
|
||||
JavacFileManager javacFileManager = FileManagers.testFileManager();
|
||||
JavaCompiler compiler = JavacTool.create();
|
||||
JavacTaskImpl task =
|
||||
(JavacTaskImpl)
|
||||
compiler.getTask(
|
||||
null,
|
||||
javacFileManager,
|
||||
null,
|
||||
options,
|
||||
ImmutableList.of(),
|
||||
ImmutableList.of(javaFileObject));
|
||||
|
||||
task.call();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
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.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
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 org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.EnabledOnOs;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
final class DocumentationGeneratorTaskListenerTest {
|
||||
@EnabledOnOs(WINDOWS)
|
||||
@Test
|
||||
void readOnlyFileSystemWindows(@TempDir Path outputDirectory) throws IOException {
|
||||
AclFileAttributeView view =
|
||||
Files.getFileAttributeView(outputDirectory, AclFileAttributeView.class);
|
||||
view.setAcl(
|
||||
view.getAcl().stream()
|
||||
.map(
|
||||
entry ->
|
||||
AclEntry.newBuilder(entry)
|
||||
.setPermissions(
|
||||
Sets.difference(entry.permissions(), ImmutableSet.of(ADD_SUBDIRECTORY)))
|
||||
.build())
|
||||
.collect(toImmutableList()));
|
||||
|
||||
readOnlyFileSystemFailsToWrite(outputDirectory.resolve("nonexistent"));
|
||||
}
|
||||
|
||||
@DisabledOnOs(WINDOWS)
|
||||
@Test
|
||||
void readOnlyFileSystemNonWindows(@TempDir Path outputDirectory) {
|
||||
assertThat(outputDirectory.toFile().setWritable(false))
|
||||
.describedAs("Failed to make test directory unwritable")
|
||||
.isTrue();
|
||||
|
||||
readOnlyFileSystemFailsToWrite(outputDirectory.resolve("nonexistent"));
|
||||
}
|
||||
|
||||
private static void readOnlyFileSystemFailsToWrite(Path outputDirectory) {
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory, "A.java", "class A {}"))
|
||||
.hasRootCauseInstanceOf(FileSystemException.class)
|
||||
.hasCauseInstanceOf(IllegalStateException.class)
|
||||
.hasMessageEndingWith("Error while creating directory with path '%s'", outputDirectory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noClassNoOutput(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(outputDirectory, "A.java", "package pkg;");
|
||||
|
||||
assertThat(outputDirectory).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void excessArguments(@TempDir Path outputDirectory) {
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory.toAbsolutePath() + " extra-arg", "A.java", "package pkg;"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Precisely one path must be provided");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static tech.picnic.errorprone.documentation.DocumentationGenerator.OUTPUT_DIRECTORY_FLAG;
|
||||
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
final class DocumentationGeneratorTest {
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"bar", "foo"})
|
||||
void getOutputPath(String path) {
|
||||
assertThat(DocumentationGenerator.getOutputPath(OUTPUT_DIRECTORY_FLAG + '=' + path))
|
||||
.isEqualTo(Path.of(path));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"", "-XoutputDirectory", "invalidOption=Test", "nothing"})
|
||||
void getOutputPathWithInvalidArgument(String pathArg) {
|
||||
assertThatThrownBy(() -> DocumentationGenerator.getOutputPath(pathArg))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("'%s' must be of the form '%s=<value>'", pathArg, OUTPUT_DIRECTORY_FLAG);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOutputPathWithInvalidPath() {
|
||||
String basePath = "path-with-null-char-\0";
|
||||
assertThatThrownBy(
|
||||
() -> DocumentationGenerator.getOutputPath(OUTPUT_DIRECTORY_FLAG + '=' + basePath))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasCauseInstanceOf(InvalidPathException.class)
|
||||
.hasMessageEndingWith("Invalid path '%s'", basePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"fullyQualifiedName": "pkg.CompleteBugChecker",
|
||||
"name": "OtherName",
|
||||
"altNames": [
|
||||
"Check"
|
||||
],
|
||||
"link": "https://error-prone.picnic.tech",
|
||||
"tags": [
|
||||
"Simplification"
|
||||
],
|
||||
"summary": "CompleteBugChecker summary",
|
||||
"explanation": "Example explanation",
|
||||
"severityLevel": "SUGGESTION",
|
||||
"canDisable": false,
|
||||
"suppressionAnnotations": [
|
||||
"com.google.errorprone.BugPattern",
|
||||
"org.junit.jupiter.api.Test"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"fullyQualifiedName": "pkg.MinimalBugChecker",
|
||||
"name": "MinimalBugChecker",
|
||||
"altNames": [],
|
||||
"link": "",
|
||||
"tags": [],
|
||||
"summary": "MinimalBugChecker summary",
|
||||
"explanation": "",
|
||||
"severityLevel": "ERROR",
|
||||
"canDisable": true,
|
||||
"suppressionAnnotations": [
|
||||
"java.lang.SuppressWarnings"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"fullyQualifiedName": "pkg.UndocumentedSuppressionBugPattern",
|
||||
"name": "UndocumentedSuppressionBugPattern",
|
||||
"altNames": [],
|
||||
"link": "",
|
||||
"tags": [],
|
||||
"summary": "UndocumentedSuppressionBugPattern summary",
|
||||
"explanation": "",
|
||||
"severityLevel": "WARNING",
|
||||
"canDisable": true,
|
||||
"suppressionAnnotations": []
|
||||
}
|
||||
@@ -267,7 +267,7 @@ Refaster's expressiveness:
|
||||
motivating example, see the two subtly different loop definitions in
|
||||
`CollectionRemoveAllFromCollectionExpression`.
|
||||
- Figure out why Refaster sometimes doesn't match the correct generic overload.
|
||||
See the `AssertThatIterableHasOneComparableElementEqualTo` template for an
|
||||
See the `AssertThatIterableHasOneComparableElementEqualTo` rule for an
|
||||
example.
|
||||
|
||||
[autorefactor]: https://autorefactor.org
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>tech.picnic.error-prone-support</groupId>
|
||||
<artifactId>error-prone-support</artifactId>
|
||||
<version>0.3.1-SNAPSHOT</version>
|
||||
<version>0.8.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>error-prone-contrib</artifactId>
|
||||
@@ -39,6 +39,14 @@
|
||||
<artifactId>error_prone_test_helpers</artifactId>
|
||||
<scope>test</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>refaster-support</artifactId>
|
||||
@@ -64,11 +72,6 @@
|
||||
<artifactId>auto-service-annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.googlejavaformat</groupId>
|
||||
<artifactId>google-java-format</artifactId>
|
||||
@@ -98,6 +101,11 @@
|
||||
<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>
|
||||
@@ -138,6 +146,10 @@
|
||||
<artifactId>value-annotations</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jooq</groupId>
|
||||
<artifactId>jooq</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jspecify</groupId>
|
||||
<artifactId>jspecify</artifactId>
|
||||
@@ -146,7 +158,7 @@
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
@@ -173,6 +185,11 @@
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
@@ -208,6 +225,11 @@
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths combine.children="append">
|
||||
<path>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>documentation-support</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>refaster-compiler</artifactId>
|
||||
@@ -221,6 +243,7 @@
|
||||
</annotationProcessorPaths>
|
||||
<compilerArgs combine.children="append">
|
||||
<arg>-Xplugin:RefasterRuleCompiler</arg>
|
||||
<arg>-Xplugin:DocumentationGenerator -XoutputDirectory=${project.build.directory}/docs</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
@@ -26,7 +27,8 @@ import javax.lang.model.element.AnnotationValue;
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "`JsonCreator.Mode` should be set for single-argument creators",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "AmbiguousJsonCreator",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class AmbiguousJsonCreator extends BugChecker implements AnnotationTreeMatcher {
|
||||
@@ -34,6 +36,9 @@ 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)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
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;
|
||||
@@ -8,6 +8,7 @@ import static com.google.errorprone.matchers.Matchers.argument;
|
||||
import static com.google.errorprone.matchers.Matchers.argumentCount;
|
||||
import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.nullLiteral;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
@@ -23,7 +24,7 @@ import com.sun.source.tree.MethodInvocationTree;
|
||||
/**
|
||||
* A {@link BugChecker} that flags AssertJ {@code isEqualTo(null)} checks for simplification.
|
||||
*
|
||||
* <p>This bug checker cannot be replaced with a simple Refaster template, as the Refaster approach
|
||||
* <p>This bug checker cannot be replaced with a simple Refaster rule, as the Refaster approach
|
||||
* would require that all overloads of {@link org.assertj.core.api.Assert#isEqualTo(Object)} (such
|
||||
* as {@link org.assertj.core.api.AbstractStringAssert#isEqualTo(String)}) are explicitly
|
||||
* enumerated. This bug checker generically matches all such current and future overloads.
|
||||
@@ -31,7 +32,8 @@ import com.sun.source.tree.MethodInvocationTree;
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Prefer `.isNull()` over `.isEqualTo(null)`",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "AssertJIsNull",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class AssertJIsNull extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
@@ -42,6 +44,9 @@ 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)) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -14,7 +15,6 @@ 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.util.ASTHelpers;
|
||||
@@ -23,12 +23,14 @@ 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.bugpatterns.util.SourceCode;
|
||||
|
||||
/** A {@link BugChecker} that flags redundant {@code @Autowired} constructor annotations. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Omit `@Autowired` on a class' sole constructor, as it is redundant",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "AutowiredConstructor",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class AutowiredConstructor extends BugChecker implements ClassTreeMatcher {
|
||||
@@ -36,6 +38,9 @@ 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);
|
||||
@@ -57,6 +62,6 @@ public final class AutowiredConstructor extends BugChecker implements ClassTreeM
|
||||
* leave flagging the unused import to Error Prone's `RemoveUnusedImports` check.
|
||||
*/
|
||||
AnnotationTree annotation = Iterables.getOnlyElement(annotations);
|
||||
return describeMatch(annotation, SuggestedFix.delete(annotation));
|
||||
return describeMatch(annotation, SourceCode.deleteWithTrailingWhitespace(annotation, state));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
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.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -31,7 +32,8 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Omit redundant syntax from annotation declarations",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "CanonicalAnnotationSyntax",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class CanonicalAnnotationSyntax extends BugChecker implements AnnotationTreeMatcher {
|
||||
@@ -44,6 +46,9 @@ public final class CanonicalAnnotationSyntax extends BugChecker implements Annot
|
||||
CanonicalAnnotationSyntax::dropRedundantValueAttribute,
|
||||
CanonicalAnnotationSyntax::dropRedundantCurlies);
|
||||
|
||||
/** Instantiates a new {@link CanonicalAnnotationSyntax} instance. */
|
||||
public CanonicalAnnotationSyntax() {}
|
||||
|
||||
@Override
|
||||
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
|
||||
return FIX_FACTORIES.stream()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
@@ -17,6 +18,7 @@ import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import java.util.stream.Collector;
|
||||
import tech.picnic.errorprone.bugpatterns.util.ThirdPartyLibrary;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@link Collector Collectors} that don't clearly express
|
||||
@@ -29,7 +31,8 @@ import java.util.stream.Collector;
|
||||
@BugPattern(
|
||||
summary =
|
||||
"Avoid `Collectors.to{List,Map,Set}` in favour of alternatives that emphasize (im)mutability",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "CollectorMutability",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class CollectorMutability extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
@@ -43,9 +46,13 @@ 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 (!COLLECTOR_METHOD.matches(tree, state)) {
|
||||
if (!ThirdPartyLibrary.GUAVA.isIntroductionAllowed(state)
|
||||
|| !COLLECTOR_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
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.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
@@ -21,12 +21,14 @@ 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.bugpatterns.util.SourceCode;
|
||||
|
||||
/** A {@link BugChecker} that flags empty methods that seemingly can simply be deleted. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Empty method can likely be deleted",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "EmptyMethod",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class EmptyMethod extends BugChecker implements MethodTreeMatcher {
|
||||
@@ -36,6 +38,9 @@ public final class EmptyMethod extends BugChecker implements MethodTreeMatcher {
|
||||
AT_LEAST_ONE,
|
||||
anyOf(isType("java.lang.Override"), isType("org.aspectj.lang.annotation.Pointcut")));
|
||||
|
||||
/** Instantiates a new {@link EmptyMethod} instance. */
|
||||
public EmptyMethod() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (tree.getBody() == null
|
||||
@@ -50,7 +55,7 @@ public final class EmptyMethod extends BugChecker implements MethodTreeMatcher {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return describeMatch(tree, SuggestedFix.delete(tree));
|
||||
return describeMatch(tree, SourceCode.deleteWithTrailingWhitespace(tree, state));
|
||||
}
|
||||
|
||||
private static boolean isInPossibleTestHelperClass(VisitorState state) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.Splitter;
|
||||
@@ -50,7 +51,8 @@ import java.util.Optional;
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Test code should follow the Google Java style",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "ErrorProneTestHelperSourceFormat",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
public final class ErrorProneTestHelperSourceFormat extends BugChecker
|
||||
@@ -70,6 +72,9 @@ public final class ErrorProneTestHelperSourceFormat extends BugChecker
|
||||
.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);
|
||||
|
||||
@@ -2,11 +2,12 @@ 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.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static java.util.stream.Collectors.collectingAndThen;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -36,7 +37,8 @@ import java.util.stream.Stream;
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Make sure `Ordering#explicit` lists all of an enum's values",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "ExplicitEnumOrdering",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class ExplicitEnumOrdering extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
@@ -44,6 +46,9 @@ public final class ExplicitEnumOrdering extends BugChecker implements MethodInvo
|
||||
private static final Matcher<ExpressionTree> EXPLICIT_ORDERING =
|
||||
staticMethod().onClass(Ordering.class.getName()).named("explicit");
|
||||
|
||||
/** Instantiates a new {@link ExplicitEnumOrdering} instance. */
|
||||
public ExplicitEnumOrdering() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!EXPLICIT_ORDERING.matches(tree, state)) {
|
||||
@@ -84,6 +89,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.format("%s.%s", typeSymbol.getSimpleName(), v));
|
||||
.map(v -> String.join(".", typeSymbol.getSimpleName(), v));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.generic;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.subOf;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.type;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.unbound;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Iterables;
|
||||
@@ -16,11 +21,14 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -32,29 +40,39 @@ 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 single-subscription semantics are undesirable one should invoke a {@code flatMap} or
|
||||
* {@code flatMapSequential} overload with an explicit concurrency level.
|
||||
* concatMap}'s sequential-subscription semantics are undesirable one should invoke a {@code
|
||||
* flatMap} or {@code flatMapSequential} overload with an explicit concurrency level.
|
||||
*
|
||||
* <p>NB: The rarely-used overload {@link Flux#flatMap(Function, Function, 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,
|
||||
* java.util.function.Supplier)} is not flagged by this check because there is no clear alternative
|
||||
* to point to.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"`Flux#flatMap` and `Flux#flatMapSequential` have subtle semantics; "
|
||||
+ "please use `Flux#concatMap` or explicitly specify the desired amount of concurrency",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "FluxFlatMapUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = ERROR,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class FluxFlatMapUsage extends BugChecker
|
||||
implements MethodInvocationTreeMatcher, MemberReferenceTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String MAX_CONCURRENCY_ARG_NAME = "MAX_CONCURRENCY";
|
||||
private static final Supplier<Type> FLUX =
|
||||
Suppliers.typeFromString("reactor.core.publisher.Flux");
|
||||
private static final Matcher<ExpressionTree> FLUX_FLATMAP =
|
||||
instanceMethod()
|
||||
.onDescendantOf("reactor.core.publisher.Flux")
|
||||
.onDescendantOf(FLUX)
|
||||
.namedAnyOf("flatMap", "flatMapSequential")
|
||||
.withParameters(Function.class.getName());
|
||||
private static final Supplier<Type> FLUX_OF_PUBLISHERS =
|
||||
VisitorState.memoize(
|
||||
generic(FLUX, subOf(generic(type("org.reactivestreams.Publisher"), unbound()))));
|
||||
|
||||
/** Instantiates a new {@link FluxFlatMapUsage} instance. */
|
||||
public FluxFlatMapUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
@@ -62,14 +80,27 @@ public final class FluxFlatMapUsage extends BugChecker
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return buildDescription(tree)
|
||||
.addFix(SuggestedFixes.renameMethodInvocation(tree, "concatMap", state))
|
||||
.addFix(
|
||||
SuggestedFix.builder()
|
||||
.postfixWith(
|
||||
Iterables.getOnlyElement(tree.getArguments()), ", " + MAX_CONCURRENCY_ARG_NAME)
|
||||
.build())
|
||||
.build();
|
||||
SuggestedFix serializationFix = SuggestedFixes.renameMethodInvocation(tree, "concatMap", state);
|
||||
SuggestedFix concurrencyCapFix =
|
||||
SuggestedFix.builder()
|
||||
.postfixWith(
|
||||
Iterables.getOnlyElement(tree.getArguments()), ", " + MAX_CONCURRENCY_ARG_NAME)
|
||||
.build();
|
||||
|
||||
Description.Builder description = buildDescription(tree);
|
||||
|
||||
if (state.getTypes().isSubtype(ASTHelpers.getType(tree), FLUX_OF_PUBLISHERS.get(state))) {
|
||||
/*
|
||||
* Nested publishers may need to be subscribed to eagerly in order to avoid a deadlock, e.g.
|
||||
* if they are produced by `Flux#groupBy`. In this case we suggest specifying an explicit
|
||||
* concurrently bound, in favour of sequential subscriptions using `Flux#concatMap`.
|
||||
*/
|
||||
description.addFix(concurrencyCapFix).addFix(serializationFix);
|
||||
} else {
|
||||
description.addFix(serializationFix).addFix(concurrencyCapFix);
|
||||
}
|
||||
|
||||
return description.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
@@ -10,6 +10,7 @@ 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.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
@@ -31,7 +32,7 @@ import com.sun.source.util.SimpleTreeVisitor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
|
||||
/**
|
||||
@@ -47,11 +48,12 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
// `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.
|
||||
// invocations, as necessary. See also a comment in the `StringJoin` check.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Defer string concatenation to the invoked method",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "FormatStringConcatenation",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class FormatStringConcatenation extends BugChecker
|
||||
@@ -127,6 +129,9 @@ public final class FormatStringConcatenation extends BugChecker
|
||||
.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)) {
|
||||
@@ -202,7 +207,7 @@ public final class FormatStringConcatenation extends BugChecker
|
||||
}
|
||||
|
||||
private static class ReplacementArgumentsConstructor
|
||||
extends SimpleTreeVisitor<Void, VisitorState> {
|
||||
extends SimpleTreeVisitor<@Nullable Void, VisitorState> {
|
||||
private final StringBuilder formatString = new StringBuilder();
|
||||
private final List<Tree> formatArguments = new ArrayList<>();
|
||||
private final String formatSpecifier;
|
||||
@@ -211,9 +216,8 @@ public final class FormatStringConcatenation extends BugChecker
|
||||
this.formatSpecifier = formatSpecifier;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Void visitBinary(BinaryTree tree, VisitorState state) {
|
||||
public @Nullable Void visitBinary(BinaryTree tree, VisitorState state) {
|
||||
if (tree.getKind() == Kind.PLUS && isStringTyped(tree, state)) {
|
||||
tree.getLeftOperand().accept(this, state);
|
||||
tree.getRightOperand().accept(this, state);
|
||||
@@ -224,15 +228,13 @@ public final class FormatStringConcatenation extends BugChecker
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Void visitParenthesized(ParenthesizedTree tree, VisitorState state) {
|
||||
public @Nullable Void visitParenthesized(ParenthesizedTree tree, VisitorState state) {
|
||||
return tree.getExpression().accept(this, state);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Void defaultAction(Tree tree, VisitorState state) {
|
||||
protected @Nullable Void defaultAction(Tree tree, VisitorState state) {
|
||||
appendExpression(tree);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static com.google.errorprone.suppliers.Suppliers.OBJECT_TYPE;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.primitives.Primitives;
|
||||
@@ -22,6 +23,7 @@ import com.google.errorprone.matchers.Matcher;
|
||||
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;
|
||||
@@ -31,18 +33,26 @@ 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 identify conversion would cause a different method overload to be selected. Depending on
|
||||
// of the identity conversion would cause a different method overload to be selected. Depending on
|
||||
// the target method such a modification may change the code's semantics or performance.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Avoid or clarify identity conversions",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "IdentityConversion",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class IdentityConversion extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> IS_CONVERSION_METHOD =
|
||||
anyOf(
|
||||
staticMethod()
|
||||
.onClassAny(
|
||||
Primitives.allWrapperTypes().stream()
|
||||
.map(Class::getName)
|
||||
.collect(toImmutableSet()))
|
||||
.named("valueOf"),
|
||||
staticMethod().onClass(String.class.getName()).named("valueOf"),
|
||||
staticMethod()
|
||||
.onClassAny(
|
||||
"com.google.common.collect.ImmutableBiMap",
|
||||
@@ -58,18 +68,17 @@ public final class IdentityConversion extends BugChecker implements MethodInvoca
|
||||
"com.google.common.collect.ImmutableTable")
|
||||
.named("copyOf"),
|
||||
staticMethod()
|
||||
.onClassAny(
|
||||
Primitives.allWrapperTypes().stream()
|
||||
.map(Class::getName)
|
||||
.collect(toImmutableSet()))
|
||||
.named("valueOf"),
|
||||
staticMethod().onClass(String.class.getName()).named("valueOf"),
|
||||
.onClass("com.google.errorprone.matchers.Matchers")
|
||||
.namedAnyOf("allOf", "anyOf"),
|
||||
staticMethod().onClass("reactor.adapter.rxjava.RxJava2Adapter"),
|
||||
staticMethod()
|
||||
.onClass("reactor.core.publisher.Flux")
|
||||
.namedAnyOf("concat", "firstWithSignal", "from", "merge"),
|
||||
staticMethod().onClass("reactor.core.publisher.Mono").namedAnyOf("from", "fromDirect"));
|
||||
|
||||
/** Instantiates a new {@link IdentityConversion} instance. */
|
||||
public IdentityConversion() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
List<? extends ExpressionTree> arguments = tree.getArguments();
|
||||
@@ -90,6 +99,15 @@ 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 "
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
@@ -11,6 +11,7 @@ import static com.google.errorprone.matchers.Matchers.hasModifier;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.Matchers.methodReturns;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
@@ -44,7 +45,8 @@ import javax.lang.model.element.Modifier;
|
||||
summary =
|
||||
"`SortedSet` properties of a `@Value.Immutable` or `@Value.Modifiable` type must be "
|
||||
+ "annotated with `@Value.NaturalOrder` or `@Value.ReverseOrder`",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "ImmutablesSortedSetComparator",
|
||||
linkType = CUSTOM,
|
||||
severity = ERROR,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class ImmutablesSortedSetComparator extends BugChecker implements MethodTreeMatcher {
|
||||
@@ -65,6 +67,9 @@ 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)) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
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.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.LambdaExpressionTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.InstanceOfTree;
|
||||
import com.sun.source.tree.LambdaExpressionTree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags lambda expressions that can be replaced with a method reference
|
||||
* of the form {@code T.class::isInstance}.
|
||||
*
|
||||
* @see MethodReferenceUsage
|
||||
*/
|
||||
// XXX: Consider folding this logic into the `MethodReferenceUsage` check.
|
||||
@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().getKind() != Kind.INSTANCE_OF) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
VariableTree param = Iterables.getOnlyElement(tree.getParameters());
|
||||
InstanceOfTree instanceOf = (InstanceOfTree) tree.getBody();
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.TEST_METHOD;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreMatchers.hasMetaAnnotation;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import javax.lang.model.element.Modifier;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags non-final and non package-private JUnit test class declarations,
|
||||
* unless abstract.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Non-abstract JUnit test classes should be declared package-private and final",
|
||||
linkType = CUSTOM,
|
||||
link = BUG_PATTERNS_BASE_URL + "JUnitClassModifiers",
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
public final class JUnitClassModifiers extends BugChecker implements ClassTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ClassTree> HAS_SPRING_CONFIGURATION_ANNOTATION =
|
||||
annotations(
|
||||
AT_LEAST_ONE,
|
||||
anyOf(
|
||||
isType("org.springframework.context.annotation.Configuration"),
|
||||
hasMetaAnnotation("org.springframework.context.annotation.Configuration")));
|
||||
private static final Matcher<ClassTree> TEST_CLASS_WITH_INCORRECT_MODIFIERS =
|
||||
allOf(
|
||||
hasMethod(TEST_METHOD),
|
||||
not(hasModifier(Modifier.ABSTRACT)),
|
||||
anyOf(
|
||||
hasModifier(Modifier.PRIVATE),
|
||||
hasModifier(Modifier.PROTECTED),
|
||||
hasModifier(Modifier.PUBLIC),
|
||||
allOf(not(hasModifier(Modifier.FINAL)), not(HAS_SPRING_CONFIGURATION_ANNOTATION))));
|
||||
|
||||
/** Instantiates a new {@link JUnitClassModifiers} instance. */
|
||||
public JUnitClassModifiers() {}
|
||||
|
||||
@Override
|
||||
public Description matchClass(ClassTree tree, VisitorState state) {
|
||||
if (!TEST_CLASS_WITH_INCORRECT_MODIFIERS.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
|
||||
SuggestedFixes.removeModifiers(
|
||||
tree.getModifiers(),
|
||||
state,
|
||||
ImmutableSet.of(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC))
|
||||
.ifPresent(fixBuilder::merge);
|
||||
|
||||
if (!HAS_SPRING_CONFIGURATION_ANNOTATION.matches(tree, state)) {
|
||||
SuggestedFixes.addModifiers(tree, state, Modifier.FINAL).ifPresent(fixBuilder::merge);
|
||||
}
|
||||
|
||||
return describeMatch(tree, fixBuilder.build());
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.enclosingClass;
|
||||
import static com.google.errorprone.matchers.Matchers.hasModifier;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static java.util.function.Predicate.not;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.JavaKeywords.isReservedKeyword;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.SETUP_OR_TEARDOWN_METHOD;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.TEST_METHOD;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
@@ -23,20 +23,12 @@ 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.source.tree.Tree;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import com.sun.tools.javac.code.Symbol.MethodSymbol;
|
||||
import java.util.Optional;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.Name;
|
||||
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
import tech.picnic.errorprone.bugpatterns.util.ConflictDetection;
|
||||
|
||||
/** A {@link BugChecker} that flags non-canonical JUnit method declarations. */
|
||||
// XXX: Consider introducing a class-level check that enforces that test classes:
|
||||
@@ -47,39 +39,27 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "JUnit method declaration can likely be improved",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "JUnitMethodDeclaration",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class JUnitMethodDeclaration extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String TEST_PREFIX = "test";
|
||||
private static final ImmutableSet<Modifier> ILLEGAL_MODIFIERS =
|
||||
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")));
|
||||
Sets.immutableEnumSet(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC);
|
||||
private static final Matcher<MethodTree> IS_LIKELY_OVERRIDDEN =
|
||||
allOf(
|
||||
not(hasModifier(Modifier.FINAL)),
|
||||
not(hasModifier(Modifier.PRIVATE)),
|
||||
enclosingClass(hasModifier(Modifier.ABSTRACT)));
|
||||
|
||||
/** Instantiates a new {@link JUnitMethodDeclaration} instance. */
|
||||
public JUnitMethodDeclaration() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (HAS_UNMODIFIABLE_SIGNATURE.matches(tree, state)) {
|
||||
if (IS_LIKELY_OVERRIDDEN.matches(tree, state) || isOverride(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
@@ -101,10 +81,11 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
|
||||
|
||||
private void suggestTestMethodRenameIfApplicable(
|
||||
MethodTree tree, SuggestedFix.Builder fixBuilder, VisitorState state) {
|
||||
tryCanonicalizeMethodName(tree)
|
||||
MethodSymbol symbol = ASTHelpers.getSymbol(tree);
|
||||
tryCanonicalizeMethodName(symbol)
|
||||
.ifPresent(
|
||||
newName ->
|
||||
findMethodRenameBlocker(newName, state)
|
||||
ConflictDetection.findMethodRenameBlocker(symbol, newName, state)
|
||||
.ifPresentOrElse(
|
||||
blocker -> reportMethodRenameBlocker(tree, blocker, state),
|
||||
() -> fixBuilder.merge(SuggestedFixes.renameMethod(tree, newName, state))));
|
||||
@@ -120,63 +101,8 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* If applicable, returns a human-readable argument against assigning the given name to an
|
||||
* existing method.
|
||||
*
|
||||
* <p>This method implements imperfect heuristics. Things it currently does not consider include
|
||||
* the following:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Whether the rename would merely introduce a method overload, rather than clashing with an
|
||||
* existing method declaration.
|
||||
* <li>Whether the rename would cause a method in a superclass to be overridden.
|
||||
* <li>Whether the rename would in fact clash with a static import. (It could be that a static
|
||||
* import of the same name is only referenced from lexical scopes in which the method under
|
||||
* consideration cannot be referenced directly.)
|
||||
* </ul>
|
||||
*/
|
||||
private static Optional<String> findMethodRenameBlocker(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())
|
||||
private static Optional<String> tryCanonicalizeMethodName(MethodSymbol symbol) {
|
||||
return Optional.of(symbol.getQualifiedName().toString())
|
||||
.filter(name -> name.startsWith(TEST_PREFIX))
|
||||
.map(name -> name.substring(TEST_PREFIX.length()))
|
||||
.filter(not(String::isEmpty))
|
||||
@@ -184,17 +110,9 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
|
||||
.filter(name -> !Character.isDigit(name.charAt(0)));
|
||||
}
|
||||
|
||||
// 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);
|
||||
private static boolean isOverride(MethodTree tree, VisitorState state) {
|
||||
return ASTHelpers.streamSuperMethods(ASTHelpers.getSymbol(tree), state.getTypes())
|
||||
.findAny()
|
||||
.isPresent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
|
||||
import static java.util.Comparator.comparing;
|
||||
import static java.util.Comparator.naturalOrder;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.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;
|
||||
@@ -37,8 +40,9 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.Nullable;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.bugpatterns.util.AnnotationAttributeMatcher;
|
||||
import tech.picnic.errorprone.bugpatterns.util.Flags;
|
||||
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
|
||||
/**
|
||||
@@ -50,7 +54,8 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Where possible, sort annotation array attributes lexicographically",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "LexicographicalAnnotationAttributeListing",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
public final class LexicographicalAnnotationAttributeListing extends BugChecker
|
||||
@@ -69,10 +74,16 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
|
||||
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 the default {@link LexicographicalAnnotationAttributeListing}. */
|
||||
/** Instantiates a default {@link LexicographicalAnnotationAttributeListing} instance. */
|
||||
public LexicographicalAnnotationAttributeListing() {
|
||||
this(ErrorProneFlags.empty());
|
||||
}
|
||||
@@ -124,7 +135,7 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
|
||||
}
|
||||
|
||||
List<? extends ExpressionTree> actualOrdering = array.getInitializers();
|
||||
ImmutableList<? extends ExpressionTree> desiredOrdering = doSort(actualOrdering, state);
|
||||
ImmutableList<? extends ExpressionTree> desiredOrdering = doSort(actualOrdering);
|
||||
if (actualOrdering.equals(desiredOrdering)) {
|
||||
/* In the (presumably) common case the elements are already sorted. */
|
||||
return Optional.empty();
|
||||
@@ -154,12 +165,12 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
|
||||
}
|
||||
|
||||
private static ImmutableList<? extends ExpressionTree> doSort(
|
||||
Iterable<? extends ExpressionTree> elements, VisitorState state) {
|
||||
Iterable<? extends ExpressionTree> elements) {
|
||||
// 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(
|
||||
e -> getStructure(e, state),
|
||||
LexicographicalAnnotationAttributeListing::getStructure,
|
||||
Comparators.lexicographical(
|
||||
Comparators.lexicographical(
|
||||
String.CASE_INSENSITIVE_ORDER.thenComparing(naturalOrder())))),
|
||||
@@ -171,38 +182,31 @@ 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, VisitorState state) {
|
||||
private static ImmutableList<ImmutableList<String>> getStructure(ExpressionTree array) {
|
||||
ImmutableList.Builder<ImmutableList<String>> nodes = ImmutableList.builder();
|
||||
|
||||
new TreeScanner<Void, Void>() {
|
||||
@Nullable
|
||||
new TreeScanner<@Nullable Void, @Nullable Void>() {
|
||||
@Override
|
||||
public Void visitIdentifier(IdentifierTree node, @Nullable Void ctx) {
|
||||
nodes.add(tokenize(node));
|
||||
return super.visitIdentifier(node, ctx);
|
||||
public @Nullable Void visitIdentifier(IdentifierTree node, @Nullable Void unused) {
|
||||
nodes.add(ImmutableList.of(node.getName().toString()));
|
||||
return super.visitIdentifier(node, unused);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Void visitLiteral(LiteralTree node, @Nullable Void ctx) {
|
||||
nodes.add(tokenize(node));
|
||||
return super.visitLiteral(node, ctx);
|
||||
public @Nullable Void visitLiteral(LiteralTree node, @Nullable Void unused) {
|
||||
Object value = ASTHelpers.constValue(node);
|
||||
nodes.add(
|
||||
value instanceof String
|
||||
? STRING_ARGUMENT_SPLITTER.splitToStream((String) value).collect(toImmutableList())
|
||||
: ImmutableList.of(String.valueOf(value)));
|
||||
|
||||
return super.visitLiteral(node, unused);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
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));
|
||||
public @Nullable Void visitPrimitiveType(PrimitiveTypeTree node, @Nullable Void unused) {
|
||||
nodes.add(ImmutableList.of(node.getPrimitiveTypeKind().toString()));
|
||||
return super.visitPrimitiveType(node, unused);
|
||||
}
|
||||
}.scan(array, null);
|
||||
|
||||
@@ -217,7 +221,7 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
|
||||
|
||||
private static ImmutableList<String> excludedAnnotations(ErrorProneFlags flags) {
|
||||
Set<String> exclusions = new HashSet<>();
|
||||
flags.getList(EXCLUDED_ANNOTATIONS_FLAG).ifPresent(exclusions::addAll);
|
||||
exclusions.addAll(Flags.getList(flags, EXCLUDED_ANNOTATIONS_FLAG));
|
||||
exclusions.addAll(BLACKLISTED_ANNOTATIONS);
|
||||
return ImmutableList.copyOf(exclusions);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
|
||||
import static com.sun.tools.javac.code.TypeAnnotations.AnnotationType.DECLARATION;
|
||||
import static com.sun.tools.javac.code.TypeAnnotations.AnnotationType.TYPE;
|
||||
import static java.util.Comparator.comparing;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.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;
|
||||
@@ -16,10 +20,14 @@ 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 java.util.Optional;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
|
||||
/**
|
||||
@@ -31,12 +39,19 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Sort annotations lexicographically where possible",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "LexicographicalAnnotationListing",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
public final class LexicographicalAnnotationListing extends BugChecker
|
||||
implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
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) {
|
||||
@@ -45,26 +60,29 @@ public final class LexicographicalAnnotationListing extends BugChecker
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
ImmutableList<? extends AnnotationTree> sortedAnnotations = sort(originalOrdering, state);
|
||||
ImmutableList<? extends AnnotationTree> sortedAnnotations =
|
||||
sort(originalOrdering, ASTHelpers.getSymbol(tree), state);
|
||||
if (originalOrdering.equals(sortedAnnotations)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
Optional<Fix> fix = tryFixOrdering(originalOrdering, sortedAnnotations, state);
|
||||
|
||||
Description.Builder description = buildDescription(originalOrdering.get(0));
|
||||
fix.ifPresent(description::addFix);
|
||||
return description.build();
|
||||
return describeMatch(
|
||||
originalOrdering.get(0), fixOrdering(originalOrdering, sortedAnnotations, state));
|
||||
}
|
||||
|
||||
private static ImmutableList<? extends AnnotationTree> sort(
|
||||
List<? extends AnnotationTree> annotations, VisitorState state) {
|
||||
List<? extends AnnotationTree> annotations, Symbol symbol, VisitorState state) {
|
||||
return annotations.stream()
|
||||
.sorted(comparing(annotation -> SourceCode.treeToString(annotation, state)))
|
||||
.sorted(
|
||||
comparing(
|
||||
(AnnotationTree annotation) ->
|
||||
ASTHelpers.getAnnotationType(annotation, symbol, state),
|
||||
BY_ANNOTATION_TYPE)
|
||||
.thenComparing(annotation -> SourceCode.treeToString(annotation, state)))
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
private static Optional<Fix> tryFixOrdering(
|
||||
private static Fix fixOrdering(
|
||||
List<? extends AnnotationTree> originalAnnotations,
|
||||
ImmutableList<? extends AnnotationTree> sortedAnnotations,
|
||||
VisitorState state) {
|
||||
@@ -75,6 +93,7 @@ public final class LexicographicalAnnotationListing extends BugChecker
|
||||
SuggestedFix.builder()
|
||||
.replace(original, SourceCode.treeToString(replacement, state)))
|
||||
.reduce(SuggestedFix.Builder::merge)
|
||||
.map(SuggestedFix.Builder::build);
|
||||
.map(SuggestedFix.Builder::build)
|
||||
.orElseThrow(() -> new VerifyException("No annotations were provided"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
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.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.VerifyException;
|
||||
@@ -36,6 +37,8 @@ import javax.lang.model.element.Name;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags lambda expressions that can be replaced with method references.
|
||||
*
|
||||
* @see IsInstanceLambdaUsage
|
||||
*/
|
||||
// XXX: Other custom expressions we could rewrite:
|
||||
// - `a -> "str" + a` to `"str"::concat`. But only if `str` is provably non-null.
|
||||
@@ -49,15 +52,22 @@ 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 into this class.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Prefer method references over lambda expressions",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "MethodReferenceUsage",
|
||||
linkType = CUSTOM,
|
||||
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) {
|
||||
/*
|
||||
@@ -120,7 +130,7 @@ public final class MethodReferenceUsage extends BugChecker implements LambdaExpr
|
||||
return Optional.empty();
|
||||
}
|
||||
Symbol sym = ASTHelpers.getSymbol(methodSelect);
|
||||
if (!sym.isStatic()) {
|
||||
if (!ASTHelpers.isStatic(sym)) {
|
||||
return constructFix(lambdaExpr, "this", methodSelect);
|
||||
}
|
||||
return constructFix(lambdaExpr, sym.owner, methodSelect);
|
||||
@@ -190,7 +200,7 @@ public final class MethodReferenceUsage extends BugChecker implements LambdaExpr
|
||||
Name sName = target.getSimpleName();
|
||||
Optional<SuggestedFix.Builder> fix = constructFix(lambdaExpr, sName, methodName);
|
||||
|
||||
if (!"java.lang".equals(target.packge().toString())) {
|
||||
if (!"java.lang".equals(ASTHelpers.enclosingPackage(target).toString())) {
|
||||
Name fqName = target.getQualifiedName();
|
||||
if (!sName.equals(fqName)) {
|
||||
return fix.map(b -> b.addImport(fqName.toString()));
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
@@ -24,8 +25,9 @@ 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,
|
||||
summary = "The Refaster rule contains a method without any Refaster annotations",
|
||||
link = BUG_PATTERNS_BASE_URL + "MissingRefasterAnnotation",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class MissingRefasterAnnotation extends BugChecker implements ClassTreeMatcher {
|
||||
@@ -38,6 +40,9 @@ public final class MissingRefasterAnnotation extends BugChecker implements Class
|
||||
isType("com.google.errorprone.refaster.annotation.BeforeTemplate"),
|
||||
isType("com.google.errorprone.refaster.annotation.AfterTemplate")));
|
||||
|
||||
/** Instantiates a new {@link MissingRefasterAnnotation} instance. */
|
||||
public MissingRefasterAnnotation() {}
|
||||
|
||||
@Override
|
||||
public Description matchClass(ClassTree tree, VisitorState state) {
|
||||
long methodTypes =
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Iterables;
|
||||
@@ -26,7 +27,8 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Don't unnecessarily use Mockito's `eq(...)`",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "MockitoStubbing",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class MockitoStubbing extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
@@ -34,6 +36,9 @@ 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();
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.generic;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.raw;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreTypes.subOf;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
@@ -15,9 +18,7 @@ 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}. */
|
||||
@@ -25,27 +26,26 @@ import java.util.Optional;
|
||||
@BugPattern(
|
||||
summary =
|
||||
"Avoid nesting `Optional`s inside `Optional`s; the resultant code is hard to reason about",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "NestedOptionals",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class NestedOptionals extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Supplier<Type> OPTIONAL = Suppliers.typeFromClass(Optional.class);
|
||||
private static final Supplier<Type> OPTIONAL_OF_OPTIONAL =
|
||||
VisitorState.memoize(generic(OPTIONAL, subOf(raw(OPTIONAL))));
|
||||
|
||||
/** Instantiates a new {@link NestedOptionals} instance. */
|
||||
public NestedOptionals() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
return 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;
|
||||
Type type = OPTIONAL_OF_OPTIONAL.get(state);
|
||||
if (type == null || !state.getTypes().isSubtype(ASTHelpers.getType(tree), type)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
List<Type> typeArguments = type.getTypeArguments();
|
||||
return !typeArguments.isEmpty()
|
||||
&& ASTHelpers.isSubtype(Iterables.getOnlyElement(typeArguments), optionalType, state);
|
||||
return describeMatch(tree);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
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.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
@@ -28,7 +29,8 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Avoid vacuous operations on known non-empty `Mono`s",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "NonEmptyMono",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = SIMPLIFICATION)
|
||||
// XXX: This check does not simplify `someFlux.defaultIfEmpty(T).{defaultIfEmpty(T),hasElements()}`,
|
||||
@@ -74,6 +76,9 @@ public final class NonEmptyMono extends BugChecker implements MethodInvocationTr
|
||||
.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)) {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.PERFORMANCE;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.VerifyException;
|
||||
@@ -44,7 +45,8 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
summary =
|
||||
"Ensure invocations of `Comparator#comparing{,Double,Int,Long}` match the return type"
|
||||
+ " of the provided function",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "PrimitiveComparison",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = PERFORMANCE)
|
||||
public final class PrimitiveComparison extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
@@ -68,6 +70,9 @@ public final class PrimitiveComparison extends BugChecker implements MethodInvoc
|
||||
.named("thenComparing")
|
||||
.withParameters(Function.class.getName()));
|
||||
|
||||
/** Instantiates a new {@link PrimitiveComparison} instance. */
|
||||
public PrimitiveComparison() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
boolean isStatic = STATIC_COMPARISON_METHOD.matches(tree, state);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
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;
|
||||
@@ -14,6 +14,7 @@ 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.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -48,6 +49,7 @@ import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import tech.picnic.errorprone.bugpatterns.util.Flags;
|
||||
import tech.picnic.errorprone.bugpatterns.util.MethodMatcherFactory;
|
||||
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
|
||||
@@ -55,15 +57,15 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Avoid redundant string conversions when possible",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "RedundantStringConversion",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
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 =
|
||||
FLAG_PREFIX + "ExtraConversionMethods";
|
||||
"RedundantStringConversion:ExtraConversionMethods";
|
||||
|
||||
@SuppressWarnings("UnnecessaryLambda")
|
||||
private static final Matcher<ExpressionTree> ANY_EXPR = (t, s) -> true;
|
||||
@@ -139,7 +141,7 @@ public final class RedundantStringConversion extends BugChecker
|
||||
|
||||
private final Matcher<MethodInvocationTree> conversionMethodMatcher;
|
||||
|
||||
/** Instantiates the default {@link RedundantStringConversion}. */
|
||||
/** Instantiates a default {@link RedundantStringConversion} instance. */
|
||||
public RedundantStringConversion() {
|
||||
this(ErrorProneFlags.empty());
|
||||
}
|
||||
@@ -372,10 +374,9 @@ public final class RedundantStringConversion extends BugChecker
|
||||
ErrorProneFlags flags) {
|
||||
// XXX: ErrorProneFlags#getList splits by comma, but method signatures may also contain commas.
|
||||
// For this class methods accepting more than one argument are not valid, but still: not nice.
|
||||
return 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);
|
||||
return anyOf(
|
||||
WELL_KNOWN_STRING_CONVERSION_METHODS,
|
||||
new MethodMatcherFactory()
|
||||
.create(Flags.getList(flags, EXTRA_STRING_CONVERSION_METHODS_FLAG)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
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.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
@@ -21,13 +22,14 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
/**
|
||||
* A {@link BugChecker} that flags unnecessary {@link Refaster#anyOf(Object[])} usages.
|
||||
*
|
||||
* <p>Note that this logic can't be implemented as a Refaster template, as the {@link Refaster}
|
||||
* class is treated specially.
|
||||
* <p>Note that this logic can't be implemented as a Refaster rule, as the {@link Refaster} class is
|
||||
* treated specially.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "`Refaster#anyOf` should be passed at least two parameters",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "RefasterAnyOfUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class RefasterAnyOfUsage extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
@@ -35,6 +37,9 @@ public final class RefasterAnyOfUsage extends BugChecker implements MethodInvoca
|
||||
private static final Matcher<ExpressionTree> REFASTER_ANY_OF =
|
||||
staticMethod().onClass(Refaster.class.getName()).named("anyOf");
|
||||
|
||||
/** Instantiates a new {@link RefasterAnyOfUsage} instance. */
|
||||
public RefasterAnyOfUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (REFASTER_ANY_OF.matches(tree, state)) {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
|
||||
import com.google.errorprone.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.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.Placeholder;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
import javax.lang.model.element.Modifier;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that suggests a canonical set of modifiers for Refaster class and method
|
||||
* definitions.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Refaster class and method definitions should specify a canonical set of modifiers",
|
||||
link = BUG_PATTERNS_BASE_URL + "RefasterRuleModifiers",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
public final class RefasterRuleModifiers extends BugChecker
|
||||
implements ClassTreeMatcher, MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<Tree> BEFORE_TEMPLATE_METHOD = hasAnnotation(BeforeTemplate.class);
|
||||
private static final Matcher<Tree> AFTER_TEMPLATE_METHOD = hasAnnotation(AfterTemplate.class);
|
||||
private static final Matcher<Tree> PLACEHOLDER_METHOD = hasAnnotation(Placeholder.class);
|
||||
private static final Matcher<Tree> REFASTER_METHOD =
|
||||
anyOf(BEFORE_TEMPLATE_METHOD, AFTER_TEMPLATE_METHOD, PLACEHOLDER_METHOD);
|
||||
|
||||
/** Instantiates a new {@link RefasterRuleModifiers} instance. */
|
||||
public RefasterRuleModifiers() {}
|
||||
|
||||
@Override
|
||||
public Description matchClass(ClassTree tree, VisitorState state) {
|
||||
if (!hasMatchingMember(tree, BEFORE_TEMPLATE_METHOD, state)) {
|
||||
/* This class does not contain a Refaster template. */
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix fix = suggestCanonicalModifiers(tree, state);
|
||||
return fix.isEmpty() ? Description.NO_MATCH : describeMatch(tree, fix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (!REFASTER_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return SuggestedFixes.removeModifiers(
|
||||
tree,
|
||||
state,
|
||||
Modifier.FINAL,
|
||||
Modifier.PRIVATE,
|
||||
Modifier.PROTECTED,
|
||||
Modifier.PUBLIC,
|
||||
Modifier.STATIC,
|
||||
Modifier.SYNCHRONIZED)
|
||||
.map(fix -> describeMatch(tree, fix))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
private static SuggestedFix suggestCanonicalModifiers(ClassTree tree, VisitorState state) {
|
||||
Set<Modifier> modifiersToAdd = EnumSet.noneOf(Modifier.class);
|
||||
Set<Modifier> modifiersToRemove =
|
||||
EnumSet.of(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC, Modifier.SYNCHRONIZED);
|
||||
|
||||
if (!hasMatchingMember(tree, PLACEHOLDER_METHOD, state)) {
|
||||
/*
|
||||
* Rules without a `@Placeholder` method should be `final`. Note that Refaster enforces
|
||||
* that `@Placeholder` methods are `abstract`, so rules _with_ such a method will
|
||||
* naturally be `abstract` and non-`final`.
|
||||
*/
|
||||
modifiersToAdd.add(Modifier.FINAL);
|
||||
modifiersToRemove.add(Modifier.ABSTRACT);
|
||||
}
|
||||
|
||||
if (ASTHelpers.findEnclosingNode(state.getPath(), ClassTree.class) != null) {
|
||||
/* Nested classes should be `static`. */
|
||||
modifiersToAdd.add(Modifier.STATIC);
|
||||
}
|
||||
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder();
|
||||
SuggestedFixes.addModifiers(tree, tree.getModifiers(), state, modifiersToAdd)
|
||||
.ifPresent(fix::merge);
|
||||
SuggestedFixes.removeModifiers(tree.getModifiers(), state, modifiersToRemove)
|
||||
.ifPresent(fix::merge);
|
||||
return fix.build();
|
||||
}
|
||||
|
||||
private static boolean hasMatchingMember(
|
||||
ClassTree tree, Matcher<Tree> matcher, VisitorState state) {
|
||||
return tree.getMembers().stream().anyMatch(member -> matcher.matches(member, state));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.ALL;
|
||||
@@ -11,6 +11,7 @@ import static com.google.errorprone.matchers.Matchers.isSameType;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static com.google.errorprone.matchers.Matchers.methodHasParameters;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
@@ -31,7 +32,8 @@ import com.sun.source.tree.Tree;
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Make sure all `@RequestMapping` method parameters are annotated",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "RequestMappingAnnotation",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class RequestMappingAnnotation extends BugChecker implements MethodTreeMatcher {
|
||||
@@ -81,6 +83,9 @@ public final class RequestMappingAnnotation extends BugChecker implements Method
|
||||
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.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
@@ -9,36 +10,72 @@ 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.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.ErrorProneFlags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Suppliers;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import tech.picnic.errorprone.bugpatterns.util.Flags;
|
||||
|
||||
/** A {@link BugChecker} that flags {@code @RequestParam} parameters with an unsupported type. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "`@RequestParam` does not support `ImmutableCollection` and `ImmutableMap` subtypes",
|
||||
linkType = NONE,
|
||||
summary =
|
||||
"By default, `@RequestParam` does not support `ImmutableCollection` and `ImmutableMap` subtypes",
|
||||
link = BUG_PATTERNS_BASE_URL + "RequestParamType",
|
||||
linkType = CUSTOM,
|
||||
severity = ERROR,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class RequestParamType extends BugChecker implements VariableTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<VariableTree> HAS_UNSUPPORTED_REQUEST_PARAM =
|
||||
allOf(
|
||||
annotations(AT_LEAST_ONE, isType("org.springframework.web.bind.annotation.RequestParam")),
|
||||
anyOf(isSubtypeOf(ImmutableCollection.class), isSubtypeOf(ImmutableMap.class)));
|
||||
private 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.
|
||||
*/
|
||||
public RequestParamType(ErrorProneFlags flags) {
|
||||
hasUnsupportedRequestParamType = hasUnsupportedRequestParamType(flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Description matchVariable(VariableTree tree, VisitorState state) {
|
||||
return HAS_UNSUPPORTED_REQUEST_PARAM.matches(tree, state)
|
||||
return hasUnsupportedRequestParamType.matches(tree, state)
|
||||
? describeMatch(tree)
|
||||
: Description.NO_MATCH;
|
||||
}
|
||||
|
||||
private static Matcher<VariableTree> hasUnsupportedRequestParamType(ErrorProneFlags flags) {
|
||||
return allOf(
|
||||
annotations(AT_LEAST_ONE, isType("org.springframework.web.bind.annotation.RequestParam")),
|
||||
anyOf(isSubtypeOf(ImmutableCollection.class), isSubtypeOf(ImmutableMap.class)),
|
||||
not(isSubtypeOfAny(Flags.getList(flags, SUPPORTED_CUSTOM_TYPES_FLAG))));
|
||||
}
|
||||
|
||||
private static Matcher<Tree> isSubtypeOfAny(ImmutableList<String> inclusions) {
|
||||
return anyOf(
|
||||
inclusions.stream()
|
||||
.map(inclusion -> isSubtypeOf(Suppliers.typeFromString(inclusion)))
|
||||
.collect(toImmutableList()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.common.AnnotationMirrors;
|
||||
import com.google.auto.service.AutoService;
|
||||
@@ -25,6 +26,7 @@ import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import tech.picnic.errorprone.bugpatterns.util.ThirdPartyLibrary;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags methods with Spring's {@code @Scheduled} annotation that lack New
|
||||
@@ -33,7 +35,8 @@ import com.sun.source.tree.Tree;
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Scheduled operation must start a new New Relic transaction",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "ScheduledTransactionTrace",
|
||||
linkType = CUSTOM,
|
||||
severity = ERROR,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class ScheduledTransactionTrace extends BugChecker implements MethodTreeMatcher {
|
||||
@@ -44,9 +47,13 @@ public final class ScheduledTransactionTrace extends BugChecker implements Metho
|
||||
private static final MultiMatcher<Tree, AnnotationTree> TRACE_ANNOTATION =
|
||||
annotations(AT_LEAST_ONE, isType(TRACE_ANNOTATION_FQCN));
|
||||
|
||||
/** Instantiates a new {@link ScheduledTransactionTrace} instance. */
|
||||
public ScheduledTransactionTrace() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (!IS_SCHEDULED.matches(tree, state)) {
|
||||
if (!ThirdPartyLibrary.NEW_RELIC_AGENT_API.isIntroductionAllowed(state)
|
||||
|| !IS_SCHEDULED.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.Splitter;
|
||||
@@ -28,12 +29,14 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
// 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",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "Slf4jLogStatement",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class Slf4jLogStatement extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
@@ -45,6 +48,9 @@ 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)) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static java.util.function.Predicate.not;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.VerifyException;
|
||||
@@ -36,7 +37,8 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
@BugPattern(
|
||||
summary =
|
||||
"Prefer the conciseness of `@{Get,Put,Post,Delete,Patch}Mapping` over `@RequestMapping`",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "SpringMvcAnnotation",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class SpringMvcAnnotation extends BugChecker implements AnnotationTreeMatcher {
|
||||
@@ -55,6 +57,9 @@ 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.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
@@ -45,7 +46,8 @@ import java.util.Optional;
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Identifier should be statically imported",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "StaticImport",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class StaticImport extends BugChecker implements MemberSelectTreeMatcher {
|
||||
@@ -99,7 +101,8 @@ public final class StaticImport extends BugChecker implements MemberSelectTreeMa
|
||||
"org.springframework.http.HttpMethod",
|
||||
"org.springframework.http.MediaType",
|
||||
"org.testng.Assert",
|
||||
"reactor.function.TupleUtils");
|
||||
"reactor.function.TupleUtils",
|
||||
"tech.picnic.errorprone.bugpatterns.util.MoreTypes");
|
||||
|
||||
/** Type members that should be statically imported. */
|
||||
@VisibleForTesting
|
||||
@@ -188,6 +191,9 @@ public final class StaticImport extends BugChecker implements MemberSelectTreeMa
|
||||
"of",
|
||||
"valueOf");
|
||||
|
||||
/** Instantiates a new {@link StaticImport} instance. */
|
||||
public StaticImport() {}
|
||||
|
||||
@Override
|
||||
public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) {
|
||||
if (!isCandidateContext(state) || !isCandidate(tree)) {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static com.sun.tools.javac.parser.Tokens.TokenKind.RPAREN;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.Fix;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.google.errorprone.util.ErrorProneTokens;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.util.Position;
|
||||
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags calls to {@link String#toLowerCase()} and {@link
|
||||
* String#toUpperCase()}, as these methods implicitly rely on the environment's default locale.
|
||||
*/
|
||||
// XXX: Also flag `String::toLowerCase` and `String::toUpperCase` method references. For these cases
|
||||
// the suggested fix should introduce a lambda expression with a parameter of which the name does
|
||||
// not coincide with the name of an existing variable name. Such functionality should likely be
|
||||
// introduced in a utility class.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Specify a `Locale` when calling `String#to{Lower,Upper}Case`",
|
||||
link = BUG_PATTERNS_BASE_URL + "StringCaseLocaleUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class StringCaseLocaleUsage extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> DEFAULT_LOCALE_CASE_CONVERSION =
|
||||
instanceMethod()
|
||||
.onExactClass(String.class.getName())
|
||||
.namedAnyOf("toLowerCase", "toUpperCase")
|
||||
.withNoParameters();
|
||||
|
||||
/** Instantiates a new {@link StringCaseLocaleUsage} instance. */
|
||||
public StringCaseLocaleUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!DEFAULT_LOCALE_CASE_CONVERSION.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
int closingParenPosition = getClosingParenPosition(tree, state);
|
||||
if (closingParenPosition == Position.NOPOS) {
|
||||
return describeMatch(tree);
|
||||
}
|
||||
|
||||
return buildDescription(tree)
|
||||
.addFix(suggestLocale(closingParenPosition, "Locale.ROOT"))
|
||||
.addFix(suggestLocale(closingParenPosition, "Locale.getDefault()"))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static Fix suggestLocale(int insertPosition, String locale) {
|
||||
return SuggestedFix.builder()
|
||||
.addImport("java.util.Locale")
|
||||
.replace(insertPosition, insertPosition, locale)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static int getClosingParenPosition(MethodInvocationTree tree, VisitorState state) {
|
||||
int startPosition = ASTHelpers.getStartPosition(tree);
|
||||
if (startPosition == Position.NOPOS) {
|
||||
return Position.NOPOS;
|
||||
}
|
||||
|
||||
return Streams.findLast(
|
||||
ErrorProneTokens.getTokens(SourceCode.treeToString(tree, state), state.context).stream()
|
||||
.filter(t -> t.kind() == RPAREN))
|
||||
.map(token -> startPosition + token.pos())
|
||||
.orElse(Position.NOPOS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
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.bugpatterns.util.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.Convert;
|
||||
import java.util.Formattable;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.bugpatterns.util.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.getName()).named("format");
|
||||
private static final Supplier<Type> CHAR_SEQUENCE_TYPE =
|
||||
Suppliers.typeFromClass(CharSequence.class);
|
||||
private static final Supplier<Type> FORMATTABLE_TYPE = Suppliers.typeFromClass(Formattable.class);
|
||||
|
||||
/** 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(), String.format("\"%s\"", Convert.quote(separator)));
|
||||
|
||||
while (arguments.hasNext()) {
|
||||
ExpressionTree argument = arguments.next();
|
||||
Type argumentType = ASTHelpers.getType(argument);
|
||||
if (isSubtype(argumentType, FORMATTABLE_TYPE, state)) {
|
||||
/*
|
||||
* `Formattable` arguments are handled specially; `String#join` is not a suitable
|
||||
* alternative.
|
||||
*/
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (!isSubtype(argumentType, CHAR_SEQUENCE_TYPE, state)) {
|
||||
/*
|
||||
* The argument was previously implicitly converted to a string; now this must happen
|
||||
* explicitly.
|
||||
*/
|
||||
fix.replace(argument, withStringConversionExpression(argument, state));
|
||||
}
|
||||
}
|
||||
|
||||
return describeMatch(tree, fix.build());
|
||||
}
|
||||
|
||||
private static boolean isSubtype(
|
||||
@Nullable Type subType, Supplier<Type> superType, VisitorState state) {
|
||||
return ASTHelpers.isSubtype(subType, superType.get(state), state);
|
||||
}
|
||||
|
||||
private static String withStringConversionExpression(
|
||||
ExpressionTree argument, VisitorState state) {
|
||||
return String.format("String.valueOf(%s)", SourceCode.treeToString(argument, state));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.NONE;
|
||||
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;
|
||||
@@ -10,6 +10,7 @@ import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
@@ -25,13 +26,17 @@ import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.OffsetTime;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
/** A {@link BugChecker} that flags illegal time-zone related operations. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"Derive the current time from an existing `Clock` Spring bean, and don't rely on a `Clock`'s time zone",
|
||||
linkType = NONE,
|
||||
link = BUG_PATTERNS_BASE_URL + "TimeZoneUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class TimeZoneUsage extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
@@ -56,10 +61,16 @@ public final class TimeZoneUsage extends BugChecker implements MethodInvocationT
|
||||
.onClassAny(
|
||||
LocalDate.class.getName(),
|
||||
LocalDateTime.class.getName(),
|
||||
LocalTime.class.getName())
|
||||
LocalTime.class.getName(),
|
||||
OffsetDateTime.class.getName(),
|
||||
OffsetTime.class.getName(),
|
||||
ZonedDateTime.class.getName())
|
||||
.named("now"),
|
||||
staticMethod().onClassAny(Instant.class.getName()).named("now").withNoParameters());
|
||||
|
||||
/** Instantiates a new {@link TimeZoneUsage} instance. */
|
||||
public TimeZoneUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
return BANNED_TIME_METHOD.matches(tree, state)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Picnic Error Prone Contrib checks. */
|
||||
@com.google.errorprone.annotations.CheckReturnValue
|
||||
@javax.annotation.ParametersAreNonnullByDefault
|
||||
@org.jspecify.annotations.NullMarked
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package tech.picnic.errorprone.bugpatterns.util;
|
||||
|
||||
import static tech.picnic.errorprone.bugpatterns.util.JavaKeywords.isValidIdentifier;
|
||||
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ImportTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.tools.javac.code.Symbol.MethodSymbol;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.util.Optional;
|
||||
|
||||
/** A set of helper methods for detecting conflicts that would be caused when applying fixes. */
|
||||
public final class ConflictDetection {
|
||||
private ConflictDetection() {}
|
||||
|
||||
/**
|
||||
* If applicable, returns a human-readable argument against assigning the given name to an
|
||||
* existing method.
|
||||
*
|
||||
* <p>This method implements imperfect heuristics. Things it currently does not consider include
|
||||
* the following:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Whether the rename would merely introduce a method overload, rather than clashing with an
|
||||
* existing method declaration in its class or a supertype.
|
||||
* <li>Whether the rename would in fact clash with a static import. (It could be that a static
|
||||
* import of the same name is only referenced from lexical scopes in which the method under
|
||||
* consideration cannot be referenced directly.)
|
||||
* </ul>
|
||||
*
|
||||
* @param method The method considered for renaming.
|
||||
* @param newName The newly proposed name for the method.
|
||||
* @param state The {@link VisitorState} to use when searching for blockers.
|
||||
* @return A human-readable argument against assigning the proposed name to the given method, or
|
||||
* {@link Optional#empty()} if no blocker was found.
|
||||
*/
|
||||
public static Optional<String> findMethodRenameBlocker(
|
||||
MethodSymbol method, String newName, VisitorState state) {
|
||||
if (isExistingMethodName(method.owner.type, newName, state)) {
|
||||
return Optional.of(
|
||||
String.format(
|
||||
"a method named `%s` is already defined in this class or a supertype", newName));
|
||||
}
|
||||
|
||||
if (isSimpleNameStaticallyImported(newName, state)) {
|
||||
return Optional.of(String.format("`%s` is already statically imported", newName));
|
||||
}
|
||||
|
||||
if (!isValidIdentifier(newName)) {
|
||||
return Optional.of(String.format("`%s` is not a valid identifier", newName));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static boolean isExistingMethodName(Type clazz, String name, VisitorState state) {
|
||||
return ASTHelpers.matchingMethods(state.getName(name), method -> true, clazz, state.getTypes())
|
||||
.findAny()
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
private static boolean isSimpleNameStaticallyImported(String simpleName, VisitorState state) {
|
||||
return state.getPath().getCompilationUnit().getImports().stream()
|
||||
.filter(ImportTree::isStatic)
|
||||
.map(ImportTree::getQualifiedIdentifier)
|
||||
.map(tree -> getStaticImportSimpleName(tree, state))
|
||||
.anyMatch(simpleName::contentEquals);
|
||||
}
|
||||
|
||||
private static CharSequence getStaticImportSimpleName(Tree tree, VisitorState state) {
|
||||
String source = SourceCode.treeToString(tree, state);
|
||||
return source.subSequence(source.lastIndexOf('.') + 1, source.length());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package tech.picnic.errorprone.bugpatterns.util;
|
||||
|
||||
/** Utility class providing documentation-related code. */
|
||||
public final class Documentation {
|
||||
/** The base URL at which Error Prone Support bug patterns are hosted. */
|
||||
public static final String BUG_PATTERNS_BASE_URL = "https://error-prone.picnic.tech/bugpatterns/";
|
||||
|
||||
private Documentation() {}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package tech.picnic.errorprone.bugpatterns.util;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.ErrorProneFlags;
|
||||
|
||||
/** Helper methods for working with {@link ErrorProneFlags}. */
|
||||
public final class Flags {
|
||||
private Flags() {}
|
||||
|
||||
/**
|
||||
* Returns the list of (comma-separated) arguments passed using the given Error Prone flag.
|
||||
*
|
||||
* @param errorProneFlags The full set of flags provided.
|
||||
* @param name The name of the flag of interest.
|
||||
* @return A non-{@code null} list of provided arguments; this list is empty if the flag was not
|
||||
* provided, or if the flag's value is the empty string.
|
||||
*/
|
||||
public static ImmutableList<String> getList(ErrorProneFlags errorProneFlags, String name) {
|
||||
return errorProneFlags
|
||||
.getList(name)
|
||||
.map(ImmutableList::copyOf)
|
||||
.filter(flags -> !flags.equals(ImmutableList.of("")))
|
||||
.orElseGet(ImmutableList::of);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,19 @@ 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.
|
||||
*
|
||||
@@ -64,7 +76,6 @@ public final class JavaKeywords {
|
||||
"void",
|
||||
"volatile",
|
||||
"while");
|
||||
|
||||
/**
|
||||
* List of all contextual keywords in the Java language.
|
||||
*
|
||||
@@ -89,13 +100,28 @@ public final class JavaKeywords {
|
||||
"var",
|
||||
"with",
|
||||
"yield");
|
||||
|
||||
/** List of all keywords in the Java language. */
|
||||
private static final ImmutableSet<String> ALL_KEYWORDS =
|
||||
Sets.union(RESERVED_KEYWORDS, CONTEXTUAL_KEYWORDS).immutableCopy();
|
||||
|
||||
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>
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -21,6 +21,9 @@ public final class MethodMatcherFactory {
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package tech.picnic.errorprone.bugpatterns.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
|
||||
/**
|
||||
* A collection of helper methods for working with the AST.
|
||||
*
|
||||
* <p>These methods are additions to the ones found in {@link
|
||||
* com.google.errorprone.util.ASTHelpers}.
|
||||
*/
|
||||
public final class MoreASTHelpers {
|
||||
private MoreASTHelpers() {}
|
||||
|
||||
/**
|
||||
* Finds methods with the specified name in given the {@link VisitorState}'s current enclosing
|
||||
* class.
|
||||
*
|
||||
* @param methodName The method name to search for.
|
||||
* @param state The {@link VisitorState} from which to derive the enclosing class of interest.
|
||||
* @return The {@link MethodTree}s of the methods with the given name in the enclosing class.
|
||||
*/
|
||||
public static ImmutableList<MethodTree> findMethods(CharSequence methodName, VisitorState state) {
|
||||
ClassTree clazz = state.findEnclosing(ClassTree.class);
|
||||
checkArgument(clazz != null, "Visited node is not enclosed by a class");
|
||||
return clazz.getMembers().stream()
|
||||
.filter(MethodTree.class::isInstance)
|
||||
.map(MethodTree.class::cast)
|
||||
.filter(method -> method.getName().contentEquals(methodName))
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether there are any methods with the specified name in given the {@link
|
||||
* VisitorState}'s current enclosing class.
|
||||
*
|
||||
* @param methodName The method name to search for.
|
||||
* @param state The {@link VisitorState} from which to derive the enclosing class of interest.
|
||||
* @return Whether there are any methods with the given name in the enclosing class.
|
||||
*/
|
||||
public static boolean methodExistsInEnclosingClass(CharSequence methodName, VisitorState state) {
|
||||
return !findMethods(methodName, state).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package tech.picnic.errorprone.bugpatterns.util;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static java.util.Objects.requireNonNullElse;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreMatchers.hasMetaAnnotation;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.matchers.AnnotationMatcherUtils;
|
||||
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.ExpressionTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.NewArrayTree;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* A collection of JUnit-specific helper methods and {@link Matcher}s.
|
||||
*
|
||||
* <p>These constants and methods are additions to the ones found in {@link
|
||||
* com.google.errorprone.matchers.JUnitMatchers}.
|
||||
*/
|
||||
public final class MoreJUnitMatchers {
|
||||
/** Matches JUnit Jupiter test methods. */
|
||||
public static final MultiMatcher<MethodTree, AnnotationTree> TEST_METHOD =
|
||||
annotations(
|
||||
AT_LEAST_ONE,
|
||||
anyOf(
|
||||
isType("org.junit.jupiter.api.Test"),
|
||||
hasMetaAnnotation("org.junit.jupiter.api.TestTemplate")));
|
||||
/** Matches JUnit Jupiter setup and teardown methods. */
|
||||
public 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")));
|
||||
/**
|
||||
* Matches methods that have a {@link org.junit.jupiter.params.provider.MethodSource} annotation.
|
||||
*/
|
||||
public static final MultiMatcher<MethodTree, AnnotationTree> HAS_METHOD_SOURCE =
|
||||
annotations(AT_LEAST_ONE, isType("org.junit.jupiter.params.provider.MethodSource"));
|
||||
|
||||
private MoreJUnitMatchers() {}
|
||||
|
||||
/**
|
||||
* Returns the names of the JUnit value factory methods specified by the given {@link
|
||||
* org.junit.jupiter.params.provider.MethodSource} annotation.
|
||||
*
|
||||
* @param methodSourceAnnotation The annotation from which to extract value factory method names.
|
||||
* @return One or more value factory names.
|
||||
*/
|
||||
static ImmutableSet<String> getMethodSourceFactoryNames(
|
||||
AnnotationTree methodSourceAnnotation, MethodTree method) {
|
||||
String methodName = method.getName().toString();
|
||||
ExpressionTree value = AnnotationMatcherUtils.getArgument(methodSourceAnnotation, "value");
|
||||
|
||||
if (!(value instanceof NewArrayTree)) {
|
||||
return ImmutableSet.of(toMethodSourceFactoryName(value, methodName));
|
||||
}
|
||||
|
||||
return ((NewArrayTree) value)
|
||||
.getInitializers().stream()
|
||||
.map(name -> toMethodSourceFactoryName(name, methodName))
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
|
||||
private static String toMethodSourceFactoryName(
|
||||
@Nullable ExpressionTree tree, String annotatedMethodName) {
|
||||
return requireNonNullElse(
|
||||
Strings.emptyToNull(ASTHelpers.constValue(tree, String.class)), annotatedMethodName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package tech.picnic.errorprone.bugpatterns.util;
|
||||
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.matchers.Matchers;
|
||||
import com.google.errorprone.predicates.TypePredicate;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
|
||||
/**
|
||||
* A collection of general-purpose {@link Matcher}s.
|
||||
*
|
||||
* <p>These methods are additions to the ones found in {@link Matchers}.
|
||||
*/
|
||||
public final class MoreMatchers {
|
||||
private MoreMatchers() {}
|
||||
|
||||
/**
|
||||
* Returns a {@link Matcher} that determines whether a given {@link AnnotationTree} has a
|
||||
* meta-annotation of the specified type.
|
||||
*
|
||||
* @param <T> The type of tree to match against.
|
||||
* @param annotationType The binary type name of the annotation (e.g.
|
||||
* "org.jspecify.annotations.Nullable", or "some.package.OuterClassName$InnerClassName")
|
||||
* @return A {@link Matcher} that matches trees with the specified meta-annotation.
|
||||
*/
|
||||
public static <T extends AnnotationTree> Matcher<T> hasMetaAnnotation(String annotationType) {
|
||||
TypePredicate typePredicate = hasAnnotation(annotationType);
|
||||
return (tree, state) -> {
|
||||
Symbol sym = ASTHelpers.getSymbol(tree);
|
||||
return sym != null && typePredicate.apply(sym.type, state);
|
||||
};
|
||||
}
|
||||
|
||||
// XXX: Consider moving to a `MoreTypePredicates` utility class.
|
||||
private static TypePredicate hasAnnotation(String annotationClassName) {
|
||||
return (type, state) -> ASTHelpers.hasAnnotation(type.tsym, annotationClassName, state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package tech.picnic.errorprone.bugpatterns.util;
|
||||
|
||||
import static java.util.stream.Collectors.toCollection;
|
||||
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.suppliers.Supplier;
|
||||
import com.google.errorprone.suppliers.Suppliers;
|
||||
import com.sun.tools.javac.code.BoundKind;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* A set of helper methods which together define a DSL for defining {@link Type types}.
|
||||
*
|
||||
* <p>These methods are meant to be statically imported. Example usage:
|
||||
*
|
||||
* <pre>{@code
|
||||
* Supplier<Type> type =
|
||||
* VisitorState.memoize(
|
||||
* generic(
|
||||
* type("reactor.core.publisher.Flux"),
|
||||
* subOf(generic(type("org.reactivestreams.Publisher"), unbound()))));
|
||||
* }</pre>
|
||||
*
|
||||
* This statement produces a memoized supplier of the type {@code Flux<? extends Publisher<?>>}.
|
||||
*/
|
||||
public final class MoreTypes {
|
||||
private MoreTypes() {}
|
||||
|
||||
/**
|
||||
* Creates a supplier of the type with the given fully qualified name.
|
||||
*
|
||||
* <p>This method should only be used when building more complex types in combination with other
|
||||
* {@link MoreTypes} methods. In other cases prefer directly calling {@link
|
||||
* Suppliers#typeFromString(String)}.
|
||||
*
|
||||
* @param typeName The type of interest.
|
||||
* @return A supplier which returns the described type if available in the given state, and {@code
|
||||
* null} otherwise.
|
||||
*/
|
||||
public static Supplier<Type> type(String typeName) {
|
||||
return Suppliers.typeFromString(typeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a supplier of the described generic type.
|
||||
*
|
||||
* @param type The base type of interest.
|
||||
* @param typeArgs The desired type arguments.
|
||||
* @return A supplier which returns the described type if available in the given state, and {@code
|
||||
* null} otherwise.
|
||||
*/
|
||||
// XXX: The given `type` should be a generic type, so perhaps `withParams` would be a better
|
||||
// method name. But the DSL wouldn't look as nice that way.
|
||||
@SafeVarargs
|
||||
@SuppressWarnings("varargs")
|
||||
public static Supplier<Type> generic(Supplier<Type> type, Supplier<Type>... typeArgs) {
|
||||
return propagateNull(
|
||||
type,
|
||||
(state, baseType) -> {
|
||||
List<Type> params =
|
||||
Arrays.stream(typeArgs).map(s -> s.get(state)).collect(toCollection(ArrayList::new));
|
||||
if (params.stream().anyMatch(Objects::isNull)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.getType(baseType, /* isArray= */ false, params);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a raw (erased, non-generic) variant of the given type.
|
||||
*
|
||||
* @param type The base type of interest.
|
||||
* @return A supplier which returns the described type if available in the given state, and {@code
|
||||
* null} otherwise.
|
||||
*/
|
||||
public static Supplier<Type> raw(Supplier<Type> type) {
|
||||
return propagateNull(type, (state, baseType) -> baseType.tsym.erasure(state.getTypes()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code ? super T} wildcard type, with {@code T} bound to the given type.
|
||||
*
|
||||
* @param type The base type of interest.
|
||||
* @return A supplier which returns the described type if available in the given state, and {@code
|
||||
* null} otherwise.
|
||||
*/
|
||||
public static Supplier<Type> superOf(Supplier<Type> type) {
|
||||
return propagateNull(
|
||||
type,
|
||||
(state, baseType) ->
|
||||
new Type.WildcardType(baseType, BoundKind.SUPER, state.getSymtab().boundClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code ? extends T} wildcard type, with {@code T} bound to the given type.
|
||||
*
|
||||
* @param type The base type of interest.
|
||||
* @return A supplier which returns the described type if available in the given state, and {@code
|
||||
* null} otherwise.
|
||||
*/
|
||||
public static Supplier<Type> subOf(Supplier<Type> type) {
|
||||
return propagateNull(
|
||||
type,
|
||||
(state, baseType) ->
|
||||
new Type.WildcardType(
|
||||
type.get(state), BoundKind.EXTENDS, state.getSymtab().boundClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unbound wildcard type ({@code ?}).
|
||||
*
|
||||
* @return A supplier which returns the described type.
|
||||
*/
|
||||
public static Supplier<Type> unbound() {
|
||||
return state ->
|
||||
new Type.WildcardType(
|
||||
state.getSymtab().objectType, BoundKind.UNBOUND, state.getSymtab().boundClass);
|
||||
}
|
||||
|
||||
private static Supplier<Type> propagateNull(
|
||||
Supplier<Type> type, BiFunction<VisitorState, Type, Type> transformer) {
|
||||
return state ->
|
||||
Optional.ofNullable(type.get(state)).map(t -> transformer.apply(state, t)).orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
package tech.picnic.errorprone.bugpatterns.util;
|
||||
|
||||
import static com.sun.tools.javac.util.Position.NOPOS;
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
/** The complement of {@link CharMatcher#whitespace()}. */
|
||||
private static final CharMatcher NON_WHITESPACE_MATCHER = CharMatcher.whitespace().negate();
|
||||
|
||||
private SourceCode() {}
|
||||
|
||||
/**
|
||||
@@ -24,4 +31,32 @@ public final class SourceCode {
|
||||
String src = state.getSourceForNode(tree);
|
||||
return src != null ? src : tree.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link SuggestedFix} for the deletion of the given {@link Tree}, including any
|
||||
* whitespace that follows it.
|
||||
*
|
||||
* <p>Removing trailing whitespace may prevent the introduction of an empty line at the start of a
|
||||
* code block; such empty lines are not removed when formatting the code using Google Java Format.
|
||||
*
|
||||
* @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} {@link SuggestedFix} similar to one produced by {@link
|
||||
* SuggestedFix#delete(Tree)}.
|
||||
*/
|
||||
public static SuggestedFix deleteWithTrailingWhitespace(Tree tree, VisitorState state) {
|
||||
CharSequence sourceCode = state.getSourceCode();
|
||||
int endPos = state.getEndPosition(tree);
|
||||
if (sourceCode == null || endPos == NOPOS) {
|
||||
/* We can't identify the trailing whitespace; delete just the tree. */
|
||||
return SuggestedFix.delete(tree);
|
||||
}
|
||||
|
||||
int whitespaceEndPos = NON_WHITESPACE_MATCHER.indexIn(sourceCode, endPos);
|
||||
return SuggestedFix.replace(
|
||||
((DiagnosticPosition) tree).getStartPosition(),
|
||||
whitespaceEndPos == -1 ? sourceCode.length() : whitespaceEndPos,
|
||||
"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package tech.picnic.errorprone.bugpatterns.util;
|
||||
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.suppliers.Supplier;
|
||||
import com.sun.tools.javac.code.ClassFinder;
|
||||
import com.sun.tools.javac.code.Symbol.CompletionFailure;
|
||||
import com.sun.tools.javac.util.Name;
|
||||
|
||||
/**
|
||||
* Utility class that helps decide whether it is appropriate to introduce references to (well-known)
|
||||
* third-party libraries.
|
||||
*
|
||||
* <p>This class should be used by {@link BugChecker}s that may otherwise suggest the introduction
|
||||
* of code that depends on possibly-not-present third-party libraries.
|
||||
*/
|
||||
// XXX: Consider giving users more fine-grained control. This would be beneficial in cases where a
|
||||
// dependency is on the classpath, but new usages are undesirable.
|
||||
public enum ThirdPartyLibrary {
|
||||
/**
|
||||
* AssertJ.
|
||||
*
|
||||
* @see <a href="https://assertj.github.io/doc">AssertJ documentation</a>
|
||||
*/
|
||||
ASSERTJ("org.assertj.core.api.Assertions"),
|
||||
/**
|
||||
* Google's Guava.
|
||||
*
|
||||
* @see <a href="https://github.com/google/guava">Guava on GitHub</a>
|
||||
*/
|
||||
GUAVA("com.google.common.collect.ImmutableList"),
|
||||
/**
|
||||
* New Relic's Java agent API.
|
||||
*
|
||||
* @see <a href="https://github.com/newrelic/newrelic-java-agent/tree/main/newrelic-api">New Relic
|
||||
* Java agent API on GitHub</a>
|
||||
*/
|
||||
NEW_RELIC_AGENT_API("com.newrelic.api.agent.Agent"),
|
||||
/**
|
||||
* VMWare's Project Reactor.
|
||||
*
|
||||
* @see <a href="https://projectreactor.io">Home page</a>
|
||||
*/
|
||||
REACTOR("reactor.core.publisher.Flux");
|
||||
|
||||
private static final String IGNORE_CLASSPATH_COMPAT_FLAG =
|
||||
"ErrorProneSupport:IgnoreClasspathCompat";
|
||||
|
||||
@SuppressWarnings("ImmutableEnumChecker" /* Supplier is deterministic. */)
|
||||
private final Supplier<Boolean> canUse;
|
||||
|
||||
/**
|
||||
* Instantiates a {@link ThirdPartyLibrary} enum value.
|
||||
*
|
||||
* @param witnessFqcn The fully-qualified class name of a type that is expected to be on the
|
||||
* classpath iff the associated third-party library is on the classpath.
|
||||
*/
|
||||
ThirdPartyLibrary(String witnessFqcn) {
|
||||
this.canUse = VisitorState.memoize(state -> canIntroduceUsage(witnessFqcn, state));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether it is okay to introduce a dependency on this well-known third party library in
|
||||
* the given context.
|
||||
*
|
||||
* @param state The context under consideration.
|
||||
* @return {@code true} iff it is okay to assume or create a dependency on this library.
|
||||
*/
|
||||
public boolean isIntroductionAllowed(VisitorState state) {
|
||||
return canUse.get(state);
|
||||
}
|
||||
|
||||
private static boolean canIntroduceUsage(String className, VisitorState state) {
|
||||
return shouldIgnoreClasspath(state) || isKnownClass(className, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to determine whether a class with the given FQCN is on the classpath.
|
||||
*
|
||||
* <p>The {@link VisitorState}'s symbol table is consulted first. If the type has not yet been
|
||||
* loaded, then an attempt is made to do so.
|
||||
*/
|
||||
private static boolean isKnownClass(String className, VisitorState state) {
|
||||
return state.getTypeFromString(className) != null || canLoadClass(className, state);
|
||||
}
|
||||
|
||||
private static boolean canLoadClass(String className, VisitorState state) {
|
||||
ClassFinder classFinder = ClassFinder.instance(state.context);
|
||||
Name binaryName = state.binaryNameFromClassname(className);
|
||||
try {
|
||||
classFinder.loadClass(state.getSymtab().unnamedModule, binaryName);
|
||||
return true;
|
||||
} catch (CompletionFailure e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldIgnoreClasspath(VisitorState state) {
|
||||
return state
|
||||
.errorProneOptions()
|
||||
.getFlags()
|
||||
.getBoolean(IGNORE_CLASSPATH_COMPAT_FLAG)
|
||||
.orElse(Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Auxiliary utilities for use by Error Prone checks. */
|
||||
@com.google.errorprone.annotations.CheckReturnValue
|
||||
@javax.annotation.ParametersAreNonnullByDefault
|
||||
@org.jspecify.annotations.NullMarked
|
||||
package tech.picnic.errorprone.bugpatterns.util;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
import static org.assertj.core.data.Percentage.withPercentage;
|
||||
@@ -9,19 +9,21 @@ import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import java.math.BigDecimal;
|
||||
import org.assertj.core.api.AbstractBigDecimalAssert;
|
||||
import org.assertj.core.api.BigDecimalAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/**
|
||||
* Refaster templates related to AssertJ assertions over {@link BigDecimal}s.
|
||||
* Refaster rules related to AssertJ assertions over {@link BigDecimal}s.
|
||||
*
|
||||
* <p>Note that, contrary to collections of Refaster templates for other {@link
|
||||
* org.assertj.core.api.NumberAssert} subtypes, these templates do not rewrite to/from {@link
|
||||
* <p>Note that, contrary to collections of Refaster rules for other {@link
|
||||
* org.assertj.core.api.NumberAssert} subtypes, these rules do not rewrite to/from {@link
|
||||
* BigDecimalAssert#isEqualTo(Object)} and {@link BigDecimalAssert#isNotEqualTo(Object)}. This is
|
||||
* because {@link BigDecimal#equals(Object)} considers not only the numeric value of compared
|
||||
* instances, but also their scale. As a result various seemingly straightforward transformations
|
||||
* would actually subtly change the assertion's semantics.
|
||||
*/
|
||||
final class AssertJBigDecimalTemplates {
|
||||
private AssertJBigDecimalTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJBigDecimalRules {
|
||||
private AssertJBigDecimalRules() {}
|
||||
|
||||
static final class AbstractBigDecimalAssertIsEqualByComparingTo {
|
||||
@BeforeTemplate
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
import static org.assertj.core.data.Percentage.withPercentage;
|
||||
@@ -8,11 +8,13 @@ import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import java.math.BigInteger;
|
||||
import org.assertj.core.api.AbstractBigIntegerAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
// XXX: If we add a rule that drops unnecessary `L` suffixes from literal longs, then the `0L`/`1L`
|
||||
// cases below can go.
|
||||
final class AssertJBigIntegerTemplates {
|
||||
private AssertJBigIntegerTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJBigIntegerRules {
|
||||
private AssertJBigIntegerRules() {}
|
||||
|
||||
static final class AbstractBigIntegerAssertIsEqualTo {
|
||||
@BeforeTemplate
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -8,9 +8,11 @@ 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 tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
final class AssertJBooleanTemplates {
|
||||
private AssertJBooleanTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJBooleanRules {
|
||||
private AssertJBooleanRules() {}
|
||||
|
||||
static final class AbstractBooleanAssertIsEqualTo {
|
||||
@BeforeTemplate
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
import static org.assertj.core.data.Percentage.withPercentage;
|
||||
@@ -7,9 +7,11 @@ import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import org.assertj.core.api.AbstractByteAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
final class AssertJByteTemplates {
|
||||
private AssertJByteTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJByteRules {
|
||||
private AssertJByteRules() {}
|
||||
|
||||
static final class AbstractByteAssertIsEqualTo {
|
||||
@BeforeTemplate
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -8,9 +8,11 @@ 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.AbstractAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
final class AssertJCharSequenceTemplates {
|
||||
private AssertJCharSequenceTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJCharSequenceRules {
|
||||
private AssertJCharSequenceRules() {}
|
||||
|
||||
static final class AssertThatCharSequenceIsEmpty {
|
||||
@BeforeTemplate
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -8,9 +8,11 @@ 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;
|
||||
|
||||
final class AssertJComparableTemplates {
|
||||
private AssertJComparableTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJComparableRules {
|
||||
private AssertJComparableRules() {}
|
||||
|
||||
static final class AssertThatIsEqualByComparingTo<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
import static org.assertj.core.data.Percentage.withPercentage;
|
||||
@@ -8,9 +8,11 @@ import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import org.assertj.core.api.AbstractDoubleAssert;
|
||||
import org.assertj.core.data.Offset;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
final class AssertJDoubleTemplates {
|
||||
private AssertJDoubleTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJDoubleRules {
|
||||
private AssertJDoubleRules() {}
|
||||
|
||||
static final class AbstractDoubleAssertIsCloseToWithOffset {
|
||||
@BeforeTemplate
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
@@ -6,9 +6,11 @@ import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import java.util.Collection;
|
||||
import org.assertj.core.api.EnumerableAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
final class AssertJEnumerableTemplates {
|
||||
private AssertJEnumerableTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJEnumerableRules {
|
||||
private AssertJEnumerableRules() {}
|
||||
|
||||
static final class EnumerableAssertIsEmpty<E> {
|
||||
@BeforeTemplate
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
import static org.assertj.core.data.Percentage.withPercentage;
|
||||
@@ -8,9 +8,11 @@ import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import org.assertj.core.api.AbstractFloatAssert;
|
||||
import org.assertj.core.data.Offset;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
final class AssertJFloatTemplates {
|
||||
private AssertJFloatTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJFloatRules {
|
||||
private AssertJFloatRules() {}
|
||||
|
||||
static final class AbstractFloatAssertIsCloseToWithOffset {
|
||||
@BeforeTemplate
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
import static org.assertj.core.data.Percentage.withPercentage;
|
||||
@@ -7,9 +7,11 @@ import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import org.assertj.core.api.AbstractIntegerAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
final class AssertJIntegerTemplates {
|
||||
private AssertJIntegerTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJIntegerRules {
|
||||
private AssertJIntegerRules() {}
|
||||
|
||||
static final class AbstractIntegerAssertIsEqualTo {
|
||||
@BeforeTemplate
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
import static org.assertj.core.data.Percentage.withPercentage;
|
||||
@@ -7,9 +7,11 @@ import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import org.assertj.core.api.AbstractLongAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
final class AssertJLongTemplates {
|
||||
private AssertJLongTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJLongRules {
|
||||
private AssertJLongRules() {}
|
||||
|
||||
static final class AbstractLongAssertIsEqualTo {
|
||||
@BeforeTemplate
|
||||
@@ -0,0 +1,248 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableBiMap;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultiset;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.ImmutableSortedMultiset;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.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 java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
import org.assertj.core.api.AbstractBooleanAssert;
|
||||
import org.assertj.core.api.AbstractMapAssert;
|
||||
import org.assertj.core.api.MapAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
@OnlineDocumentation
|
||||
final class AssertJMapRules {
|
||||
private AssertJMapRules() {}
|
||||
|
||||
// XXX: Reduce boilerplate using a `Matcher` that identifies "empty" instances.
|
||||
static final class AbstractMapAssertIsEmpty<K, V> {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("unchecked")
|
||||
void before(AbstractMapAssert<?, ?, K, V> mapAssert) {
|
||||
Refaster.anyOf(
|
||||
mapAssert.containsExactlyEntriesOf(
|
||||
Refaster.anyOf(
|
||||
ImmutableMap.of(),
|
||||
ImmutableBiMap.of(),
|
||||
ImmutableSortedMap.of(),
|
||||
new HashMap<>(),
|
||||
new LinkedHashMap<>(),
|
||||
new TreeMap<>())),
|
||||
mapAssert.hasSameSizeAs(
|
||||
Refaster.anyOf(
|
||||
ImmutableMap.of(),
|
||||
ImmutableBiMap.of(),
|
||||
ImmutableSortedMap.of(),
|
||||
new HashMap<>(),
|
||||
new LinkedHashMap<>(),
|
||||
new TreeMap<>())),
|
||||
mapAssert.isEqualTo(
|
||||
Refaster.anyOf(
|
||||
ImmutableMap.of(),
|
||||
ImmutableBiMap.of(),
|
||||
ImmutableSortedMap.of(),
|
||||
new HashMap<>(),
|
||||
new LinkedHashMap<>(),
|
||||
new TreeMap<>())),
|
||||
mapAssert.containsOnlyKeys(
|
||||
Refaster.anyOf(
|
||||
ImmutableList.of(),
|
||||
new ArrayList<>(),
|
||||
ImmutableSet.of(),
|
||||
new HashSet<>(),
|
||||
new LinkedHashSet<>(),
|
||||
ImmutableSortedSet.of(),
|
||||
new TreeSet<>(),
|
||||
ImmutableMultiset.of(),
|
||||
ImmutableSortedMultiset.of())),
|
||||
mapAssert.containsExactly(),
|
||||
mapAssert.containsOnly(),
|
||||
mapAssert.containsOnlyKeys());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
void after(AbstractMapAssert<?, ?, K, V> mapAssert) {
|
||||
mapAssert.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return mapAssert.isNotEqualTo(
|
||||
Refaster.anyOf(
|
||||
ImmutableMap.of(),
|
||||
ImmutableBiMap.of(),
|
||||
ImmutableSortedMap.of(),
|
||||
new HashMap<>(),
|
||||
new LinkedHashMap<>(),
|
||||
new TreeMap<>()));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert) {
|
||||
return mapAssert.isNotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
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<K, V> map) {
|
||||
return mapAssert.isEqualTo(map);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert, Map<K, 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<K, V> map) {
|
||||
return mapAssert.hasSize(map.size());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert, Map<K, V> 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 AssertThatMapContainsValue<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(Map<K, V> map, V value) {
|
||||
return assertThat(map.containsValue(value)).isTrue();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, V value) {
|
||||
return assertThat(map).containsValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapDoesNotContainValue<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(Map<K, V> map, V value) {
|
||||
return assertThat(map.containsValue(value)).isFalse();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, V value) {
|
||||
return assertThat(map).doesNotContainValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -19,10 +19,12 @@ import org.assertj.core.api.AbstractIntegerAssert;
|
||||
import org.assertj.core.api.AbstractLongAssert;
|
||||
import org.assertj.core.api.AbstractShortAssert;
|
||||
import org.assertj.core.api.NumberAssert;
|
||||
import tech.picnic.errorprone.refaster.util.IsCharacter;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
import tech.picnic.errorprone.refaster.matchers.IsCharacter;
|
||||
|
||||
final class AssertJNumberTemplates {
|
||||
private AssertJNumberTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJNumberRules {
|
||||
private AssertJNumberRules() {}
|
||||
|
||||
static final class NumberAssertIsPositive {
|
||||
@BeforeTemplate
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -10,9 +10,11 @@ import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import org.assertj.core.api.AbstractBooleanAssert;
|
||||
import org.assertj.core.api.AbstractStringAssert;
|
||||
import org.assertj.core.api.ObjectAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
final class AssertJObjectTemplates {
|
||||
private AssertJObjectTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJObjectRules {
|
||||
private AssertJObjectRules() {}
|
||||
|
||||
static final class AssertThatIsInstanceOf<S, T> {
|
||||
@BeforeTemplate
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -14,13 +14,14 @@ import org.assertj.core.api.AbstractObjectAssert;
|
||||
import org.assertj.core.api.AbstractOptionalAssert;
|
||||
import org.assertj.core.api.ObjectAssert;
|
||||
import org.assertj.core.api.OptionalAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
final class AssertJOptionalTemplates {
|
||||
private AssertJOptionalTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJOptionalRules {
|
||||
private AssertJOptionalRules() {}
|
||||
|
||||
static final class AssertThatOptional<T> {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("NullAway")
|
||||
ObjectAssert<T> before(Optional<T> optional) {
|
||||
return assertThat(optional.orElseThrow());
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -9,9 +9,11 @@ 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;
|
||||
|
||||
final class AssertJPrimitiveTemplates {
|
||||
private AssertJPrimitiveTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJPrimitiveRules {
|
||||
private AssertJPrimitiveRules() {}
|
||||
|
||||
static final class AssertThatIsEqualTo {
|
||||
@BeforeTemplate
|
||||
@@ -1,14 +1,11 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableBiMap;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultiset;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.ImmutableSortedMultiset;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
@@ -22,9 +19,7 @@ import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -32,19 +27,16 @@ import java.util.OptionalDouble;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Stream;
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
import org.assertj.core.api.AbstractBooleanAssert;
|
||||
import org.assertj.core.api.AbstractCollectionAssert;
|
||||
import org.assertj.core.api.AbstractComparableAssert;
|
||||
import org.assertj.core.api.AbstractDoubleAssert;
|
||||
import org.assertj.core.api.AbstractIntegerAssert;
|
||||
import org.assertj.core.api.AbstractLongAssert;
|
||||
import org.assertj.core.api.AbstractMapAssert;
|
||||
import org.assertj.core.api.IterableAssert;
|
||||
import org.assertj.core.api.ListAssert;
|
||||
import org.assertj.core.api.MapAssert;
|
||||
@@ -53,9 +45,10 @@ import org.assertj.core.api.ObjectEnumerableAssert;
|
||||
import org.assertj.core.api.OptionalDoubleAssert;
|
||||
import org.assertj.core.api.OptionalIntAssert;
|
||||
import org.assertj.core.api.OptionalLongAssert;
|
||||
import tech.picnic.errorprone.refaster.util.IsArray;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
import tech.picnic.errorprone.refaster.matchers.IsArray;
|
||||
|
||||
/** Refaster templates related to AssertJ expressions and statements. */
|
||||
/** Refaster rules related to AssertJ expressions and statements. */
|
||||
// XXX: Most `AbstractIntegerAssert` rules can also be applied for other primitive types. Generate
|
||||
// these in separate files.
|
||||
// XXX: Also do for BigInteger/BigDecimal?
|
||||
@@ -63,7 +56,7 @@ import tech.picnic.errorprone.refaster.util.IsArray;
|
||||
// ^ And variants.
|
||||
// XXX: Consider splitting this class into multiple classes.
|
||||
// XXX: Some of these rules may not apply given the updated TestNG rewrite rules. Review.
|
||||
// XXX: For the templates that "unwrap" explicitly enumerated collections, also introduce variants
|
||||
// XXX: For the rules that "unwrap" explicitly enumerated collections, also introduce variants
|
||||
// with explicitly enumerated sorted collections. (Requires that the type bound is Comparable.)
|
||||
// XXX: Handle `.isEqualTo(explicitlyEnumeratedCollection)`. Can be considered equivalent to
|
||||
// `.containsOnly(elements)`. (This does mean the auto-generated code needs to be more advanced.
|
||||
@@ -101,7 +94,7 @@ import tech.picnic.errorprone.refaster.util.IsArray;
|
||||
// (etc.)
|
||||
// XXX: Look into using Assertions#contentOf(URL url, Charset charset) instead of our own test
|
||||
// method.
|
||||
// XXX: Write Optional templates also for `OptionalInt` and variants.
|
||||
// XXX: Write `Optional` rules also for `OptionalInt` and variants.
|
||||
// XXX: Write plugin to flag `assertThat(compileTimeConstant)` occurrences. Also other likely
|
||||
// candidates, such as `assertThat(ImmutableSet(foo, bar)).XXX`
|
||||
// XXX: Write generic plugin to replace explicit array parameters with varargs (`new int[] {1, 2}`
|
||||
@@ -118,10 +111,11 @@ import tech.picnic.errorprone.refaster.util.IsArray;
|
||||
// XXX: `assertThat(ImmutableList.sortedCopyOf(cmp, values)).somethingExactOrder` -> just compare
|
||||
// "in any order".
|
||||
// XXX: Turns out a lot of this is also covered by https://github.com/palantir/assertj-automation.
|
||||
// See how we can combine these things. Do note that (at present) their Refaster templates don't
|
||||
// See how we can combine these things. Do note that (at present) their Refaster rules don't
|
||||
// show up as Error Prone checks. So we'd have to build an integration for that.
|
||||
final class AssertJTemplates {
|
||||
private AssertJTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJRules {
|
||||
private AssertJRules() {}
|
||||
|
||||
//
|
||||
// OptionalDouble
|
||||
@@ -491,7 +485,7 @@ final class AssertJTemplates {
|
||||
}
|
||||
|
||||
// XXX: This overload is here because `assertThat` has an overload for `Comparable` types.
|
||||
// Unfortunately this still doesn't convince Refaster to match this template in the context of
|
||||
// Unfortunately this still doesn't convince Refaster to match this rule in the context of
|
||||
// Comparable types. Figure out why! Note that this also affects the `AssertThatOptional` rule.
|
||||
static final class AssertThatIterableHasOneComparableElementEqualTo<
|
||||
S extends Comparable<? super S>, T extends S> {
|
||||
@@ -564,173 +558,8 @@ final class AssertJTemplates {
|
||||
// Map
|
||||
//
|
||||
|
||||
static final class AssertThatMapIsEmpty<K, V> {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("unchecked")
|
||||
void before(AbstractMapAssert<?, ?, K, V> mapAssert) {
|
||||
Refaster.anyOf(
|
||||
mapAssert.containsExactlyEntriesOf(
|
||||
Refaster.anyOf(
|
||||
ImmutableMap.of(),
|
||||
ImmutableBiMap.of(),
|
||||
ImmutableSortedMap.of(),
|
||||
new HashMap<>(),
|
||||
new LinkedHashMap<>(),
|
||||
new TreeMap<>())),
|
||||
mapAssert.hasSameSizeAs(
|
||||
Refaster.anyOf(
|
||||
ImmutableMap.of(),
|
||||
ImmutableBiMap.of(),
|
||||
ImmutableSortedMap.of(),
|
||||
new HashMap<>(),
|
||||
new LinkedHashMap<>(),
|
||||
new TreeMap<>())),
|
||||
mapAssert.isEqualTo(
|
||||
Refaster.anyOf(
|
||||
ImmutableMap.of(),
|
||||
ImmutableBiMap.of(),
|
||||
ImmutableSortedMap.of(),
|
||||
new HashMap<>(),
|
||||
new LinkedHashMap<>(),
|
||||
new TreeMap<>())),
|
||||
mapAssert.containsOnlyKeys(
|
||||
Refaster.anyOf(
|
||||
ImmutableList.of(),
|
||||
new ArrayList<>(),
|
||||
ImmutableSet.of(),
|
||||
new HashSet<>(),
|
||||
new LinkedHashSet<>(),
|
||||
ImmutableSortedSet.of(),
|
||||
new TreeSet<>(),
|
||||
ImmutableMultiset.of(),
|
||||
ImmutableSortedMultiset.of())),
|
||||
mapAssert.containsExactly(),
|
||||
mapAssert.containsOnly(),
|
||||
mapAssert.containsOnlyKeys());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
void after(AbstractMapAssert<?, ?, K, V> mapAssert) {
|
||||
mapAssert.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Find a better name.
|
||||
static final class AssertThatMapIsEmpty2<K, V> {
|
||||
@BeforeTemplate
|
||||
void before(Map<K, V> map) {
|
||||
Refaster.anyOf(
|
||||
assertThat(map).hasSize(0),
|
||||
assertThat(map.isEmpty()).isTrue(),
|
||||
assertThat(map.size()).isEqualTo(0L),
|
||||
assertThat(map.size()).isNotPositive());
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
void before2(Map<K, V> map) {
|
||||
assertThat(Refaster.anyOf(map.keySet(), map.values())).isEmpty();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
void after(Map<K, V> map) {
|
||||
assertThat(map).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapIsNotEmpty<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractMapAssert<?, ?, K, V> before(AbstractMapAssert<?, ?, K, V> mapAssert) {
|
||||
return mapAssert.isNotEqualTo(
|
||||
Refaster.anyOf(
|
||||
ImmutableMap.of(),
|
||||
ImmutableBiMap.of(),
|
||||
ImmutableSortedMap.of(),
|
||||
new HashMap<>(),
|
||||
new LinkedHashMap<>(),
|
||||
new TreeMap<>()));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert) {
|
||||
return mapAssert.isNotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Find a better name.
|
||||
static final class AssertThatMapIsNotEmpty2<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractAssert<?, ?> before(Map<K, V> map) {
|
||||
return Refaster.anyOf(
|
||||
assertThat(map.isEmpty()).isFalse(),
|
||||
assertThat(map.size()).isNotEqualTo(0),
|
||||
assertThat(map.size()).isPositive(),
|
||||
assertThat(Refaster.anyOf(map.keySet(), map.values())).isNotEmpty());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map) {
|
||||
return assertThat(map).isNotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapHasSize<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractAssert<?, ?> before(Map<K, V> map, int length) {
|
||||
return Refaster.anyOf(
|
||||
assertThat(map.size()).isEqualTo(length),
|
||||
assertThat(Refaster.anyOf(map.keySet(), map.values())).hasSize(length));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, int length) {
|
||||
return assertThat(map).hasSize(length);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapsHaveSameSize<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractAssert<?, ?> before(Map<K, V> map1, Map<K, V> map2) {
|
||||
return assertThat(map1)
|
||||
.hasSize(Refaster.anyOf(map2.size(), map2.keySet().size(), map2.values().size()));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map1, Map<K, V> map2) {
|
||||
return assertThat(map1).hasSameSizeAs(map2);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Should also add a rule (elsewhere) to simplify `map.keySet().contains(key)`.
|
||||
static final class AssertThatMapContainsKey<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(Map<K, V> map, K key) {
|
||||
return assertThat(map.containsKey(key)).isTrue();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, K key) {
|
||||
return assertThat(map).containsKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapDoesNotContainKey<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(Map<K, V> map, K key) {
|
||||
return assertThat(map.containsKey(key)).isFalse();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, K key) {
|
||||
return assertThat(map).doesNotContainKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: To match in all cases there'll need to be a `@BeforeTemplate` variant for each
|
||||
// `assertThat` overload. Consider defining a `BugChecker` instead.
|
||||
static final class AssertThatMapContainsEntry<K, V> {
|
||||
@BeforeTemplate
|
||||
ObjectAssert<?> before(Map<K, V> map, K key, V value) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
import static org.assertj.core.data.Percentage.withPercentage;
|
||||
@@ -7,9 +7,11 @@ import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import org.assertj.core.api.AbstractShortAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
final class AssertJShortTemplates {
|
||||
private AssertJShortTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJShortRules {
|
||||
private AssertJShortRules() {}
|
||||
|
||||
static final class AbstractShortAssertIsEqualTo {
|
||||
@BeforeTemplate
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -8,9 +8,11 @@ import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
import org.assertj.core.api.AbstractStringAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
final class AssertJStringTemplates {
|
||||
private AssertJStringTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJStringRules {
|
||||
private AssertJStringRules() {}
|
||||
|
||||
static final class AbstractStringAssertStringIsEmpty {
|
||||
@BeforeTemplate
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -16,18 +16,20 @@ import java.io.IOException;
|
||||
import org.assertj.core.api.AbstractObjectAssert;
|
||||
import org.assertj.core.api.AbstractThrowableAssert;
|
||||
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/**
|
||||
* Refaster templates related to AssertJ assertions over expressions that may throw a {@link
|
||||
* Throwable} subtype.
|
||||
* Refaster rules related to AssertJ assertions over expressions that may throw a {@link Throwable}
|
||||
* subtype.
|
||||
*
|
||||
* <p>For reasons of consistency we prefer {@link
|
||||
* org.assertj.core.api.Assertions#assertThatThrownBy} over static methods for specific exception
|
||||
* types. Note that only the most common assertion expressions are rewritten here; covering all
|
||||
* cases would require the implementation of an Error Prone check instead.
|
||||
*/
|
||||
final class AssertJThrowingCallableTemplates {
|
||||
private AssertJThrowingCallableTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJThrowingCallableRules {
|
||||
private AssertJThrowingCallableRules() {}
|
||||
|
||||
static final class AssertThatThrownByIllegalArgumentException {
|
||||
@BeforeTemplate
|
||||
@@ -429,8 +431,8 @@ final class AssertJThrowingCallableTemplates {
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Drop this template in favour of a generic Error Prone check that flags
|
||||
// `String.format(...)` arguments to a wide range of format methods.
|
||||
// XXX: Drop this rule in favour of a generic Error Prone check that flags `String.format(...)`
|
||||
// arguments to a wide range of format methods.
|
||||
static final class AbstractThrowableAssertHasMessage {
|
||||
@BeforeTemplate
|
||||
AbstractThrowableAssert<?, ? extends Throwable> before(
|
||||
@@ -449,8 +451,8 @@ final class AssertJThrowingCallableTemplates {
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Drop this template in favour of a generic Error Prone check that flags
|
||||
// `String.format(...)` arguments to a wide range of format methods.
|
||||
// XXX: Drop this rule in favour of a generic Error Prone check that flags `String.format(...)`
|
||||
// arguments to a wide range of format methods.
|
||||
static final class AbstractThrowableAssertWithFailMessage {
|
||||
@BeforeTemplate
|
||||
AbstractThrowableAssert<?, ? extends Throwable> before(
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkElementIndex;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
@@ -19,22 +19,21 @@ import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.Nullable;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/**
|
||||
* Assorted Refaster templates that do not (yet) belong in one of the other classes with more
|
||||
* topical Refaster templates.
|
||||
* Assorted Refaster rules that do not (yet) belong in one of the other classes with more topical
|
||||
* Refaster rules.
|
||||
*/
|
||||
final class AssortedTemplates {
|
||||
private AssortedTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssortedRules {
|
||||
private AssortedRules() {}
|
||||
|
||||
/** Prefer {@link Objects#checkIndex(int, int)} over the Guava alternative. */
|
||||
static final class CheckIndex {
|
||||
@@ -44,36 +43,30 @@ final class AssortedTemplates {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
int after(int index, int size) {
|
||||
return checkIndex(index, size);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: We could add a rule for `new EnumMap(Map<K, ? extends V> m)`, but that constructor does
|
||||
// not allow an empty non-EnumMap to be provided.
|
||||
static final class CreateEnumMap<K extends Enum<K>, V> {
|
||||
/**
|
||||
* Prefer {@link Objects#checkIndex(int, int)} over less descriptive or more verbose alternatives.
|
||||
*
|
||||
* <p>If a custom error message is desired, consider using Guava's {@link
|
||||
* com.google.common.base.Preconditions#checkElementIndex(int, int, String)}.
|
||||
*/
|
||||
static final class CheckIndexConditional {
|
||||
@BeforeTemplate
|
||||
Map<K, V> before() {
|
||||
return new HashMap<>();
|
||||
void before(int index, int size) {
|
||||
if (index < 0 || index >= size) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Map<K, V> after() {
|
||||
return new EnumMap<>(Refaster.<K>clazz());
|
||||
}
|
||||
}
|
||||
|
||||
static final class MapGetOrNull<K, V, L> {
|
||||
@BeforeTemplate
|
||||
@Nullable
|
||||
V before(Map<K, V> map, L key) {
|
||||
return map.getOrDefault(key, null);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@Nullable
|
||||
V after(Map<K, V> map, L key) {
|
||||
return map.get(key);
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
void after(int index, int size) {
|
||||
checkIndex(index, size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +110,7 @@ final class AssortedTemplates {
|
||||
}
|
||||
|
||||
/** Don't unnecessarily repeat boolean expressions. */
|
||||
// XXX: This template captures only the simplest case. `@AlsoNegation` doesn't help. Consider
|
||||
// XXX: This rule captures only the simplest case. `@AlsoNegation` doesn't help. Consider
|
||||
// contributing a Refaster patch, which handles the negation in the `@BeforeTemplate` more
|
||||
// intelligently.
|
||||
static final class LogicalImplication {
|
||||
@@ -172,8 +165,8 @@ final class AssortedTemplates {
|
||||
* Collections#disjoint(Collection, Collection)}.
|
||||
*/
|
||||
// XXX: Other copy operations could be elided too, but these are most common after application of
|
||||
// the `DisjointSets` template defined above. If we ever introduce a generic "makes a copy"
|
||||
// stand-in, use it here.
|
||||
// the `DisjointSets` rule defined above. If we ever introduce a generic "makes a copy" stand-in,
|
||||
// use it here.
|
||||
static final class DisjointCollections<T> {
|
||||
@BeforeTemplate
|
||||
boolean before(Collection<T> collection1, Collection<T> collection2) {
|
||||
@@ -203,32 +196,6 @@ final class AssortedTemplates {
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't unnecessarily use {@link Map#entrySet()}. */
|
||||
static final class MapKeyStream<K, V> {
|
||||
@BeforeTemplate
|
||||
Stream<K> before(Map<K, V> map) {
|
||||
return map.entrySet().stream().map(Map.Entry::getKey);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Stream<K> after(Map<K, V> map) {
|
||||
return map.keySet().stream();
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't unnecessarily use {@link Map#entrySet()}. */
|
||||
static final class MapValueStream<K, V> {
|
||||
@BeforeTemplate
|
||||
Stream<V> before(Map<K, V> map) {
|
||||
return map.entrySet().stream().map(Map.Entry::getValue);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Stream<V> after(Map<K, V> map) {
|
||||
return map.values().stream();
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Splitter#splitToStream(CharSequence)} over less efficient alternatives. */
|
||||
static final class SplitToStream {
|
||||
@BeforeTemplate
|
||||
@@ -1,13 +1,15 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
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.math.BigDecimal;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/** Refaster templates related to expressions dealing with {@link BigDecimal}s. */
|
||||
final class BigDecimalTemplates {
|
||||
private BigDecimalTemplates() {}
|
||||
/** 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 {
|
||||
@@ -48,17 +50,18 @@ final class BigDecimalTemplates {
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link BigDecimal#valueOf(long)} over the associated constructor. */
|
||||
// XXX: Ideally we'd also rewrite `BigDecimal.valueOf("<some-integer-value>")`, but it doesn't
|
||||
// appear that's currently possible with Error Prone.
|
||||
static final class BigDecimalFactoryMethod {
|
||||
/** 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
|
||||
BigDecimal before(long value) {
|
||||
BigDecimal before(double value) {
|
||||
return new BigDecimal(value);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
BigDecimal after(long value) {
|
||||
BigDecimal after(double value) {
|
||||
return BigDecimal.valueOf(value);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -17,14 +17,17 @@ import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.IntFunction;
|
||||
import java.util.stream.Stream;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/** Refaster templates related to expressions dealing with (arbitrary) collections. */
|
||||
/** Refaster rules related to expressions dealing with (arbitrary) collections. */
|
||||
// XXX: There are other Guava `Iterables` methods that should not be called if the input is known to
|
||||
// be a `Collection`. Add those here.
|
||||
final class CollectionTemplates {
|
||||
private CollectionTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class CollectionRules {
|
||||
private CollectionRules() {}
|
||||
|
||||
/**
|
||||
* Prefer {@link Collection#isEmpty()} over alternatives that consult the collection's size or are
|
||||
@@ -402,6 +405,19 @@ final class CollectionTemplates {
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Collection#forEach(Consumer)} over more contrived alternatives. */
|
||||
static final class CollectionForEach<T> {
|
||||
@BeforeTemplate
|
||||
void before(Collection<T> collection, Consumer<? super T> consumer) {
|
||||
collection.stream().forEach(consumer);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
void after(Collection<T> collection, Consumer<? super T> consumer) {
|
||||
collection.forEach(consumer);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: collection.stream().noneMatch(e -> e.equals(other))
|
||||
// ^ This is !collection.contains(other). Do we already rewrite variations on this?
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refastertemplates;
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static java.util.Comparator.comparing;
|
||||
@@ -15,18 +15,23 @@ import com.google.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.Repeated;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.function.BinaryOperator;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.ToDoubleFunction;
|
||||
import java.util.function.ToIntFunction;
|
||||
import java.util.function.ToLongFunction;
|
||||
import java.util.stream.Stream;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/** Refaster templates related to expressions dealing with {@link Comparator}s. */
|
||||
final class ComparatorTemplates {
|
||||
private ComparatorTemplates() {}
|
||||
/** Refaster rules related to expressions dealing with {@link Comparator}s. */
|
||||
@OnlineDocumentation
|
||||
final class ComparatorRules {
|
||||
private ComparatorRules() {}
|
||||
|
||||
/** Prefer {@link Comparator#naturalOrder()} over more complicated constructs. */
|
||||
static final class NaturalOrder<T extends Comparable<? super T>> {
|
||||
@@ -34,7 +39,10 @@ final class ComparatorTemplates {
|
||||
@BeforeTemplate
|
||||
Comparator<T> before() {
|
||||
return Refaster.anyOf(
|
||||
comparing(Refaster.anyOf(identity(), v -> v)), Comparator.<T>reverseOrder().reversed());
|
||||
T::compareTo,
|
||||
comparing(Refaster.anyOf(identity(), v -> v)),
|
||||
Collections.<T>reverseOrder(reverseOrder()),
|
||||
Comparator.<T>reverseOrder().reversed());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -48,11 +56,15 @@ final class ComparatorTemplates {
|
||||
static final class ReverseOrder<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
Comparator<T> before() {
|
||||
return Comparator.<T>naturalOrder().reversed();
|
||||
return Refaster.anyOf(
|
||||
Collections.reverseOrder(),
|
||||
Collections.<T>reverseOrder(naturalOrder()),
|
||||
Comparator.<T>naturalOrder().reversed());
|
||||
}
|
||||
|
||||
// XXX: Add `@UseImportPolicy(STATIC_IMPORT_ALWAYS)` if/when
|
||||
// https://github.com/google/error-prone/pull/3584 is merged and released.
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
Comparator<T> after() {
|
||||
return reverseOrder();
|
||||
}
|
||||
@@ -186,15 +198,54 @@ final class ComparatorTemplates {
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Comparable#compareTo(Object)}} over more verbose alternatives. */
|
||||
static final class CompareTo<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
int before(T value1, T value2) {
|
||||
return Refaster.anyOf(
|
||||
Comparator.<T>naturalOrder().compare(value1, value2),
|
||||
Comparator.<T>reverseOrder().compare(value2, value1));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
int after(T value1, T value2) {
|
||||
return value1.compareTo(value2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid unnecessary creation of a {@link Stream} to determine the minimum of a known collection
|
||||
* of values.
|
||||
*/
|
||||
static final class MinOfVarargs<T> {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("StreamOfArray" /* In practice individual values are provided. */)
|
||||
T before(@Repeated T value, Comparator<T> cmp) {
|
||||
return Stream.of(Refaster.asVarargs(value)).min(cmp).orElseThrow();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
T after(@Repeated T value, Comparator<T> cmp) {
|
||||
return Collections.min(Arrays.asList(value), cmp);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Comparators#min(Comparable, Comparable)}} over more verbose alternatives. */
|
||||
static final class MinOfPairNaturalOrder<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
T before(T value1, T value2) {
|
||||
return Collections.min(
|
||||
Refaster.anyOf(
|
||||
Arrays.asList(value1, value2),
|
||||
ImmutableList.of(value1, value2),
|
||||
ImmutableSet.of(value1, value2)));
|
||||
return Refaster.anyOf(
|
||||
value1.compareTo(value2) <= 0 ? value1 : value2,
|
||||
value1.compareTo(value2) > 0 ? value2 : value1,
|
||||
value2.compareTo(value1) < 0 ? value2 : value1,
|
||||
value2.compareTo(value1) >= 0 ? value1 : value2,
|
||||
Comparators.min(value1, value2, naturalOrder()),
|
||||
Comparators.max(value1, value2, reverseOrder()),
|
||||
Collections.min(
|
||||
Refaster.anyOf(
|
||||
Arrays.asList(value1, value2),
|
||||
ImmutableList.of(value1, value2),
|
||||
ImmutableSet.of(value1, value2))));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -209,12 +260,17 @@ final class ComparatorTemplates {
|
||||
static final class MinOfPairCustomOrder<T> {
|
||||
@BeforeTemplate
|
||||
T before(T value1, T value2, Comparator<T> cmp) {
|
||||
return Collections.min(
|
||||
Refaster.anyOf(
|
||||
Arrays.asList(value1, value2),
|
||||
ImmutableList.of(value1, value2),
|
||||
ImmutableSet.of(value1, value2)),
|
||||
cmp);
|
||||
return Refaster.anyOf(
|
||||
cmp.compare(value1, value2) <= 0 ? value1 : value2,
|
||||
cmp.compare(value1, value2) > 0 ? value2 : value1,
|
||||
cmp.compare(value2, value1) < 0 ? value2 : value1,
|
||||
cmp.compare(value2, value1) >= 0 ? value1 : value2,
|
||||
Collections.min(
|
||||
Refaster.anyOf(
|
||||
Arrays.asList(value1, value2),
|
||||
ImmutableList.of(value1, value2),
|
||||
ImmutableSet.of(value1, value2)),
|
||||
cmp));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -223,15 +279,39 @@ final class ComparatorTemplates {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid unnecessary creation of a {@link Stream} to determine the maximum of a known collection
|
||||
* of values.
|
||||
*/
|
||||
static final class MaxOfVarargs<T> {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("StreamOfArray" /* In practice individual values are provided. */)
|
||||
T before(@Repeated T value, Comparator<T> cmp) {
|
||||
return Stream.of(Refaster.asVarargs(value)).max(cmp).orElseThrow();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
T after(@Repeated T value, Comparator<T> cmp) {
|
||||
return Collections.max(Arrays.asList(value), cmp);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Comparators#max(Comparable, Comparable)}} over more verbose alternatives. */
|
||||
static final class MaxOfPairNaturalOrder<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
T before(T value1, T value2) {
|
||||
return Collections.max(
|
||||
Refaster.anyOf(
|
||||
Arrays.asList(value1, value2),
|
||||
ImmutableList.of(value1, value2),
|
||||
ImmutableSet.of(value1, value2)));
|
||||
return Refaster.anyOf(
|
||||
value1.compareTo(value2) >= 0 ? value1 : value2,
|
||||
value1.compareTo(value2) < 0 ? value2 : value1,
|
||||
value2.compareTo(value1) > 0 ? value2 : value1,
|
||||
value2.compareTo(value1) <= 0 ? value1 : value2,
|
||||
Comparators.max(value1, value2, naturalOrder()),
|
||||
Comparators.min(value1, value2, reverseOrder()),
|
||||
Collections.max(
|
||||
Refaster.anyOf(
|
||||
Arrays.asList(value1, value2),
|
||||
ImmutableList.of(value1, value2),
|
||||
ImmutableSet.of(value1, value2))));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -246,12 +326,17 @@ final class ComparatorTemplates {
|
||||
static final class MaxOfPairCustomOrder<T> {
|
||||
@BeforeTemplate
|
||||
T before(T value1, T value2, Comparator<T> cmp) {
|
||||
return Collections.max(
|
||||
Refaster.anyOf(
|
||||
Arrays.asList(value1, value2),
|
||||
ImmutableList.of(value1, value2),
|
||||
ImmutableSet.of(value1, value2)),
|
||||
cmp);
|
||||
return Refaster.anyOf(
|
||||
cmp.compare(value1, value2) >= 0 ? value1 : value2,
|
||||
cmp.compare(value1, value2) < 0 ? value2 : value1,
|
||||
cmp.compare(value2, value1) > 0 ? value2 : value1,
|
||||
cmp.compare(value2, value1) <= 0 ? value1 : value2,
|
||||
Collections.max(
|
||||
Refaster.anyOf(
|
||||
Arrays.asList(value1, value2),
|
||||
ImmutableList.of(value1, value2),
|
||||
ImmutableSet.of(value1, value2)),
|
||||
cmp));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -259,4 +344,36 @@ final class ComparatorTemplates {
|
||||
return Comparators.max(value1, value2, cmp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer a method reference to {@link Comparators#min(Comparable, Comparable)} over calling
|
||||
* {@link BinaryOperator#minBy(Comparator)} with {@link Comparator#naturalOrder()}.
|
||||
*/
|
||||
static final class ComparatorsMin<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
BinaryOperator<T> before() {
|
||||
return BinaryOperator.minBy(naturalOrder());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
BinaryOperator<T> after() {
|
||||
return Comparators::min;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer a method reference to {@link Comparators#max(Comparable, Comparable)} over calling
|
||||
* {@link BinaryOperator#minBy(Comparator)} with {@link Comparator#naturalOrder()}.
|
||||
*/
|
||||
static final class ComparatorsMax<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
BinaryOperator<T> before() {
|
||||
return BinaryOperator.maxBy(naturalOrder());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
BinaryOperator<T> after() {
|
||||
return Comparators::max;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user