mirror of
https://github.com/jlengrand/error-prone-support.git
synced 2026-03-10 08:11:25 +00:00
Compare commits
948 Commits
rossendrij
...
sschroever
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf80180103 | ||
|
|
194a828c67 | ||
|
|
1f83eada44 | ||
|
|
34b57b76bc | ||
|
|
cd3c2aab5d | ||
|
|
b39e322a67 | ||
|
|
ad9d2dd534 | ||
|
|
d3307645cb | ||
|
|
2185a0397a | ||
|
|
c4a9f6fab7 | ||
|
|
98b6b7ec0c | ||
|
|
cc6211d560 | ||
|
|
8855ba33a0 | ||
|
|
e87b02cfe3 | ||
|
|
21a16e8803 | ||
|
|
d3cc77ed93 | ||
|
|
c2365c01c3 | ||
|
|
1d0d1d6cae | ||
|
|
cce897ed4a | ||
|
|
28bb4f6895 | ||
|
|
1fe67677b4 | ||
|
|
433b8b90c0 | ||
|
|
1f50772433 | ||
|
|
382b79989c | ||
|
|
1cc792c615 | ||
|
|
b5ace6e044 | ||
|
|
1f71ccccf7 | ||
|
|
57fa6ae2b8 | ||
|
|
da3ec2ce90 | ||
|
|
b8abceab73 | ||
|
|
c594a16c7c | ||
|
|
2c95de3879 | ||
|
|
048167b021 | ||
|
|
bf06625211 | ||
|
|
9a2a1915eb | ||
|
|
a01e5e4cf1 | ||
|
|
6195ede1f5 | ||
|
|
03ba14b229 | ||
|
|
c771d5f6b9 | ||
|
|
0ba806432d | ||
|
|
f66e513042 | ||
|
|
0a3537669a | ||
|
|
c96fdf5ed2 | ||
|
|
a4ab37d2a9 | ||
|
|
07fe6df3af | ||
|
|
79ac13809f | ||
|
|
f7f561bc9e | ||
|
|
90066f87d1 | ||
|
|
32d5c114c1 | ||
|
|
e3d94a9ac4 | ||
|
|
e086be4fea | ||
|
|
5f80fb5370 | ||
|
|
ba27fc588d | ||
|
|
000c33c85f | ||
|
|
a926e5534c | ||
|
|
e65e2ce730 | ||
|
|
d4be298022 | ||
|
|
4f6d32191e | ||
|
|
aa592e5e16 | ||
|
|
a183f921c9 | ||
|
|
7d4dc16d6f | ||
|
|
33ebcac257 | ||
|
|
4849bb5ae2 | ||
|
|
a73624da1d | ||
|
|
7ef4537ff4 | ||
|
|
7e0e1216ec | ||
|
|
aa1cfd9071 | ||
|
|
0aa612073f | ||
|
|
9e6d35569f | ||
|
|
dc65917ef1 | ||
|
|
f12474e8e6 | ||
|
|
8f152b1135 | ||
|
|
b88fc8e542 | ||
|
|
8f2ac01ac8 | ||
|
|
51317fbace | ||
|
|
e48492628e | ||
|
|
b2552e6feb | ||
|
|
0c2180b151 | ||
|
|
b404737433 | ||
|
|
54bba95ffa | ||
|
|
6222bcb0d4 | ||
|
|
641bb5c566 | ||
|
|
09317abb18 | ||
|
|
dff67fecbc | ||
|
|
d126336742 | ||
|
|
b8eabff9bc | ||
|
|
0b04e0fb3f | ||
|
|
14506ed392 | ||
|
|
664adb4aa4 | ||
|
|
f35d62061d | ||
|
|
a89f986763 | ||
|
|
6ad94b5c29 | ||
|
|
63fe19b487 | ||
|
|
fbd9b0689c | ||
|
|
c7a288cf29 | ||
|
|
931632d90b | ||
|
|
71de432645 | ||
|
|
97a4cb0227 | ||
|
|
6b1cb4c2e9 | ||
|
|
e0cb7eb1f9 | ||
|
|
a2f44f82f2 | ||
|
|
0ec8ebe8ab | ||
|
|
e1be5d23e9 | ||
|
|
12f2feaacf | ||
|
|
423a306719 | ||
|
|
b5327f2e97 | ||
|
|
059e1dbbe7 | ||
|
|
cef7f3409f | ||
|
|
82bcb405c3 | ||
|
|
0440df8911 | ||
|
|
25eaf11171 | ||
|
|
3578a8cbec | ||
|
|
4e1158d4df | ||
|
|
0109632b70 | ||
|
|
1650b6b00d | ||
|
|
967a446909 | ||
|
|
4d30329448 | ||
|
|
66967fb903 | ||
|
|
e6854a9147 | ||
|
|
1ca0f536c4 | ||
|
|
d569156a6b | ||
|
|
e7c3d39059 | ||
|
|
9d66379486 | ||
|
|
d01002de06 | ||
|
|
0c857b3d90 | ||
|
|
cc36aa993c | ||
|
|
379bbf3f83 | ||
|
|
a0b1f7091e | ||
|
|
626246bcc0 | ||
|
|
22272d6059 | ||
|
|
ff3be8ae3f | ||
|
|
7c2078b771 | ||
|
|
7529b99251 | ||
|
|
a966c1a17a | ||
|
|
cc3e30f253 | ||
|
|
b73b05a5c4 | ||
|
|
74de2535f5 | ||
|
|
f0ff5c3fd6 | ||
|
|
738a6706fd | ||
|
|
fd97ac3676 | ||
|
|
5b011312e1 | ||
|
|
ff3f963820 | ||
|
|
94edab3b4f | ||
|
|
276529fd34 | ||
|
|
f5022efe68 | ||
|
|
49e7313e1c | ||
|
|
b3462b0a1e | ||
|
|
a5b71410ae | ||
|
|
6b73f03b22 | ||
|
|
5d120252ac | ||
|
|
351b886e6c | ||
|
|
ad5dd92b9a | ||
|
|
e9a5295e80 | ||
|
|
7ae9356c9e | ||
|
|
5618de49f4 | ||
|
|
17c7b396d2 | ||
|
|
eafb73814a | ||
|
|
7793006b5e | ||
|
|
7733ceaaec | ||
|
|
53e0a0cb41 | ||
|
|
1d25b78c4c | ||
|
|
b70da48d70 | ||
|
|
01fd608aac | ||
|
|
dd0369a645 | ||
|
|
74b1104890 | ||
|
|
a230bbff12 | ||
|
|
100305ed1f | ||
|
|
96c836e6a9 | ||
|
|
97acfbc3bc | ||
|
|
9ce7c6b40b | ||
|
|
57a43a1c85 | ||
|
|
89c4969a31 | ||
|
|
dbd4853e3e | ||
|
|
62a7dacf4d | ||
|
|
d23684ee6d | ||
|
|
fd150af6c6 | ||
|
|
a1a865b87a | ||
|
|
64000ebb07 | ||
|
|
6122d7a39f | ||
|
|
ca01bbb74a | ||
|
|
4ce9f92489 | ||
|
|
e8d14993a1 | ||
|
|
88860800b1 | ||
|
|
e76dd5c31b | ||
|
|
701db7d61e | ||
|
|
8781a9ede5 | ||
|
|
dc14c4e970 | ||
|
|
a435b657ae | ||
|
|
00edc5ea1f | ||
|
|
7383a11da7 | ||
|
|
e4e17664d0 | ||
|
|
5972a8e225 | ||
|
|
01b30d767b | ||
|
|
c2426d3a8d | ||
|
|
8130ddf59c | ||
|
|
c3b950f114 | ||
|
|
fa026de336 | ||
|
|
2ff3095fca | ||
|
|
2b2d9f498e | ||
|
|
2a5dd7d56d | ||
|
|
dd021b3757 | ||
|
|
c77cc9a35d | ||
|
|
7a7a09d208 | ||
|
|
62077dacbb | ||
|
|
3a76f91d18 | ||
|
|
82c0dd95ce | ||
|
|
a424a3e949 | ||
|
|
3f530475ab | ||
|
|
24621b7c20 | ||
|
|
746e757e16 | ||
|
|
df42013d5b | ||
|
|
0336626dc9 | ||
|
|
4e2ceeb252 | ||
|
|
05809b1c85 | ||
|
|
75679396df | ||
|
|
f1882caf88 | ||
|
|
605d045760 | ||
|
|
b492a4afd3 | ||
|
|
583e51c9b0 | ||
|
|
9ada0783dd | ||
|
|
7ed3e63d2e | ||
|
|
465f83b855 | ||
|
|
24f8b2f499 | ||
|
|
f466c75bf3 | ||
|
|
c48db5202c | ||
|
|
629cb577ea | ||
|
|
b13d445e56 | ||
|
|
b1683a31e9 | ||
|
|
ffe4db2cf8 | ||
|
|
da5eea8329 | ||
|
|
5596b584ac | ||
|
|
4800522a09 | ||
|
|
9a77a3f35f | ||
|
|
ef75b80e7b | ||
|
|
7c618f7e2d | ||
|
|
c03ead9e5d | ||
|
|
8fd87a5b6b | ||
|
|
82025a729f | ||
|
|
7d912d1b04 | ||
|
|
a726514e98 | ||
|
|
aa7de5777c | ||
|
|
6e6efb3c13 | ||
|
|
03fd050eae | ||
|
|
d1467e0424 | ||
|
|
a2e32ed147 | ||
|
|
258708e6a8 | ||
|
|
67106a9725 | ||
|
|
f979576ab5 | ||
|
|
aa3fd03569 | ||
|
|
5602251667 | ||
|
|
05cb6387b9 | ||
|
|
4e242a5b11 | ||
|
|
87297c9a1d | ||
|
|
15ff85c4ea | ||
|
|
2c8469fa93 | ||
|
|
f4e0fd2cc3 | ||
|
|
34fd692064 | ||
|
|
26bc7e6e8e | ||
|
|
56cb86a7e1 | ||
|
|
19cd802ec5 | ||
|
|
1fabb26ed2 | ||
|
|
c1ced1e962 | ||
|
|
030ed25be8 | ||
|
|
ee0c04a44e | ||
|
|
87f79dae60 | ||
|
|
1f4a68416f | ||
|
|
c98fe9273d | ||
|
|
8b5d1803e4 | ||
|
|
507a19d10e | ||
|
|
43fcbb770d | ||
|
|
7779631320 | ||
|
|
48e2f57cb6 | ||
|
|
352829c61f | ||
|
|
37d95605b0 | ||
|
|
6131599d9d | ||
|
|
9c158f11d4 | ||
|
|
be818486bd | ||
|
|
1e80097bee | ||
|
|
431c3e67ac | ||
|
|
e612631cb8 | ||
|
|
a6bce960cb | ||
|
|
e19c2de99d | ||
|
|
7b193f2fd5 | ||
|
|
852442b55b | ||
|
|
d758fab524 | ||
|
|
88ed3ea9a9 | ||
|
|
7fb0e551d4 | ||
|
|
4af7b21bf5 | ||
|
|
fc5f3bd4cc | ||
|
|
35a5944b81 | ||
|
|
17cc8e0c31 | ||
|
|
b5ba4fa208 | ||
|
|
26b941bbc5 | ||
|
|
eb3b540537 | ||
|
|
6a11b59517 | ||
|
|
62abd51f4d | ||
|
|
67a128a9b6 | ||
|
|
526227e8ed | ||
|
|
c2e837819e | ||
|
|
fd70be081e | ||
|
|
2c47244571 | ||
|
|
8375f1650f | ||
|
|
c4daa04ce1 | ||
|
|
08f0e52104 | ||
|
|
10070bd4c8 | ||
|
|
3c38fd3495 | ||
|
|
d54ec83844 | ||
|
|
e03d0de366 | ||
|
|
b40c5d61b9 | ||
|
|
6d27e46baf | ||
|
|
8f48497322 | ||
|
|
38554be3d1 | ||
|
|
28cdb91913 | ||
|
|
bd6693d23e | ||
|
|
a3ec76845a | ||
|
|
e3b310761d | ||
|
|
774052f13a | ||
|
|
bad7684639 | ||
|
|
dd72b5a08a | ||
|
|
3eb30c5f93 | ||
|
|
2cc0539f8f | ||
|
|
9210c30a0a | ||
|
|
ba362ed460 | ||
|
|
ab33adfab8 | ||
|
|
006665ee6d | ||
|
|
08c49b9a58 | ||
|
|
777aa970f1 | ||
|
|
a040310870 | ||
|
|
d6ab366f31 | ||
|
|
d9fefa6824 | ||
|
|
c8b3a6df82 | ||
|
|
782a790def | ||
|
|
706b5f401f | ||
|
|
9f500fd5bb | ||
|
|
f541cd2e52 | ||
|
|
fc862aad94 | ||
|
|
42810bc00c | ||
|
|
f17d736356 | ||
|
|
705acc9164 | ||
|
|
bb969a8b80 | ||
|
|
c5a700c555 | ||
|
|
2837a04433 | ||
|
|
74222e8fa5 | ||
|
|
dc36ff2c0b | ||
|
|
09208aa49a | ||
|
|
b81ec973a1 | ||
|
|
8fb57b5bab | ||
|
|
f4aaa5852c | ||
|
|
2148b7ede4 | ||
|
|
b2320779e7 | ||
|
|
ef0d65d360 | ||
|
|
d29fde8856 | ||
|
|
524c7efb48 | ||
|
|
a62acfd7b5 | ||
|
|
c40e1d6691 | ||
|
|
57a22bf9de | ||
|
|
ec982fe011 | ||
|
|
1860e24e65 | ||
|
|
cce248c306 | ||
|
|
96aca8ea2b | ||
|
|
70d2bf9016 | ||
|
|
cee3c58d07 | ||
|
|
c141ebe05d | ||
|
|
f5a8c412af | ||
|
|
93440826ed | ||
|
|
80dcae319e | ||
|
|
7371d03db8 | ||
|
|
c6b98e61ff | ||
|
|
ce8f9f60c8 | ||
|
|
cdf27acd9c | ||
|
|
49e5fd1273 | ||
|
|
5085db25c0 | ||
|
|
125d24bc13 | ||
|
|
ea02144bff | ||
|
|
cc2c49edc3 | ||
|
|
8bc878a05c | ||
|
|
b399ef8910 | ||
|
|
7dba641a79 | ||
|
|
da1528129f | ||
|
|
d0bbc5c14b | ||
|
|
da532c79c7 | ||
|
|
04e2900a48 | ||
|
|
f097095398 | ||
|
|
c53a3f64b6 | ||
|
|
cdfcecc204 | ||
|
|
4f4b3fb865 | ||
|
|
cce36d24df | ||
|
|
3bbae43da8 | ||
|
|
3217a6974d | ||
|
|
7b71e4ea3e | ||
|
|
3df6dc957d | ||
|
|
a1ecd816ff | ||
|
|
03af05889a | ||
|
|
f52a93cc4e | ||
|
|
610085393c | ||
|
|
08e55fdfb6 | ||
|
|
137ec4c573 | ||
|
|
7cf569cca3 | ||
|
|
d53db2981c | ||
|
|
454e8662b1 | ||
|
|
c2f217f055 | ||
|
|
45ced8b9d8 | ||
|
|
666fe0d49c | ||
|
|
e50a7e1795 | ||
|
|
f403b988d5 | ||
|
|
3ee28f2c05 | ||
|
|
bb122388f5 | ||
|
|
760b1ddf31 | ||
|
|
6229fa9245 | ||
|
|
7d728e956e | ||
|
|
52245c6310 | ||
|
|
4b69fe9de9 | ||
|
|
3405962703 | ||
|
|
7d3d6a3cf8 | ||
|
|
97fa90b64e | ||
|
|
ced1ce625d | ||
|
|
de224deffa | ||
|
|
deebd21d34 | ||
|
|
ab84ef4c12 | ||
|
|
f675cf0139 | ||
|
|
e7e35c7571 | ||
|
|
f3c5aee7f5 | ||
|
|
b344d4640c | ||
|
|
393aebca9f | ||
|
|
32d50ab6fe | ||
|
|
c807568b9c | ||
|
|
4dd2aa12cc | ||
|
|
2f2e7e7a35 | ||
|
|
e0c795d248 | ||
|
|
554a3e634c | ||
|
|
7a3ae7c646 | ||
|
|
6d24540d01 | ||
|
|
7637ffee24 | ||
|
|
12d2b52e38 | ||
|
|
53daabe5df | ||
|
|
ee0884e65f | ||
|
|
3af81d8b10 | ||
|
|
ebd64c1077 | ||
|
|
929f1dd1c7 | ||
|
|
9ddd91a50e | ||
|
|
a1227ca710 | ||
|
|
44e0904357 | ||
|
|
9b54c73dc0 | ||
|
|
8ace5b7e9a | ||
|
|
94ffc5d495 | ||
|
|
977019c5bf | ||
|
|
6514236514 | ||
|
|
6d23fbdd35 | ||
|
|
b6f14c073a | ||
|
|
ae22e0ec5e | ||
|
|
6e6f8d9f7b | ||
|
|
68d0bed36c | ||
|
|
2edbc85c28 | ||
|
|
0fefb6985e | ||
|
|
64f9d6b7a2 | ||
|
|
b0f99e7c0b | ||
|
|
320c4175c9 | ||
|
|
e9829d93bf | ||
|
|
b273502e88 | ||
|
|
8c6bd1b6e7 | ||
|
|
0c1817c589 | ||
|
|
73cf28e7ff | ||
|
|
8a0abf5957 | ||
|
|
5fb4aed3ad | ||
|
|
aef9c5da7a | ||
|
|
7069e7a6d8 | ||
|
|
334c374ca1 | ||
|
|
57cd084f82 | ||
|
|
0b3be1b75b | ||
|
|
902538fd4a | ||
|
|
50f6b770e4 | ||
|
|
47e0a779bd | ||
|
|
973d3c3cd9 | ||
|
|
edb7290e2e | ||
|
|
d5c45e003f | ||
|
|
f784c64150 | ||
|
|
978c90db9d | ||
|
|
ae89a37934 | ||
|
|
8f1d1df747 | ||
|
|
04368e9243 | ||
|
|
156df71616 | ||
|
|
64b1c7eea4 | ||
|
|
80d0d85826 | ||
|
|
d30c99a28f | ||
|
|
29c23542da | ||
|
|
62c1c277ae | ||
|
|
8580e89008 | ||
|
|
06c8b164e9 | ||
|
|
fd9d3157bc | ||
|
|
a623f73c1c | ||
|
|
f9d0cd99d6 | ||
|
|
9bec3de372 | ||
|
|
4164514c5b | ||
|
|
c3cd535b16 | ||
|
|
64195279cc | ||
|
|
61c9f67f66 | ||
|
|
4bb14b01ec | ||
|
|
b267b4dba8 | ||
|
|
03f0e0493b | ||
|
|
2111c81784 | ||
|
|
43d50f2ef9 | ||
|
|
2d972fd975 | ||
|
|
ee265a87ae | ||
|
|
6b4fba62da | ||
|
|
e883e28e34 | ||
|
|
d84de6efba | ||
|
|
4dca61a144 | ||
|
|
dc9597a603 | ||
|
|
ec9853ac88 | ||
|
|
5bb1dd1a10 | ||
|
|
fd6a45ebd8 | ||
|
|
82d4677509 | ||
|
|
1fdf1016b7 | ||
|
|
80e537fce2 | ||
|
|
d85897ea62 | ||
|
|
c5bde3999d | ||
|
|
575d494303 | ||
|
|
844ef84d55 | ||
|
|
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 | ||
|
|
fb6fe5a96e | ||
|
|
e37da2a1ed | ||
|
|
7bef1c8e67 | ||
|
|
0160eafca0 | ||
|
|
891fecd297 | ||
|
|
7f18bd9030 | ||
|
|
1473a70de8 | ||
|
|
c0c3ce2644 | ||
|
|
100d5c86f7 | ||
|
|
e12f99975b | ||
|
|
791113669f | ||
|
|
564bc7e1d1 | ||
|
|
43bcbeaa98 | ||
|
|
d682b7d41f | ||
|
|
72a124a20e | ||
|
|
326f3328a7 | ||
|
|
ae7068f464 | ||
|
|
066931fe53 | ||
|
|
3874ca9be2 | ||
|
|
84ba3946d9 | ||
|
|
b675ff680f | ||
|
|
8e7d04a24c | ||
|
|
bfc951b61f | ||
|
|
b30562bbd8 | ||
|
|
9be85204ae | ||
|
|
6fbf1b0cb2 | ||
|
|
5a428e6e29 | ||
|
|
71163c0061 | ||
|
|
880be0dbdd | ||
|
|
62fe10f2cd | ||
|
|
f7ec28368a | ||
|
|
e34c2baf7c | ||
|
|
184ba8af51 | ||
|
|
63c3aa8259 | ||
|
|
7daabeee48 | ||
|
|
0acfd8a723 | ||
|
|
000fcefe92 | ||
|
|
1e3ad0fe32 | ||
|
|
b88a668819 | ||
|
|
4c8e125dcb | ||
|
|
4ab5dc4f32 | ||
|
|
7c667334cc | ||
|
|
b4b2afd130 | ||
|
|
4445c93f6e | ||
|
|
5a7d7ff89b | ||
|
|
7ef75e8f07 | ||
|
|
a1f2418805 | ||
|
|
39bfcc75ca | ||
|
|
753cdce29e | ||
|
|
36654883e5 | ||
|
|
d5372934ec | ||
|
|
f810530599 | ||
|
|
6928381403 | ||
|
|
5657a48552 | ||
|
|
50aaf77a9e | ||
|
|
b8e22ffef0 | ||
|
|
17035a1623 | ||
|
|
e7dacd19d7 | ||
|
|
e64af1dde0 | ||
|
|
a58630bccf | ||
|
|
9ab5bbe042 | ||
|
|
4ca75c6cf6 | ||
|
|
7883b31eb6 | ||
|
|
ef751ce785 | ||
|
|
130c3d0bc3 | ||
|
|
c89e3905bf | ||
|
|
21421ce753 | ||
|
|
c39d1251d2 | ||
|
|
9bc732b4fe | ||
|
|
74100b6c41 | ||
|
|
624f2ce753 | ||
|
|
967017eed9 | ||
|
|
b06945b833 | ||
|
|
ef562c1644 | ||
|
|
efbde936dc | ||
|
|
c57653dd5b | ||
|
|
12b03e95b1 | ||
|
|
c2f24ac739 | ||
|
|
4f5ea8beac | ||
|
|
21646ffcb1 | ||
|
|
c58dceb9df | ||
|
|
90ef2f4042 | ||
|
|
459a498d6c | ||
|
|
78035644dc | ||
|
|
ef67d41512 | ||
|
|
4cecff923a | ||
|
|
38a57db994 | ||
|
|
336557cf8e | ||
|
|
9055dfff19 | ||
|
|
3712a15195 | ||
|
|
9d487e4a88 | ||
|
|
b2b086761c | ||
|
|
ff64247b6d | ||
|
|
bc7443c72d | ||
|
|
abf4d68fba | ||
|
|
5c5f7d849e | ||
|
|
65c4694936 | ||
|
|
a45291c7d8 | ||
|
|
71012f31ab | ||
|
|
6e0905c033 | ||
|
|
af5ac85428 | ||
|
|
0329c25f78 | ||
|
|
9e67e2b795 | ||
|
|
4bafea05f4 | ||
|
|
8ce9cab2dd | ||
|
|
ae30625524 | ||
|
|
dc0046ebfc | ||
|
|
ad6d774818 | ||
|
|
405f5874ac | ||
|
|
bf5199ea3d | ||
|
|
c500516bb4 | ||
|
|
85d68a4f34 | ||
|
|
46467951dd | ||
|
|
6f7ce2067f | ||
|
|
f30ba36d18 | ||
|
|
65736ce83f | ||
|
|
17d5805d5a | ||
|
|
ce01b62832 | ||
|
|
4ee555c62b | ||
|
|
a24bbbe99d | ||
|
|
679e83bc48 | ||
|
|
0bdc171613 | ||
|
|
9b05ec62c6 | ||
|
|
72866183f5 | ||
|
|
9d7f569be5 | ||
|
|
f72adca292 | ||
|
|
5148143ae5 | ||
|
|
3d7fbbf7a2 | ||
|
|
8b6864d8a0 | ||
|
|
a03017b5e6 | ||
|
|
a227436f19 | ||
|
|
3f6558b7c0 | ||
|
|
ce06396521 | ||
|
|
268766a32a | ||
|
|
5366effd74 | ||
|
|
0a63361500 | ||
|
|
a074e2fdd6 | ||
|
|
f06d4e41cf | ||
|
|
cf7bb657fa | ||
|
|
8277b43955 | ||
|
|
691f2c8311 | ||
|
|
362518b0f4 | ||
|
|
b9a3840e25 | ||
|
|
14d8bddbec | ||
|
|
6c7a0b81ea | ||
|
|
3a3825f7ba | ||
|
|
31e54cc990 | ||
|
|
775a2688ca | ||
|
|
3d854a0cc5 | ||
|
|
6ee013da58 | ||
|
|
962d18dcb5 | ||
|
|
aab308a190 | ||
|
|
009bd5d0d7 | ||
|
|
8e57f64e31 | ||
|
|
53e81f3611 | ||
|
|
8745105251 | ||
|
|
3eb3c20b1d | ||
|
|
121618f277 | ||
|
|
5cf4194168 | ||
|
|
c434cd318c | ||
|
|
22aa9cb213 | ||
|
|
3edc483e7c | ||
|
|
fff5d7b329 | ||
|
|
4ca80952ab | ||
|
|
8cecb8bf30 | ||
|
|
a937bf0ddf | ||
|
|
0b71c2b576 | ||
|
|
239d38d69f | ||
|
|
ab57aa5eb6 | ||
|
|
49267337cb | ||
|
|
c201fe1fd2 | ||
|
|
fb20b6f93d | ||
|
|
9264c25b57 | ||
|
|
ca628eef6c | ||
|
|
62168dcd4b | ||
|
|
96a82eaf85 | ||
|
|
0c615b4c15 | ||
|
|
6304130ae3 | ||
|
|
1674e99ba4 | ||
|
|
75bbc8e5b5 | ||
|
|
fe3ed2be63 | ||
|
|
f36350f3ba | ||
|
|
4888ad3aaf | ||
|
|
9daf73f148 | ||
|
|
b08ca514c1 | ||
|
|
4eed7dd0e8 | ||
|
|
618c62e7a5 | ||
|
|
b46f075ddf | ||
|
|
64436ff9b6 | ||
|
|
e028077f1e | ||
|
|
5aa386b5ba | ||
|
|
a480fe908d | ||
|
|
b2316c744c | ||
|
|
9334babfe8 | ||
|
|
b6632d393b | ||
|
|
53d191ff4f | ||
|
|
b8ddf3ac20 | ||
|
|
e859c2774f | ||
|
|
1cb4ce97ae | ||
|
|
92fe96286f | ||
|
|
d7531abceb | ||
|
|
cc7074a62f | ||
|
|
91922454b6 | ||
|
|
eaa98c985c | ||
|
|
94908d6a37 | ||
|
|
e8d9221424 | ||
|
|
fea8f5b2c0 | ||
|
|
2199accedc | ||
|
|
83670aeddf | ||
|
|
e8977bea67 | ||
|
|
0d051064bb | ||
|
|
06913b6a5b | ||
|
|
d2bbee3ed9 |
51
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
51
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: 🐛 Bug report
|
||||
about: Create a report to help us improve.
|
||||
title: ""
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
### Describe the bug
|
||||
|
||||
<!-- Provide a clear and concise description of what the bug or issue is. -->
|
||||
|
||||
- [ ] I have verified that the issue is reproducible against the latest version
|
||||
of the project.
|
||||
- [ ] I have searched through existing issues to verify that this issue is not
|
||||
already known.
|
||||
|
||||
### Minimal Reproducible Example
|
||||
|
||||
<!-- Provide a clear and concise description of what happened. Please include
|
||||
steps on how to reproduce the issue. -->
|
||||
|
||||
```java
|
||||
If applicable and possible, please replace this section with the code that
|
||||
triggered the isue.
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Logs</summary>
|
||||
|
||||
```sh
|
||||
Please replace this sentence with log output, if applicable.
|
||||
```
|
||||
</details>
|
||||
|
||||
### Expected behavior
|
||||
|
||||
<!-- Provide a clear and concise description of what you expected to happen. -->
|
||||
|
||||
### Setup
|
||||
|
||||
<!-- Please complete the following information: -->
|
||||
|
||||
- Operating system (e.g. MacOS Monterey).
|
||||
- Java version (i.e. `java --version`, e.g. `17.0.10`).
|
||||
- Error Prone version (e.g. `2.25.0`).
|
||||
- Error Prone Support version (e.g. `0.15.0`).
|
||||
|
||||
### Additional context
|
||||
|
||||
<!-- Provide any other context about the problem here. -->
|
||||
58
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
58
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: 🚀 Request a new feature
|
||||
about: Suggest a new feature.
|
||||
title: ""
|
||||
labels: new feature
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
### Problem
|
||||
|
||||
<!-- Here, describe the context of the problem that you're facing, and which
|
||||
you'd like to be solved through Error Prone Support. -->
|
||||
|
||||
### Description of the proposed new feature
|
||||
|
||||
<!-- Please indicate the type of improvement. -->
|
||||
|
||||
- [ ] Support a stylistic preference.
|
||||
- [ ] Avoid a common gotcha, or potential problem.
|
||||
- [ ] Improve performance.
|
||||
|
||||
<!--
|
||||
Here, provide a clear and concise description of the desired change.
|
||||
|
||||
If possible, provide a simple and minimal example using the following format:
|
||||
-->
|
||||
|
||||
I would like to rewrite the following code:
|
||||
```java
|
||||
// XXX: Write the code to match here.
|
||||
```
|
||||
|
||||
to:
|
||||
```java
|
||||
// XXX: Write the desired code here.
|
||||
```
|
||||
|
||||
### Considerations
|
||||
|
||||
<!--
|
||||
Here, mention any other aspects to consider. Relevant questions:
|
||||
|
||||
- If applicable, is the rewrite operation a clear improvement in all cases?
|
||||
- Are there special cases to consider?
|
||||
- Can we further generalize the proposed solution?
|
||||
- Are there alternative solutions we should consider?
|
||||
-->
|
||||
|
||||
### Participation
|
||||
|
||||
<!-- Pull requests are very welcome, and we happily review contributions. Are
|
||||
you up for the challenge? :D -->
|
||||
|
||||
- [ ] I am willing to submit a pull request to implement this improvement.
|
||||
|
||||
### Additional context
|
||||
|
||||
<!-- Provide any other context about the request here. -->
|
||||
29
.github/release.yml
vendored
Normal file
29
.github/release.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- "ignore-changelog"
|
||||
categories:
|
||||
- title: ":rocket: New Error Prone checks and Refaster rules"
|
||||
labels:
|
||||
- "new feature"
|
||||
- title: ":sparkles: Improvements"
|
||||
labels:
|
||||
- "improvement"
|
||||
- title: ":warning: Update considerations and deprecations"
|
||||
labels:
|
||||
- "breaking change"
|
||||
- "deprecation"
|
||||
- title: ":bug: Bug fixes"
|
||||
labels:
|
||||
- "bug"
|
||||
- "bug fix"
|
||||
- title: ":books: Documentation, test and build improvements"
|
||||
labels:
|
||||
- "documentation"
|
||||
- "chore"
|
||||
- title: ":chart_with_upwards_trend: Dependency upgrades"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- title: "Other changes"
|
||||
labels:
|
||||
- "*"
|
||||
48
.github/workflows/build.yml
vendored
Normal file
48
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Build and verify
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master ]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-22.04 ]
|
||||
jdk: [ 17.0.10, 21.0.2 ]
|
||||
distribution: [ temurin ]
|
||||
experimental: [ false ]
|
||||
include:
|
||||
- os: macos-14
|
||||
jdk: 17.0.10
|
||||
distribution: temurin
|
||||
experimental: false
|
||||
- os: windows-2022
|
||||
jdk: 17.0.10
|
||||
distribution: temurin
|
||||
experimental: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
steps:
|
||||
# We run the build twice for each supported JDK: once against the
|
||||
# original Error Prone release, using only Error Prone checks available
|
||||
# on Maven Central, and once against the Picnic Error Prone fork,
|
||||
# additionally enabling all checks defined in this project and any Error
|
||||
# Prone checks available only from other artifact repositories.
|
||||
- name: Check out code and set up JDK and Maven
|
||||
uses: s4u/setup-maven-action@6d44c18d67d9e1549907b8815efa5e4dada1801b # v1.12.0
|
||||
with:
|
||||
java-version: ${{ matrix.jdk }}
|
||||
java-distribution: ${{ matrix.distribution }}
|
||||
maven-version: 3.9.6
|
||||
- name: Display build environment details
|
||||
run: mvn --version
|
||||
- name: Build project against vanilla Error Prone, compile Javadoc
|
||||
run: mvn -T1C install javadoc:jar
|
||||
- name: Build project with self-check against Error Prone fork
|
||||
run: mvn -T1C clean verify -Perror-prone-fork -Pnon-maven-central -Pself-check -s settings.xml
|
||||
- name: Remove installed project artifacts
|
||||
run: mvn build-helper:remove-project-artifact
|
||||
|
||||
# XXX: Enable Codecov once we "go public".
|
||||
40
.github/workflows/codeql.yml
vendored
Normal file
40
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
# Analyzes the code using GitHub's default CodeQL query database.
|
||||
# Identified issues are registered with GitHub's code scanning dashboard. When
|
||||
# a pull request is analyzed, any offending lines are annotated. See
|
||||
# https://codeql.github.com for details.
|
||||
name: CodeQL analysis
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '0 4 * * 1'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
analyze:
|
||||
strategy:
|
||||
matrix:
|
||||
language: [ java, ruby ]
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code and set up JDK and Maven
|
||||
uses: s4u/setup-maven-action@6d44c18d67d9e1549907b8815efa5e4dada1801b # v1.12.0
|
||||
with:
|
||||
java-version: 17.0.10
|
||||
java-distribution: temurin
|
||||
maven-version: 3.9.6
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Perform minimal build
|
||||
if: matrix.language == 'java'
|
||||
run: mvn -T1C clean package -DskipTests -Dverification.skip
|
||||
- name: Perform CodeQL analysis
|
||||
uses: github/codeql-action/analyze@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2
|
||||
with:
|
||||
category: /language:${{ matrix.language }}
|
||||
51
.github/workflows/deploy-website.yml
vendored
Normal file
51
.github/workflows/deploy-website.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Update `error-prone.picnic.tech` website content
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master, website ]
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ruby/setup-ruby@bd03e04863f52d169e18a2b190e8fa6b84938215 # v1.170.0
|
||||
with:
|
||||
working-directory: ./website
|
||||
bundler-cache: true
|
||||
- name: Configure Github Pages
|
||||
uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d # v4.0.0
|
||||
- name: Generate documentation
|
||||
run: ./generate-docs.sh
|
||||
- name: Build website with Jekyll
|
||||
working-directory: ./website
|
||||
run: bundle exec jekyll build
|
||||
- name: Validate HTML output
|
||||
working-directory: ./website
|
||||
# XXX: Drop `--disable_external true` once we fully adopted the
|
||||
# "Refaster rules" terminology on our website and in the code.
|
||||
run: bundle exec htmlproofer --disable_external true --check-external-hash false ./_site
|
||||
- name: Upload website as artifact
|
||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
|
||||
with:
|
||||
path: ./website/_site
|
||||
deploy:
|
||||
if: github.ref == 'refs/heads/website'
|
||||
needs: build
|
||||
permissions:
|
||||
id-token: write
|
||||
pages: write
|
||||
runs-on: ubuntu-22.04
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@decdde0ac072f6dcbe43649d82d9c635fff5b4e4 # v4.0.4
|
||||
36
.github/workflows/openssf-scorecard.yml
vendored
Normal file
36
.github/workflows/openssf-scorecard.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# Analyzes the code base and GitHub project configuration for adherence to
|
||||
# security best practices for open source software. Identified issues are
|
||||
# registered with GitHub's code scanning dashboard. When a pull request is
|
||||
# analyzed, any offending lines are annotated. See
|
||||
# https://securityscorecards.dev for details.
|
||||
name: OpenSSF Scorecard update
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '0 4 * * 1'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
analyze:
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
id-token: write
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run OpenSSF Scorecard analysis
|
||||
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
publish_results: ${{ github.ref == 'refs/heads/master' }}
|
||||
- name: Update GitHub's code scanning dashboard
|
||||
uses: github/codeql-action/upload-sarif@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
34
.github/workflows/pitest-analyze-pr.yml
vendored
Normal file
34
.github/workflows/pitest-analyze-pr.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# 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 and set up JDK and Maven
|
||||
uses: s4u/setup-maven-action@6d44c18d67d9e1549907b8815efa5e4dada1801b # v1.12.0
|
||||
with:
|
||||
checkout-fetch-depth: 2
|
||||
java-version: 17.0.10
|
||||
java-distribution: temurin
|
||||
maven-version: 3.9.6
|
||||
- name: Run Pitest
|
||||
# By running with features `+GIT(from[HEAD~1]), +gitci`, Pitest only
|
||||
# analyzes lines changed in the associated pull request, as GitHub
|
||||
# exposes the changes unique to the PR as a single commit on top of the
|
||||
# target branch. See https://blog.pitest.org/pitest-pr-setup for
|
||||
# details.
|
||||
run: mvn test pitest:mutationCoverage -DargLine.xmx=2048m -Dverification.skip -Dfeatures="+GIT(from[HEAD~1]), +gitci"
|
||||
- name: Aggregate Pitest reports
|
||||
run: mvn pitest-git:aggregate -DkilledEmoji=":tada:" -DmutantEmoji=":zombie:" -DtrailingText="Mutation testing report by [Pitest](https://pitest.org/). Review any surviving mutants by inspecting the line comments under [_Files changed_](${{ github.event.number }}/files)."
|
||||
- name: Upload Pitest reports as artifact
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.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
|
||||
jobs:
|
||||
update-pr:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
permissions:
|
||||
actions: read
|
||||
checks: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code and set up JDK and Maven
|
||||
uses: s4u/setup-maven-action@6d44c18d67d9e1549907b8815efa5e4dada1801b # v1.12.0
|
||||
with:
|
||||
java-version: 17.0.10
|
||||
java-distribution: temurin
|
||||
maven-version: 3.9.6
|
||||
- name: Download Pitest analysis artifact
|
||||
uses: dawidd6/action-download-artifact@72aaadce3bc708349fc665eee3785cbb1b6e51d0 # v3.1.1
|
||||
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
|
||||
39
.github/workflows/run-integration-tests.yml
vendored
Normal file
39
.github/workflows/run-integration-tests.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
# If requested by means of a pull request comment, runs integration tests
|
||||
# against the project, using the code found on the pull request branch.
|
||||
# XXX: Generalize this to a matrix build of multiple integration tests,
|
||||
# possibly using multiple JDK or OS versions.
|
||||
# XXX: Investigate whether the comment can specify which integration tests run
|
||||
# run. See this example of a dynamic build matrix:
|
||||
# https://docs.github.com/en/actions/learn-github-actions/expressions#example-returning-a-json-object
|
||||
name: "Integration tests"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [ created ]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
run-integration-tests:
|
||||
name: On-demand integration test
|
||||
if: |
|
||||
github.event.issue.pull_request && contains(github.event.comment.body, '/integration-test')
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code and set up JDK and Maven
|
||||
uses: s4u/setup-maven-action@6d44c18d67d9e1549907b8815efa5e4dada1801b # v1.12.0
|
||||
with:
|
||||
checkout-ref: "refs/pull/${{ github.event.issue.number }}/head"
|
||||
java-version: 17.0.10
|
||||
java-distribution: temurin
|
||||
maven-version: 3.9.6
|
||||
- name: Install project to local Maven repository
|
||||
run: mvn -T1C install -DskipTests -Dverification.skip
|
||||
- name: Run integration test
|
||||
run: xvfb-run ./integration-tests/checkstyle.sh "${{ runner.temp }}/artifacts"
|
||||
- name: Upload artifacts on failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: integration-test-checkstyle
|
||||
path: "${{ runner.temp }}/artifacts"
|
||||
- name: Remove installed project artifacts
|
||||
run: mvn build-helper:remove-project-artifact
|
||||
35
.github/workflows/sonarcloud.yml
vendored
Normal file
35
.github/workflows/sonarcloud.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# Analyzes the code base using SonarCloud. See
|
||||
# https://sonarcloud.io/project/overview?id=PicnicSupermarket_error-prone-support.
|
||||
name: SonarCloud analysis
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '0 4 * * 1'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
analyze:
|
||||
# Analysis of code in forked repositories is skipped, as such workflow runs
|
||||
# do not have access to the requisite secrets.
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code and set up JDK and Maven
|
||||
uses: s4u/setup-maven-action@6d44c18d67d9e1549907b8815efa5e4dada1801b # v1.12.0
|
||||
with:
|
||||
checkout-fetch-depth: 0
|
||||
java-version: 17.0.10
|
||||
java-distribution: temurin
|
||||
maven-version: 3.9.6
|
||||
- name: Create missing `test` directory
|
||||
# XXX: Drop this step in favour of actually having a test.
|
||||
run: mkdir refaster-compiler/src/test
|
||||
- name: Perform SonarCloud analysis
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
run: mvn -T1C jacoco:prepare-agent verify jacoco:report sonar:sonar -Dverification.skip -Dsonar.projectKey=PicnicSupermarket_error-prone-support
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,3 +9,6 @@
|
||||
target
|
||||
*.iml
|
||||
*.swp
|
||||
|
||||
# The Git repositories checked out by the integration test framework.
|
||||
integration-tests/.repos
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
-XX:ReservedCodeCacheSize=512m
|
||||
-XX:SoftRefLRUPolicyMSPerMB=10
|
||||
-XX:+UseParallelGC
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
|
||||
@@ -6,5 +10,4 @@
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
|
||||
|
||||
3
.mvn/maven.config
Normal file
3
.mvn/maven.config
Normal file
@@ -0,0 +1,3 @@
|
||||
--batch-mode
|
||||
--errors
|
||||
--strict-checksums
|
||||
26
.renovaterc.json
Normal file
26
.renovaterc.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"helpers:pinGitHubActionDigests"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"^org\\.springframework:spring-framework-bom$",
|
||||
"^org\\.springframework\\.boot:spring-boot[a-z-]*$"
|
||||
],
|
||||
"separateMinorPatch": true
|
||||
},
|
||||
{
|
||||
"matchDepNames": [
|
||||
"github/codeql-action",
|
||||
"ruby/setup-ruby"
|
||||
],
|
||||
"schedule": "every 4 weeks on Monday"
|
||||
}
|
||||
],
|
||||
"reviewers": [
|
||||
"rickie",
|
||||
"Stephan202"
|
||||
]
|
||||
}
|
||||
38
.travis.yml
38
.travis.yml
@@ -1,38 +0,0 @@
|
||||
---
|
||||
dist: bionic
|
||||
language: java
|
||||
jdk: openjdk11
|
||||
addons:
|
||||
sonarcloud:
|
||||
organization: picnic-technologies
|
||||
token: "${SONARCLOUD_TOKEN}"
|
||||
install:
|
||||
- mvn io.takari:maven:wrapper
|
||||
script:
|
||||
# We run the build twice: once against the original Error Prone release,
|
||||
# using only Error Prone checks available on Maven Central, and once against
|
||||
# the Picnic Error Prone fork, additionally enabling all checks defined in
|
||||
# this project and any Error Prone checks available only from other artifact
|
||||
# repositories.
|
||||
- ./mvnw clean install
|
||||
- ./mvnw clean install -Perror-prone-fork -Pnon-maven-central -Pself-check -s settings.xml
|
||||
# XXX: Enable SonarCloud once we "go public".
|
||||
# ./mvnw jacoco:prepare-agent surefire:test jacoco:report sonar:sonar
|
||||
- ./mvnw jacoco:prepare-agent surefire:test jacoco:report
|
||||
before_cache:
|
||||
# Don't cache the artifacts we just generated, for multiple reasons: (1) we
|
||||
# shouldn't need them next time around and (2) if we do, that indicates a
|
||||
# dependency issue which might otherwise go unnoticed until next time we bump
|
||||
# the project's version (i.e., when tagging).
|
||||
- find "${HOME}/.m2/repository" -depth -name '*-SNAPSHOT' -exec rm -r '{}' \;
|
||||
cache:
|
||||
directories:
|
||||
# The local Maven repository in which third party dependencies are stored.
|
||||
- ${HOME}/.m2/repository
|
||||
# The Takari Maven Wrapper's storage for downloaded Maven distributions.
|
||||
- ${HOME}/.m2/wrapper
|
||||
# The SonarQube analysis cache.
|
||||
- ${HOME}/.sonar/cache
|
||||
# XXX: Enable Codecov once we "go public".
|
||||
#after_success:
|
||||
# - bash <(curl -s https://codecov.io/bash)
|
||||
85
CONTRIBUTING.md
Normal file
85
CONTRIBUTING.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for checking this document! This project is free software, and we
|
||||
(the maintainers) encourage and value any contribution.
|
||||
|
||||
Here are some guidelines to help you get started.
|
||||
|
||||
## 🐛 Reporting a bug
|
||||
|
||||
Like any non-trivial piece of software, this library is probably not bug-free.
|
||||
If you found a bug, feel free to [report the issue][error-prone-support-issues]
|
||||
on GitHub.
|
||||
|
||||
Before doing so, please:
|
||||
- Verify that the issue is reproducible against the latest version of the
|
||||
project.
|
||||
- Search through the existing set of issues to see whether the problem is
|
||||
already known. With some luck a solution is already in place, or a workaround
|
||||
may have been provided.
|
||||
|
||||
When filing a bug report, please include the following:
|
||||
- Any relevant information about your environment. This should generally
|
||||
include the output of `java --version`, as well as the version of Error Prone
|
||||
you're using.
|
||||
- A description of what is going on (e.g. logging output, stacktraces).
|
||||
- A minimum reproducible example, so that other developers can try to reproduce
|
||||
(and optionally fix) the bug.
|
||||
- Any additional information that may be relevant.
|
||||
|
||||
## 💡 Reporting an improvement
|
||||
|
||||
If you would like to see an improvement, you can file a [GitHub
|
||||
issue][error-prone-support-issues]. This is also a good idea when you're
|
||||
already working towards opening a pull request, as this allows for discussion
|
||||
around the idea.
|
||||
|
||||
## 🚀 Opening a pull request
|
||||
|
||||
All submissions, including submissions by project members, require approval by
|
||||
at least two reviewers. We use [GitHub pull
|
||||
requests][error-prone-support-pulls] for this purpose.
|
||||
|
||||
Before opening a pull request, please check whether there are any existing
|
||||
(open or closed) issues or pull requests addressing the same problem. This
|
||||
avoids double work or lots of time spent on a solution that may ultimately not
|
||||
be accepted. When in doubt, make sure to first raise an
|
||||
[issue][error-prone-support-issues] to discuss the idea.
|
||||
|
||||
To the extent possible, the pull request process guards our coding guidelines.
|
||||
Some pointers:
|
||||
- Try to make sure that the
|
||||
[`./run-full-build.sh`][error-prone-support-full-build] script completes
|
||||
successfully, ideally before opening a pull request. See the [development
|
||||
instructions][error-prone-support-developing] for details on how to
|
||||
efficiently resolve many of the errors and warnings that may be reported. (In
|
||||
particular, make sure to run `mvn fmt:format` and
|
||||
[`./apply-error-prone-suggestions.sh`][error-prone-support-patch].) That
|
||||
said, if you feel that the build fails for invalid or debatable reasons, or
|
||||
if you're unsure how to best resolve an issue, don't let that discourage you
|
||||
from opening a PR with a failing build; we can have a look at the issue
|
||||
together!
|
||||
- Checks should be _topical_: ideally they address a single concern.
|
||||
- Where possible checks should provide _fixes_, and ideally these are
|
||||
completely behavior-preserving. In order for a check to be adopted by users
|
||||
it must not "get in the way". So for a check that addresses a relatively
|
||||
trivial stylistic concern it is doubly important that the violations it
|
||||
detects can be auto-patched.
|
||||
- Make sure you have read Error Prone's [criteria for new
|
||||
checks][error-prone-criteria]. Most guidelines described there apply to this
|
||||
project as well, except that this project _does_ focus quite heavy on style
|
||||
enforcement. But that just makes the previous point doubly important.
|
||||
- Make sure that a check's [(mutation) test
|
||||
coverage][error-prone-support-mutation-tests] is or remains about as high as
|
||||
it can be. Not only does this lead to better tests, it also points out
|
||||
opportunities to simplify the code.
|
||||
- Please restrict the scope of a pull request to a single feature or fix. Don't
|
||||
sneak in unrelated changes; instead just open more than one pull request 😉.
|
||||
|
||||
[error-prone-criteria]: https://errorprone.info/docs/criteria
|
||||
[error-prone-support-developing]: https://github.com/PicnicSupermarket/error-prone-support/tree/master#-developing-error-prone-support
|
||||
[error-prone-support-full-build]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-full-build.sh
|
||||
[error-prone-support-issues]: https://github.com/PicnicSupermarket/error-prone-support/issues
|
||||
[error-prone-support-mutation-tests]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-branch-mutation-tests.sh
|
||||
[error-prone-support-patch]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/apply-error-prone-suggestions.sh
|
||||
[error-prone-support-pulls]: https://github.com/PicnicSupermarket/error-prone-support/pulls
|
||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2024 Picnic Technologies BV
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
325
README.md
Normal file
325
README.md
Normal file
@@ -0,0 +1,325 @@
|
||||
<div align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="website/assets/images/logo-dark.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="website/assets/images/logo.svg">
|
||||
<img alt="Error Prone Support logo" src="website/assets/images/logo.svg" width="50%">
|
||||
</picture>
|
||||
</div>
|
||||
|
||||
# Error Prone Support
|
||||
|
||||
Error Prone Support is a [Picnic][picnic-blog]-opinionated extension of
|
||||
Google's [Error Prone][error-prone-orig-repo]. It aims to improve code quality,
|
||||
focussing on maintainability, consistency and avoidance of common pitfalls.
|
||||
|
||||
> Error Prone is a static analysis tool for Java that catches common
|
||||
> programming mistakes at compile-time.
|
||||
|
||||
To learn more about Error Prone (Support), how you can start using Error Prone
|
||||
in practice, and how we use it at Picnic, watch the conference talk
|
||||
[_Automating away bugs with Error Prone in practice_][conference-talk]. Also
|
||||
consider checking out the blog post [_Picnic loves Error Prone: producing
|
||||
high-quality and consistent Java code_][picnic-blog-ep-post].
|
||||
|
||||
[![Maven Central][maven-central-badge]][maven-central-search]
|
||||
[![Reproducible Builds][reproducible-builds-badge]][reproducible-builds-report]
|
||||
[![OpenSSF Best Practices][openssf-best-practices-badge]][openssf-best-practices-checklist]
|
||||
[![OpenSSF Scorecard][openssf-scorecard-badge]][openssf-scorecard-report]
|
||||
[![CodeQL Analysis][codeql-badge]][codeql-master]
|
||||
[![GitHub Actions][github-actions-build-badge]][github-actions-build-master]
|
||||
[![Mutation tested with PIT][pitest-badge]][pitest]
|
||||
[![Quality Gate Status][sonarcloud-quality-badge]][sonarcloud-quality-master]
|
||||
[![Maintainability Rating][sonarcloud-maintainability-badge]][sonarcloud-maintainability-master]
|
||||
[![Reliability Rating][sonarcloud-reliability-badge]][sonarcloud-reliability-master]
|
||||
[![Security Rating][sonarcloud-security-badge]][sonarcloud-security-master]
|
||||
[![Coverage][sonarcloud-coverage-badge]][sonarcloud-coverage-master]
|
||||
[![Duplicated Lines (%)][sonarcloud-duplication-badge]][sonarcloud-duplication-master]
|
||||
[![Technical Debt][sonarcloud-technical-debt-badge]][sonarcloud-technical-debt-master]
|
||||
[![License][license-badge]][license]
|
||||
[![PRs Welcome][pr-badge]][contributing]
|
||||
|
||||
[Getting started](#-getting-started) •
|
||||
[Developing Error Prone Support](#-developing-error-prone-support) •
|
||||
[How it works](#-how-it-works) • [Contributing](#%EF%B8%8F-contributing)
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Getting started
|
||||
|
||||
### Installation
|
||||
|
||||
This library is built on top of [Error Prone][error-prone-orig-repo]. To use
|
||||
it, read the installation guide for Maven or Gradle below. The library requires
|
||||
that your build is executed using JDK 17 or above, but supports builds that
|
||||
[target][baeldung-java-source-target-options] older versions of Java.
|
||||
|
||||
#### Maven
|
||||
|
||||
1. First, follow Error Prone's [installation
|
||||
guide][error-prone-installation-guide].
|
||||
2. Next, edit your `pom.xml` file to add one or more Error Prone Support
|
||||
modules to the `annotationProcessorPaths` of the `maven-compiler-plugin`:
|
||||
|
||||
```xml
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<!-- Prefer using the latest release. -->
|
||||
<version>3.12.0</version>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<!-- Error Prone itself. -->
|
||||
<path>
|
||||
<groupId>com.google.errorprone</groupId>
|
||||
<artifactId>error_prone_core</artifactId>
|
||||
<version>${error-prone.version}</version>
|
||||
</path>
|
||||
<!-- Error Prone Support's additional bug checkers. -->
|
||||
<path>
|
||||
<groupId>tech.picnic.error-prone-support</groupId>
|
||||
<artifactId>error-prone-contrib</artifactId>
|
||||
<version>${error-prone-support.version}</version>
|
||||
</path>
|
||||
<!-- Error Prone Support's Refaster rules. -->
|
||||
<path>
|
||||
<groupId>tech.picnic.error-prone-support</groupId>
|
||||
<artifactId>refaster-runner</artifactId>
|
||||
<version>${error-prone-support.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
<compilerArgs>
|
||||
<arg>
|
||||
-Xplugin:ErrorProne
|
||||
<!-- Add other Error Prone flags here. See
|
||||
https://errorprone.info/docs/flags. -->
|
||||
</arg>
|
||||
<arg>-XDcompilePolicy=simple</arg>
|
||||
</compilerArgs>
|
||||
<!-- Enable this if you'd like to fail your build upon warnings. -->
|
||||
<!-- <failOnWarning>true</failOnWarning> -->
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
```
|
||||
|
||||
<!-- XXX: Reference `oss-parent`'s `pom.xml` once that project also uses Error
|
||||
Prone Support. Alternatively reference this project's `self-check` profile
|
||||
definition. -->
|
||||
|
||||
#### Gradle
|
||||
|
||||
1. First, follow the [installation
|
||||
guide][error-prone-gradle-installation-guide] of the
|
||||
`gradle-errorprone-plugin`.
|
||||
2. Next, edit your `build.gradle` file to add one or more Error Prone Support
|
||||
modules:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
// Error Prone itself.
|
||||
errorprone("com.google.errorprone:error_prone_core:${errorProneVersion}")
|
||||
// Error Prone Support's additional bug checkers.
|
||||
errorprone("tech.picnic.error-prone-support:error-prone-contrib:${errorProneSupportVersion}")
|
||||
// Error Prone Support's Refaster rules.
|
||||
errorprone("tech.picnic.error-prone-support:refaster-runner:${errorProneSupportVersion}")
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.errorprone.disableWarningsInGeneratedCode = true
|
||||
// Add other Error Prone flags here. See:
|
||||
// - https://github.com/tbroyer/gradle-errorprone-plugin#configuration
|
||||
// - https://errorprone.info/docs/flags
|
||||
}
|
||||
```
|
||||
|
||||
### Seeing it in action
|
||||
|
||||
Consider the following example code:
|
||||
|
||||
```java
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class Example {
|
||||
static BigDecimal getNumber() {
|
||||
return BigDecimal.valueOf(0);
|
||||
}
|
||||
|
||||
public ImmutableSet<Integer> getSet() {
|
||||
ImmutableSet<Integer> set = ImmutableSet.of(1);
|
||||
return ImmutableSet.copyOf(set);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If the [installation](#installation) was successful, then building the above
|
||||
code with Maven should yield two compiler warnings:
|
||||
|
||||
```sh
|
||||
$ mvn clean install
|
||||
...
|
||||
[INFO] Example.java:[9,34] [Refaster Rule] BigDecimalRules.BigDecimalZero: Refactoring opportunity
|
||||
(see https://error-prone.picnic.tech/refasterrules/BigDecimalRules#BigDecimalZero)
|
||||
Did you mean 'return BigDecimal.ZERO;'?
|
||||
...
|
||||
[WARNING] Example.java:[13,35] [IdentityConversion] This method invocation appears redundant; remove it or suppress this warning and add a comment explaining its purpose
|
||||
(see https://error-prone.picnic.tech/bugpatterns/IdentityConversion)
|
||||
Did you mean 'return set;' or '@SuppressWarnings("IdentityConversion") public ImmutableSet<Integer> getSet() {'?
|
||||
...
|
||||
```
|
||||
|
||||
Two things are kicking in here:
|
||||
|
||||
1. An Error Prone [`BugChecker`][error-prone-bugchecker] that flags unnecessary
|
||||
[identity conversions][bug-checks-identity-conversion].
|
||||
2. A [Refaster][refaster] rule capable of
|
||||
[rewriting][refaster-rules-bigdecimal] expressions of the form
|
||||
`BigDecimal.valueOf(0)` and `new BigDecimal(0)` to `BigDecimal.ZERO`.
|
||||
|
||||
Be sure to check out all [bug checks][bug-checks] and [refaster
|
||||
rules][refaster-rules].
|
||||
|
||||
## 👷 Developing Error Prone Support
|
||||
|
||||
This is a [Maven][maven] project, so running `mvn clean install` performs a
|
||||
full clean build and installs the library to your local Maven repository.
|
||||
|
||||
Once you've made changes, the build may fail due to a warning or error emitted
|
||||
by static code analysis. The flags and commands listed below allow you to
|
||||
suppress or (in a large subset of cases) automatically fix such cases. Make
|
||||
sure to carefully check the available options, as this can save you significant
|
||||
amounts of development time!
|
||||
|
||||
Relevant Maven build parameters:
|
||||
|
||||
- `-Dverification.warn` makes the warnings and errors emitted by various
|
||||
plugins and the Java compiler non-fatal, where possible.
|
||||
- `-Dverification.skip` disables various non-essential plugins and compiles the
|
||||
code with minimal checks (i.e. without linting, Error Prone checks, etc.).
|
||||
- `-Dversion.error-prone=some-version` runs the build using the specified
|
||||
version of Error Prone. This is useful e.g. when testing a locally built
|
||||
Error Prone SNAPSHOT.
|
||||
- `-Perror-prone-fork` runs the build using Picnic's [Error Prone
|
||||
fork][error-prone-fork-repo], hosted on [Jitpack][error-prone-fork-jitpack].
|
||||
This fork generally contains a few changes on top of the latest Error Prone
|
||||
release.
|
||||
- `-Pself-check` runs the checks defined by this project against itself.
|
||||
Pending a release of [google/error-prone#3301][error-prone-pull-3301], this
|
||||
flag must currently be used in combination with `-Perror-prone-fork`.
|
||||
|
||||
Other highly relevant commands:
|
||||
|
||||
- `mvn fmt:format` formats the code using
|
||||
[`google-java-format`][google-java-format].
|
||||
- [`./run-full-build.sh`][script-run-full-build] builds the project twice,
|
||||
where the second pass validates compatbility with Picnic's [Error Prone
|
||||
fork][error-prone-fork-repo] and compliance of the code with any rules
|
||||
defined within this project. (Consider running this before [opening a pull
|
||||
request][contributing-pull-request], as the PR checks also perform this
|
||||
validation.)
|
||||
- [`./apply-error-prone-suggestions.sh`][script-apply-error-prone-suggestions]
|
||||
applies Error Prone and Error Prone Support code suggestions to this project.
|
||||
Before running this command, make sure to have installed the project (`mvn
|
||||
clean install`) and make sure that the current working directory does not
|
||||
contain unstaged or uncommited changes.
|
||||
- [`./run-branch-mutation-tests.sh`][script-run-branch-mutation-tests] uses
|
||||
[Pitest][pitest] to run mutation tests against code that is modified relative
|
||||
to the upstream default branch. The results can be reviewed by opening the
|
||||
respective `target/pit-reports/index.html` files. One can use
|
||||
[`./run-mutation-tests.sh`][script-run-mutation-tests] to run mutation tests
|
||||
against _all_ code in the current working directory. For more information
|
||||
check the [PIT Maven plugin][pitest-maven].
|
||||
|
||||
When running the project's tests in IntelliJ IDEA, you might see the following
|
||||
error:
|
||||
|
||||
```
|
||||
java: exporting a package from system module jdk.compiler is not allowed with --release
|
||||
```
|
||||
|
||||
If this happens, go to _Settings -> Build, Execution, Deployment -> Compiler ->
|
||||
Java Compiler_ and deselect the option _Use '--release' option for
|
||||
cross-compilation (Java 9 and later)_. See [IDEA-288052][idea-288052] for
|
||||
details.
|
||||
|
||||
## 💡 How it works
|
||||
|
||||
This project provides additional [`BugChecker`][error-prone-bugchecker]
|
||||
implementations.
|
||||
|
||||
<!-- XXX: Extend this section. -->
|
||||
|
||||
## ✍️ Contributing
|
||||
|
||||
Want to report or fix a bug, suggest or add a new feature, or improve the
|
||||
documentation? That's awesome! Please read our [contribution
|
||||
guidelines][contributing].
|
||||
|
||||
### Security
|
||||
|
||||
If you want to report a security vulnerability, please do so through a private
|
||||
channel; please see our [security policy][security] for details.
|
||||
|
||||
[baeldung-java-source-target-options]: https://www.baeldung.com/java-source-target-options
|
||||
[bug-checks]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/
|
||||
[bug-checks-identity-conversion]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/IdentityConversion.java
|
||||
[codeql-badge]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/codeql.yml/badge.svg?branch=master&event=push
|
||||
[codeql-master]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/codeql.yml?query=branch:master+event:push
|
||||
[conference-talk]: https://www.youtube.com/watch?v=-47WD-3wKBs
|
||||
[contributing]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/CONTRIBUTING.md
|
||||
[contributing-pull-request]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/CONTRIBUTING.md#-opening-a-pull-request
|
||||
[error-prone-bugchecker]: https://github.com/google/error-prone/blob/master/check_api/src/main/java/com/google/errorprone/bugpatterns/BugChecker.java
|
||||
[error-prone-fork-jitpack]: https://jitpack.io/#PicnicSupermarket/error-prone
|
||||
[error-prone-fork-repo]: https://github.com/PicnicSupermarket/error-prone
|
||||
[error-prone-gradle-installation-guide]: https://github.com/tbroyer/gradle-errorprone-plugin
|
||||
[error-prone-installation-guide]: https://errorprone.info/docs/installation#maven
|
||||
[error-prone-orig-repo]: https://github.com/google/error-prone
|
||||
[error-prone-pull-3301]: https://github.com/google/error-prone/pull/3301
|
||||
[github-actions-build-badge]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/build.yml/badge.svg
|
||||
[github-actions-build-master]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/build.yml?query=branch:master&event=push
|
||||
[google-java-format]: https://github.com/google/google-java-format
|
||||
[idea-288052]: https://youtrack.jetbrains.com/issue/IDEA-288052
|
||||
[license-badge]: https://img.shields.io/github/license/PicnicSupermarket/error-prone-support
|
||||
[license]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/LICENSE.md
|
||||
[maven-central-badge]: https://img.shields.io/maven-central/v/tech.picnic.error-prone-support/error-prone-support?color=blue
|
||||
[maven-central-search]: https://search.maven.org/artifact/tech.picnic.error-prone-support/error-prone-support
|
||||
[maven]: https://maven.apache.org
|
||||
[openssf-best-practices-badge]: https://bestpractices.coreinfrastructure.org/projects/7199/badge
|
||||
[openssf-best-practices-checklist]: https://bestpractices.coreinfrastructure.org/projects/7199
|
||||
[openssf-scorecard-badge]: https://img.shields.io/ossf-scorecard/github.com/PicnicSupermarket/error-prone-support?label=openssf%20scorecard
|
||||
[openssf-scorecard-report]: https://securityscorecards.dev/viewer/?uri=github.com/PicnicSupermarket/error-prone-support
|
||||
[picnic-blog-ep-post]: https://blog.picnic.nl/picnic-loves-error-prone-producing-high-quality-and-consistent-java-code-b8a566be6886
|
||||
[picnic-blog]: https://blog.picnic.nl
|
||||
[pitest-badge]: https://img.shields.io/badge/-Mutation%20tested%20with%20PIT-blue.svg
|
||||
[pitest]: https://pitest.org
|
||||
[pitest-maven]: https://pitest.org/quickstart/maven
|
||||
[pr-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
|
||||
[refaster]: https://errorprone.info/docs/refaster
|
||||
[refaster-rules-bigdecimal]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/BigDecimalRules.java
|
||||
[refaster-rules]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/
|
||||
[reproducible-builds-badge]: https://img.shields.io/badge/Reproducible_Builds-ok-success?labelColor=1e5b96
|
||||
[reproducible-builds-report]: https://github.com/jvm-repo-rebuild/reproducible-central/blob/master/content/tech/picnic/error-prone-support/error-prone-support/README.md
|
||||
[script-apply-error-prone-suggestions]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/apply-error-prone-suggestions.sh
|
||||
[script-run-branch-mutation-tests]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-branch-mutation-tests.sh
|
||||
[script-run-full-build]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-full-build.sh
|
||||
[script-run-mutation-tests]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/run-mutation-tests.sh
|
||||
[security]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/SECURITY.md
|
||||
[sonarcloud-coverage-badge]: https://sonarcloud.io/api/project_badges/measure?project=PicnicSupermarket_error-prone-support&metric=coverage
|
||||
[sonarcloud-coverage-master]: https://sonarcloud.io/component_measures?id=PicnicSupermarket_error-prone-support&metric=coverage
|
||||
[sonarcloud-duplication-badge]: https://sonarcloud.io/api/project_badges/measure?project=PicnicSupermarket_error-prone-support&metric=duplicated_lines_density
|
||||
[sonarcloud-duplication-master]: https://sonarcloud.io/component_measures?id=PicnicSupermarket_error-prone-support&metric=duplicated_lines_density
|
||||
[sonarcloud-maintainability-badge]: https://sonarcloud.io/api/project_badges/measure?project=PicnicSupermarket_error-prone-support&metric=sqale_rating
|
||||
[sonarcloud-maintainability-master]: https://sonarcloud.io/component_measures?id=PicnicSupermarket_error-prone-support&metric=sqale_rating
|
||||
[sonarcloud-quality-badge]: https://sonarcloud.io/api/project_badges/measure?project=PicnicSupermarket_error-prone-support&metric=alert_status
|
||||
[sonarcloud-quality-master]: https://sonarcloud.io/summary/new_code?id=PicnicSupermarket_error-prone-support
|
||||
[sonarcloud-reliability-badge]: https://sonarcloud.io/api/project_badges/measure?project=PicnicSupermarket_error-prone-support&metric=reliability_rating
|
||||
[sonarcloud-reliability-master]: https://sonarcloud.io/component_measures?id=PicnicSupermarket_error-prone-support&metric=reliability_rating
|
||||
[sonarcloud-security-badge]: https://sonarcloud.io/api/project_badges/measure?project=PicnicSupermarket_error-prone-support&metric=security_rating
|
||||
[sonarcloud-security-master]: https://sonarcloud.io/component_measures?id=PicnicSupermarket_error-prone-support&metric=security_rating
|
||||
[sonarcloud-technical-debt-badge]: https://sonarcloud.io/api/project_badges/measure?project=PicnicSupermarket_error-prone-support&metric=sqale_index
|
||||
[sonarcloud-technical-debt-master]: https://sonarcloud.io/component_measures?id=PicnicSupermarket_error-prone-support&metric=sqale_index
|
||||
23
SECURITY.md
Normal file
23
SECURITY.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Security policy
|
||||
|
||||
We take security seriously. We are mindful of Error Prone Support's place in
|
||||
the software supply chain, and the risks and responsibilities that come with
|
||||
this.
|
||||
|
||||
## Supported versions
|
||||
|
||||
This project uses [semantic versioning][semantic-versioning]. In general, only
|
||||
the latest version of this software is supported. That said, if users have a
|
||||
compelling reason to ask for patch release of an older major release, then we
|
||||
will seriously consider such a request. We do urge users to stay up-to-date and
|
||||
use the latest release where feasible.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
To report a vulnerability, please visit the [security
|
||||
advisories][security-advisories] page and click _Report a vulnerability_. We
|
||||
will take such reports seriously and work with you to resolve the issue in a
|
||||
timely manner.
|
||||
|
||||
[security-advisories]: https://github.com/PicnicSupermarket/error-prone-support/security/advisories
|
||||
[semantic-versioning]: https://semver.org
|
||||
26
apply-error-prone-suggestions.sh
Executable file
26
apply-error-prone-suggestions.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Compiles the code using Error Prone and applies its suggestions. The set of
|
||||
# checks applied can optionally be restricted by name.
|
||||
#
|
||||
# As this script may modify the project's code, it is important to execute it
|
||||
# in a clean Git working directory.
|
||||
|
||||
set -e -u -o pipefail
|
||||
|
||||
if [ "${#}" -gt 1 ]; then
|
||||
echo "Usage: ${0} [PatchChecks]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
patchChecks=${1:-}
|
||||
|
||||
mvn clean test-compile fmt:format \
|
||||
-s "$(dirname "${0}")/settings.xml" \
|
||||
-T 1.0C \
|
||||
-Perror-prone \
|
||||
-Perror-prone-fork \
|
||||
-Ppatch \
|
||||
-Pself-check \
|
||||
-Derror-prone.patch-checks="${patchChecks}" \
|
||||
-Dverification.skip
|
||||
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=27/10/2025
|
||||
keyVersion=1
|
||||
signature=aQLVt0J//7nXDAlJuBe9ru7Dq6oApcLh17rxsgHoJqBkUCd2zvrtRxkcPm5PmF1I8gBDT9PHb+kTf6Kcfz+D1fT0wRrZv38ip56521AQPD6zjRSqak9O2Gisjo+QTPMD7oS009aSWKzQ1SMdtuZ+KIRvw4Gl+frtYzexqnGWEUQ\=
|
||||
packages=tech.picnic.errorprone.*
|
||||
type=OSSS
|
||||
98
documentation-support/pom.xml
Normal file
98
documentation-support/pom.xml
Normal file
@@ -0,0 +1,98 @@
|
||||
<?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.15.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>documentation-support</artifactId>
|
||||
|
||||
<name>Picnic :: Error Prone Support :: Documentation Support</name>
|
||||
<description>Data extraction support for the purpose of documentation generation.</description>
|
||||
<url>https://error-prone.picnic.tech</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_annotation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_check_api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_test_helpers</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.module</groupId>
|
||||
<artifactId>jackson-module-parameter-names</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto</groupId>
|
||||
<artifactId>auto-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.service</groupId>
|
||||
<artifactId>auto-service-annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.value</groupId>
|
||||
<artifactId>auto-value-annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jspecify</groupId>
|
||||
<artifactId>jspecify</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- XXX: Explicitly declared as a workaround for
|
||||
https://github.com/pitest/pitest-junit5-plugin/issues/105. -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-params</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,147 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.google.auto.common.AnnotationMirrors;
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.tools.javac.code.Attribute;
|
||||
import com.sun.tools.javac.code.Symbol.ClassSymbol;
|
||||
import java.net.URI;
|
||||
import java.util.Optional;
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
import tech.picnic.errorprone.documentation.BugPatternExtractor.BugPatternDocumentation;
|
||||
|
||||
/**
|
||||
* An {@link Extractor} that describes how to extract data from a {@code @BugPattern} annotation.
|
||||
*/
|
||||
@Immutable
|
||||
@AutoService(Extractor.class)
|
||||
@SuppressWarnings("rawtypes" /* See https://github.com/google/auto/issues/870. */)
|
||||
public final class BugPatternExtractor implements Extractor<BugPatternDocumentation> {
|
||||
/** Instantiates a new {@link BugPatternExtractor} instance. */
|
||||
public BugPatternExtractor() {}
|
||||
|
||||
@Override
|
||||
public String identifier() {
|
||||
return "bugpattern";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<BugPatternDocumentation> tryExtract(ClassTree tree, VisitorState state) {
|
||||
ClassSymbol symbol = ASTHelpers.getSymbol(tree);
|
||||
BugPattern annotation = symbol.getAnnotation(BugPattern.class);
|
||||
if (annotation == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(
|
||||
BugPatternDocumentation.create(
|
||||
state.getPath().getCompilationUnit().getSourceFile().toUri(),
|
||||
symbol.getQualifiedName().toString(),
|
||||
annotation.name().isEmpty() ? tree.getSimpleName().toString() : annotation.name(),
|
||||
ImmutableList.copyOf(annotation.altNames()),
|
||||
annotation.link(),
|
||||
ImmutableList.copyOf(annotation.tags()),
|
||||
annotation.summary(),
|
||||
annotation.explanation(),
|
||||
annotation.severity(),
|
||||
annotation.disableable(),
|
||||
annotation.documentSuppression()
|
||||
? getSuppressionAnnotations(tree)
|
||||
: ImmutableList.of()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fully-qualified class names of suppression annotations specified by the {@link
|
||||
* BugPattern} annotation located on the given tree.
|
||||
*
|
||||
* @implNote This method cannot simply invoke {@link BugPattern#suppressionAnnotations()}, as that
|
||||
* will yield an "Attempt to access Class objects for TypeMirrors" exception.
|
||||
*/
|
||||
private static ImmutableList<String> getSuppressionAnnotations(ClassTree tree) {
|
||||
AnnotationTree annotationTree =
|
||||
ASTHelpers.getAnnotationWithSimpleName(
|
||||
ASTHelpers.getAnnotations(tree), BugPattern.class.getSimpleName());
|
||||
requireNonNull(annotationTree, "BugPattern annotation must be present");
|
||||
|
||||
Attribute.Array types =
|
||||
doCast(
|
||||
AnnotationMirrors.getAnnotationValue(
|
||||
ASTHelpers.getAnnotationMirror(annotationTree), "suppressionAnnotations"),
|
||||
Attribute.Array.class);
|
||||
|
||||
return types.getValue().stream()
|
||||
.map(v -> doCast(v, Attribute.Class.class).classType.toString())
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T extends AnnotationValue> T doCast(AnnotationValue value, Class<T> target) {
|
||||
verify(target.isInstance(value), "Value '%s' is not of type '%s'", value, target);
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
@JsonDeserialize(as = AutoValue_BugPatternExtractor_BugPatternDocumentation.class)
|
||||
abstract static class BugPatternDocumentation {
|
||||
static BugPatternDocumentation create(
|
||||
URI source,
|
||||
String fullyQualifiedName,
|
||||
String name,
|
||||
ImmutableList<String> altNames,
|
||||
String link,
|
||||
ImmutableList<String> tags,
|
||||
String summary,
|
||||
String explanation,
|
||||
SeverityLevel severityLevel,
|
||||
boolean canDisable,
|
||||
ImmutableList<String> suppressionAnnotations) {
|
||||
return new AutoValue_BugPatternExtractor_BugPatternDocumentation(
|
||||
source,
|
||||
fullyQualifiedName,
|
||||
name,
|
||||
altNames,
|
||||
link,
|
||||
tags,
|
||||
summary,
|
||||
explanation,
|
||||
severityLevel,
|
||||
canDisable,
|
||||
suppressionAnnotations);
|
||||
}
|
||||
|
||||
abstract URI source();
|
||||
|
||||
abstract String fullyQualifiedName();
|
||||
|
||||
abstract String name();
|
||||
|
||||
abstract ImmutableList<String> altNames();
|
||||
|
||||
abstract String link();
|
||||
|
||||
abstract ImmutableList<String> tags();
|
||||
|
||||
abstract String summary();
|
||||
|
||||
abstract String explanation();
|
||||
|
||||
abstract SeverityLevel severityLevel();
|
||||
|
||||
abstract boolean canDisable();
|
||||
|
||||
abstract ImmutableList<String> suppressionAnnotations();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static java.util.function.Predicate.not;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.util.TreeScanner;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.TestCases;
|
||||
|
||||
/**
|
||||
* An {@link Extractor} that describes how to extract data from classes that test a {@code
|
||||
* BugChecker}.
|
||||
*/
|
||||
// XXX: Handle other methods from `{BugCheckerRefactoring,Compilation}TestHelper`:
|
||||
// - Indicate which custom arguments are specified, if any.
|
||||
// - For replacement tests, indicate which `FixChooser` is used.
|
||||
// - ... (We don't use all optional features; TBD what else to support.)
|
||||
@Immutable
|
||||
@AutoService(Extractor.class)
|
||||
@SuppressWarnings("rawtypes" /* See https://github.com/google/auto/issues/870. */)
|
||||
public final class BugPatternTestExtractor implements Extractor<TestCases> {
|
||||
/** Instantiates a new {@link BugPatternTestExtractor} instance. */
|
||||
public BugPatternTestExtractor() {}
|
||||
|
||||
@Override
|
||||
public String identifier() {
|
||||
return "bugpattern-test";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<TestCases> tryExtract(ClassTree tree, VisitorState state) {
|
||||
BugPatternTestCollector collector = new BugPatternTestCollector();
|
||||
|
||||
collector.scan(tree, state);
|
||||
|
||||
return Optional.of(collector.getCollectedTests())
|
||||
.filter(not(ImmutableList::isEmpty))
|
||||
.map(
|
||||
tests ->
|
||||
new AutoValue_BugPatternTestExtractor_TestCases(
|
||||
state.getPath().getCompilationUnit().getSourceFile().toUri(),
|
||||
ASTHelpers.getSymbol(tree).className(),
|
||||
tests));
|
||||
}
|
||||
|
||||
private static final class BugPatternTestCollector
|
||||
extends TreeScanner<@Nullable Void, VisitorState> {
|
||||
private static final Matcher<ExpressionTree> COMPILATION_HELPER_DO_TEST =
|
||||
instanceMethod()
|
||||
.onDescendantOf("com.google.errorprone.CompilationTestHelper")
|
||||
.named("doTest");
|
||||
private static final Matcher<ExpressionTree> TEST_HELPER_NEW_INSTANCE =
|
||||
staticMethod()
|
||||
.onDescendantOfAny(
|
||||
"com.google.errorprone.CompilationTestHelper",
|
||||
"com.google.errorprone.BugCheckerRefactoringTestHelper")
|
||||
.named("newInstance")
|
||||
.withParameters(Class.class.getCanonicalName(), Class.class.getCanonicalName());
|
||||
private static final Matcher<ExpressionTree> IDENTIFICATION_SOURCE_LINES =
|
||||
instanceMethod()
|
||||
.onDescendantOf("com.google.errorprone.CompilationTestHelper")
|
||||
.named("addSourceLines");
|
||||
private static final Matcher<ExpressionTree> REPLACEMENT_DO_TEST =
|
||||
instanceMethod()
|
||||
.onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper")
|
||||
.named("doTest");
|
||||
private static final Matcher<ExpressionTree> REPLACEMENT_EXPECT_UNCHANGED =
|
||||
instanceMethod()
|
||||
.onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper.ExpectOutput")
|
||||
.named("expectUnchanged");
|
||||
private static final Matcher<ExpressionTree> REPLACEMENT_OUTPUT_SOURCE_LINES =
|
||||
instanceMethod()
|
||||
.onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper.ExpectOutput")
|
||||
.namedAnyOf("addOutputLines", "expectUnchanged");
|
||||
|
||||
private final List<TestCase> collectedTestCases = new ArrayList<>();
|
||||
|
||||
private ImmutableList<TestCase> getCollectedTests() {
|
||||
return ImmutableList.copyOf(collectedTestCases);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Void visitMethodInvocation(MethodInvocationTree node, VisitorState state) {
|
||||
boolean isReplacementTest = REPLACEMENT_DO_TEST.matches(node, state);
|
||||
if (isReplacementTest || COMPILATION_HELPER_DO_TEST.matches(node, state)) {
|
||||
getClassUnderTest(node, state)
|
||||
.ifPresent(
|
||||
classUnderTest -> {
|
||||
List<TestEntry> entries = new ArrayList<>();
|
||||
if (isReplacementTest) {
|
||||
extractReplacementTestCases(node, entries, state);
|
||||
} else {
|
||||
extractIdentificationTestCases(node, entries, state);
|
||||
}
|
||||
|
||||
if (!entries.isEmpty()) {
|
||||
collectedTestCases.add(
|
||||
new AutoValue_BugPatternTestExtractor_TestCase(
|
||||
classUnderTest, ImmutableList.copyOf(entries).reverse()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return super.visitMethodInvocation(node, state);
|
||||
}
|
||||
|
||||
private static Optional<String> getClassUnderTest(
|
||||
MethodInvocationTree tree, VisitorState state) {
|
||||
if (TEST_HELPER_NEW_INSTANCE.matches(tree, state)) {
|
||||
return Optional.ofNullable(ASTHelpers.getSymbol(tree.getArguments().get(0)))
|
||||
.filter(s -> !s.type.allparams().isEmpty())
|
||||
.map(s -> s.type.allparams().get(0).tsym.getQualifiedName().toString());
|
||||
}
|
||||
|
||||
ExpressionTree receiver = ASTHelpers.getReceiver(tree);
|
||||
return receiver instanceof MethodInvocationTree methodInvocation
|
||||
? getClassUnderTest(methodInvocation, state)
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
private static void extractIdentificationTestCases(
|
||||
MethodInvocationTree tree, List<TestEntry> sink, VisitorState state) {
|
||||
if (IDENTIFICATION_SOURCE_LINES.matches(tree, state)) {
|
||||
String path = ASTHelpers.constValue(tree.getArguments().get(0), String.class);
|
||||
Optional<String> sourceCode =
|
||||
getSourceCode(tree).filter(s -> s.contains("// BUG: Diagnostic"));
|
||||
if (path != null && sourceCode.isPresent()) {
|
||||
sink.add(
|
||||
new AutoValue_BugPatternTestExtractor_IdentificationTestEntry(
|
||||
path, sourceCode.orElseThrow()));
|
||||
}
|
||||
}
|
||||
|
||||
ExpressionTree receiver = ASTHelpers.getReceiver(tree);
|
||||
if (receiver instanceof MethodInvocationTree methodInvocation) {
|
||||
extractIdentificationTestCases(methodInvocation, sink, state);
|
||||
}
|
||||
}
|
||||
|
||||
private static void extractReplacementTestCases(
|
||||
MethodInvocationTree tree, List<TestEntry> sink, VisitorState state) {
|
||||
if (REPLACEMENT_OUTPUT_SOURCE_LINES.matches(tree, state)) {
|
||||
/*
|
||||
* Retrieve the method invocation that contains the input source code. Note that this cast
|
||||
* is safe, because this code is guarded by an earlier call to `#getClassUnderTest(..)`,
|
||||
* which ensures that `tree` is part of a longer method invocation chain.
|
||||
*/
|
||||
MethodInvocationTree inputTree = (MethodInvocationTree) ASTHelpers.getReceiver(tree);
|
||||
|
||||
String path = ASTHelpers.constValue(inputTree.getArguments().get(0), String.class);
|
||||
Optional<String> inputCode = getSourceCode(inputTree);
|
||||
if (path != null && inputCode.isPresent()) {
|
||||
Optional<String> outputCode =
|
||||
REPLACEMENT_EXPECT_UNCHANGED.matches(tree, state) ? inputCode : getSourceCode(tree);
|
||||
|
||||
if (outputCode.isPresent() && !inputCode.equals(outputCode)) {
|
||||
sink.add(
|
||||
new AutoValue_BugPatternTestExtractor_ReplacementTestEntry(
|
||||
path, inputCode.orElseThrow(), outputCode.orElseThrow()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExpressionTree receiver = ASTHelpers.getReceiver(tree);
|
||||
if (receiver instanceof MethodInvocationTree methodInvocation) {
|
||||
extractReplacementTestCases(methodInvocation, sink, state);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: This logic is duplicated in `ErrorProneTestSourceFormat`. Can we do better?
|
||||
private static Optional<String> getSourceCode(MethodInvocationTree tree) {
|
||||
List<? extends ExpressionTree> sourceLines =
|
||||
tree.getArguments().subList(1, tree.getArguments().size());
|
||||
StringBuilder source = new StringBuilder();
|
||||
|
||||
for (ExpressionTree sourceLine : sourceLines) {
|
||||
String value = ASTHelpers.constValue(sourceLine, String.class);
|
||||
if (value == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
source.append(value).append('\n');
|
||||
}
|
||||
|
||||
return Optional.of(source.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
@JsonDeserialize(as = AutoValue_BugPatternTestExtractor_TestCases.class)
|
||||
abstract static class TestCases {
|
||||
static TestCases create(URI source, String testClass, ImmutableList<TestCase> testCases) {
|
||||
return new AutoValue_BugPatternTestExtractor_TestCases(source, testClass, testCases);
|
||||
}
|
||||
|
||||
abstract URI source();
|
||||
|
||||
abstract String testClass();
|
||||
|
||||
abstract ImmutableList<TestCase> testCases();
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
@JsonDeserialize(as = AutoValue_BugPatternTestExtractor_TestCase.class)
|
||||
abstract static class TestCase {
|
||||
static TestCase create(String classUnderTest, ImmutableList<TestEntry> entries) {
|
||||
return new AutoValue_BugPatternTestExtractor_TestCase(classUnderTest, entries);
|
||||
}
|
||||
|
||||
abstract String classUnderTest();
|
||||
|
||||
abstract ImmutableList<TestEntry> entries();
|
||||
}
|
||||
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(AutoValue_BugPatternTestExtractor_IdentificationTestEntry.class),
|
||||
@JsonSubTypes.Type(AutoValue_BugPatternTestExtractor_ReplacementTestEntry.class)
|
||||
})
|
||||
@JsonTypeInfo(include = As.EXISTING_PROPERTY, property = "type", use = JsonTypeInfo.Id.DEDUCTION)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonPropertyOrder("type")
|
||||
interface TestEntry {
|
||||
TestType type();
|
||||
|
||||
String path();
|
||||
|
||||
enum TestType {
|
||||
IDENTIFICATION,
|
||||
REPLACEMENT
|
||||
}
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class IdentificationTestEntry implements TestEntry {
|
||||
static IdentificationTestEntry create(String path, String code) {
|
||||
return new AutoValue_BugPatternTestExtractor_IdentificationTestEntry(path, code);
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
@Override
|
||||
public final TestType type() {
|
||||
return TestType.IDENTIFICATION;
|
||||
}
|
||||
|
||||
abstract String code();
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class ReplacementTestEntry implements TestEntry {
|
||||
static ReplacementTestEntry create(String path, String input, String output) {
|
||||
return new AutoValue_BugPatternTestExtractor_ReplacementTestEntry(path, input, output);
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
@Override
|
||||
public final TestType type() {
|
||||
return TestType.REPLACEMENT;
|
||||
}
|
||||
|
||||
abstract String input();
|
||||
|
||||
abstract String output();
|
||||
}
|
||||
}
|
||||
@@ -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,92 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.CompilationUnitTree;
|
||||
import com.sun.source.util.TaskEvent;
|
||||
import com.sun.source.util.TaskEvent.Kind;
|
||||
import com.sun.source.util.TaskListener;
|
||||
import com.sun.source.util.TreePath;
|
||||
import com.sun.tools.javac.api.JavacTrees;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ServiceLoader;
|
||||
import javax.tools.JavaFileObject;
|
||||
|
||||
/**
|
||||
* A {@link TaskListener} that identifies and extracts relevant content for documentation generation
|
||||
* and writes it to disk.
|
||||
*/
|
||||
// XXX: Find a better name for this class; it doesn't generate documentation per se.
|
||||
final class DocumentationGeneratorTaskListener implements TaskListener {
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private static final ImmutableList<Extractor<?>> EXTRACTORS =
|
||||
(ImmutableList)
|
||||
ImmutableList.copyOf(
|
||||
ServiceLoader.load(
|
||||
Extractor.class, DocumentationGeneratorTaskListener.class.getClassLoader()));
|
||||
|
||||
private final Context context;
|
||||
private final Path docsPath;
|
||||
|
||||
DocumentationGeneratorTaskListener(Context context, Path path) {
|
||||
this.context = context;
|
||||
this.docsPath = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void started(TaskEvent taskEvent) {
|
||||
if (taskEvent.getKind() == Kind.ANALYZE) {
|
||||
createDocsDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finished(TaskEvent taskEvent) {
|
||||
if (taskEvent.getKind() != Kind.ANALYZE) {
|
||||
return;
|
||||
}
|
||||
|
||||
JavaFileObject sourceFile = taskEvent.getSourceFile();
|
||||
CompilationUnitTree compilationUnit = taskEvent.getCompilationUnit();
|
||||
ClassTree classTree = JavacTrees.instance(context).getTree(taskEvent.getTypeElement());
|
||||
if (sourceFile == null || compilationUnit == null || classTree == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
VisitorState state =
|
||||
VisitorState.createForUtilityPurposes(context)
|
||||
.withPath(new TreePath(new TreePath(compilationUnit), classTree));
|
||||
|
||||
for (Extractor<?> extractor : EXTRACTORS) {
|
||||
extractor
|
||||
.tryExtract(classTree, state)
|
||||
.ifPresent(
|
||||
data ->
|
||||
writeToFile(
|
||||
extractor.identifier(), getSimpleClassName(sourceFile.toUri()), data));
|
||||
}
|
||||
}
|
||||
|
||||
private void createDocsDirectory() {
|
||||
try {
|
||||
Files.createDirectories(docsPath);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Error while creating directory with path '%s'", docsPath), e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> void writeToFile(String identifier, String className, T data) {
|
||||
Json.write(docsPath.resolve(String.format("%s-%s.json", identifier, className)), data);
|
||||
}
|
||||
|
||||
private static String getSimpleClassName(URI path) {
|
||||
return Paths.get(path).getFileName().toString().replace(".java", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Interface implemented by classes that define how to extract data of some type {@link T} from a
|
||||
* given {@link ClassTree}.
|
||||
*
|
||||
* @param <T> The type of data that is extracted.
|
||||
*/
|
||||
@Immutable
|
||||
interface Extractor<T> {
|
||||
/**
|
||||
* Returns the unique identifier of this extractor.
|
||||
*
|
||||
* @return A non-{@code null} string.
|
||||
*/
|
||||
String identifier();
|
||||
|
||||
/**
|
||||
* Attempts to extract an instance of type {@link T} using the provided arguments.
|
||||
*
|
||||
* @param tree The {@link ClassTree} to analyze and from which to extract an instance of type
|
||||
* {@link T}.
|
||||
* @param state A {@link VisitorState} describing the context in which the given {@link ClassTree}
|
||||
* is found.
|
||||
* @return An instance of type {@link T}, if possible.
|
||||
*/
|
||||
Optional<T> tryExtract(ClassTree tree, VisitorState state);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.guava.GuavaModule;
|
||||
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
|
||||
import com.google.errorprone.annotations.FormatMethod;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Utility class that offers mutually consistent JSON serialization and deserialization operations,
|
||||
* without further specifying the exact schema used.
|
||||
*/
|
||||
final class Json {
|
||||
private static final ObjectMapper OBJECT_MAPPER =
|
||||
new ObjectMapper()
|
||||
.setVisibility(PropertyAccessor.FIELD, Visibility.ANY)
|
||||
.registerModules(new GuavaModule(), new ParameterNamesModule());
|
||||
|
||||
private Json() {}
|
||||
|
||||
static <T> T read(Path path, Class<T> clazz) {
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(path.toFile(), clazz);
|
||||
} catch (IOException e) {
|
||||
throw failure(e, "Failure reading from '%s'", path);
|
||||
}
|
||||
}
|
||||
|
||||
static <T> void write(Path path, T object) {
|
||||
try {
|
||||
OBJECT_MAPPER.writeValue(path.toFile(), object);
|
||||
} catch (IOException e) {
|
||||
throw failure(e, "Failure writing to '%s'", path);
|
||||
}
|
||||
}
|
||||
|
||||
@FormatMethod
|
||||
private static UncheckedIOException failure(IOException cause, String format, Object... args) {
|
||||
return new UncheckedIOException(String.format(format, args), cause);
|
||||
}
|
||||
}
|
||||
@@ -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,142 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import tech.picnic.errorprone.documentation.BugPatternExtractor.BugPatternDocumentation;
|
||||
|
||||
final class BugPatternExtractorTest {
|
||||
@Test
|
||||
void noBugPattern(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerWithoutAnnotation.java",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"public final class TestCheckerWithoutAnnotation extends BugChecker {}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void minimalBugPattern(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"MinimalBugChecker.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.BugPattern;",
|
||||
"import com.google.errorprone.BugPattern.SeverityLevel;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"@BugPattern(summary = \"MinimalBugChecker summary\", severity = SeverityLevel.ERROR)",
|
||||
"public final class MinimalBugChecker extends BugChecker {}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"MinimalBugChecker",
|
||||
BugPatternDocumentation.create(
|
||||
URI.create("file:///MinimalBugChecker.java"),
|
||||
"pkg.MinimalBugChecker",
|
||||
"MinimalBugChecker",
|
||||
ImmutableList.of(),
|
||||
"",
|
||||
ImmutableList.of(),
|
||||
"MinimalBugChecker summary",
|
||||
"",
|
||||
ERROR,
|
||||
/* canDisable= */ true,
|
||||
ImmutableList.of(SuppressWarnings.class.getCanonicalName())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void completeBugPattern(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"CompleteBugChecker.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.BugPattern;",
|
||||
"import com.google.errorprone.BugPattern.SeverityLevel;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import org.junit.jupiter.api.Test;",
|
||||
"",
|
||||
"@BugPattern(",
|
||||
" name = \"OtherName\",",
|
||||
" summary = \"CompleteBugChecker summary\",",
|
||||
" linkType = BugPattern.LinkType.CUSTOM,",
|
||||
" link = \"https://error-prone.picnic.tech\",",
|
||||
" explanation = \"Example explanation\",",
|
||||
" severity = SeverityLevel.SUGGESTION,",
|
||||
" altNames = \"Check\",",
|
||||
" tags = BugPattern.StandardTags.SIMPLIFICATION,",
|
||||
" disableable = false,",
|
||||
" suppressionAnnotations = {BugPattern.class, Test.class})",
|
||||
"public final class CompleteBugChecker extends BugChecker {}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"CompleteBugChecker",
|
||||
BugPatternDocumentation.create(
|
||||
URI.create("file:///CompleteBugChecker.java"),
|
||||
"pkg.CompleteBugChecker",
|
||||
"OtherName",
|
||||
ImmutableList.of("Check"),
|
||||
"https://error-prone.picnic.tech",
|
||||
ImmutableList.of("Simplification"),
|
||||
"CompleteBugChecker summary",
|
||||
"Example explanation",
|
||||
SUGGESTION,
|
||||
/* canDisable= */ false,
|
||||
ImmutableList.of(BugPattern.class.getCanonicalName(), "org.junit.jupiter.api.Test")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void undocumentedSuppressionBugPattern(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"UndocumentedSuppressionBugPattern.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.BugPattern;",
|
||||
"import com.google.errorprone.BugPattern.SeverityLevel;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"@BugPattern(",
|
||||
" summary = \"UndocumentedSuppressionBugPattern summary\",",
|
||||
" severity = SeverityLevel.WARNING,",
|
||||
" documentSuppression = false)",
|
||||
"public final class UndocumentedSuppressionBugPattern extends BugChecker {}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"UndocumentedSuppressionBugPattern",
|
||||
BugPatternDocumentation.create(
|
||||
URI.create("file:///UndocumentedSuppressionBugPattern.java"),
|
||||
"pkg.UndocumentedSuppressionBugPattern",
|
||||
"UndocumentedSuppressionBugPattern",
|
||||
ImmutableList.of(),
|
||||
"",
|
||||
ImmutableList.of(),
|
||||
"UndocumentedSuppressionBugPattern summary",
|
||||
"",
|
||||
WARNING,
|
||||
/* canDisable= */ true,
|
||||
ImmutableList.of()));
|
||||
}
|
||||
|
||||
private static void verifyGeneratedFileContent(
|
||||
Path outputDirectory, String testClass, BugPatternDocumentation expected) {
|
||||
assertThat(outputDirectory.resolve(String.format("bugpattern-%s.json", testClass)))
|
||||
.exists()
|
||||
.returns(expected, path -> Json.read(path, BugPatternDocumentation.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.IdentificationTestEntry;
|
||||
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.ReplacementTestEntry;
|
||||
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.TestCase;
|
||||
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.TestCases;
|
||||
|
||||
final class BugPatternTestExtractorTest {
|
||||
@Test
|
||||
void noTestClass(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerWithoutAnnotation.java",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"public final class TestCheckerWithoutAnnotation extends BugChecker {}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noDoTestInvocation(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\");",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\");",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullBugCheckerInstance(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance((Class<BugChecker>) null, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance((Class<BugChecker>) null, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rawBugCheckerInstance(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" @SuppressWarnings(\"unchecked\")",
|
||||
" void m() {",
|
||||
" @SuppressWarnings(\"rawtypes\")",
|
||||
" Class bugChecker = TestChecker.class;",
|
||||
"",
|
||||
" CompilationTestHelper.newInstance(bugChecker, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(bugChecker, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void scannerSupplierInstance(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import com.google.errorprone.scanner.ScannerSupplier;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(",
|
||||
" ScannerSupplier.fromBugCheckerClasses(TestChecker.class), getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(",
|
||||
" ScannerSupplier.fromBugCheckerClasses(TestChecker.class), getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonCompileTimeConstantStrings(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(toString() + \"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .addSourceLines(\"B.java\", \"// BUG: Diagnostic contains:\", \"class B {}\", toString())",
|
||||
" .doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(toString() + \"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .addInputLines(\"B.java\", \"class B {}\", toString())",
|
||||
" .addOutputLines(\"B.java\", \"class B { /* This is a change. */ }\")",
|
||||
" .addInputLines(\"C.java\", \"class C {}\")",
|
||||
" .addOutputLines(\"C.java\", \"class C { /* This is a change. */ }\", toString())",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonFluentTestHelperExpressions(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper testHelper =",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"class A {}\");",
|
||||
" testHelper.doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.ExpectOutput expectedOutput =",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\");",
|
||||
" expectedOutput.addOutputLines(\"A.java\", \"class A {}\").doTest();",
|
||||
" expectedOutput.expectUnchanged().doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noSource(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass()).doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass()).doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noDiagnostics(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A {}\")",
|
||||
" .addInputLines(\"B.java\", \"class B {}\")",
|
||||
" .expectUnchanged()",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleFileCompilationTestHelper(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"SingleFileCompilationTestHelperTest.java",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class SingleFileCompilationTestHelperTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"SingleFileCompilationTestHelperTest",
|
||||
TestCases.create(
|
||||
URI.create("file:///SingleFileCompilationTestHelperTest.java"),
|
||||
"SingleFileCompilationTestHelperTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"SingleFileCompilationTestHelperTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
IdentificationTestEntry.create(
|
||||
"A.java", "// BUG: Diagnostic contains:\nclass A {}\n"))))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleFileCompilationTestHelperWithSetArgs(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"SingleFileCompilationTestHelperWithSetArgsTest.java",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class SingleFileCompilationTestHelperWithSetArgsTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .setArgs(\"-XepAllSuggestionsAsWarnings\")",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"SingleFileCompilationTestHelperWithSetArgsTest",
|
||||
TestCases.create(
|
||||
URI.create("file:///SingleFileCompilationTestHelperWithSetArgsTest.java"),
|
||||
"SingleFileCompilationTestHelperWithSetArgsTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"SingleFileCompilationTestHelperWithSetArgsTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
IdentificationTestEntry.create(
|
||||
"A.java", "// BUG: Diagnostic contains:\nclass A {}\n"))))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiFileCompilationTestHelper(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"MultiFileCompilationTestHelperTest.java",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class MultiFileCompilationTestHelperTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .addSourceLines(\"B.java\", \"// BUG: Diagnostic contains:\", \"class B {}\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"MultiFileCompilationTestHelperTest",
|
||||
TestCases.create(
|
||||
URI.create("file:///MultiFileCompilationTestHelperTest.java"),
|
||||
"MultiFileCompilationTestHelperTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"MultiFileCompilationTestHelperTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
IdentificationTestEntry.create(
|
||||
"A.java", "// BUG: Diagnostic contains:\nclass A {}\n"),
|
||||
IdentificationTestEntry.create(
|
||||
"B.java", "// BUG: Diagnostic contains:\nclass B {}\n"))))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleFileBugCheckerRefactoringTestHelper(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"SingleFileBugCheckerRefactoringTestHelperTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class SingleFileBugCheckerRefactoringTestHelperTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"SingleFileBugCheckerRefactoringTestHelperTest",
|
||||
TestCases.create(
|
||||
URI.create("file:///SingleFileBugCheckerRefactoringTestHelperTest.java"),
|
||||
"SingleFileBugCheckerRefactoringTestHelperTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"SingleFileBugCheckerRefactoringTestHelperTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
ReplacementTestEntry.create(
|
||||
"A.java", "class A {}\n", "class A { /* This is a change. */ }\n"))))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestMode(
|
||||
@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers;",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .setArgs(\"-XepAllSuggestionsAsWarnings\")",
|
||||
" .setFixChooser(FixChoosers.SECOND)",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .doTest(TestMode.TEXT_MATCH);",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest",
|
||||
TestCases.create(
|
||||
URI.create(
|
||||
"file:///SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest.java"),
|
||||
"SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
ReplacementTestEntry.create(
|
||||
"A.java", "class A {}\n", "class A { /* This is a change. */ }\n"))))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiFileBugCheckerRefactoringTestHelper(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"MultiFileBugCheckerRefactoringTestHelperTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class MultiFileBugCheckerRefactoringTestHelperTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .addInputLines(\"B.java\", \"class B {}\")",
|
||||
" .addOutputLines(\"B.java\", \"class B { /* This is a change. */ }\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"MultiFileBugCheckerRefactoringTestHelperTest",
|
||||
TestCases.create(
|
||||
URI.create("file:///MultiFileBugCheckerRefactoringTestHelperTest.java"),
|
||||
"MultiFileBugCheckerRefactoringTestHelperTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"MultiFileBugCheckerRefactoringTestHelperTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
ReplacementTestEntry.create(
|
||||
"A.java", "class A {}\n", "class A { /* This is a change. */ }\n"),
|
||||
ReplacementTestEntry.create(
|
||||
"B.java", "class B {}\n", "class B { /* This is a change. */ }\n"))))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void compilationAndBugCheckerRefactoringTestHelpers(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"CompilationAndBugCheckerRefactoringTestHelpersTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class CompilationAndBugCheckerRefactoringTestHelpersTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"CompilationAndBugCheckerRefactoringTestHelpersTest",
|
||||
TestCases.create(
|
||||
URI.create("file:///CompilationAndBugCheckerRefactoringTestHelpersTest.java"),
|
||||
"CompilationAndBugCheckerRefactoringTestHelpersTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"CompilationAndBugCheckerRefactoringTestHelpersTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
IdentificationTestEntry.create(
|
||||
"A.java", "// BUG: Diagnostic contains:\nclass A {}\n"))),
|
||||
TestCase.create(
|
||||
"CompilationAndBugCheckerRefactoringTestHelpersTest.TestChecker",
|
||||
ImmutableList.of(
|
||||
ReplacementTestEntry.create(
|
||||
"A.java", "class A {}\n", "class A { /* This is a change. */ }\n"))))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void compilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNames(
|
||||
@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"final class CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest {",
|
||||
" private static class CustomTestChecker extends BugChecker {}",
|
||||
"",
|
||||
" private static class CustomTestChecker2 extends BugChecker {}",
|
||||
"",
|
||||
" void m() {",
|
||||
" CompilationTestHelper.newInstance(CustomTestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"// BUG: Diagnostic contains:\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
"",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(CustomTestChecker2.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A { /* This is a change. */ }\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyGeneratedFileContent(
|
||||
outputDirectory,
|
||||
"CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest",
|
||||
TestCases.create(
|
||||
URI.create(
|
||||
"file:///CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest.java"),
|
||||
"pkg.CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest",
|
||||
ImmutableList.of(
|
||||
TestCase.create(
|
||||
"pkg.CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest.CustomTestChecker",
|
||||
ImmutableList.of(
|
||||
IdentificationTestEntry.create(
|
||||
"A.java", "// BUG: Diagnostic contains:\nclass A {}\n"))),
|
||||
TestCase.create(
|
||||
"pkg.CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest.CustomTestChecker2",
|
||||
ImmutableList.of(
|
||||
ReplacementTestEntry.create(
|
||||
"A.java", "class A {}\n", "class A { /* This is a change. */ }\n"))))));
|
||||
}
|
||||
|
||||
private static void verifyGeneratedFileContent(
|
||||
Path outputDirectory, String testClass, TestCases expected) {
|
||||
assertThat(outputDirectory.resolve(String.format("bugpattern-test-%s.json", testClass)))
|
||||
.exists()
|
||||
.returns(expected, path -> Json.read(path, TestCases.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.FileManagers;
|
||||
import com.google.errorprone.FileObjects;
|
||||
import com.sun.tools.javac.api.JavacTaskImpl;
|
||||
import com.sun.tools.javac.api.JavacTool;
|
||||
import com.sun.tools.javac.file.JavacFileManager;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.tools.Diagnostic;
|
||||
import javax.tools.JavaCompiler;
|
||||
import javax.tools.JavaFileObject;
|
||||
|
||||
// XXX: Generalize and move this class so that it can also be used by `refaster-compiler`.
|
||||
// XXX: This class is supported by the `ErrorProneTestHelperSourceFormat` check, but until that
|
||||
// support is covered by unit tests, make sure to update that logic if this class or its methods are
|
||||
// moved/renamed.
|
||||
public final class Compilation {
|
||||
private Compilation() {}
|
||||
|
||||
public static void compileWithDocumentationGenerator(
|
||||
Path outputDirectory, String path, String... lines) {
|
||||
compileWithDocumentationGenerator(outputDirectory.toAbsolutePath().toString(), path, lines);
|
||||
}
|
||||
|
||||
public static void compileWithDocumentationGenerator(
|
||||
String outputDirectory, String path, String... lines) {
|
||||
/*
|
||||
* The compiler options specified here largely match those used by Error Prone's
|
||||
* `CompilationTestHelper`. A key difference is the stricter linting configuration. When
|
||||
* compiling using JDK 21+, these lint options also require that certain JDK modules are
|
||||
* explicitly exported.
|
||||
*/
|
||||
compile(
|
||||
ImmutableList.of(
|
||||
"--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
|
||||
"--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
|
||||
"--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
|
||||
"-encoding",
|
||||
"UTF-8",
|
||||
"-parameters",
|
||||
"-proc:none",
|
||||
"-Werror",
|
||||
"-Xlint:all,-serial",
|
||||
"-Xplugin:DocumentationGenerator -XoutputDirectory=" + outputDirectory,
|
||||
"-XDdev",
|
||||
"-XDcompilePolicy=simple"),
|
||||
FileObjects.forSourceLines(path, lines));
|
||||
}
|
||||
|
||||
private static void compile(ImmutableList<String> options, JavaFileObject javaFileObject) {
|
||||
JavacFileManager javacFileManager = FileManagers.testFileManager();
|
||||
JavaCompiler compiler = JavacTool.create();
|
||||
|
||||
List<Diagnostic<?>> diagnostics = new ArrayList<>();
|
||||
JavacTaskImpl task =
|
||||
(JavacTaskImpl)
|
||||
compiler.getTask(
|
||||
null,
|
||||
javacFileManager,
|
||||
diagnostics::add,
|
||||
options,
|
||||
ImmutableList.of(),
|
||||
ImmutableList.of(javaFileObject));
|
||||
|
||||
Boolean result = task.call();
|
||||
assertThat(diagnostics).isEmpty();
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.nio.file.attribute.AclEntryPermission.ADD_SUBDIRECTORY;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.condition.OS.WINDOWS;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystemException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.AclEntry;
|
||||
import java.nio.file.attribute.AclFileAttributeView;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.EnabledOnOs;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
final class DocumentationGeneratorTaskListenerTest {
|
||||
@EnabledOnOs(WINDOWS)
|
||||
@Test
|
||||
void readOnlyFileSystemWindows(@TempDir Path outputDirectory) throws IOException {
|
||||
AclFileAttributeView view =
|
||||
Files.getFileAttributeView(outputDirectory, AclFileAttributeView.class);
|
||||
view.setAcl(
|
||||
view.getAcl().stream()
|
||||
.map(
|
||||
entry ->
|
||||
AclEntry.newBuilder(entry)
|
||||
.setPermissions(
|
||||
Sets.difference(entry.permissions(), ImmutableSet.of(ADD_SUBDIRECTORY)))
|
||||
.build())
|
||||
.collect(toImmutableList()));
|
||||
|
||||
readOnlyFileSystemFailsToWrite(outputDirectory.resolve("nonexistent"));
|
||||
}
|
||||
|
||||
@DisabledOnOs(WINDOWS)
|
||||
@Test
|
||||
void readOnlyFileSystemNonWindows(@TempDir Path outputDirectory) {
|
||||
assertThat(outputDirectory.toFile().setWritable(false))
|
||||
.describedAs("Failed to make test directory unwritable")
|
||||
.isTrue();
|
||||
|
||||
readOnlyFileSystemFailsToWrite(outputDirectory.resolve("nonexistent"));
|
||||
}
|
||||
|
||||
private static void readOnlyFileSystemFailsToWrite(Path outputDirectory) {
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory, "A.java", "class A {}"))
|
||||
.hasRootCauseInstanceOf(FileSystemException.class)
|
||||
.hasCauseInstanceOf(IllegalStateException.class)
|
||||
.hasMessageEndingWith("Error while creating directory with path '%s'", outputDirectory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noClassNoOutput(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(outputDirectory, "A.java", "package pkg;");
|
||||
|
||||
assertThat(outputDirectory).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void excessArguments(@TempDir Path outputDirectory) {
|
||||
String actualOutputDirectory = outputDirectory.toAbsolutePath() + " extra-arg";
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
actualOutputDirectory, "A.java", "package pkg;"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Precisely one path must be provided");
|
||||
}
|
||||
|
||||
@Test
|
||||
void extraction(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"DocumentationGeneratorTaskListenerTestClass.java",
|
||||
"class DocumentationGeneratorTaskListenerTestClass {}");
|
||||
|
||||
assertThat(
|
||||
outputDirectory.resolve(
|
||||
"documentation-generator-task-listener-test-DocumentationGeneratorTaskListenerTestClass.json"))
|
||||
.content(UTF_8)
|
||||
.isEqualToIgnoringWhitespace(
|
||||
"""
|
||||
{
|
||||
"className": "DocumentationGeneratorTaskListenerTestClass",
|
||||
"path": [
|
||||
"CLASS: DocumentationGeneratorTaskListenerTestClass",
|
||||
"COMPILATION_UNIT"
|
||||
]
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@AutoService(Extractor.class)
|
||||
@SuppressWarnings("rawtypes" /* See https://github.com/google/auto/issues/870. */)
|
||||
public static final class TestExtractor implements Extractor<ExtractionParameters> {
|
||||
@Override
|
||||
public String identifier() {
|
||||
return "documentation-generator-task-listener-test";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ExtractionParameters> tryExtract(ClassTree tree, VisitorState state) {
|
||||
return Optional.of(tree.getSimpleName().toString())
|
||||
.filter(n -> n.contains(DocumentationGeneratorTaskListenerTest.class.getSimpleName()))
|
||||
.map(
|
||||
className ->
|
||||
new AutoValue_DocumentationGeneratorTaskListenerTest_ExtractionParameters(
|
||||
className,
|
||||
Streams.stream(state.getPath())
|
||||
.map(TestExtractor::describeTree)
|
||||
.collect(toImmutableList())));
|
||||
}
|
||||
|
||||
private static String describeTree(Tree tree) {
|
||||
return (tree instanceof ClassTree clazz)
|
||||
? String.join(": ", String.valueOf(tree.getKind()), clazz.getSimpleName())
|
||||
: tree.getKind().toString();
|
||||
}
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class ExtractionParameters {
|
||||
abstract String className();
|
||||
|
||||
abstract ImmutableList<String> path();
|
||||
}
|
||||
}
|
||||
@@ -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,62 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
final class JsonTest {
|
||||
private static final TestObject TEST_OBJECT = new AutoValue_JsonTest_TestObject("foo", 42);
|
||||
private static final String TEST_JSON = "{\"string\":\"foo\",\"number\":42}";
|
||||
|
||||
@Test
|
||||
void write(@TempDir Path directory) {
|
||||
Path file = directory.resolve("test.json");
|
||||
|
||||
Json.write(file, TEST_OBJECT);
|
||||
|
||||
assertThat(file).content(UTF_8).isEqualTo(TEST_JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeFailure(@TempDir Path directory) {
|
||||
assertThatThrownBy(() -> Json.write(directory, TEST_OBJECT))
|
||||
.isInstanceOf(UncheckedIOException.class)
|
||||
.hasMessageContaining("Failure writing to '%s'", directory)
|
||||
.hasCauseInstanceOf(FileNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void read(@TempDir Path directory) throws IOException {
|
||||
Path file = directory.resolve("test.json");
|
||||
|
||||
Files.writeString(file, TEST_JSON, UTF_8);
|
||||
|
||||
assertThat(Json.read(file, TestObject.class)).isEqualTo(TEST_OBJECT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readFailure(@TempDir Path directory) {
|
||||
assertThatThrownBy(() -> Json.read(directory, TestObject.class))
|
||||
.isInstanceOf(UncheckedIOException.class)
|
||||
.hasMessageContaining("Failure reading from '%s'", directory)
|
||||
.hasCauseInstanceOf(FileNotFoundException.class);
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
@JsonDeserialize(as = AutoValue_JsonTest_TestObject.class)
|
||||
abstract static class TestObject {
|
||||
abstract String string();
|
||||
|
||||
abstract int number();
|
||||
}
|
||||
}
|
||||
@@ -11,71 +11,21 @@ request.
|
||||
|
||||
### Building
|
||||
|
||||
This is a [Maven][maven] project, so running `mvn clean install` performs a
|
||||
full clean build. Some relevant flags:
|
||||
- `-Dverification.warn` makes the warnings and errors emitted by various
|
||||
plugins and the Java compiler non-fatal, where possible.
|
||||
- `-Dverification.skip` disables various non-essential plugins and compiles the
|
||||
code with minimal checks (i.e. without linting, Error Prone checks, etc.)
|
||||
- `-Dversion.error-prone=some-version` runs the build using the specified
|
||||
version of Error Prone. This is useful e.g. when testing a locally built
|
||||
Error Prone SNAPSHOT.
|
||||
- `-Perror-prone-fork` run the build using Picnic's [Error Prone
|
||||
fork][error-prone-fork-repo], hosted on [Jitpack][error-prone-fork-jitpack].
|
||||
This fork generally contains a few changes on top of the latest Error Prone
|
||||
release.
|
||||
|
||||
Two other goals that one may find relevant:
|
||||
- `mvn fmt:format` formats the code using
|
||||
[`google-java-format`][google-java-format].
|
||||
- `mvn pitest:mutationCoverage` runs mutation tests using [PIT][pitest]. The
|
||||
results can be reviewed by opening the respective
|
||||
`target/pit-reports/index.html` files. For more information check the [PIT
|
||||
Maven plugin][pitest-maven].
|
||||
|
||||
When loading the project in IntelliJ IDEA (and perhaps other IDEs) errors about
|
||||
the inaccessibility of `com.sun.tools.javac.*` classes may be reported. If this
|
||||
happens, configure your IDE to enable the `add-exports` profile.
|
||||
See the main [readme][main-readme].
|
||||
|
||||
### Contribution guidelines
|
||||
|
||||
To the extend possible, the pull request process guards our coding guidelines.
|
||||
Some pointers:
|
||||
- Checks should we _topical_: Ideally they address a single concern.
|
||||
- Where possible checks should provide _fixes_, and ideally these are
|
||||
completely behavior preserving. In order for a check to be adopted by users
|
||||
it must not "get in the way". So for a check which addresses a relatively
|
||||
trivial stylistic concern it is doubly important that the violations it
|
||||
detects can be auto-patched.
|
||||
- Make sure you have read Error Prone's [criteria for new
|
||||
checks][error-prone-criteria]. Most guidelines described there apply to this
|
||||
project as well, except that this project _does_ focus quite heavy on style
|
||||
enforcement. But that just makes the previous point doubly important.
|
||||
- Make sure that a check's (mutation) coverage is or remains about as high as
|
||||
it can be. Not only does this lead to better tests, it also points out
|
||||
opportunities to simplify the code.
|
||||
- Please restrict the scope of a pull request to a single feature or fix. Don't
|
||||
sneak in unrelated changes.
|
||||
- When in doubt about whether a pull request will be accepted, please first
|
||||
file an issue to discuss it.
|
||||
See our [contributing guidelines][main-contributing].
|
||||
|
||||
### Our wishlist
|
||||
|
||||
We expect the following tasks to help improve the quality of this open source
|
||||
project:
|
||||
|
||||
- Publish the artifact to Maven Central, then document the coordinates in this
|
||||
`README.md`.
|
||||
- Document how to enable the checks.
|
||||
- Document how to apply patches.
|
||||
- Document each of the checks.
|
||||
- Add Travis CI, [SonarQube][sonarcloud] and [Codecov][codecov]
|
||||
integrations.
|
||||
- Investigate whether it makes sense to include license headers in each file.
|
||||
If so, set that up and enforce it.
|
||||
- Add non-Java file formatting support, like we have internally at Picnic.
|
||||
(I.e., somehow open-source that stuff.)
|
||||
- Add relevant "badges" at the top of this `README.md`.
|
||||
- Auto-generate a website listing each of the checks, just like the Error Prone
|
||||
[bug patterns page][error-prone-bug-patterns]. The [Error Prone
|
||||
repository][error-prone-repo] contains code for this.
|
||||
@@ -93,7 +43,7 @@ project:
|
||||
- Improve an existing check (see `XXX`-marked comments in the code) or write a
|
||||
new one (see the list of suggestions below).
|
||||
|
||||
### Ideas for new checks
|
||||
### BugChecker extension ideas
|
||||
|
||||
The following is a list of checks we'd like to see implemented:
|
||||
|
||||
@@ -103,7 +53,7 @@ The following is a list of checks we'd like to see implemented:
|
||||
signature groups. Using Error Prone's method matchers forbidden method calls
|
||||
can easily be identified. But Error Prone can go one step further by
|
||||
auto-patching violations. For each violation two fixes can be proposed: a
|
||||
purely behavior-preserving fix which makes the platform-dependent behavior
|
||||
purely behavior-preserving fix, which makes the platform-dependent behavior
|
||||
explicit, and another which replaces the platform-dependent behavior with the
|
||||
preferred alternative. (Such as using `UTF-8` instead of the system default
|
||||
charset.)
|
||||
@@ -113,128 +63,125 @@ The following is a list of checks we'd like to see implemented:
|
||||
functionality.
|
||||
- A subset of the refactor operations provided by the Eclipse-specific
|
||||
[AutoRefactor][autorefactor] plugin.
|
||||
- A check which replaces fully qualified types with simple types in contexts
|
||||
- A check that replaces fully qualified types with simple types in contexts
|
||||
where this does not introduce ambiguity. Should consider both actual Java
|
||||
code and Javadoc `@link` references.
|
||||
- A check which simplifies array expressions. It would replace empty array
|
||||
- A check that simplifies array expressions. It would replace empty array
|
||||
expressions of the form `new int[] {}` with `new int[0]`. Statements of the
|
||||
form `byte[] arr = new byte[] {'c'};` would be shortened to `byte[] arr =
|
||||
{'c'};`.
|
||||
- A check which replaces expressions of the form `String.format("some prefix
|
||||
- A check that replaces expressions of the form `String.format("some prefix
|
||||
%s", arg)` with `"some prefix " + arg`, and similar for simple suffixes. Can
|
||||
perhaps be generalized further, though it's unclear how far. (Well, a
|
||||
`String.format` call without arguments can certainly be simplified, too.)
|
||||
- A check which replaces single-character strings with `char`s where possible.
|
||||
- A check that replaces single-character strings with `char`s where possible.
|
||||
For example as argument to `StringBuilder.append` and in string
|
||||
concatenations.
|
||||
- A check which adds or removes the first `Locale` argument to `String.format`
|
||||
- A check that adds or removes the first `Locale` argument to `String.format`
|
||||
and similar calls as necessary. (For example, a format string containing only
|
||||
`%s` placeholders is locale-insensitive unless any of the arguments is a
|
||||
`Formattable`, while `%f` placeholders _are_ locale-sensitive.)
|
||||
- A check which replaces `String.replaceAll` with `String.replace` if the first
|
||||
- A check that replaces `String.replaceAll` with `String.replace` if the first
|
||||
argument is certainly not a regular expression. And if both arguments are
|
||||
single-character strings then the `(char, char)` overload can be invoked
|
||||
instead.
|
||||
- A check which flags (and ideally, replaces) `try-finally` constructs with
|
||||
- A check that flags (and ideally, replaces) `try-finally` constructs with
|
||||
equivalent `try-with-resources` constructs.
|
||||
- A check which drops exceptions declared in `throws` clauses if they are (a)
|
||||
- A check that drops exceptions declared in `throws` clauses if they are (a)
|
||||
not actually thrown and (b) the associated method cannot be overridden.
|
||||
- A check which tries to statically import certain methods whenever used, if
|
||||
- A check that tries to statically import certain methods whenever used, if
|
||||
possible. The set of targeted methods should be configurable, but may default
|
||||
to e.g. `java.util.Function.identity()`, the static methods exposed by
|
||||
`java.util.stream.Collectors` and the various Guava collector factory
|
||||
methods.
|
||||
- A check which replaces `new Random().someMethod()` calls with
|
||||
`ThreadLocalRandom.current().someMethod()` calls, so as to avoid unnecessary
|
||||
- A check that replaces `new Random().someMethod()` calls with
|
||||
`ThreadLocalRandom.current().someMethod()` calls, to avoid unnecessary
|
||||
synchronization.
|
||||
- A check which drops `this.` from `this.someMethod()` calls and which
|
||||
- A check that drops `this.` from `this.someMethod()` calls and which
|
||||
optionally does the same for fields, if no ambiguity arises.
|
||||
- A check which replaces `Integer.valueOf` calls with `Integer.parseInt` or
|
||||
vice versa in order to prevent auto (un)boxing, and likewise for other number
|
||||
- A check that replaces `Integer.valueOf` calls with `Integer.parseInt` or vice
|
||||
versa in order to prevent auto (un)boxing, and likewise for other number
|
||||
types.
|
||||
- A check which flags nullable collections.
|
||||
- A check which flags `AutoCloseable` resources not managed by a
|
||||
- A check that flags nullable collections.
|
||||
- A check that flags `AutoCloseable` resources not managed by a
|
||||
`try-with-resources` construct. Certain subtypes, such as jOOQ's `DSLContext`
|
||||
should be excluded.
|
||||
- A check which flags `java.time` methods which implicitly consult the system
|
||||
- A check that flags `java.time` methods which implicitly consult the system
|
||||
clock, suggesting that a passed-in `Clock` is used instead.
|
||||
- A check which flags public methods on public classes which reference
|
||||
- A check that flags public methods on public classes which reference
|
||||
non-public types. This can cause `IllegalAccessError`s and
|
||||
`BootstrapMethodError`s at runtime.
|
||||
- A check which swaps the LHS and RHS in expressions of the form
|
||||
- A check that swaps the LHS and RHS in expressions of the form
|
||||
`nonConstant.equals(someNonNullConstant)`.
|
||||
- A check which annotates methods which only throw an exception with
|
||||
- A check that annotates methods which only throw an exception with
|
||||
`@Deprecated` or ` @DoNotCall`.
|
||||
- A check which flags imports from other test classes.
|
||||
- A Guava-specific check which replaces `Joiner.join` calls with `String.join`
|
||||
- A check that flags imports from other test classes.
|
||||
- A Guava-specific check that replaces `Joiner.join` calls with `String.join`
|
||||
calls in those cases where the latter is a proper substitute for the former.
|
||||
- A Guava-specific check which flags `{Immutable,}Multimap` type usages
|
||||
where `{Immutable,}{List,Set}Multimap` would be more appropriate.
|
||||
- A Guava-specific check which rewrites `if (conditional) { throw new
|
||||
- A Guava-specific check that flags `{Immutable,}Multimap` type usages where
|
||||
`{Immutable,}{List,Set}Multimap` would be more appropriate.
|
||||
- A Guava-specific check that rewrites `if (conditional) { throw new
|
||||
IllegalArgumentException(); }` and variants to an equivalent `checkArgument`
|
||||
statement. Idem for other exception types.
|
||||
- A Guava-specific check which replaces simple anonymous `CacheLoader` subclass
|
||||
- A Guava-specific check that replaces simple anonymous `CacheLoader` subclass
|
||||
declarations with `CacheLoader.from(someLambda)`.
|
||||
- A Spring-specific check which enforces that methods with the `@Scheduled`
|
||||
annotation are also annotated with New Relic's `@Trace` annotation. Such
|
||||
methods should ideally not also represent Spring MVC endpoints.
|
||||
- A Spring-specific check which enforces that `@RequestMapping` annotations,
|
||||
- A Spring-specific check that enforces that `@RequestMapping` annotations,
|
||||
when applied to a method, explicitly specify one or more target HTTP methods.
|
||||
- A Spring-specific check which looks for classes in which all
|
||||
`@RequestMapping` annotations (and the various aliases) specify the same
|
||||
`path`/`value` property and then moves that path to the class level.
|
||||
- A Spring-specific check which flags `@Value("some.property")` annotations, as
|
||||
- A Spring-specific check that looks for classes in which all `@RequestMapping`
|
||||
annotations (and the various aliases) specify the same `path`/`value`
|
||||
property and then moves that path to the class level.
|
||||
- A Spring-specific check that flags `@Value("some.property")` annotations, as
|
||||
these almost certainly should be `@Value("${some.property}")`.
|
||||
- A Spring-specific check which drops the `required` attribute from
|
||||
- A Spring-specific check that drops the `required` attribute from
|
||||
`@RequestParam` annotations when the `defaultValue` attribute is also
|
||||
specified.
|
||||
- A Spring-specific check which rewrites a class which uses field injection to
|
||||
- A Spring-specific check that rewrites a class which uses field injection to
|
||||
one which uses constructor injection. This check wouldn't be strictly
|
||||
behavior preserving, but could be used for a one-off code base migration.
|
||||
- A Spring-specific check which disallows field injection, except in
|
||||
- A Spring-specific check that disallows field injection, except in
|
||||
`AbstractTestNGSpringContextTests` subclasses. (One known edge case:
|
||||
self-injections so that a bean can call itself through an implicit proxy.)
|
||||
- A Spring-specific check which verifies that public methods on all classes
|
||||
- A Spring-specific check that verifies that public methods on all classes
|
||||
whose name matches a certain pattern, e.g. `.*Service`, are annotated
|
||||
`@Secured`.
|
||||
- A Spring-specific check which verifies that annotations such as
|
||||
- A Spring-specific check that verifies that annotations such as
|
||||
`@RequestParam` are only present in `@RestController` classes.
|
||||
- A Spring-specific check which disallows `@ResponseStatus` on MVC endpoint
|
||||
- A Spring-specific check that disallows `@ResponseStatus` on MVC endpoint
|
||||
methods, as this prevents communication of error status codes.
|
||||
- A Hibernate Validator-specific check which looks for `@UnwrapValidatedValue`
|
||||
- A Hibernate Validator-specific check that looks for `@UnwrapValidatedValue`
|
||||
usages and migrates the associated constraint annotations to the generic type
|
||||
argument to which they (are presumed to) apply.
|
||||
- A TestNG-specific check which drops method-level `@Test` annotations if a
|
||||
- A TestNG-specific check that drops method-level `@Test` annotations if a
|
||||
matching/more specific annotation is already present at the class level.
|
||||
- A TestNG-specific check which enforces that all tests are in a group.
|
||||
- A TestNG-specific check which flags field assignments in
|
||||
- A TestNG-specific check that enforces that all tests are in a group.
|
||||
- A TestNG-specific check that flags field assignments in
|
||||
`@BeforeMethod`-annotated methods unless the class is annotated
|
||||
`@Test(singleThreaded = true)`.
|
||||
- A TestNG-specific check which flags usages of the `expectedExceptions`
|
||||
- A TestNG-specific check that flags usages of the `expectedExceptions`
|
||||
attribute of the `@Test` annotation, pointing to `assertThrows`.
|
||||
- A Jongo-specific check which disallows the creation of sparse indices, in
|
||||
- A Jongo-specific check that disallows the creation of sparse indices, in
|
||||
favour of partial indices.
|
||||
- An Immutables-specific check which replaces
|
||||
- An Immutables-specific check that replaces
|
||||
`checkState`/`IllegalStateException` usages inside a `@Value.Check`-annotated
|
||||
method with `checkArgument`/`IllegalArgument`, since the method is invoked
|
||||
when a caller attempts to create an immutable instance.
|
||||
- An Immutables-specific check which disallows references to collection types
|
||||
- An Immutables-specific check that disallows references to collection types
|
||||
other than the Guava immutable collections, including inside generic type
|
||||
arguments.
|
||||
- An SLF4J-specific check which drops or adds a trailing dot from log messages,
|
||||
- An SLF4J-specific check that drops or adds a trailing dot from log messages,
|
||||
as applicable.
|
||||
- A Mockito-specific check which identifies sequences of statements which mock
|
||||
a significant number of methods on a single object with "default data"; such
|
||||
- A Mockito-specific check that identifies sequences of statements which mock a
|
||||
significant number of methods on a single object with "default data"; such
|
||||
constructions can often benefit from a different type of default answer, such
|
||||
as `Answers.RETURNS_MOCKS`.
|
||||
- An RxJava-specific check which flags `.toCompletable()` calls on expressions
|
||||
- An RxJava-specific check that flags `.toCompletable()` calls on expressions
|
||||
of type `Single<Completable>` etc., as most likely
|
||||
`.flatMapCompletable(Functions.identity())` was meant instead. Idem for other
|
||||
variations.
|
||||
- An RxJava-specific check which flags `expr.firstOrError()` calls and suggests
|
||||
- An RxJava-specific check that flags `expr.firstOrError()` calls and suggests
|
||||
`expr.switchIfEmpty(Single.error(...))`, so that an application-specific
|
||||
exception is thrown instead of `NoSuchElementException`.
|
||||
- An RxJava-specific check which flags use of `#assertValueSet` without
|
||||
- An RxJava-specific check that flags use of `#assertValueSet` without
|
||||
`#assertValueCount`, as the former method doesn't do what one may intuitively
|
||||
expect it to do. See ReactiveX/RxJava#6151.
|
||||
|
||||
@@ -247,6 +194,7 @@ but on the flip side Refaster is much less expressive. While this gap can never
|
||||
be fully closed, there are some ways in which Refaster's scope of utility could
|
||||
be extended. The following is a non-exhaustive list of ideas on how to extend
|
||||
Refaster's expressiveness:
|
||||
|
||||
- Allow more control over _which_ methods are statically imported by
|
||||
`@UseImportPolicy`. Sometimes the `@AfterTemplate` contains more than one
|
||||
static method invocation, and only a subset should be statically imported.
|
||||
@@ -261,9 +209,9 @@ Refaster's expressiveness:
|
||||
pure expression. Introduce a way to express such a constraint. For example,
|
||||
rewriting `optional1.map(Optional::of).orElse(optional2)` to `optional1.or(()
|
||||
-> optional2)` is not behavior preserving if evaluation of `optional2` has
|
||||
side-effects.
|
||||
side effects.
|
||||
- Similarly, certain refactoring operations are only valid if one of the
|
||||
matches expressions is not `@Nullable`. It'd be nice to be able to express
|
||||
matched expressions is not `@Nullable`. It'd be nice to be able to express
|
||||
this.
|
||||
- Generalize `@Placeholder` support such that rules can reference e.g. "any
|
||||
concrete unary method". This would allow refactorings such as
|
||||
@@ -276,10 +224,6 @@ Refaster's expressiveness:
|
||||
to be lost. In such a case don't statically import the method, so that the
|
||||
generic type information can be retained. (There may be cases where generic
|
||||
type information should even be _added_. Find an example.)
|
||||
- Upon application of a template Refaster can throw a _No binding for
|
||||
Key{identifier=someAfterTemplateParam}_ exception. When this happens the
|
||||
template is invalid. Instead perform this check at compile time, such that
|
||||
such malformed templates cannot be defined in the first place.
|
||||
- Provide a way to express "match if (not) annotated (with _X_)". See #1 for a
|
||||
motivating example.
|
||||
- Provide a way to place match constraints on compile time constants. For
|
||||
@@ -288,59 +232,49 @@ Refaster's expressiveness:
|
||||
- Provide a way to express transformations of compile-time constants. This
|
||||
would allow one to e.g. rewrite single-character strings to chars or vice
|
||||
versa, thereby accommodating a target API. Another example would be to
|
||||
replace SLF4J's `{}` place holders with `%s` or vice versa. Yet another
|
||||
replace SLF4J's `{}` placeholders with `%s` or vice versa. Yet another
|
||||
example would be to rewrite `BigDecimal.valueOf("<some-long-value>")` to
|
||||
`BigDecimal.valueOf(theParsedLongValue)`.
|
||||
- More generally, investigate ways to plug in in fully dynamic behavior, e.g.
|
||||
by providing hooks using which arbitrary predicates/transformations can be
|
||||
plugged in. The result would be a Refaster/`BugChecker` hybrid. A feature
|
||||
such as this could form the basis for many other features listed here. (As a
|
||||
concrete example, consider the ability to reference
|
||||
- More generally, investigate ways to plug in fully dynamic behavior, e.g. by
|
||||
providing hooks which enable plugging in arbitrary
|
||||
predicates/transformations. The result would be a Refaster/`BugChecker`
|
||||
hybrid. A feature like this could form the basis for many other features
|
||||
listed here. (As a concrete example, consider the ability to reference
|
||||
`com.google.errorprone.matchers.Matcher` implementations.)
|
||||
- Provide a way to match lambda expressions and method references which match a
|
||||
specified functional interface. This would allow rewrites such as
|
||||
`Mono.fromCallable(this::doesNotThrowCheckException)` ->
|
||||
`Mono.fromSupplier(this::doesNotThrowCheckException)`.
|
||||
- Provide an extension API using which methods or expressions can be defined
|
||||
based on functional properties. A motivating example is the Java Collections
|
||||
- Provide an extension API that enables defining methods or expressions based
|
||||
on functional properties. A motivating example is the Java Collections
|
||||
framework, which allows many ways to define (im)mutable (un)ordered
|
||||
collections with(out) duplicates. One could then express things like "match
|
||||
any method call with collects its inputs into an immutable ordered list". An
|
||||
any method call that collects its inputs into an immutable ordered list". An
|
||||
enum analogous to `java.util.stream.Collector.Characteristics` could be used.
|
||||
Out of the box JDK and Guava collection factory methods could be classified,
|
||||
with the user having the option to extend the classification.
|
||||
- Refaster currently unconditionally ignores expressions containing comments.
|
||||
Provide two additional modes: (a) match and drop the comments or (b)
|
||||
transport the comments to before/after the replaced expression.
|
||||
- Extend Refaster to drop imports that come become unnecessary as a result of a
|
||||
refactoring. This e.g. allows one to replace a statically import TestNG
|
||||
- Extend Refaster to drop imports that become unnecessary as a result of a
|
||||
refactoring. This e.g. allows one to replace a statically imported TestNG
|
||||
`fail(...)` invocation with a statically imported equivalent AssertJ
|
||||
`fail(...)` invocation. (Observe that without an impor cleanup this
|
||||
`fail(...)` invocation. (Observe that without an import cleanup this
|
||||
replacement would cause a compilation error.)
|
||||
- Extend the `@Repeated` match semantics such that it also covers non-varargs
|
||||
methods. For a motivating example see google/error-prone#568.
|
||||
- When matching explicit type references, also match super types. For a
|
||||
motivating example, see the two subtly difference loop definitions in
|
||||
`CollectionRemoveAllFromCollectionBlock`.
|
||||
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
|
||||
[bettercodehub]: https://bettercodehub.com
|
||||
[checkstyle-external-project-tests]: https://github.com/checkstyle/checkstyle/blob/master/wercker.yml
|
||||
[codecov]: https://codecov.io
|
||||
[error-prone-bug-patterns]: https://errorprone.info/bugpatterns
|
||||
[error-prone-criteria]: https://errorprone.info/docs/criteria
|
||||
[error-prone-fork-jitpack]: https://jitpack.io/#PicnicSupermarket/error-prone
|
||||
[error-prone-fork-repo]: https://github.com/PicnicSupermarket/error-prone
|
||||
[error-prone]: https://errorprone.info
|
||||
[error-prone-repo]: https://github.com/google/error-prone
|
||||
[forbidden-apis]: https://github.com/policeman-tools/forbidden-apis
|
||||
[fossa]: https://fossa.io
|
||||
[google-java-format]: https://github.com/google/google-java-format
|
||||
[maven]: https://maven.apache.org
|
||||
[main-contributing]: ../CONTRIBUTING.md
|
||||
[main-readme]: ../README.md
|
||||
[modernizer-maven-plugin]: https://github.com/gaul/modernizer-maven-plugin
|
||||
[sonarcloud]: https://sonarcloud.io
|
||||
[pitest]: https://pitest.org
|
||||
[pitest-maven]: https://pitest.org/quickstart/maven
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
<parent>
|
||||
<groupId>tech.picnic.error-prone-support</groupId>
|
||||
<artifactId>error-prone-support</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<version>0.15.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>error-prone-contrib</artifactId>
|
||||
|
||||
<name>Picnic :: Error Prone Support :: Contrib</name>
|
||||
<description>Extra Error Prone plugins by Picnic.</description>
|
||||
<url>https://error-prone.picnic.tech</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
@@ -37,28 +38,33 @@
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_test_helpers</artifactId>
|
||||
<scope>test</scope>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>refaster-resource-compiler</artifactId>
|
||||
<artifactId>documentation-support</artifactId>
|
||||
<!-- This dependency is declared only as a hint to Maven that
|
||||
compilation depends on it; see the `maven-compiler-plugin`'s
|
||||
`annotationProcessorPaths` configuration below. -->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>error-prone-utils</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>refaster-support</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>refaster-test-support</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto</groupId>
|
||||
<artifactId>auto-common</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -67,13 +73,8 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.errorprone</groupId>
|
||||
<artifactId>javac</artifactId>
|
||||
<groupId>com.google.auto.value</groupId>
|
||||
<artifactId>auto-value-annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -81,11 +82,6 @@
|
||||
<artifactId>guava</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.newrelic.agent.java</groupId>
|
||||
<artifactId>newrelic-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
@@ -101,6 +97,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>
|
||||
@@ -121,6 +122,11 @@
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.inject</groupId>
|
||||
<artifactId>javax.inject</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
@@ -137,15 +143,22 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-library</artifactId>
|
||||
<groupId>org.immutables</groupId>
|
||||
<artifactId>value-annotations</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jspecify</groupId>
|
||||
<artifactId>jspecify</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- XXX: Explicitly declared as a workaround for
|
||||
https://github.com/pitest/pitest-junit5-plugin/issues/105. -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
@@ -161,6 +174,36 @@
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
<artifactId>mongodb-driver-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openrewrite</groupId>
|
||||
<artifactId>rewrite-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openrewrite</groupId>
|
||||
<artifactId>rewrite-java</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openrewrite</groupId>
|
||||
<artifactId>rewrite-java-11</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openrewrite</groupId>
|
||||
<artifactId>rewrite-templating</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openrewrite</groupId>
|
||||
<artifactId>rewrite-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.reactivestreams</groupId>
|
||||
<artifactId>reactive-streams</artifactId>
|
||||
@@ -171,11 +214,6 @@
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-beans</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
@@ -201,6 +239,11 @@
|
||||
<artifactId>spring-boot-test</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testng</groupId>
|
||||
<artifactId>testng</artifactId>
|
||||
@@ -211,23 +254,22 @@
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.coveo</groupId>
|
||||
<artifactId>fmt-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<additionalSourceDirectories>
|
||||
<additionalSourceDirectory>${basedir}/src/test/resources</additionalSourceDirectory>
|
||||
</additionalSourceDirectories>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths combine.children="append">
|
||||
<!-- XXX: Drop the version declarations once
|
||||
properly supported. See
|
||||
https://youtrack.jetbrains.com/issue/IDEA-342187. -->
|
||||
<path>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>refaster-resource-compiler</artifactId>
|
||||
<artifactId>documentation-support</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>refaster-compiler</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
@@ -237,21 +279,10 @@
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
<compilerArgs combine.children="append">
|
||||
<arg>-Xplugin:RefasterRuleResourceCompiler</arg>
|
||||
<arg>-Xplugin:DocumentationGenerator -XoutputDirectory=${project.build.directory}/docs</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<configuration>
|
||||
<ignoredUnusedDeclaredDependencies>
|
||||
<!-- XXX: Figure out why the plugin thinks this
|
||||
dependency is unused. -->
|
||||
<ignoredUnusedDeclaredDependency>${project.groupId}:refaster-support</ignoredUnusedDeclaredDependency>
|
||||
</ignoredUnusedDeclaredDependencies>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.AnnotationTreeMatcher;
|
||||
@@ -17,24 +18,27 @@ import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import java.util.Map;
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
|
||||
/** A {@link BugChecker} which flags ambiguous {@code @JsonCreator}s in enums. */
|
||||
/** A {@link BugChecker} that flags ambiguous {@code @JsonCreator}s in enums. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "AmbiguousJsonCreator",
|
||||
summary = "`JsonCreator.Mode` should be set for single-argument creators",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.WARNING,
|
||||
tags = StandardTags.LIKELY_ERROR)
|
||||
public final class AmbiguousJsonCreatorCheck extends BugChecker implements AnnotationTreeMatcher {
|
||||
link = BUG_PATTERNS_BASE_URL + "AmbiguousJsonCreator",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class AmbiguousJsonCreator extends BugChecker implements AnnotationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<AnnotationTree> IS_JSON_CREATOR_ANNOTATION =
|
||||
isType("com.fasterxml.jackson.annotation.JsonCreator");
|
||||
|
||||
/** Instantiates a new {@link AmbiguousJsonCreator} instance. */
|
||||
public AmbiguousJsonCreator() {}
|
||||
|
||||
@Override
|
||||
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
|
||||
if (!IS_JSON_CREATOR_ANNOTATION.matches(tree, state)) {
|
||||
@@ -42,7 +46,7 @@ public final class AmbiguousJsonCreatorCheck extends BugChecker implements Annot
|
||||
}
|
||||
|
||||
ClassTree clazz = state.findEnclosing(ClassTree.class);
|
||||
if (clazz == null || clazz.getKind() != Tree.Kind.ENUM) {
|
||||
if (clazz == null || clazz.getKind() != Kind.ENUM) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.argument;
|
||||
import static com.google.errorprone.matchers.Matchers.argumentCount;
|
||||
import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.nullLiteral;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags AssertJ {@code isEqualTo(null)} checks for simplification.
|
||||
*
|
||||
* <p>This bug checker cannot be replaced with a simple Refaster rule, as the Refaster approach
|
||||
* would require that all overloads of {@link org.assertj.core.api.Assert#isEqualTo(Object)} (such
|
||||
* as {@link org.assertj.core.api.AbstractStringAssert#isEqualTo(String)}) are explicitly
|
||||
* enumerated. This bug checker generically matches all such current and future overloads.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Prefer `.isNull()` over `.isEqualTo(null)`",
|
||||
link = BUG_PATTERNS_BASE_URL + "AssertJIsNull",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class AssertJIsNull extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<MethodInvocationTree> ASSERT_IS_EQUAL_TO_NULL =
|
||||
allOf(
|
||||
instanceMethod().onDescendantOf("org.assertj.core.api.Assert").named("isEqualTo"),
|
||||
argumentCount(1),
|
||||
argument(0, nullLiteral()));
|
||||
|
||||
/** Instantiates a new {@link AssertJIsNull} instance. */
|
||||
public AssertJIsNull() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!ASSERT_IS_EQUAL_TO_NULL.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix.Builder fix =
|
||||
SuggestedFix.builder().merge(SuggestedFixes.renameMethodInvocation(tree, "isNull", state));
|
||||
tree.getArguments().forEach(arg -> fix.merge(SuggestedFix.delete(arg)));
|
||||
|
||||
return describeMatch(tree, fix.build());
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,46 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.MultiMatcher;
|
||||
import com.google.errorprone.matchers.MultiMatcher.MultiMatchResult;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import java.util.List;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/** A {@link BugChecker} which flags redundant {@code @Autowired} constructor annotations. */
|
||||
/** A {@link BugChecker} that flags redundant {@code @Autowired} constructor annotations. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "AutowiredConstructor",
|
||||
summary = "Omit `@Autowired` on a class' sole constructor, as it is redundant",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class AutowiredConstructorCheck extends BugChecker implements ClassTreeMatcher {
|
||||
link = BUG_PATTERNS_BASE_URL + "AutowiredConstructor",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class AutowiredConstructor extends BugChecker implements ClassTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final MultiMatcher<Tree, AnnotationTree> AUTOWIRED_ANNOTATION =
|
||||
annotations(AT_LEAST_ONE, isType("org.springframework.beans.factory.annotation.Autowired"));
|
||||
|
||||
/** Instantiates a new {@link AutowiredConstructor} instance. */
|
||||
public AutowiredConstructor() {}
|
||||
|
||||
@Override
|
||||
public Description matchClass(ClassTree tree, VisitorState state) {
|
||||
List<MethodTree> constructors = ASTHelpers.getConstructors(tree);
|
||||
@@ -43,20 +48,18 @@ public final class AutowiredConstructorCheck extends BugChecker implements Class
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
List<AnnotationTree> annotations =
|
||||
AUTOWIRED_ANNOTATION
|
||||
.multiMatchResult(Iterables.getOnlyElement(constructors), state)
|
||||
.matchingNodes();
|
||||
if (annotations.size() != 1) {
|
||||
MultiMatchResult<AnnotationTree> hasAutowiredAnnotation =
|
||||
AUTOWIRED_ANNOTATION.multiMatchResult(Iterables.getOnlyElement(constructors), state);
|
||||
if (!hasAutowiredAnnotation.matches()) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
/*
|
||||
* This is the only `@Autowired` constructor: suggest that it be removed. Note that this likely
|
||||
* means that the associated import can be removed as well. Rather than adding code for this case we
|
||||
* leave flagging the unused import to Error Prone's `RemoveUnusedImports` check.
|
||||
* means that the associated import can be removed as well. Rather than adding code for this
|
||||
* case we leave flagging the unused import to Error Prone's `RemoveUnusedImports` check.
|
||||
*/
|
||||
AnnotationTree annotation = Iterables.getOnlyElement(annotations);
|
||||
return describeMatch(annotation, SuggestedFix.delete(annotation));
|
||||
AnnotationTree annotation = hasAutowiredAnnotation.onlyMatchingNode();
|
||||
return describeMatch(annotation, SourceCode.deleteWithTrailingWhitespace(annotation, state));
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.AnnotationTreeMatcher;
|
||||
@@ -17,32 +19,34 @@ import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.AssignmentTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.NewArrayTree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/** A {@link BugChecker} which flags annotations that could be written more concisely. */
|
||||
/** A {@link BugChecker} that flags annotations that could be written more concisely. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "CanonicalAnnotationSyntax",
|
||||
summary = "Omit redundant syntax from annotation declarations",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class CanonicalAnnotationSyntaxCheck extends BugChecker
|
||||
implements AnnotationTreeMatcher {
|
||||
link = BUG_PATTERNS_BASE_URL + "CanonicalAnnotationSyntax",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class CanonicalAnnotationSyntax extends BugChecker implements AnnotationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Pattern TRAILING_ARRAY_COMMA = Pattern.compile(",\\s*}$");
|
||||
private static final ImmutableSet<BiFunction<AnnotationTree, VisitorState, Optional<Fix>>>
|
||||
FIX_FACTORIES =
|
||||
ImmutableSet.of(
|
||||
CanonicalAnnotationSyntaxCheck::dropRedundantParentheses,
|
||||
CanonicalAnnotationSyntaxCheck::dropRedundantValueAttribute,
|
||||
CanonicalAnnotationSyntaxCheck::dropRedundantCurlies);
|
||||
CanonicalAnnotationSyntax::dropRedundantParentheses,
|
||||
CanonicalAnnotationSyntax::dropRedundantValueAttribute,
|
||||
CanonicalAnnotationSyntax::dropRedundantCurlies);
|
||||
|
||||
/** Instantiates a new {@link CanonicalAnnotationSyntax} instance. */
|
||||
public CanonicalAnnotationSyntax() {}
|
||||
|
||||
@Override
|
||||
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
|
||||
@@ -86,8 +90,8 @@ public final class CanonicalAnnotationSyntaxCheck extends BugChecker
|
||||
ExpressionTree arg = args.get(0);
|
||||
if (state.getSourceForNode(arg) == null) {
|
||||
/*
|
||||
* The annotation argument isn't doesn't have a source representation, e.g. because `value`
|
||||
* isn't assigned to explicitly.
|
||||
* The annotation argument doesn't have a source representation, e.g. because `value` isn't
|
||||
* assigned explicitly.
|
||||
*/
|
||||
return Optional.empty();
|
||||
}
|
||||
@@ -102,7 +106,8 @@ public final class CanonicalAnnotationSyntaxCheck extends BugChecker
|
||||
return Optional.of(
|
||||
SuggestedFix.replace(
|
||||
arg,
|
||||
simplifyAttributeValue(expr, state).orElseGet(() -> Util.treeToString(expr, state))));
|
||||
simplifyAttributeValue(expr, state)
|
||||
.orElseGet(() -> SourceCode.treeToString(expr, state))));
|
||||
}
|
||||
|
||||
private static Optional<Fix> dropRedundantCurlies(AnnotationTree tree, VisitorState state) {
|
||||
@@ -113,7 +118,7 @@ public final class CanonicalAnnotationSyntaxCheck extends BugChecker
|
||||
* the expression as a whole.
|
||||
*/
|
||||
ExpressionTree value =
|
||||
(arg.getKind() == Kind.ASSIGNMENT) ? ((AssignmentTree) arg).getExpression() : arg;
|
||||
(arg instanceof AssignmentTree assignment) ? assignment.getExpression() : arg;
|
||||
|
||||
/* Store a fix for each expression that was successfully simplified. */
|
||||
simplifyAttributeValue(value, state)
|
||||
@@ -124,24 +129,21 @@ public final class CanonicalAnnotationSyntaxCheck extends BugChecker
|
||||
}
|
||||
|
||||
private static Optional<String> simplifyAttributeValue(ExpressionTree expr, VisitorState state) {
|
||||
if (expr.getKind() != Kind.NEW_ARRAY) {
|
||||
/* There are no curly braces or commas to be dropped here. */
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
NewArrayTree array = (NewArrayTree) expr;
|
||||
return simplifySingletonArray(array, state).or(() -> dropTrailingComma(array, state));
|
||||
/* Drop curly braces or commas if possible. */
|
||||
return expr instanceof NewArrayTree newArray
|
||||
? simplifySingletonArray(newArray, state).or(() -> dropTrailingComma(newArray, state))
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
/** Returns the expression describing the array's sole element, if any. */
|
||||
private static Optional<String> simplifySingletonArray(NewArrayTree array, VisitorState state) {
|
||||
return Optional.of(array.getInitializers())
|
||||
.filter(initializers -> initializers.size() == 1)
|
||||
.map(initializers -> Util.treeToString(initializers.get(0), state));
|
||||
.map(initializers -> SourceCode.treeToString(initializers.get(0), state));
|
||||
}
|
||||
|
||||
private static Optional<String> dropTrailingComma(NewArrayTree array, VisitorState state) {
|
||||
String src = Util.treeToString(array, state);
|
||||
String src = SourceCode.treeToString(array, state);
|
||||
return Optional.of(TRAILING_ARRAY_COMMA.matcher(src))
|
||||
.filter(Matcher::find)
|
||||
.map(m -> src.substring(0, m.start()) + '}');
|
||||
@@ -0,0 +1,91 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anything;
|
||||
import static com.google.errorprone.matchers.Matchers.classLiteral;
|
||||
import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.receiverOfInvocation;
|
||||
import static com.google.errorprone.matchers.Matchers.toType;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Var;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.BinaryTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.util.TreePath;
|
||||
import com.sun.tools.javac.code.Symbol.MethodSymbol;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags invocations of {@link Class#getName()} where {@link
|
||||
* Class#getCanonicalName()} was likely meant.
|
||||
*
|
||||
* <p>For top-level types these two methods generally return the same result, but for nested types
|
||||
* the former separates identifiers using a dollar sign ({@code $}) rather than a dot ({@code .}).
|
||||
*
|
||||
* @implNote This check currently only flags {@link Class#getName()} invocations on class literals,
|
||||
* and doesn't flag method references. This avoids false positives, such as suggesting use of
|
||||
* {@link Class#getCanonicalName()} in contexts where the canonical name is {@code null}.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "This code should likely use the type's canonical name",
|
||||
link = BUG_PATTERNS_BASE_URL + "CanonicalClassNameUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class CanonicalClassNameUsage extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> GET_NAME_INVOCATION =
|
||||
toType(
|
||||
MethodInvocationTree.class,
|
||||
allOf(
|
||||
receiverOfInvocation(classLiteral(anything())),
|
||||
instanceMethod().onExactClass(Class.class.getCanonicalName()).named("getName")));
|
||||
private static final Pattern CANONICAL_NAME_USING_TYPES =
|
||||
Pattern.compile("(com\\.google\\.errorprone|tech\\.picnic\\.errorprone)\\..*");
|
||||
|
||||
/** Instantiates a new {@link CanonicalClassNameUsage} instance. */
|
||||
public CanonicalClassNameUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!GET_NAME_INVOCATION.matches(tree, state) || !isPassedToCanonicalNameUsingType(state)) {
|
||||
/*
|
||||
* This is not a `class.getName()` invocation of which the result is passed to another method
|
||||
* known to accept canonical type names.
|
||||
*/
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return describeMatch(
|
||||
tree, SuggestedFixes.renameMethodInvocation(tree, "getCanonicalName", state));
|
||||
}
|
||||
|
||||
private static boolean isPassedToCanonicalNameUsingType(VisitorState state) {
|
||||
@Var TreePath path = state.getPath().getParentPath();
|
||||
while (path.getLeaf() instanceof BinaryTree) {
|
||||
path = path.getParentPath();
|
||||
}
|
||||
|
||||
return path.getLeaf() instanceof MethodInvocationTree methodInvocation
|
||||
&& isOwnedByCanonicalNameUsingType(ASTHelpers.getSymbol(methodInvocation));
|
||||
}
|
||||
|
||||
private static boolean isOwnedByCanonicalNameUsingType(MethodSymbol symbol) {
|
||||
return CANONICAL_NAME_USING_TYPES.matcher(symbol.owner.getQualifiedName()).matches();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
import tech.picnic.errorprone.utils.ThirdPartyLibrary;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@link Collector Collectors} that don't clearly express
|
||||
* (im)mutability.
|
||||
*
|
||||
* <p>Replacing such collectors with alternatives that produce immutable collections is preferred.
|
||||
* Do note that Guava's immutable collections are null-hostile.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"Avoid `Collectors.to{List,Map,Set}` in favor of collectors that emphasize (im)mutability",
|
||||
link = BUG_PATTERNS_BASE_URL + "CollectorMutability",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class CollectorMutability extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> COLLECTOR_METHOD =
|
||||
staticMethod().onClass(Collectors.class.getCanonicalName());
|
||||
private static final Matcher<ExpressionTree> LIST_COLLECTOR =
|
||||
staticMethod().anyClass().named("toList");
|
||||
private static final Matcher<ExpressionTree> MAP_COLLECTOR =
|
||||
staticMethod().anyClass().named("toMap");
|
||||
private static final Matcher<ExpressionTree> SET_COLLECTOR =
|
||||
staticMethod().anyClass().named("toSet");
|
||||
|
||||
/** Instantiates a new {@link CollectorMutability} instance. */
|
||||
public CollectorMutability() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!ThirdPartyLibrary.GUAVA.isIntroductionAllowed(state)
|
||||
|| !COLLECTOR_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (LIST_COLLECTOR.matches(tree, state)) {
|
||||
return suggestToCollectionAlternatives(
|
||||
tree,
|
||||
ImmutableList.class.getCanonicalName() + ".toImmutableList",
|
||||
ArrayList.class.getCanonicalName(),
|
||||
state);
|
||||
}
|
||||
|
||||
if (MAP_COLLECTOR.matches(tree, state)) {
|
||||
return suggestToMapAlternatives(tree, state);
|
||||
}
|
||||
|
||||
if (SET_COLLECTOR.matches(tree, state)) {
|
||||
return suggestToCollectionAlternatives(
|
||||
tree,
|
||||
ImmutableSet.class.getCanonicalName() + ".toImmutableSet",
|
||||
HashSet.class.getCanonicalName(),
|
||||
state);
|
||||
}
|
||||
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
private Description suggestToCollectionAlternatives(
|
||||
MethodInvocationTree tree,
|
||||
String immutableReplacement,
|
||||
String mutableReplacement,
|
||||
VisitorState state) {
|
||||
SuggestedFix.Builder mutableFix = SuggestedFix.builder();
|
||||
String toCollectionSelect =
|
||||
SuggestedFixes.qualifyStaticImport(
|
||||
Collectors.class.getCanonicalName() + ".toCollection", mutableFix, state);
|
||||
String mutableCollection = SuggestedFixes.qualifyType(state, mutableFix, mutableReplacement);
|
||||
|
||||
return buildDescription(tree)
|
||||
.addFix(replaceMethodInvocation(tree, immutableReplacement, state))
|
||||
.addFix(
|
||||
mutableFix
|
||||
.replace(tree, String.format("%s(%s::new)", toCollectionSelect, mutableCollection))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Description suggestToMapAlternatives(MethodInvocationTree tree, VisitorState state) {
|
||||
int argCount = tree.getArguments().size();
|
||||
if (argCount > 3) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix.Builder mutableFix = SuggestedFix.builder();
|
||||
String hashMap =
|
||||
SuggestedFixes.qualifyType(state, mutableFix, HashMap.class.getCanonicalName());
|
||||
|
||||
return buildDescription(tree)
|
||||
.addFix(
|
||||
replaceMethodInvocation(
|
||||
tree, ImmutableMap.class.getCanonicalName() + ".toImmutableMap", state))
|
||||
.addFix(
|
||||
mutableFix
|
||||
.postfixWith(
|
||||
tree.getArguments().get(argCount - 1),
|
||||
(argCount == 2 ? ", (a, b) -> { throw new IllegalStateException(); }" : "")
|
||||
+ String.format(", %s::new", hashMap))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static SuggestedFix replaceMethodInvocation(
|
||||
MethodInvocationTree tree, String fullyQualifiedReplacement, VisitorState state) {
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder();
|
||||
String replacement = SuggestedFixes.qualifyStaticImport(fullyQualifiedReplacement, fix, state);
|
||||
fix.merge(SuggestedFix.replace(tree.getMethodSelect(), replacement));
|
||||
return fix.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.argument;
|
||||
import static com.google.errorprone.matchers.Matchers.isSameType;
|
||||
import static com.google.errorprone.matchers.Matchers.isVariable;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static com.google.errorprone.matchers.Matchers.returnStatement;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.toType;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.BlockTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AssignmentTree;
|
||||
import com.sun.source.tree.BlockTree;
|
||||
import com.sun.source.tree.ExpressionStatementTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.IdentifierTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.ReturnTree;
|
||||
import com.sun.source.tree.StatementTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.TryTree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import com.sun.source.util.TreeScanner;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.utils.MoreASTHelpers;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags unnecessary local variable assignments preceding a return
|
||||
* statement.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Variable assignment is redundant; value can be returned directly",
|
||||
link = BUG_PATTERNS_BASE_URL + "DirectReturn",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class DirectReturn extends BugChecker implements BlockTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<StatementTree> VARIABLE_RETURN = returnStatement(isVariable());
|
||||
private static final Matcher<ExpressionTree> MOCKITO_MOCK_OR_SPY_WITH_IMPLICIT_TYPE =
|
||||
allOf(
|
||||
not(
|
||||
toType(
|
||||
MethodInvocationTree.class,
|
||||
argument(0, isSameType(Class.class.getCanonicalName())))),
|
||||
staticMethod().onClass("org.mockito.Mockito").namedAnyOf("mock", "spy"));
|
||||
|
||||
/** Instantiates a new {@link DirectReturn} instance. */
|
||||
public DirectReturn() {}
|
||||
|
||||
@Override
|
||||
public Description matchBlock(BlockTree tree, VisitorState state) {
|
||||
List<? extends StatementTree> statements = tree.getStatements();
|
||||
if (statements.size() < 2) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
StatementTree finalStatement = statements.get(statements.size() - 1);
|
||||
if (!VARIABLE_RETURN.matches(finalStatement, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
Symbol variableSymbol = ASTHelpers.getSymbol(((ReturnTree) finalStatement).getExpression());
|
||||
StatementTree precedingStatement = statements.get(statements.size() - 2);
|
||||
|
||||
return tryMatchAssignment(variableSymbol, precedingStatement)
|
||||
.filter(
|
||||
resultExpr ->
|
||||
canInlineToReturnStatement(resultExpr, state)
|
||||
&& !isIdentifierSymbolReferencedInAssociatedFinallyBlock(variableSymbol, state))
|
||||
.map(
|
||||
resultExpr ->
|
||||
describeMatch(
|
||||
precedingStatement,
|
||||
SuggestedFix.builder()
|
||||
.replace(
|
||||
precedingStatement,
|
||||
String.format("return %s;", SourceCode.treeToString(resultExpr, state)))
|
||||
.delete(finalStatement)
|
||||
.build()))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
private static Optional<ExpressionTree> tryMatchAssignment(Symbol targetSymbol, Tree tree) {
|
||||
if (tree instanceof ExpressionStatementTree expressionStatement) {
|
||||
return tryMatchAssignment(targetSymbol, expressionStatement.getExpression());
|
||||
}
|
||||
|
||||
if (tree instanceof AssignmentTree assignment) {
|
||||
return targetSymbol.equals(ASTHelpers.getSymbol(assignment.getVariable()))
|
||||
? Optional.of(assignment.getExpression())
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
if (tree instanceof VariableTree declaration) {
|
||||
return declaration.getModifiers().getAnnotations().isEmpty()
|
||||
&& targetSymbol.equals(ASTHelpers.getSymbol(declaration))
|
||||
? Optional.ofNullable(declaration.getInitializer())
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether inlining the given expression to the associated return statement can likely be
|
||||
* done without changing the expression's return type.
|
||||
*
|
||||
* <p>Inlining an expression generally does not change its return type, but in rare cases the
|
||||
* operation may have a functional impact. The sole case considered here is the inlining of a
|
||||
* Mockito mock or spy construction without an explicit type. In such a case the type created
|
||||
* depends on context, such as the method's return type.
|
||||
*/
|
||||
private static boolean canInlineToReturnStatement(
|
||||
ExpressionTree expressionTree, VisitorState state) {
|
||||
return !MOCKITO_MOCK_OR_SPY_WITH_IMPLICIT_TYPE.matches(expressionTree, state)
|
||||
|| MoreASTHelpers.findMethodExitedOnReturn(state)
|
||||
.filter(m -> MoreASTHelpers.areSameType(expressionTree, m.getReturnType(), state))
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the given identifier {@link Symbol} is referenced in a {@code finally} block that
|
||||
* is executed <em>after</em> control flow returns from the {@link VisitorState#getPath() current
|
||||
* location}.
|
||||
*/
|
||||
private static boolean isIdentifierSymbolReferencedInAssociatedFinallyBlock(
|
||||
Symbol symbol, VisitorState state) {
|
||||
return Streams.zip(
|
||||
Streams.stream(state.getPath()).skip(1),
|
||||
Streams.stream(state.getPath()),
|
||||
(tree, child) -> {
|
||||
if (!(tree instanceof TryTree tryTree)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BlockTree finallyBlock = tryTree.getFinallyBlock();
|
||||
return !child.equals(finallyBlock) ? finallyBlock : null;
|
||||
})
|
||||
.anyMatch(finallyBlock -> referencesIdentifierSymbol(symbol, finallyBlock));
|
||||
}
|
||||
|
||||
private static boolean referencesIdentifierSymbol(Symbol symbol, @Nullable BlockTree tree) {
|
||||
return Boolean.TRUE.equals(
|
||||
new TreeScanner<Boolean, @Nullable Void>() {
|
||||
@Override
|
||||
public Boolean visitIdentifier(IdentifierTree node, @Nullable Void unused) {
|
||||
return symbol.equals(ASTHelpers.getSymbol(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean reduce(Boolean r1, Boolean r2) {
|
||||
return Boolean.TRUE.equals(r1) || Boolean.TRUE.equals(r2);
|
||||
}
|
||||
}.scan(tree, null));
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,50 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.tools.javac.code.Symbol.MethodSymbol;
|
||||
import java.util.Optional;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/** A {@link BugChecker} which flags empty methods that seemingly can simply be deleted. */
|
||||
/** A {@link BugChecker} that flags empty methods that seemingly can simply be deleted. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "EmptyMethod",
|
||||
summary = "Empty method can likely be deleted",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class EmptyMethodCheck extends BugChecker implements MethodTreeMatcher {
|
||||
link = BUG_PATTERNS_BASE_URL + "EmptyMethod",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class EmptyMethod extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<Tree> PERMITTED_ANNOTATION =
|
||||
annotations(
|
||||
AT_LEAST_ONE,
|
||||
anyOf(isType("java.lang.Override"), isType("org.aspectj.lang.annotation.Pointcut")));
|
||||
anyOf(
|
||||
isType(Override.class.getCanonicalName()),
|
||||
isType("org.aspectj.lang.annotation.Pointcut")));
|
||||
|
||||
/** Instantiates a new {@link EmptyMethod} instance. */
|
||||
public EmptyMethod() {}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("java:S1067" /* Chaining disjunctions like this does not impact readability. */)
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (tree.getBody() == null
|
||||
|| !tree.getBody().getStatements().isEmpty()
|
||||
@@ -48,12 +54,11 @@ public final class EmptyMethodCheck extends BugChecker implements MethodTreeMatc
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
MethodSymbol sym = ASTHelpers.getSymbol(tree);
|
||||
if (sym == null || ASTHelpers.methodCanBeOverridden(sym)) {
|
||||
if (ASTHelpers.methodCanBeOverridden(ASTHelpers.getSymbol(tree))) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return describeMatch(tree, SuggestedFix.delete(tree));
|
||||
return describeMatch(tree, SourceCode.deleteWithTrailingWhitespace(tree, state));
|
||||
}
|
||||
|
||||
private static boolean isInPossibleTestHelperClass(VisitorState state) {
|
||||
@@ -2,8 +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.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static java.util.stream.Collectors.collectingAndThen;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -12,9 +16,6 @@ import com.google.common.collect.Multimaps;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
@@ -30,21 +31,23 @@ import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} which flags {@link Ordering#explicit(Object, Object[])}} invocations listing
|
||||
* A {@link BugChecker} that flags {@link Ordering#explicit(Object, Object[])}} invocations listing
|
||||
* a subset of an enum type's values.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "ExplicitEnumOrdering",
|
||||
summary = "Make sure `Ordering#explicit` lists all of an enum's values",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.WARNING,
|
||||
tags = StandardTags.FRAGILE_CODE)
|
||||
public final class ExplicitEnumOrderingCheck extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
link = BUG_PATTERNS_BASE_URL + "ExplicitEnumOrdering",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class ExplicitEnumOrdering extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> EXPLICIT_ORDERING =
|
||||
staticMethod().onClass(Ordering.class.getName()).named("explicit");
|
||||
staticMethod().onClass(Ordering.class.getCanonicalName()).named("explicit");
|
||||
|
||||
/** Instantiates a new {@link ExplicitEnumOrdering} instance. */
|
||||
public ExplicitEnumOrdering() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
@@ -69,11 +72,11 @@ public final class ExplicitEnumOrderingCheck extends BugChecker
|
||||
List<? extends ExpressionTree> expressions) {
|
||||
return expressions.stream()
|
||||
.map(ASTHelpers::getSymbol)
|
||||
.filter(Symbol::isEnum)
|
||||
.filter(s -> s != null && s.isEnum())
|
||||
.collect(
|
||||
collectingAndThen(
|
||||
toImmutableSetMultimap(Symbol::asType, Symbol::toString),
|
||||
ExplicitEnumOrderingCheck::getMissingEnumValues));
|
||||
ExplicitEnumOrdering::getMissingEnumValues));
|
||||
}
|
||||
|
||||
private static ImmutableSet<String> getMissingEnumValues(
|
||||
@@ -86,6 +89,6 @@ public final class ExplicitEnumOrderingCheck extends BugChecker
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.generic;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.subOf;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.type;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.unbound;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MemberReferenceTreeMatcher;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Supplier;
|
||||
import com.google.errorprone.suppliers.Suppliers;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MemberReferenceTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.util.function.Function;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags usages of {@link Flux#flatMap(Function)} and {@link
|
||||
* Flux#flatMapSequential(Function)}.
|
||||
*
|
||||
* <p>{@link Flux#flatMap(Function)} and {@link Flux#flatMapSequential(Function)} eagerly perform up
|
||||
* to {@link reactor.util.concurrent.Queues#SMALL_BUFFER_SIZE} subscriptions. Additionally, the
|
||||
* former interleaves values as they are emitted, yielding nondeterministic results. In most cases
|
||||
* {@link Flux#concatMap(Function)} should be preferred, as it produces consistent results and
|
||||
* avoids potentially saturating the thread pool on which subscription happens. If {@code
|
||||
* concatMap}'s sequential-subscription semantics are undesirable one should invoke a {@code
|
||||
* flatMap} or {@code flatMapSequential} overload with an explicit concurrency level.
|
||||
*
|
||||
* <p>NB: The rarely-used overload {@link Flux#flatMap(Function, Function,
|
||||
* java.util.function.Supplier)} is not flagged by this check because there is no clear alternative
|
||||
* to point to.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"""
|
||||
`Flux#flatMap` and `Flux#flatMapSequential` have subtle semantics; please use \
|
||||
`Flux#concatMap` or explicitly specify the desired amount of concurrency""",
|
||||
link = BUG_PATTERNS_BASE_URL + "FluxFlatMapUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = ERROR,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class FluxFlatMapUsage extends BugChecker
|
||||
implements MethodInvocationTreeMatcher, MemberReferenceTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String MAX_CONCURRENCY_ARG_NAME = "MAX_CONCURRENCY";
|
||||
private static final Supplier<Type> FLUX =
|
||||
Suppliers.typeFromString("reactor.core.publisher.Flux");
|
||||
private static final Matcher<ExpressionTree> FLUX_FLATMAP =
|
||||
instanceMethod()
|
||||
.onDescendantOf(FLUX)
|
||||
.namedAnyOf("flatMap", "flatMapSequential")
|
||||
.withParameters(Function.class.getCanonicalName());
|
||||
private static final Supplier<Type> FLUX_OF_PUBLISHERS =
|
||||
VisitorState.memoize(
|
||||
generic(FLUX, subOf(generic(type("org.reactivestreams.Publisher"), unbound()))));
|
||||
|
||||
/** Instantiates a new {@link FluxFlatMapUsage} instance. */
|
||||
public FluxFlatMapUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!FLUX_FLATMAP.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix serializationFix = SuggestedFixes.renameMethodInvocation(tree, "concatMap", state);
|
||||
SuggestedFix concurrencyCapFix =
|
||||
SuggestedFix.postfixWith(
|
||||
Iterables.getOnlyElement(tree.getArguments()), ", " + MAX_CONCURRENCY_ARG_NAME);
|
||||
|
||||
Description.Builder description = buildDescription(tree);
|
||||
|
||||
if (state.getTypes().isSubtype(ASTHelpers.getType(tree), FLUX_OF_PUBLISHERS.get(state))) {
|
||||
/*
|
||||
* Nested publishers may need to be subscribed to eagerly in order to avoid a deadlock, e.g.
|
||||
* if they are produced by `Flux#groupBy`. In this case we suggest specifying an explicit
|
||||
* concurrently bound, in favour of sequential subscriptions using `Flux#concatMap`.
|
||||
*/
|
||||
description.addFix(concurrencyCapFix).addFix(serializationFix);
|
||||
} else {
|
||||
description.addFix(serializationFix).addFix(concurrencyCapFix);
|
||||
}
|
||||
|
||||
return description.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) {
|
||||
if (!FLUX_FLATMAP.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
// Method references are expected to occur very infrequently; generating both variants of
|
||||
// suggested fixes is not worth the trouble.
|
||||
return describeMatch(tree);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MemberReferenceTreeMatcher;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MemberReferenceTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} which flags usages of {@link Flux#flatMap(Function)} and {@link
|
||||
* Flux#flatMapSequential(Function)}.
|
||||
*
|
||||
* <p>{@link Flux#flatMap(Function)} and {@link Flux#flatMapSequential(Function)} eagerly perform up
|
||||
* to {@link reactor.util.concurrent.Queues#SMALL_BUFFER_SIZE} subscriptions. Additionally, the
|
||||
* former interleaves values as they are emitted, yielding nondeterministic results. In most cases
|
||||
* {@link Flux#concatMap(Function)} should be preferred, as it produces consistent results and
|
||||
* avoids potentially saturating the thread pool on which subscription happens. If {@code
|
||||
* concatMap}'s single-subscription semantics are undesirable one should invoke a {@code flatMap} or
|
||||
* {@code flatMapSequential} overload with an explicit concurrency level.
|
||||
*
|
||||
* <p>NB: The rarely-used overload {@link Flux#flatMap(Function, Function, Supplier)} is not flagged
|
||||
* by this check because there is no clear alternative to point to.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "FluxFlatMapUsage",
|
||||
summary =
|
||||
"`Flux#flatMap` and `Flux#flatMapSequential` have subtle semantics; "
|
||||
+ "please use `Flux#concatMap` or explicitly specify the desired amount of concurrency",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.ERROR,
|
||||
tags = StandardTags.LIKELY_ERROR)
|
||||
public final class FluxFlatMapUsageCheck extends BugChecker
|
||||
implements MethodInvocationTreeMatcher, MemberReferenceTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String MAX_CONCURRENCY_ARG_NAME = "MAX_CONCURRENCY";
|
||||
private static final Matcher<ExpressionTree> FLUX_FLATMAP =
|
||||
instanceMethod()
|
||||
.onDescendantOf("reactor.core.publisher.Flux")
|
||||
.namedAnyOf("flatMap", "flatMapSequential")
|
||||
.withParameters(Function.class.getName());
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!FLUX_FLATMAP.matches(tree, state)) {
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) {
|
||||
if (!FLUX_FLATMAP.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
// Method references are expected to occur very infrequently; generating both variants of
|
||||
// suggested fixes is not worth the trouble.
|
||||
return describeMatch(tree);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.CONCURRENCY;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.PERFORMANCE;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Supplier;
|
||||
import com.google.errorprone.suppliers.Suppliers;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import com.sun.tools.javac.util.Position;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import tech.picnic.errorprone.utils.ThirdPartyLibrary;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@link reactor.core.publisher.Flux} operator usages that may
|
||||
* implicitly cause the calling thread to be blocked.
|
||||
*
|
||||
* <p>Note that the methods flagged here are not themselves blocking, but iterating over the
|
||||
* resulting {@link Iterable} or {@link Stream} may be.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Avoid iterating over `Flux`es in an implicitly blocking manner",
|
||||
link = BUG_PATTERNS_BASE_URL + "FluxImplicitBlock",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = {CONCURRENCY, PERFORMANCE})
|
||||
public final class FluxImplicitBlock extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> FLUX_WITH_IMPLICIT_BLOCK =
|
||||
instanceMethod()
|
||||
.onDescendantOf("reactor.core.publisher.Flux")
|
||||
.namedAnyOf("toIterable", "toStream")
|
||||
.withNoParameters();
|
||||
private static final Supplier<Type> STREAM =
|
||||
Suppliers.typeFromString(Stream.class.getCanonicalName());
|
||||
|
||||
/** Instantiates a new {@link FluxImplicitBlock} instance. */
|
||||
public FluxImplicitBlock() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!FLUX_WITH_IMPLICIT_BLOCK.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
Description.Builder description =
|
||||
buildDescription(tree).addFix(SuggestedFixes.addSuppressWarnings(state, canonicalName()));
|
||||
if (ThirdPartyLibrary.GUAVA.isIntroductionAllowed(state)) {
|
||||
description.addFix(
|
||||
suggestBlockingElementCollection(
|
||||
tree, ImmutableList.class.getCanonicalName() + ".toImmutableList", state));
|
||||
}
|
||||
description.addFix(
|
||||
suggestBlockingElementCollection(
|
||||
tree, Collectors.class.getCanonicalName() + ".toList", state));
|
||||
|
||||
return description.build();
|
||||
}
|
||||
|
||||
private static SuggestedFix suggestBlockingElementCollection(
|
||||
MethodInvocationTree tree, String fullyQualifiedCollectorMethod, VisitorState state) {
|
||||
SuggestedFix.Builder importSuggestion = SuggestedFix.builder();
|
||||
String replacementMethodInvocation =
|
||||
SuggestedFixes.qualifyStaticImport(fullyQualifiedCollectorMethod, importSuggestion, state);
|
||||
|
||||
boolean isStream =
|
||||
ASTHelpers.isSubtype(ASTHelpers.getResultType(tree), STREAM.get(state), state);
|
||||
String replacement =
|
||||
String.format(
|
||||
".collect(%s()).block()%s", replacementMethodInvocation, isStream ? ".stream()" : "");
|
||||
return importSuggestion.merge(replaceMethodInvocation(tree, replacement, state)).build();
|
||||
}
|
||||
|
||||
private static SuggestedFix.Builder replaceMethodInvocation(
|
||||
MethodInvocationTree tree, String replacement, VisitorState state) {
|
||||
int startPosition = state.getEndPosition(ASTHelpers.getReceiver(tree));
|
||||
int endPosition = state.getEndPosition(tree);
|
||||
|
||||
checkState(
|
||||
startPosition != Position.NOPOS && endPosition != Position.NOPOS,
|
||||
"Cannot locate method to be replaced in source code");
|
||||
|
||||
return SuggestedFix.builder().replace(startPosition, endPosition, replacement);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anyMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
@@ -7,12 +10,12 @@ import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Verify;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
@@ -29,12 +32,15 @@ import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import com.sun.source.util.SimpleTreeVisitor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Formatter;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} which flags string concatenations that produce a format string; in such
|
||||
* cases the string concatenation should instead be deferred to the invoked method.
|
||||
* A {@link BugChecker} that flags string concatenations that produce a format string; in such cases
|
||||
* the string concatenation should instead be deferred to the invoked method.
|
||||
*
|
||||
* @implNote This checker is based on the implementation of {@link
|
||||
* com.google.errorprone.bugpatterns.flogger.FloggerStringConcatenation}.
|
||||
@@ -44,18 +50,19 @@ import java.util.Optional;
|
||||
// should introduce special handling of `Formattable` arguments, as this check would replace a
|
||||
// `Formattable#toString` invocation with a `Formattable#formatTo` invocation. But likely that
|
||||
// should be considered a bug fix, too.
|
||||
// XXX: Introduce a separate check which adds/removes the `Locale` parameter to `String.format`
|
||||
// invocations, as necessary.
|
||||
// XXX: Introduce a separate check that adds/removes the `Locale` parameter to `String.format`
|
||||
// invocations, as necessary. See also a comment in the `StringJoin` check.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "FormatStringConcatenation",
|
||||
summary = "Defer string concatenation to the invoked method",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.WARNING,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class FormatStringConcatenationCheck extends BugChecker
|
||||
link = BUG_PATTERNS_BASE_URL + "FormatStringConcatenation",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class FormatStringConcatenation extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* AssertJ exposes varargs {@code fail} methods with a {@link Throwable}-accepting overload, the
|
||||
* latter of which should not be flagged.
|
||||
@@ -64,7 +71,8 @@ public final class FormatStringConcatenationCheck extends BugChecker
|
||||
anyMethod()
|
||||
.anyClass()
|
||||
.withAnyName()
|
||||
.withParameters(String.class.getName(), Throwable.class.getName());
|
||||
.withParameters(String.class.getCanonicalName(), Throwable.class.getCanonicalName());
|
||||
|
||||
// XXX: Drop some of these methods if we use Refaster to replace some with others.
|
||||
private static final Matcher<ExpressionTree> ASSERTJ_FORMAT_METHOD =
|
||||
anyOf(
|
||||
@@ -113,19 +121,22 @@ public final class FormatStringConcatenationCheck extends BugChecker
|
||||
private static final Matcher<ExpressionTree> GUAVA_FORMAT_METHOD =
|
||||
anyOf(
|
||||
staticMethod()
|
||||
.onClass("com.google.common.base.Preconditions")
|
||||
.onClass(Preconditions.class.getCanonicalName())
|
||||
.namedAnyOf("checkArgument", "checkNotNull", "checkState"),
|
||||
staticMethod().onClass("com.google.common.base.Verify").named("verify"));
|
||||
staticMethod().onClass(Verify.class.getCanonicalName()).named("verify"));
|
||||
// XXX: Add `PrintWriter`, maybe others.
|
||||
private static final Matcher<ExpressionTree> JDK_FORMAT_METHOD =
|
||||
anyOf(
|
||||
staticMethod().onClass("java.lang.String").named("format"),
|
||||
instanceMethod().onExactClass("java.util.Formatter").named("format"));
|
||||
staticMethod().onClass(String.class.getCanonicalName()).named("format"),
|
||||
instanceMethod().onExactClass(Formatter.class.getCanonicalName()).named("format"));
|
||||
private static final Matcher<ExpressionTree> SLF4J_FORMAT_METHOD =
|
||||
instanceMethod()
|
||||
.onDescendantOf("org.slf4j.Logger")
|
||||
.namedAnyOf("debug", "error", "info", "trace", "warn");
|
||||
|
||||
/** Instantiates a new {@link FormatStringConcatenation} instance. */
|
||||
public FormatStringConcatenation() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (hasNonConstantStringConcatenationArgument(tree, 0, state)) {
|
||||
@@ -201,7 +212,7 @@ public final class FormatStringConcatenationCheck 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,7 +222,7 @@ public final class FormatStringConcatenationCheck extends BugChecker
|
||||
}
|
||||
|
||||
@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);
|
||||
@@ -223,19 +234,19 @@ public final class FormatStringConcatenationCheck extends BugChecker
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitParenthesized(ParenthesizedTree tree, VisitorState state) {
|
||||
public @Nullable Void visitParenthesized(ParenthesizedTree tree, VisitorState state) {
|
||||
return tree.getExpression().accept(this, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void defaultAction(Tree tree, VisitorState state) {
|
||||
protected @Nullable Void defaultAction(Tree tree, VisitorState state) {
|
||||
appendExpression(tree);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void appendExpression(Tree tree) {
|
||||
if (tree instanceof LiteralTree) {
|
||||
formatString.append(((LiteralTree) tree).getValue());
|
||||
if (tree instanceof LiteralTree literal) {
|
||||
formatString.append(literal.getValue());
|
||||
} else {
|
||||
formatString.append(formatSpecifier);
|
||||
formatArguments.add(tree);
|
||||
@@ -246,7 +257,7 @@ public final class FormatStringConcatenationCheck extends BugChecker
|
||||
return state.getConstantExpression(formatString.toString())
|
||||
+ ", "
|
||||
+ formatArguments.stream()
|
||||
.map(tree -> Util.treeToString(tree, state))
|
||||
.map(tree -> SourceCode.treeToString(tree, state))
|
||||
.collect(joining(", "));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static com.google.errorprone.suppliers.Suppliers.OBJECT_TYPE;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableBiMap;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableMultiset;
|
||||
import com.google.common.collect.ImmutableRangeMap;
|
||||
import com.google.common.collect.ImmutableRangeSet;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSetMultimap;
|
||||
import com.google.common.collect.ImmutableTable;
|
||||
import com.google.common.primitives.Primitives;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.bugpatterns.TypesWithUndefinedEquality;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.matchers.Matchers;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.google.errorprone.util.ASTHelpers.TargetType;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import com.sun.tools.javac.code.Types;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/** A {@link BugChecker} that flags redundant identity conversions. */
|
||||
// XXX: Consider detecting cases where a flagged expression is passed to a method, and where removal
|
||||
// of the identity conversion would cause a different method overload to be selected. Depending on
|
||||
// the target method such a modification may change the code's semantics or performance.
|
||||
// XXX: Also flag `Stream#map`, `Mono#map` and `Flux#map` invocations where the given transformation
|
||||
// is effectively the identity operation.
|
||||
// XXX: Also flag nullary instance method invocations that represent an identity conversion, such as
|
||||
// `Boolean#booleanValue()`, `Byte#byteValue()` and friends.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Avoid or clarify identity conversions",
|
||||
link = BUG_PATTERNS_BASE_URL + "IdentityConversion",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class IdentityConversion extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> IS_CONVERSION_METHOD =
|
||||
anyOf(
|
||||
staticMethod()
|
||||
.onClassAny(
|
||||
Primitives.allWrapperTypes().stream()
|
||||
.map(Class::getName)
|
||||
.collect(toImmutableSet()))
|
||||
.named("valueOf"),
|
||||
staticMethod().onClass(String.class.getCanonicalName()).named("valueOf"),
|
||||
staticMethod()
|
||||
.onClassAny(
|
||||
ImmutableBiMap.class.getCanonicalName(),
|
||||
ImmutableList.class.getCanonicalName(),
|
||||
ImmutableListMultimap.class.getCanonicalName(),
|
||||
ImmutableMap.class.getCanonicalName(),
|
||||
ImmutableMultimap.class.getCanonicalName(),
|
||||
ImmutableMultiset.class.getCanonicalName(),
|
||||
ImmutableRangeMap.class.getCanonicalName(),
|
||||
ImmutableRangeSet.class.getCanonicalName(),
|
||||
ImmutableSet.class.getCanonicalName(),
|
||||
ImmutableSetMultimap.class.getCanonicalName(),
|
||||
ImmutableTable.class.getCanonicalName())
|
||||
.named("copyOf"),
|
||||
staticMethod().onClass(Matchers.class.getCanonicalName()).namedAnyOf("allOf", "anyOf"),
|
||||
staticMethod().onClass("reactor.adapter.rxjava.RxJava2Adapter"),
|
||||
staticMethod()
|
||||
.onClass("reactor.core.publisher.Flux")
|
||||
.namedAnyOf("concat", "firstWithSignal", "from", "merge"),
|
||||
staticMethod().onClass("reactor.core.publisher.Mono").namedAnyOf("from", "fromDirect"));
|
||||
|
||||
/** Instantiates a new {@link IdentityConversion} instance. */
|
||||
public IdentityConversion() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
List<? extends ExpressionTree> arguments = tree.getArguments();
|
||||
if (arguments.size() != 1 || !IS_CONVERSION_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
ExpressionTree sourceTree = arguments.get(0);
|
||||
Type sourceType = ASTHelpers.getType(sourceTree);
|
||||
Type resultType = ASTHelpers.getType(tree);
|
||||
TargetType targetType = ASTHelpers.targetType(state);
|
||||
if (sourceType == null || resultType == null || targetType == null) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (!state.getTypes().isSameType(sourceType, resultType)
|
||||
&& !isConvertibleWithWellDefinedEquality(sourceType, targetType.type(), state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (sourceType.isPrimitive()
|
||||
&& state.getPath().getParentPath().getLeaf() instanceof MemberSelectTree) {
|
||||
/*
|
||||
* The result of the conversion method is dereferenced, while the source type is a primitive:
|
||||
* dropping the conversion would yield uncompilable code.
|
||||
*/
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return buildDescription(tree)
|
||||
.setMessage(
|
||||
"""
|
||||
This method invocation appears redundant; remove it or suppress this warning and add a \
|
||||
comment explaining its purpose""")
|
||||
.addFix(SuggestedFix.replace(tree, SourceCode.treeToString(sourceTree, state)))
|
||||
.addFix(SuggestedFixes.addSuppressWarnings(state, canonicalName()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static boolean isConvertibleWithWellDefinedEquality(
|
||||
Type sourceType, Type targetType, VisitorState state) {
|
||||
Types types = state.getTypes();
|
||||
return !types.isSameType(targetType, OBJECT_TYPE.get(state))
|
||||
&& types.isConvertible(sourceType, targetType)
|
||||
&& Arrays.stream(TypesWithUndefinedEquality.values())
|
||||
.noneMatch(b -> b.matchesType(sourceType, state) || b.matchesType(targetType, state));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.enclosingClass;
|
||||
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
|
||||
import static com.google.errorprone.matchers.Matchers.hasModifier;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.Matchers.methodReturns;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import java.util.SortedSet;
|
||||
import javax.lang.model.element.Modifier;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@link SortedSet} property declarations inside
|
||||
* {@code @Value.Immutable}- and {@code @Value.Modifiable}-annotated types that lack a
|
||||
* {@code @Value.NaturalOrder} or {@code @Value.ReverseOrder} annotation.
|
||||
*
|
||||
* <p>Without such an annotation:
|
||||
*
|
||||
* <ul>
|
||||
* <li>deserialization of the enclosing type requires that the associated JSON property is
|
||||
* present, contrary to the way in which Immutables handles other collection properties; and
|
||||
* <li>different instances may use different comparator implementations (e.g. deserialization
|
||||
* would default to natural order sorting), potentially leading to subtle bugs.
|
||||
* </ul>
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"""
|
||||
`SortedSet` properties of a `@Value.Immutable` or `@Value.Modifiable` type must be \
|
||||
annotated with `@Value.NaturalOrder` or `@Value.ReverseOrder`""",
|
||||
link = BUG_PATTERNS_BASE_URL + "ImmutablesSortedSetComparator",
|
||||
linkType = CUSTOM,
|
||||
severity = ERROR,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class ImmutablesSortedSetComparator extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<MethodTree> METHOD_LACKS_ANNOTATION =
|
||||
allOf(
|
||||
methodReturns(isSubtypeOf(SortedSet.class)),
|
||||
anyOf(
|
||||
allOf(
|
||||
hasModifier(Modifier.ABSTRACT),
|
||||
enclosingClass(
|
||||
anyOf(
|
||||
hasAnnotation("org.immutables.value.Value.Immutable"),
|
||||
hasAnnotation("org.immutables.value.Value.Modifiable")))),
|
||||
hasAnnotation("org.immutables.value.Value.Default")),
|
||||
not(
|
||||
anyOf(
|
||||
hasAnnotation("org.immutables.value.Value.NaturalOrder"),
|
||||
hasAnnotation("org.immutables.value.Value.ReverseOrder"))));
|
||||
|
||||
/** Instantiates a new {@link ImmutablesSortedSetComparator} instance. */
|
||||
public ImmutablesSortedSetComparator() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (!METHOD_LACKS_ANNOTATION.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix.Builder builder = SuggestedFix.builder();
|
||||
String valueTypeIdentifier =
|
||||
SuggestedFixes.qualifyType(state, builder, "org.immutables.value.Value");
|
||||
return describeMatch(
|
||||
tree,
|
||||
builder.prefixWith(tree, String.format("@%s.NaturalOrder ", valueTypeIdentifier)).build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.LambdaExpressionTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.InstanceOfTree;
|
||||
import com.sun.source.tree.LambdaExpressionTree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags lambda expressions that can be replaced with a method reference
|
||||
* of the form {@code T.class::isInstance}.
|
||||
*/
|
||||
// XXX: Consider folding this logic into the `MethodReferenceUsage` check of the
|
||||
// `error-prone-experimental` module.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Prefer `Class::isInstance` method reference over equivalent lambda expression",
|
||||
link = BUG_PATTERNS_BASE_URL + "IsInstanceLambdaUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class IsInstanceLambdaUsage extends BugChecker implements LambdaExpressionTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** Instantiates a new {@link IsInstanceLambdaUsage} instance. */
|
||||
public IsInstanceLambdaUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) {
|
||||
if (tree.getParameters().size() != 1
|
||||
|| !(tree.getBody() instanceof InstanceOfTree instanceOf)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
VariableTree param = Iterables.getOnlyElement(tree.getParameters());
|
||||
if (!ASTHelpers.getSymbol(param).equals(ASTHelpers.getSymbol(instanceOf.getExpression()))) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return describeMatch(
|
||||
tree,
|
||||
SuggestedFix.replace(
|
||||
tree, SourceCode.treeToString(instanceOf.getType(), state) + ".class::isInstance"));
|
||||
}
|
||||
}
|
||||
@@ -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.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreJUnitMatchers.TEST_METHOD;
|
||||
import static tech.picnic.errorprone.utils.MoreMatchers.hasMetaAnnotation;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import javax.lang.model.element.Modifier;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags non-final and non package-private JUnit test class declarations,
|
||||
* unless abstract.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Non-abstract JUnit test classes should be declared package-private and final",
|
||||
linkType = CUSTOM,
|
||||
link = BUG_PATTERNS_BASE_URL + "JUnitClassModifiers",
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
public final class JUnitClassModifiers extends BugChecker implements ClassTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ClassTree> HAS_SPRING_CONFIGURATION_ANNOTATION =
|
||||
annotations(
|
||||
AT_LEAST_ONE,
|
||||
anyOf(
|
||||
isType("org.springframework.context.annotation.Configuration"),
|
||||
hasMetaAnnotation("org.springframework.context.annotation.Configuration")));
|
||||
private static final Matcher<ClassTree> TEST_CLASS_WITH_INCORRECT_MODIFIERS =
|
||||
allOf(
|
||||
hasMethod(TEST_METHOD),
|
||||
not(hasModifier(Modifier.ABSTRACT)),
|
||||
anyOf(
|
||||
hasModifier(Modifier.PRIVATE),
|
||||
hasModifier(Modifier.PROTECTED),
|
||||
hasModifier(Modifier.PUBLIC),
|
||||
allOf(not(hasModifier(Modifier.FINAL)), not(HAS_SPRING_CONFIGURATION_ANNOTATION))));
|
||||
|
||||
/** Instantiates a new {@link JUnitClassModifiers} instance. */
|
||||
public JUnitClassModifiers() {}
|
||||
|
||||
@Override
|
||||
public Description matchClass(ClassTree tree, VisitorState state) {
|
||||
if (!TEST_CLASS_WITH_INCORRECT_MODIFIERS.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
|
||||
SuggestedFixes.removeModifiers(
|
||||
tree.getModifiers(),
|
||||
state,
|
||||
ImmutableSet.of(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC))
|
||||
.ifPresent(fixBuilder::merge);
|
||||
|
||||
if (!HAS_SPRING_CONFIGURATION_ANNOTATION.matches(tree, state)) {
|
||||
SuggestedFixes.addModifiers(tree, state, Modifier.FINAL).ifPresent(fixBuilder::merge);
|
||||
}
|
||||
|
||||
return describeMatch(tree, fixBuilder.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.enclosingClass;
|
||||
import static com.google.errorprone.matchers.Matchers.hasModifier;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static java.util.function.Predicate.not;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreJUnitMatchers.SETUP_OR_TEARDOWN_METHOD;
|
||||
import static tech.picnic.errorprone.utils.MoreJUnitMatchers.TEST_METHOD;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.tools.javac.code.Symbol.MethodSymbol;
|
||||
import java.util.Optional;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import tech.picnic.errorprone.utils.ConflictDetection;
|
||||
|
||||
/** A {@link BugChecker} that flags non-canonical JUnit method declarations. */
|
||||
// XXX: Consider introducing a class-level check that enforces that test classes:
|
||||
// 1. Are named `*Test` or `Abstract*TestCase`.
|
||||
// 2. If not `abstract`, are package-private and don't have public methods and subclasses.
|
||||
// 3. Only have private fields.
|
||||
// XXX: If implemented, the current logic could flag only `private` JUnit methods.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "JUnit method declaration can likely be improved",
|
||||
link = BUG_PATTERNS_BASE_URL + "JUnitMethodDeclaration",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class JUnitMethodDeclaration extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String TEST_PREFIX = "test";
|
||||
private static final ImmutableSet<Modifier> ILLEGAL_MODIFIERS =
|
||||
Sets.immutableEnumSet(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC);
|
||||
private static final Matcher<MethodTree> IS_LIKELY_OVERRIDDEN =
|
||||
allOf(
|
||||
not(hasModifier(Modifier.FINAL)),
|
||||
not(hasModifier(Modifier.PRIVATE)),
|
||||
enclosingClass(hasModifier(Modifier.ABSTRACT)));
|
||||
|
||||
/** Instantiates a new {@link JUnitMethodDeclaration} instance. */
|
||||
public JUnitMethodDeclaration() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (IS_LIKELY_OVERRIDDEN.matches(tree, state) || isOverride(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
boolean isTestMethod = TEST_METHOD.matches(tree, state);
|
||||
if (!isTestMethod && !SETUP_OR_TEARDOWN_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
|
||||
SuggestedFixes.removeModifiers(tree.getModifiers(), state, ILLEGAL_MODIFIERS)
|
||||
.ifPresent(fixBuilder::merge);
|
||||
|
||||
if (isTestMethod) {
|
||||
suggestTestMethodRenameIfApplicable(tree, fixBuilder, state);
|
||||
}
|
||||
|
||||
return fixBuilder.isEmpty() ? Description.NO_MATCH : describeMatch(tree, fixBuilder.build());
|
||||
}
|
||||
|
||||
private void suggestTestMethodRenameIfApplicable(
|
||||
MethodTree tree, SuggestedFix.Builder fixBuilder, VisitorState state) {
|
||||
MethodSymbol symbol = ASTHelpers.getSymbol(tree);
|
||||
tryCanonicalizeMethodName(symbol)
|
||||
.ifPresent(
|
||||
newName ->
|
||||
ConflictDetection.findMethodRenameBlocker(symbol, newName, state)
|
||||
.ifPresentOrElse(
|
||||
blocker -> reportMethodRenameBlocker(tree, blocker, state),
|
||||
() -> fixBuilder.merge(SuggestedFixes.renameMethod(tree, newName, state))));
|
||||
}
|
||||
|
||||
private void reportMethodRenameBlocker(MethodTree tree, String reason, VisitorState state) {
|
||||
state.reportMatch(
|
||||
buildDescription(tree)
|
||||
.setMessage(
|
||||
String.format(
|
||||
"This method's name should not redundantly start with `%s` (but note that %s)",
|
||||
TEST_PREFIX, reason))
|
||||
.build());
|
||||
}
|
||||
|
||||
private static Optional<String> tryCanonicalizeMethodName(MethodSymbol symbol) {
|
||||
return Optional.of(symbol.getQualifiedName().toString())
|
||||
.filter(name -> name.startsWith(TEST_PREFIX))
|
||||
.map(name -> name.substring(TEST_PREFIX.length()))
|
||||
.filter(not(String::isEmpty))
|
||||
.map(name -> Character.toLowerCase(name.charAt(0)) + name.substring(1))
|
||||
.filter(name -> !Character.isDigit(name.charAt(0)));
|
||||
}
|
||||
|
||||
private static boolean isOverride(MethodTree tree, VisitorState state) {
|
||||
return ASTHelpers.streamSuperMethods(ASTHelpers.getSymbol(tree), state.getTypes())
|
||||
.findAny()
|
||||
.isPresent();
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static java.util.function.Predicate.not;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.matchers.MultiMatcher;
|
||||
import com.google.errorprone.predicates.TypePredicate;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import java.util.Optional;
|
||||
import javax.lang.model.element.Modifier;
|
||||
|
||||
/** A {@link BugChecker} which flags non-canonical JUnit method declarations. */
|
||||
// XXX: Consider introducing a class-level check which enforces that test classes:
|
||||
// 1. Are named `*Test` or `Abstract*TestCase`.
|
||||
// 2. If not `abstract`, don't have public methods and subclasses.
|
||||
// 3. Only have private fields.
|
||||
// XXX: If implemented, the current logic could flag only `private` JUnit methods.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "JUnitMethodDeclaration",
|
||||
summary = "JUnit method declaration can likely be improved",
|
||||
linkType = BugPattern.LinkType.NONE,
|
||||
severity = BugPattern.SeverityLevel.SUGGESTION,
|
||||
tags = BugPattern.StandardTags.SIMPLIFICATION)
|
||||
public final class JUnitMethodDeclarationCheck extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String TEST_PREFIX = "test";
|
||||
private static final ImmutableSet<Modifier> ILLEGAL_MODIFIERS =
|
||||
ImmutableSet.of(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC);
|
||||
private static final MultiMatcher<MethodTree, AnnotationTree> OVERRIDE_METHOD =
|
||||
annotations(AT_LEAST_ONE, isType("java.lang.Override"));
|
||||
private static final MultiMatcher<MethodTree, AnnotationTree> TEST_METHOD =
|
||||
annotations(
|
||||
AT_LEAST_ONE,
|
||||
anyOf(
|
||||
isType("org.junit.jupiter.api.Test"),
|
||||
hasMetaAnnotation("org.junit.jupiter.api.TestTemplate")));
|
||||
private static final MultiMatcher<MethodTree, AnnotationTree> SETUP_OR_TEARDOWN_METHOD =
|
||||
annotations(
|
||||
AT_LEAST_ONE,
|
||||
anyOf(
|
||||
isType("org.junit.jupiter.api.AfterAll"),
|
||||
isType("org.junit.jupiter.api.AfterEach"),
|
||||
isType("org.junit.jupiter.api.BeforeAll"),
|
||||
isType("org.junit.jupiter.api.BeforeEach")));
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
// XXX: Perhaps we should also skip analysis of non-`private` non-`final` methods in abstract
|
||||
// classes?
|
||||
if (OVERRIDE_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
boolean isTestMethod = TEST_METHOD.matches(tree, state);
|
||||
if (!isTestMethod && !SETUP_OR_TEARDOWN_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
SuggestedFix.Builder builder = SuggestedFix.builder();
|
||||
SuggestedFixes.removeModifiers(tree.getModifiers(), state, ILLEGAL_MODIFIERS)
|
||||
.ifPresent(builder::merge);
|
||||
|
||||
if (isTestMethod) {
|
||||
// XXX: In theory this rename could clash with an existing method or static import. In that
|
||||
// case we should emit a warning without a suggested replacement.
|
||||
tryCanonicalizeMethodName(tree, state).ifPresent(builder::merge);
|
||||
}
|
||||
|
||||
return builder.isEmpty() ? Description.NO_MATCH : describeMatch(tree, builder.build());
|
||||
}
|
||||
|
||||
private static Optional<SuggestedFix> tryCanonicalizeMethodName(
|
||||
MethodTree tree, VisitorState state) {
|
||||
return Optional.ofNullable(ASTHelpers.getSymbol(tree))
|
||||
.map(sym -> sym.getQualifiedName().toString())
|
||||
.filter(name -> name.startsWith(TEST_PREFIX))
|
||||
.map(name -> name.substring(TEST_PREFIX.length()))
|
||||
.filter(not(String::isEmpty))
|
||||
.map(name -> Character.toLowerCase(name.charAt(0)) + name.substring(1))
|
||||
.filter(name -> !Character.isDigit(name.charAt(0)))
|
||||
.map(name -> SuggestedFixes.renameMethod(tree, name, state));
|
||||
}
|
||||
|
||||
// XXX: Move to a `MoreMatchers` utility class.
|
||||
private static Matcher<AnnotationTree> hasMetaAnnotation(String annotationClassName) {
|
||||
TypePredicate typePredicate = hasAnnotation(annotationClassName);
|
||||
return (tree, state) -> {
|
||||
Symbol sym = ASTHelpers.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreMatchers.hasMetaAnnotation;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.matchers.MultiMatcher;
|
||||
import com.google.errorprone.matchers.MultiMatcher.MultiMatchResult;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags nullary {@link
|
||||
* org.junit.jupiter.params.ParameterizedTest @ParameterizedTest} test methods.
|
||||
*
|
||||
* <p>Such tests are unnecessarily executed more than necessary. This checker suggests annotating
|
||||
* the method with {@link org.junit.jupiter.api.Test @Test}, and to drop all declared {@link
|
||||
* org.junit.jupiter.params.provider.ArgumentsSource argument sources}.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Nullary JUnit test methods should not be parameterized",
|
||||
link = BUG_PATTERNS_BASE_URL + "JUnitNullaryParameterizedTestDeclaration",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class JUnitNullaryParameterizedTestDeclaration extends BugChecker
|
||||
implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final MultiMatcher<MethodTree, AnnotationTree> IS_PARAMETERIZED_TEST =
|
||||
annotations(AT_LEAST_ONE, isType("org.junit.jupiter.params.ParameterizedTest"));
|
||||
private static final Matcher<AnnotationTree> IS_ARGUMENT_SOURCE =
|
||||
anyOf(
|
||||
isType("org.junit.jupiter.params.provider.ArgumentsSource"),
|
||||
isType("org.junit.jupiter.params.provider.ArgumentsSources"),
|
||||
hasMetaAnnotation("org.junit.jupiter.params.provider.ArgumentsSource"));
|
||||
|
||||
/** Instantiates a new {@link JUnitNullaryParameterizedTestDeclaration} instance. */
|
||||
public JUnitNullaryParameterizedTestDeclaration() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (!tree.getParameters().isEmpty()) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
MultiMatchResult<AnnotationTree> isParameterizedTest =
|
||||
IS_PARAMETERIZED_TEST.multiMatchResult(tree, state);
|
||||
if (!isParameterizedTest.matches()) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
/*
|
||||
* This method is vacuously parameterized. Suggest replacing `@ParameterizedTest` with `@Test`.
|
||||
* (As each method is checked independently, we cannot in general determine whether this
|
||||
* suggestion makes a `ParameterizedTest` type import obsolete; that task is left to Error
|
||||
* Prone's `RemoveUnusedImports` check.)
|
||||
*/
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder();
|
||||
fix.merge(
|
||||
SuggestedFix.replace(
|
||||
isParameterizedTest.onlyMatchingNode(),
|
||||
'@' + SuggestedFixes.qualifyType(state, fix, "org.junit.jupiter.api.Test")));
|
||||
|
||||
/*
|
||||
* Also suggest dropping all (explicit and implicit) `@ArgumentsSource`s. No attempt is made to
|
||||
* assess whether a dropped `@MethodSource` also makes the referenced factory method(s) unused.
|
||||
*/
|
||||
tree.getModifiers().getAnnotations().stream()
|
||||
.filter(a -> IS_ARGUMENT_SOURCE.matches(a, state))
|
||||
.forEach(a -> fix.merge(SourceCode.deleteWithTrailingWhitespace(a, state)));
|
||||
|
||||
return describeMatch(tree, fix.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.ALL;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anything;
|
||||
import static com.google.errorprone.matchers.Matchers.argument;
|
||||
import static com.google.errorprone.matchers.Matchers.argumentCount;
|
||||
import static com.google.errorprone.matchers.Matchers.classLiteral;
|
||||
import static com.google.errorprone.matchers.Matchers.hasArguments;
|
||||
import static com.google.errorprone.matchers.Matchers.isPrimitiveOrBoxedPrimitiveType;
|
||||
import static com.google.errorprone.matchers.Matchers.isSameType;
|
||||
import static com.google.errorprone.matchers.Matchers.methodHasParameters;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.toType;
|
||||
import static java.util.function.Predicate.not;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreJUnitMatchers.HAS_METHOD_SOURCE;
|
||||
import static tech.picnic.errorprone.utils.MoreJUnitMatchers.getMethodSourceFactoryNames;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.LambdaExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.NewArrayTree;
|
||||
import com.sun.source.tree.ReturnTree;
|
||||
import com.sun.source.util.TreeScanner;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.DoubleStream;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.LongStream;
|
||||
import java.util.stream.Stream;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags JUnit tests with a {@link
|
||||
* org.junit.jupiter.params.provider.MethodSource} annotation that can be replaced with an
|
||||
* equivalent {@link org.junit.jupiter.params.provider.ValueSource} annotation.
|
||||
*/
|
||||
// XXX: Where applicable, also flag `@MethodSource` annotations that reference multiple value
|
||||
// factory methods (or that repeat the same value factory method multiple times).
|
||||
// XXX: Support inlining of overloaded value factory methods.
|
||||
// XXX: Support inlining of value factory methods referenced by multiple `@MethodSource`
|
||||
// annotations.
|
||||
// XXX: Support value factory return expressions of the form `Stream.of(a, b,
|
||||
// c).map(Arguments::argument)`.
|
||||
// XXX: Support simplification of test methods that accept additional injected parameters such as
|
||||
// `TestInfo`; such parameters should be ignored for the purpose of this check.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Prefer `@ValueSource` over a `@MethodSource` where possible and reasonable",
|
||||
linkType = CUSTOM,
|
||||
link = BUG_PATTERNS_BASE_URL + "JUnitValueSource",
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class JUnitValueSource extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> SUPPORTED_VALUE_FACTORY_VALUES =
|
||||
anyOf(
|
||||
isArrayArgumentValueCandidate(),
|
||||
toType(
|
||||
MethodInvocationTree.class,
|
||||
allOf(
|
||||
staticMethod()
|
||||
.onClass("org.junit.jupiter.params.provider.Arguments")
|
||||
.namedAnyOf("arguments", "of"),
|
||||
argumentCount(1),
|
||||
argument(0, isArrayArgumentValueCandidate()))));
|
||||
private static final Matcher<ExpressionTree> ARRAY_OF_SUPPORTED_SINGLE_VALUE_ARGUMENTS =
|
||||
isSingleDimensionArrayCreationWithAllElementsMatching(SUPPORTED_VALUE_FACTORY_VALUES);
|
||||
private static final Matcher<ExpressionTree> ENUMERATION_OF_SUPPORTED_SINGLE_VALUE_ARGUMENTS =
|
||||
toType(
|
||||
MethodInvocationTree.class,
|
||||
allOf(
|
||||
staticMethod()
|
||||
.onClassAny(
|
||||
Stream.class.getCanonicalName(),
|
||||
IntStream.class.getCanonicalName(),
|
||||
LongStream.class.getCanonicalName(),
|
||||
DoubleStream.class.getCanonicalName(),
|
||||
List.class.getCanonicalName(),
|
||||
Set.class.getCanonicalName(),
|
||||
ImmutableList.class.getCanonicalName(),
|
||||
ImmutableSet.class.getCanonicalName())
|
||||
.named("of"),
|
||||
hasArguments(AT_LEAST_ONE, anything()),
|
||||
hasArguments(ALL, SUPPORTED_VALUE_FACTORY_VALUES)));
|
||||
private static final Matcher<MethodTree> IS_UNARY_METHOD_WITH_SUPPORTED_PARAMETER =
|
||||
methodHasParameters(
|
||||
anyOf(
|
||||
isPrimitiveOrBoxedPrimitiveType(),
|
||||
isSameType(String.class),
|
||||
isSameType(state -> state.getSymtab().classType)));
|
||||
|
||||
/** Instantiates a new {@link JUnitValueSource} instance. */
|
||||
public JUnitValueSource() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (!IS_UNARY_METHOD_WITH_SUPPORTED_PARAMETER.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
Type parameterType = ASTHelpers.getType(Iterables.getOnlyElement(tree.getParameters()));
|
||||
|
||||
return findMethodSourceAnnotation(tree, state)
|
||||
.flatMap(
|
||||
methodSourceAnnotation ->
|
||||
getSoleLocalFactoryName(methodSourceAnnotation, tree)
|
||||
.filter(factory -> !hasSiblingReferencingValueFactory(tree, factory, state))
|
||||
.flatMap(factory -> findSiblingWithName(tree, factory, state))
|
||||
.flatMap(
|
||||
factoryMethod ->
|
||||
tryConstructValueSourceFix(
|
||||
parameterType, methodSourceAnnotation, factoryMethod, state))
|
||||
.map(fix -> describeMatch(methodSourceAnnotation, fix)))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the value factory method pointed to by the given {@code @MethodSource}
|
||||
* annotation, if it (a) is the only one and (b) is a method in the same class as the annotated
|
||||
* method.
|
||||
*/
|
||||
private static Optional<String> getSoleLocalFactoryName(
|
||||
AnnotationTree methodSourceAnnotation, MethodTree method) {
|
||||
return getElementIfSingleton(getMethodSourceFactoryNames(methodSourceAnnotation, method))
|
||||
.filter(name -> name.indexOf('#') < 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the given method has a sibling method in the same class that depends on the
|
||||
* specified value factory method.
|
||||
*/
|
||||
private static boolean hasSiblingReferencingValueFactory(
|
||||
MethodTree tree, String valueFactory, VisitorState state) {
|
||||
return findMatchingSibling(tree, m -> hasValueFactory(m, valueFactory, state), state)
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
private static Optional<MethodTree> findSiblingWithName(
|
||||
MethodTree tree, String methodName, VisitorState state) {
|
||||
return findMatchingSibling(tree, m -> m.getName().contentEquals(methodName), state);
|
||||
}
|
||||
|
||||
private static Optional<MethodTree> findMatchingSibling(
|
||||
MethodTree tree, Predicate<? super MethodTree> predicate, VisitorState state) {
|
||||
return state.findEnclosing(ClassTree.class).getMembers().stream()
|
||||
.filter(MethodTree.class::isInstance)
|
||||
.map(MethodTree.class::cast)
|
||||
.filter(not(tree::equals))
|
||||
.filter(predicate)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private static boolean hasValueFactory(
|
||||
MethodTree tree, String valueFactoryMethodName, VisitorState state) {
|
||||
return findMethodSourceAnnotation(tree, state).stream()
|
||||
.anyMatch(
|
||||
annotation ->
|
||||
getMethodSourceFactoryNames(annotation, tree).contains(valueFactoryMethodName));
|
||||
}
|
||||
|
||||
private static Optional<AnnotationTree> findMethodSourceAnnotation(
|
||||
MethodTree tree, VisitorState state) {
|
||||
return HAS_METHOD_SOURCE.multiMatchResult(tree, state).matchingNodes().stream().findFirst();
|
||||
}
|
||||
|
||||
private static Optional<SuggestedFix> tryConstructValueSourceFix(
|
||||
Type parameterType,
|
||||
AnnotationTree methodSourceAnnotation,
|
||||
MethodTree valueFactoryMethod,
|
||||
VisitorState state) {
|
||||
return getSingleReturnExpression(valueFactoryMethod)
|
||||
.flatMap(expression -> tryExtractValueSourceAttributeValue(expression, state))
|
||||
.map(
|
||||
valueSourceAttributeValue -> {
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder();
|
||||
String valueSource =
|
||||
SuggestedFixes.qualifyType(
|
||||
state, fix, "org.junit.jupiter.params.provider.ValueSource");
|
||||
return fix.replace(
|
||||
methodSourceAnnotation,
|
||||
String.format(
|
||||
"@%s(%s = %s)",
|
||||
valueSource,
|
||||
toValueSourceAttributeName(parameterType),
|
||||
valueSourceAttributeValue))
|
||||
.delete(valueFactoryMethod)
|
||||
.build();
|
||||
});
|
||||
}
|
||||
|
||||
// XXX: This pattern also occurs a few times inside Error Prone; contribute upstream.
|
||||
private static Optional<ExpressionTree> getSingleReturnExpression(MethodTree methodTree) {
|
||||
List<ExpressionTree> returnExpressions = new ArrayList<>();
|
||||
new TreeScanner<@Nullable Void, @Nullable Void>() {
|
||||
@Override
|
||||
public @Nullable Void visitClass(ClassTree node, @Nullable Void unused) {
|
||||
/* Ignore `return` statements inside anonymous/local classes. */
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Void visitReturn(ReturnTree node, @Nullable Void unused) {
|
||||
returnExpressions.add(node.getExpression());
|
||||
return super.visitReturn(node, unused);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Void visitLambdaExpression(
|
||||
LambdaExpressionTree node, @Nullable Void unused) {
|
||||
/* Ignore `return` statements inside lambda expressions. */
|
||||
return null;
|
||||
}
|
||||
}.scan(methodTree, null);
|
||||
|
||||
return getElementIfSingleton(returnExpressions);
|
||||
}
|
||||
|
||||
private static Optional<String> tryExtractValueSourceAttributeValue(
|
||||
ExpressionTree tree, VisitorState state) {
|
||||
List<? extends ExpressionTree> arguments;
|
||||
if (ENUMERATION_OF_SUPPORTED_SINGLE_VALUE_ARGUMENTS.matches(tree, state)) {
|
||||
arguments = ((MethodInvocationTree) tree).getArguments();
|
||||
} else if (ARRAY_OF_SUPPORTED_SINGLE_VALUE_ARGUMENTS.matches(tree, state)) {
|
||||
arguments = ((NewArrayTree) tree).getInitializers();
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/*
|
||||
* Join the values into a comma-separated string, unwrapping `Arguments` factory method
|
||||
* invocations if applicable.
|
||||
*/
|
||||
return Optional.of(
|
||||
arguments.stream()
|
||||
.map(
|
||||
arg ->
|
||||
arg instanceof MethodInvocationTree methodInvocation
|
||||
? Iterables.getOnlyElement(methodInvocation.getArguments())
|
||||
: arg)
|
||||
.map(argument -> SourceCode.treeToString(argument, state))
|
||||
.collect(joining(", ")))
|
||||
.map(value -> arguments.size() > 1 ? String.format("{%s}", value) : value);
|
||||
}
|
||||
|
||||
private static String toValueSourceAttributeName(Type type) {
|
||||
String typeString = type.tsym.name.toString();
|
||||
|
||||
return switch (typeString) {
|
||||
case "Class" -> "classes";
|
||||
case "Character" -> "chars";
|
||||
case "Integer" -> "ints";
|
||||
default -> typeString.toLowerCase(Locale.ROOT) + 's';
|
||||
};
|
||||
}
|
||||
|
||||
private static <T> Optional<T> getElementIfSingleton(Collection<T> collection) {
|
||||
return Optional.of(collection)
|
||||
.filter(elements -> elements.size() == 1)
|
||||
.map(Iterables::getOnlyElement);
|
||||
}
|
||||
|
||||
private static Matcher<ExpressionTree> isSingleDimensionArrayCreationWithAllElementsMatching(
|
||||
Matcher<? super ExpressionTree> elementMatcher) {
|
||||
return (tree, state) -> {
|
||||
if (!(tree instanceof NewArrayTree newArray)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return newArray.getDimensions().isEmpty()
|
||||
&& !newArray.getInitializers().isEmpty()
|
||||
&& newArray.getInitializers().stream()
|
||||
.allMatch(element -> elementMatcher.matches(element, state));
|
||||
};
|
||||
}
|
||||
|
||||
private static Matcher<ExpressionTree> isArrayArgumentValueCandidate() {
|
||||
return anyOf(classLiteral(anything()), (tree, state) -> ASTHelpers.constValue(tree) != null);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,20 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
|
||||
import static java.util.Comparator.comparing;
|
||||
import static java.util.Comparator.naturalOrder;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Comparators;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.ErrorProneFlags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
@@ -28,7 +31,6 @@ import com.sun.source.tree.LiteralTree;
|
||||
import com.sun.source.tree.NewArrayTree;
|
||||
import com.sun.source.tree.PrimitiveTypeTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import com.sun.source.util.TreeScanner;
|
||||
import com.sun.tools.javac.code.Symtab;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
@@ -37,21 +39,30 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import javax.inject.Inject;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.utils.AnnotationAttributeMatcher;
|
||||
import tech.picnic.errorprone.utils.Flags;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} which flags annotation array listings which aren't sorted lexicographically.
|
||||
* A {@link BugChecker} that flags annotation array listings which aren't sorted lexicographically.
|
||||
*
|
||||
* <p>The idea behind this checker is that maintaining a sorted sequence simplifies conflict
|
||||
* resolution, and can even avoid it if two branches add the same entry.
|
||||
*/
|
||||
// XXX: In some places we declare a `@SuppressWarnings` annotation with a final value of
|
||||
// `key-to-resolve-AnnotationUseStyle-and-TrailingComment-check-conflict`. That entry must stay
|
||||
// last. Consider adding (generic?) support for such cases.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "LexicographicalAnnotationAttributeListing",
|
||||
summary = "Where possible, sort annotation array attributes lexicographically",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.STYLE)
|
||||
public final class LexicographicalAnnotationAttributeListingCheck extends BugChecker
|
||||
link = BUG_PATTERNS_BASE_URL + "LexicographicalAnnotationAttributeListing",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
@SuppressWarnings("java:S2160" /* Super class equality definition suffices. */)
|
||||
public final class LexicographicalAnnotationAttributeListing extends BugChecker
|
||||
implements AnnotationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final ImmutableSet<String> BLACKLISTED_ANNOTATIONS =
|
||||
@@ -60,24 +71,35 @@ public final class LexicographicalAnnotationAttributeListingCheck extends BugChe
|
||||
"com.fasterxml.jackson.annotation.JsonPropertyOrder#value",
|
||||
"io.swagger.annotations.ApiImplicitParams#value",
|
||||
"io.swagger.v3.oas.annotations.Parameters#value",
|
||||
"javax.xml.bind.annotation.XmlType#propOrder");
|
||||
"javax.xml.bind.annotation.XmlType#propOrder",
|
||||
"org.springframework.context.annotation.PropertySource#value",
|
||||
"org.springframework.test.context.TestPropertySource#locations",
|
||||
"org.springframework.test.context.TestPropertySource#value");
|
||||
private static final String FLAG_PREFIX = "LexicographicalAnnotationAttributeListing:";
|
||||
private static final String INCLUDED_ANNOTATIONS_FLAG = FLAG_PREFIX + "Includes";
|
||||
private static final String EXCLUDED_ANNOTATIONS_FLAG = FLAG_PREFIX + "Excludes";
|
||||
|
||||
/**
|
||||
* The splitter applied to string-typed annotation arguments prior to lexicographical sorting. By
|
||||
* splitting on {@code =}, strings that represent e.g. inline Spring property declarations are
|
||||
* properly sorted by key, then value.
|
||||
*/
|
||||
private static final Splitter STRING_ARGUMENT_SPLITTER = Splitter.on('=');
|
||||
|
||||
private final AnnotationAttributeMatcher matcher;
|
||||
|
||||
/** Instantiates the default {@link LexicographicalAnnotationAttributeListingCheck}. */
|
||||
public LexicographicalAnnotationAttributeListingCheck() {
|
||||
/** Instantiates a default {@link LexicographicalAnnotationAttributeListing} instance. */
|
||||
public LexicographicalAnnotationAttributeListing() {
|
||||
this(ErrorProneFlags.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a customized {@link LexicographicalAnnotationAttributeListingCheck}.
|
||||
* Instantiates a customized {@link LexicographicalAnnotationAttributeListing}.
|
||||
*
|
||||
* @param flags Any provided command line flags.
|
||||
*/
|
||||
public LexicographicalAnnotationAttributeListingCheck(ErrorProneFlags flags) {
|
||||
@Inject
|
||||
LexicographicalAnnotationAttributeListing(ErrorProneFlags flags) {
|
||||
matcher = createAnnotationAttributeMatcher(flags);
|
||||
}
|
||||
|
||||
@@ -102,13 +124,9 @@ public final class LexicographicalAnnotationAttributeListingCheck extends BugChe
|
||||
}
|
||||
|
||||
private static Optional<NewArrayTree> extractArray(ExpressionTree expr) {
|
||||
if (expr.getKind() == Kind.ASSIGNMENT) {
|
||||
return extractArray(((AssignmentTree) expr).getExpression());
|
||||
}
|
||||
|
||||
return Optional.of(expr)
|
||||
.filter(e -> e.getKind() == Kind.NEW_ARRAY)
|
||||
.map(NewArrayTree.class::cast);
|
||||
return expr instanceof AssignmentTree assignment
|
||||
? extractArray(assignment.getExpression())
|
||||
: Optional.of(expr).filter(NewArrayTree.class::isInstance).map(NewArrayTree.class::cast);
|
||||
}
|
||||
|
||||
private static Optional<SuggestedFix.Builder> suggestSorting(
|
||||
@@ -119,7 +137,7 @@ public final class LexicographicalAnnotationAttributeListingCheck extends BugChe
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -128,7 +146,7 @@ public final class LexicographicalAnnotationAttributeListingCheck extends BugChe
|
||||
/* The elements aren't sorted. Suggest the sorted alternative. */
|
||||
String suggestion =
|
||||
desiredOrdering.stream()
|
||||
.map(expr -> Util.treeToString(expr, state))
|
||||
.map(expr -> SourceCode.treeToString(expr, state))
|
||||
.collect(joining(", ", "{", "}"));
|
||||
return Optional.of(SuggestedFix.builder().replace(array, suggestion));
|
||||
}
|
||||
@@ -149,12 +167,12 @@ public final class LexicographicalAnnotationAttributeListingCheck extends BugChe
|
||||
}
|
||||
|
||||
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())))),
|
||||
@@ -166,35 +184,31 @@ public final class LexicographicalAnnotationAttributeListingCheck extends BugChe
|
||||
* 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>() {
|
||||
new TreeScanner<@Nullable Void, @Nullable Void>() {
|
||||
@Override
|
||||
public Void visitIdentifier(IdentifierTree node, 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitLiteral(LiteralTree node, 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 str
|
||||
? STRING_ARGUMENT_SPLITTER.splitToStream(str).collect(toImmutableList())
|
||||
: ImmutableList.of(String.valueOf(value)));
|
||||
|
||||
return super.visitLiteral(node, unused);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitPrimitiveType(PrimitiveTypeTree node, Void ctx) {
|
||||
nodes.add(tokenize(node));
|
||||
return super.visitPrimitiveType(node, ctx);
|
||||
}
|
||||
|
||||
private ImmutableList<String> tokenize(Tree node) {
|
||||
/*
|
||||
* Tokens are split on `=` so that e.g. inline Spring property declarations are properly
|
||||
* sorted by key, then value.
|
||||
*/
|
||||
return ImmutableList.copyOf(Util.treeToString(node, state).split("=", -1));
|
||||
public @Nullable Void visitPrimitiveType(PrimitiveTypeTree node, @Nullable Void unused) {
|
||||
nodes.add(ImmutableList.of(node.getPrimitiveTypeKind().toString()));
|
||||
return super.visitPrimitiveType(node, unused);
|
||||
}
|
||||
}.scan(array, null);
|
||||
|
||||
@@ -204,12 +218,15 @@ public final class LexicographicalAnnotationAttributeListingCheck extends BugChe
|
||||
private static AnnotationAttributeMatcher createAnnotationAttributeMatcher(
|
||||
ErrorProneFlags flags) {
|
||||
return AnnotationAttributeMatcher.create(
|
||||
flags.getList(INCLUDED_ANNOTATIONS_FLAG), excludedAnnotations(flags));
|
||||
flags.get(INCLUDED_ANNOTATIONS_FLAG).isPresent()
|
||||
? Optional.of(flags.getListOrEmpty(INCLUDED_ANNOTATIONS_FLAG))
|
||||
: Optional.empty(),
|
||||
excludedAnnotations(flags));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
|
||||
import static com.sun.tools.javac.code.TypeAnnotations.AnnotationType.DECLARATION;
|
||||
import static com.sun.tools.javac.code.TypeAnnotations.AnnotationType.TYPE;
|
||||
import static java.util.Comparator.comparing;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.VerifyException;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
import com.google.errorprone.fixes.Fix;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import com.sun.tools.javac.code.TypeAnnotations.AnnotationType;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags annotations that are not lexicographically sorted.
|
||||
*
|
||||
* <p>The idea behind this checker is that maintaining a sorted sequence simplifies conflict
|
||||
* resolution, and can even avoid it if two branches add the same annotation.
|
||||
*/
|
||||
// XXX: Currently this checker only flags method-level annotations. It should likely also flag
|
||||
// type-, field- and parameter-level annotations.
|
||||
// XXX: Duplicate entries are often a mistake. Consider introducing a similar `BugChecker` that
|
||||
// flags duplicates.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Sort annotations lexicographically where possible",
|
||||
link = BUG_PATTERNS_BASE_URL + "LexicographicalAnnotationListing",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
public final class LexicographicalAnnotationListing extends BugChecker
|
||||
implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* A comparator that minimally reorders {@link AnnotationType}s, such that declaration annotations
|
||||
* are placed before type annotations.
|
||||
*/
|
||||
@SuppressWarnings({
|
||||
"java:S1067",
|
||||
"java:S3358"
|
||||
} /* Avoiding the nested ternary operator hurts readability. */)
|
||||
private static final Comparator<@Nullable AnnotationType> BY_ANNOTATION_TYPE =
|
||||
(a, b) ->
|
||||
(a == null || a == DECLARATION) && b == TYPE
|
||||
? -1
|
||||
: (a == TYPE && b == DECLARATION ? 1 : 0);
|
||||
|
||||
/** Instantiates a new {@link LexicographicalAnnotationListing} instance. */
|
||||
public LexicographicalAnnotationListing() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
List<? extends AnnotationTree> originalOrdering = tree.getModifiers().getAnnotations();
|
||||
if (originalOrdering.size() < 2) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
ImmutableList<? extends AnnotationTree> sortedAnnotations =
|
||||
sort(originalOrdering, ASTHelpers.getSymbol(tree), state);
|
||||
if (originalOrdering.equals(sortedAnnotations)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return describeMatch(
|
||||
originalOrdering.get(0), fixOrdering(originalOrdering, sortedAnnotations, state));
|
||||
}
|
||||
|
||||
private static ImmutableList<? extends AnnotationTree> sort(
|
||||
List<? extends AnnotationTree> annotations, Symbol symbol, VisitorState state) {
|
||||
return annotations.stream()
|
||||
.sorted(
|
||||
comparing(
|
||||
(AnnotationTree annotation) ->
|
||||
ASTHelpers.getAnnotationType(annotation, symbol, state),
|
||||
BY_ANNOTATION_TYPE)
|
||||
.thenComparing(annotation -> SourceCode.treeToString(annotation, state)))
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
private static Fix fixOrdering(
|
||||
List<? extends AnnotationTree> originalAnnotations,
|
||||
ImmutableList<? extends AnnotationTree> sortedAnnotations,
|
||||
VisitorState state) {
|
||||
return Streams.zip(
|
||||
originalAnnotations.stream(),
|
||||
sortedAnnotations.stream(),
|
||||
(original, replacement) ->
|
||||
SuggestedFix.builder()
|
||||
.replace(original, SourceCode.treeToString(replacement, state)))
|
||||
.reduce(SuggestedFix.Builder::merge)
|
||||
.map(SuggestedFix.Builder::build)
|
||||
.orElseThrow(() -> new VerifyException("No annotations were provided"));
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static java.util.Comparator.comparing;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
import com.google.errorprone.fixes.Fix;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags annotations that are not lexicographically sorted.
|
||||
*
|
||||
* <p>The idea behind this checker is that maintaining a sorted sequence simplifies conflict
|
||||
* resolution, and can even avoid it if two branches add the same annotation.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "LexicographicalAnnotationListing",
|
||||
summary = "Sort annotations lexicographically where possible",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.STYLE)
|
||||
public final class LexicographicalAnnotationListingCheck extends BugChecker
|
||||
implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
List<? extends AnnotationTree> originalOrdering = tree.getModifiers().getAnnotations();
|
||||
if (originalOrdering.size() < 2) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
ImmutableList<? extends AnnotationTree> sortedAnnotations = sort(originalOrdering, state);
|
||||
if (originalOrdering.equals(sortedAnnotations)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
Optional<Fix> fix = tryFixOrdering(originalOrdering, sortedAnnotations, state);
|
||||
|
||||
Description.Builder description = buildDescription(tree);
|
||||
fix.ifPresent(description::addFix);
|
||||
return description.build();
|
||||
}
|
||||
|
||||
private static ImmutableList<? extends AnnotationTree> sort(
|
||||
List<? extends AnnotationTree> annotations, VisitorState state) {
|
||||
return annotations.stream()
|
||||
.sorted(comparing(annotation -> Util.treeToString(annotation, state)))
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
private static Optional<Fix> tryFixOrdering(
|
||||
List<? extends AnnotationTree> originalAnnotations,
|
||||
ImmutableList<? extends AnnotationTree> sortedAnnotations,
|
||||
VisitorState state) {
|
||||
return Streams.zip(
|
||||
originalAnnotations.stream(),
|
||||
sortedAnnotations.stream(),
|
||||
(original, replacement) ->
|
||||
SuggestedFix.builder().replace(original, Util.treeToString(replacement, state)))
|
||||
.reduce(SuggestedFix.Builder::merge)
|
||||
.map(SuggestedFix.Builder::build);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.MultiMatcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
|
||||
/** A {@link BugChecker} that flags likely missing Refaster annotations. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "MissingRefasterAnnotation",
|
||||
summary = "The Refaster template contains a method without any Refaster annotations",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.WARNING,
|
||||
tags = StandardTags.LIKELY_ERROR)
|
||||
public final class MissingRefasterAnnotationCheck extends BugChecker implements ClassTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final MultiMatcher<Tree, AnnotationTree> REFASTER_ANNOTATION =
|
||||
annotations(
|
||||
AT_LEAST_ONE,
|
||||
anyOf(
|
||||
isType("com.google.errorprone.refaster.annotation.Placeholder"),
|
||||
isType("com.google.errorprone.refaster.annotation.BeforeTemplate"),
|
||||
isType("com.google.errorprone.refaster.annotation.AfterTemplate")));
|
||||
|
||||
@Override
|
||||
public Description matchClass(ClassTree tree, VisitorState state) {
|
||||
long methodTypes =
|
||||
tree.getMembers().stream()
|
||||
.filter(member -> member.getKind() == Tree.Kind.METHOD)
|
||||
.map(MethodTree.class::cast)
|
||||
.filter(method -> !ASTHelpers.isGeneratedConstructor(method))
|
||||
.map(method -> REFASTER_ANNOTATION.matches(method, state))
|
||||
.distinct()
|
||||
.count();
|
||||
|
||||
return methodTypes < 2 ? Description.NO_MATCH : buildDescription(tree).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.argument;
|
||||
import static com.google.errorprone.matchers.Matchers.isSameType;
|
||||
import static com.google.errorprone.matchers.Matchers.isVariable;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import java.util.List;
|
||||
import tech.picnic.errorprone.utils.MoreASTHelpers;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags the use of {@link org.mockito.Mockito#mock(Class)} and {@link
|
||||
* org.mockito.Mockito#spy(Class)} where instead the type to be mocked or spied can be derived from
|
||||
* context.
|
||||
*/
|
||||
// XXX: This check currently does not flag method invocation arguments. When adding support for
|
||||
// this, consider that in some cases the type to be mocked or spied must be specified explicitly so
|
||||
// as to disambiguate between method overloads.
|
||||
// XXX: This check currently does not flag (implicit or explicit) lambda return expressions.
|
||||
// XXX: This check currently does not drop suppressions that become obsolete after the
|
||||
// suggested fix is applied; consider adding support for this.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Don't unnecessarily pass a type to Mockito's `mock(Class)` and `spy(Class)` methods",
|
||||
link = BUG_PATTERNS_BASE_URL + "MockitoMockClassReference",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class MockitoMockClassReference extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<MethodInvocationTree> MOCKITO_MOCK_OR_SPY_WITH_HARDCODED_TYPE =
|
||||
allOf(
|
||||
argument(0, allOf(isSameType(Class.class.getCanonicalName()), not(isVariable()))),
|
||||
staticMethod().onClass("org.mockito.Mockito").namedAnyOf("mock", "spy"));
|
||||
|
||||
/** Instantiates a new {@link MockitoMockClassReference} instance. */
|
||||
public MockitoMockClassReference() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!MOCKITO_MOCK_OR_SPY_WITH_HARDCODED_TYPE.matches(tree, state)
|
||||
|| !isTypeDerivableFromContext(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
List<? extends ExpressionTree> arguments = tree.getArguments();
|
||||
return describeMatch(tree, SuggestedFixes.removeElement(arguments.get(0), arguments, state));
|
||||
}
|
||||
|
||||
// XXX: Use switch pattern matching once the targeted JDK supports this.
|
||||
private static boolean isTypeDerivableFromContext(MethodInvocationTree tree, VisitorState state) {
|
||||
Tree parent = state.getPath().getParentPath().getLeaf();
|
||||
return switch (parent.getKind()) {
|
||||
case VARIABLE ->
|
||||
!ASTHelpers.hasImplicitType((VariableTree) parent, state)
|
||||
&& MoreASTHelpers.areSameType(tree, parent, state);
|
||||
case ASSIGNMENT -> MoreASTHelpers.areSameType(tree, parent, state);
|
||||
case RETURN ->
|
||||
MoreASTHelpers.findMethodExitedOnReturn(state)
|
||||
.filter(m -> MoreASTHelpers.areSameType(tree, m.getReturnType(), state))
|
||||
.isPresent();
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
@@ -17,23 +18,27 @@ import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import java.util.List;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} which flags method invocations for which all arguments are wrapped using
|
||||
* A {@link BugChecker} that flags method invocations for which all arguments are wrapped using
|
||||
* {@link org.mockito.Mockito#eq}; this is redundant.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "MockitoStubbing",
|
||||
summary = "Don't unnecessarily use Mockito's `eq(...)`",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class MockitoStubbingCheck extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
link = BUG_PATTERNS_BASE_URL + "MockitoStubbing",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class MockitoStubbing extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> MOCKITO_EQ_METHOD =
|
||||
staticMethod().onClass("org.mockito.ArgumentMatchers").named("eq");
|
||||
|
||||
/** Instantiates a new {@link MockitoStubbing} instance. */
|
||||
public MockitoStubbing() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
List<? extends ExpressionTree> arguments = tree.getArguments();
|
||||
@@ -45,7 +50,7 @@ public final class MockitoStubbingCheck extends BugChecker implements MethodInvo
|
||||
for (ExpressionTree arg : arguments) {
|
||||
suggestedFix.replace(
|
||||
arg,
|
||||
Util.treeToString(
|
||||
SourceCode.treeToString(
|
||||
Iterables.getOnlyElement(((MethodInvocationTree) arg).getArguments()), state));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.PERFORMANCE;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags usages of MongoDB {@code $text} filter usages.
|
||||
*
|
||||
* @see <a href="https://www.mongodb.com/docs/manual/text-search/">MongoDB Text Search</a>
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"""
|
||||
Avoid MongoDB's `$text` filter operator, as it can trigger heavy queries and even cause \
|
||||
the server to run out of memory""",
|
||||
link = BUG_PATTERNS_BASE_URL + "MongoDBTextFilterUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = PERFORMANCE)
|
||||
public final class MongoDBTextFilterUsage extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> MONGO_FILTERS_TEXT_METHOD =
|
||||
staticMethod().onClass("com.mongodb.client.model.Filters").named("text");
|
||||
|
||||
/** Instantiates a new {@link MongoDBTextFilterUsage} instance. */
|
||||
public MongoDBTextFilterUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
return MONGO_FILTERS_TEXT_METHOD.matches(tree, state)
|
||||
? describeMatch(tree)
|
||||
: Description.NO_MATCH;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreMatchers.isSubTypeOf;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.generic;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.raw;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.subOf;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Supplier;
|
||||
import com.google.errorprone.suppliers.Suppliers;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.util.Optional;
|
||||
|
||||
/** A {@link BugChecker} that flags nesting of {@link Optional Optionals}. */
|
||||
// XXX: Extend this checker to also flag method return types and variable/field types.
|
||||
// XXX: Consider generalizing this checker and `NestedPublishers` to a single `NestedMonad` check,
|
||||
// which e.g. also flags nested `Stream`s. Alternatively, combine these and other checkers into an
|
||||
// even more generic `ConfusingType` checker.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"Avoid nesting `Optional`s inside `Optional`s; the resultant code is hard to reason about",
|
||||
link = BUG_PATTERNS_BASE_URL + "NestedOptionals",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class NestedOptionals extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Supplier<Type> OPTIONAL = Suppliers.typeFromClass(Optional.class);
|
||||
private static final Matcher<Tree> IS_OPTIONAL_OF_OPTIONAL =
|
||||
isSubTypeOf(VisitorState.memoize(generic(OPTIONAL, subOf(raw(OPTIONAL)))));
|
||||
|
||||
/** Instantiates a new {@link NestedOptionals} instance. */
|
||||
public NestedOptionals() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
return IS_OPTIONAL_OF_OPTIONAL.matches(tree, state)
|
||||
? describeMatch(tree)
|
||||
: Description.NO_MATCH;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static com.google.errorprone.matchers.Matchers.typePredicateMatcher;
|
||||
import static com.google.errorprone.predicates.TypePredicates.allOf;
|
||||
import static com.google.errorprone.predicates.TypePredicates.not;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.utils.MoreTypePredicates.hasTypeParameter;
|
||||
import static tech.picnic.errorprone.utils.MoreTypePredicates.isSubTypeOf;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.generic;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.raw;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.subOf;
|
||||
import static tech.picnic.errorprone.utils.MoreTypes.type;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Supplier;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@code Publisher<? extends Publisher>} instances, unless the
|
||||
* nested {@link org.reactivestreams.Publisher} is a {@link reactor.core.publisher.GroupedFlux}.
|
||||
*/
|
||||
// XXX: See the `NestedOptionals` check for some ideas on how to generalize this kind of checker.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"""
|
||||
Avoid `Publisher`s that emit other `Publishers`s; the resultant code is hard to reason \
|
||||
about""",
|
||||
link = BUG_PATTERNS_BASE_URL + "NestedPublishers",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class NestedPublishers extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Supplier<Type> PUBLISHER = type("org.reactivestreams.Publisher");
|
||||
private static final Matcher<ExpressionTree> IS_NON_GROUPED_PUBLISHER_OF_PUBLISHERS =
|
||||
typePredicateMatcher(
|
||||
allOf(
|
||||
isSubTypeOf(generic(PUBLISHER, subOf(raw(PUBLISHER)))),
|
||||
not(
|
||||
hasTypeParameter(
|
||||
0, isSubTypeOf(raw(type("reactor.core.publisher.GroupedFlux")))))));
|
||||
|
||||
/** Instantiates a new {@link NestedPublishers} instance. */
|
||||
public NestedPublishers() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
return IS_NON_GROUPED_PUBLISHER_OF_PUBLISHERS.matches(tree, state)
|
||||
? describeMatch(tree)
|
||||
: Description.NO_MATCH;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import java.util.function.BiFunction;
|
||||
import reactor.core.publisher.Mono;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@link Mono} operations that are known to be vacuous, given that
|
||||
* they are invoked on a {@link Mono} that is known not to complete empty.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Avoid vacuous operations on known non-empty `Mono`s",
|
||||
link = BUG_PATTERNS_BASE_URL + "NonEmptyMono",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = SIMPLIFICATION)
|
||||
// XXX: This check does not simplify `someFlux.defaultIfEmpty(T).{defaultIfEmpty(T),hasElements()}`,
|
||||
// as `someFlux.defaultIfEmpty(T)` yields a `Flux` rather than a `Mono`. Consider adding support for
|
||||
// these cases.
|
||||
// XXX: Given more advanced analysis many more expressions could be flagged. Consider
|
||||
// `Mono.just(someValue)`, `Flux.just(someNonEmptySequence)`,
|
||||
// `someMono.switchIfEmpty(someProvablyNonEmptyMono)` and many other variants.
|
||||
// XXX: Consider implementing a similar check for `Publisher`s that are known to complete without
|
||||
// emitting a value (e.g. `Mono.empty()`, `someFlux.then()`, ...), or known not to complete normally
|
||||
// (`Mono.never()`, `someFlux.repeat()`, `Mono.error(...)`, ...). The latter category could
|
||||
// potentially be split out further.
|
||||
@SuppressWarnings("java:S1192" /* Factoring out repeated method names impacts readability. */)
|
||||
public final class NonEmptyMono extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> MONO_SIZE_CHECK =
|
||||
instanceMethod()
|
||||
.onDescendantOf("reactor.core.publisher.Mono")
|
||||
.namedAnyOf("defaultIfEmpty", "single", "switchIfEmpty");
|
||||
private static final Matcher<ExpressionTree> NON_EMPTY_MONO =
|
||||
anyOf(
|
||||
instanceMethod()
|
||||
.onDescendantOf("reactor.core.publisher.Flux")
|
||||
.namedAnyOf(
|
||||
"all",
|
||||
"any",
|
||||
"collect",
|
||||
"collectList",
|
||||
"collectMap",
|
||||
"collectMultimap",
|
||||
"collectSortedList",
|
||||
"count",
|
||||
"elementAt",
|
||||
"hasElement",
|
||||
"hasElements",
|
||||
"last",
|
||||
"reduceWith",
|
||||
"single"),
|
||||
instanceMethod()
|
||||
.onDescendantOf("reactor.core.publisher.Flux")
|
||||
.named("reduce")
|
||||
.withParameters(Object.class.getCanonicalName(), BiFunction.class.getCanonicalName()),
|
||||
instanceMethod()
|
||||
.onDescendantOf("reactor.core.publisher.Mono")
|
||||
.namedAnyOf("defaultIfEmpty", "hasElement", "single"));
|
||||
|
||||
/** Instantiates a new {@link NonEmptyMono} instance. */
|
||||
public NonEmptyMono() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!MONO_SIZE_CHECK.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
ExpressionTree receiver = ASTHelpers.getReceiver(tree);
|
||||
if (!NON_EMPTY_MONO.matches(receiver, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return describeMatch(
|
||||
tree, SuggestedFix.replace(tree, SourceCode.treeToString(receiver, state)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
|
||||
import static tech.picnic.errorprone.bugpatterns.StaticImport.STATIC_IMPORT_CANDIDATE_MEMBERS;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSetMultimap;
|
||||
import com.google.common.collect.ImmutableTable;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.CompilationUnitTree;
|
||||
import com.sun.source.tree.IdentifierTree;
|
||||
import com.sun.source.tree.ImportTree;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.util.TreeScanner;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import java.time.Clock;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags static imports of type members that should *not* be statically
|
||||
* imported.
|
||||
*/
|
||||
// XXX: This check is closely linked to `StaticImport`. Consider merging the two.
|
||||
// XXX: Add suppression support. If qualification of one more more identifiers is suppressed, then
|
||||
// the associated static import should *not* be removed.
|
||||
// XXX: Also introduce logic that disallows statically importing `ZoneOffset.ofHours` and other
|
||||
// `ofXXX`-style methods.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Member should not be statically imported",
|
||||
link = BUG_PATTERNS_BASE_URL + "NonStaticImport",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
public final class NonStaticImport extends BugChecker implements CompilationUnitTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Types whose members should not be statically imported, unless exempted by {@link
|
||||
* StaticImport#STATIC_IMPORT_CANDIDATE_MEMBERS}.
|
||||
*
|
||||
* <p>Types listed here should be mutually exclusive with {@link
|
||||
* StaticImport#STATIC_IMPORT_CANDIDATE_TYPES}.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final ImmutableSet<String> NON_STATIC_IMPORT_CANDIDATE_TYPES =
|
||||
ImmutableSet.of(
|
||||
ASTHelpers.class.getCanonicalName(),
|
||||
Clock.class.getCanonicalName(),
|
||||
Strings.class.getCanonicalName(),
|
||||
VisitorState.class.getCanonicalName(),
|
||||
ZoneOffset.class.getCanonicalName(),
|
||||
"com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode",
|
||||
"reactor.core.publisher.Flux",
|
||||
"reactor.core.publisher.Mono");
|
||||
|
||||
/**
|
||||
* Type members that should never be statically imported.
|
||||
*
|
||||
* <p>Please note that:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Types listed by {@link #NON_STATIC_IMPORT_CANDIDATE_TYPES} and members listed by {@link
|
||||
* #NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS} should be omitted from this collection.
|
||||
* <li>This collection should be mutually exclusive with {@link
|
||||
* StaticImport#STATIC_IMPORT_CANDIDATE_MEMBERS}.
|
||||
* </ul>
|
||||
*/
|
||||
// XXX: Perhaps the set of exempted `java.util.Collections` methods is too strict. For now any
|
||||
// method name that could be considered "too vague" or could conceivably mean something else in a
|
||||
// specific context is left out.
|
||||
static final ImmutableSetMultimap<String, String> NON_STATIC_IMPORT_CANDIDATE_MEMBERS =
|
||||
ImmutableSetMultimap.<String, String>builder()
|
||||
.putAll(
|
||||
Collections.class.getCanonicalName(),
|
||||
"addAll",
|
||||
"copy",
|
||||
"fill",
|
||||
"list",
|
||||
"max",
|
||||
"min",
|
||||
"nCopies",
|
||||
"rotate",
|
||||
"sort",
|
||||
"swap")
|
||||
.put(Locale.class.getCanonicalName(), "ROOT")
|
||||
.put(Optional.class.getCanonicalName(), "empty")
|
||||
.putAll(Pattern.class.getCanonicalName(), "compile", "matches", "quote")
|
||||
.put(Predicates.class.getCanonicalName(), "contains")
|
||||
.put("org.springframework.http.MediaType", "ALL")
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Identifiers that should never be statically imported.
|
||||
*
|
||||
* <p>Please note that:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Identifiers listed by {@link StaticImport#STATIC_IMPORT_CANDIDATE_MEMBERS} should be
|
||||
* mutually exclusive with identifiers listed here.
|
||||
* <li>This list should contain a superset of the identifiers flagged by {@link
|
||||
* com.google.errorprone.bugpatterns.BadImport}.
|
||||
* </ul>
|
||||
*/
|
||||
static final ImmutableSet<String> NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS =
|
||||
ImmutableSet.of(
|
||||
"builder",
|
||||
"copyOf",
|
||||
"create",
|
||||
"from",
|
||||
"getDefaultInstance",
|
||||
"INSTANCE",
|
||||
"MAX",
|
||||
"MAX_VALUE",
|
||||
"MIN",
|
||||
"MIN_VALUE",
|
||||
"newBuilder",
|
||||
"newInstance",
|
||||
"of",
|
||||
"parse",
|
||||
"valueOf");
|
||||
|
||||
/** Instantiates a new {@link NonStaticImport} instance. */
|
||||
public NonStaticImport() {}
|
||||
|
||||
@Override
|
||||
public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) {
|
||||
ImmutableTable<String, String, UndesiredStaticImport> undesiredStaticImports =
|
||||
getUndesiredStaticImports(tree, state);
|
||||
|
||||
if (!undesiredStaticImports.isEmpty()) {
|
||||
replaceUndesiredStaticImportUsages(tree, undesiredStaticImports, state);
|
||||
|
||||
for (UndesiredStaticImport staticImport : undesiredStaticImports.values()) {
|
||||
state.reportMatch(
|
||||
describeMatch(staticImport.importTree(), staticImport.fixBuilder().build()));
|
||||
}
|
||||
}
|
||||
|
||||
/* Any violations have been flagged against the offending static import statement. */
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
private static ImmutableTable<String, String, UndesiredStaticImport> getUndesiredStaticImports(
|
||||
CompilationUnitTree tree, VisitorState state) {
|
||||
ImmutableTable.Builder<String, String, UndesiredStaticImport> imports =
|
||||
ImmutableTable.builder();
|
||||
for (ImportTree importTree : tree.getImports()) {
|
||||
Tree qualifiedIdentifier = importTree.getQualifiedIdentifier();
|
||||
if (importTree.isStatic() && qualifiedIdentifier instanceof MemberSelectTree memberSelect) {
|
||||
String type = SourceCode.treeToString(memberSelect.getExpression(), state);
|
||||
String member = memberSelect.getIdentifier().toString();
|
||||
if (shouldNotBeStaticallyImported(type, member)) {
|
||||
imports.put(
|
||||
type,
|
||||
member,
|
||||
new AutoValue_NonStaticImport_UndesiredStaticImport(
|
||||
importTree, SuggestedFix.builder().removeStaticImport(type + '.' + member)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imports.build();
|
||||
}
|
||||
|
||||
private static boolean shouldNotBeStaticallyImported(String type, String member) {
|
||||
return (NON_STATIC_IMPORT_CANDIDATE_TYPES.contains(type)
|
||||
&& !STATIC_IMPORT_CANDIDATE_MEMBERS.containsEntry(type, member))
|
||||
|| NON_STATIC_IMPORT_CANDIDATE_MEMBERS.containsEntry(type, member)
|
||||
|| NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS.contains(member);
|
||||
}
|
||||
|
||||
private static void replaceUndesiredStaticImportUsages(
|
||||
CompilationUnitTree tree,
|
||||
ImmutableTable<String, String, UndesiredStaticImport> undesiredStaticImports,
|
||||
VisitorState state) {
|
||||
new TreeScanner<@Nullable Void, @Nullable Void>() {
|
||||
@Override
|
||||
public @Nullable Void visitIdentifier(IdentifierTree node, @Nullable Void unused) {
|
||||
Symbol symbol = ASTHelpers.getSymbol(node);
|
||||
if (symbol != null) {
|
||||
UndesiredStaticImport staticImport =
|
||||
undesiredStaticImports.get(
|
||||
symbol.owner.getQualifiedName().toString(), symbol.name.toString());
|
||||
if (staticImport != null) {
|
||||
SuggestedFix.Builder fix = staticImport.fixBuilder();
|
||||
fix.prefixWith(node, SuggestedFixes.qualifyType(state, fix, symbol.owner) + '.');
|
||||
}
|
||||
}
|
||||
|
||||
return super.visitIdentifier(node, unused);
|
||||
}
|
||||
}.scan(tree, null);
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class UndesiredStaticImport {
|
||||
abstract ImportTree importTree();
|
||||
|
||||
abstract SuggestedFix.Builder fixBuilder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.PERFORMANCE;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.VerifyException;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.Fix;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.IdentifierTree;
|
||||
import com.sun.source.tree.LambdaExpressionTree;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Symtab;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import com.sun.tools.javac.code.Types;
|
||||
import com.sun.tools.javac.tree.JCTree.JCMemberReference;
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@code Comparator#comparing*} invocations that can be replaced
|
||||
* with an equivalent alternative so as to avoid unnecessary (un)boxing.
|
||||
*/
|
||||
// XXX: Add more documentation. Explain how this is useful in the face of refactoring to more
|
||||
// specific types.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"""
|
||||
Ensure invocations of `Comparator#comparing{,Double,Int,Long}` match the return type of \
|
||||
the provided function""",
|
||||
link = BUG_PATTERNS_BASE_URL + "PrimitiveComparison",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = PERFORMANCE)
|
||||
@SuppressWarnings("java:S1192" /* Factoring out repeated method names impacts readability. */)
|
||||
public final class PrimitiveComparison extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> STATIC_COMPARISON_METHOD =
|
||||
anyOf(
|
||||
staticMethod()
|
||||
.onClass(Comparator.class.getCanonicalName())
|
||||
.namedAnyOf("comparingInt", "comparingLong", "comparingDouble"),
|
||||
staticMethod()
|
||||
.onClass(Comparator.class.getCanonicalName())
|
||||
.named("comparing")
|
||||
.withParameters(Function.class.getCanonicalName()));
|
||||
private static final Matcher<ExpressionTree> INSTANCE_COMPARISON_METHOD =
|
||||
anyOf(
|
||||
instanceMethod()
|
||||
.onDescendantOf(Comparator.class.getCanonicalName())
|
||||
.namedAnyOf("thenComparingInt", "thenComparingLong", "thenComparingDouble"),
|
||||
instanceMethod()
|
||||
.onDescendantOf(Comparator.class.getCanonicalName())
|
||||
.named("thenComparing")
|
||||
.withParameters(Function.class.getCanonicalName()));
|
||||
|
||||
/** Instantiates a new {@link PrimitiveComparison} instance. */
|
||||
public PrimitiveComparison() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
boolean isStatic = STATIC_COMPARISON_METHOD.matches(tree, state);
|
||||
if (!isStatic && !INSTANCE_COMPARISON_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return getPotentiallyBoxedReturnType(tree.getArguments().get(0))
|
||||
.flatMap(cmpType -> attemptMethodInvocationReplacement(tree, cmpType, isStatic, state))
|
||||
.map(fix -> describeMatch(tree, fix))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
private static Optional<Fix> attemptMethodInvocationReplacement(
|
||||
MethodInvocationTree tree, Type cmpType, boolean isStatic, VisitorState state) {
|
||||
String actualMethodName = ASTHelpers.getSymbol(tree).getSimpleName().toString();
|
||||
String preferredMethodName = getPreferredMethod(cmpType, isStatic, state);
|
||||
if (actualMethodName.equals(preferredMethodName)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(
|
||||
suggestFix(
|
||||
tree, prefixTypeArgumentsIfRelevant(preferredMethodName, tree, cmpType, state), state));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefixes the given method name with generic type parameters if it replaces a {@code
|
||||
* Comparator#comparing{,Double,Long,Int}} method which also has generic type parameters.
|
||||
*
|
||||
* <p>Such type parameters are retained as they are likely required.
|
||||
*
|
||||
* <p>Note that any type parameter to {@code Comparator#thenComparing} is likely redundant, and in
|
||||
* any case becomes obsolete once that method is replaced with {@code
|
||||
* Comparator#thenComparing{Double,Long,Int}}. Conversion in the opposite direction does not
|
||||
* require the introduction of a generic type parameter.
|
||||
*/
|
||||
private static String prefixTypeArgumentsIfRelevant(
|
||||
String preferredMethodName, MethodInvocationTree tree, Type cmpType, VisitorState state) {
|
||||
if (tree.getTypeArguments().isEmpty() || preferredMethodName.startsWith("then")) {
|
||||
return preferredMethodName;
|
||||
}
|
||||
|
||||
String typeArguments =
|
||||
Stream.concat(
|
||||
Stream.of(SourceCode.treeToString(tree.getTypeArguments().get(0), state)),
|
||||
Stream.of(cmpType.tsym.getSimpleName())
|
||||
.filter(u -> "comparing".equals(preferredMethodName)))
|
||||
.collect(joining(", ", "<", ">"));
|
||||
|
||||
return typeArguments + preferredMethodName;
|
||||
}
|
||||
|
||||
private static String getPreferredMethod(Type cmpType, boolean isStatic, VisitorState state) {
|
||||
Types types = state.getTypes();
|
||||
Symtab symtab = state.getSymtab();
|
||||
|
||||
if (types.isSubtype(cmpType, symtab.intType)) {
|
||||
return isStatic ? "comparingInt" : "thenComparingInt";
|
||||
}
|
||||
|
||||
if (types.isSubtype(cmpType, symtab.longType)) {
|
||||
return isStatic ? "comparingLong" : "thenComparingLong";
|
||||
}
|
||||
|
||||
if (types.isSubtype(cmpType, symtab.doubleType)) {
|
||||
return isStatic ? "comparingDouble" : "thenComparingDouble";
|
||||
}
|
||||
|
||||
return isStatic ? "comparing" : "thenComparing";
|
||||
}
|
||||
|
||||
// XXX: Use switch pattern matching once the targeted JDK supports this.
|
||||
private static Optional<Type> getPotentiallyBoxedReturnType(ExpressionTree tree) {
|
||||
if (tree instanceof LambdaExpressionTree lambdaExpression) {
|
||||
/* Return the lambda expression's actual return type. */
|
||||
return Optional.ofNullable(ASTHelpers.getType(lambdaExpression.getBody()));
|
||||
}
|
||||
|
||||
// XXX: The match against a concrete type and reference to one of its fields is fragile. Do
|
||||
// better.
|
||||
if (tree instanceof JCMemberReference memberReference) {
|
||||
/* Return the method's declared return type. */
|
||||
Type subType = memberReference.referentType;
|
||||
return Optional.of(subType.getReturnType());
|
||||
}
|
||||
|
||||
/* This appears to be a genuine `{,ToInt,ToLong,ToDouble}Function`. */
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// XXX: Use switch pattern matching once the targeted JDK supports this.
|
||||
private static Fix suggestFix(
|
||||
MethodInvocationTree tree, String preferredMethodName, VisitorState state) {
|
||||
ExpressionTree expr = tree.getMethodSelect();
|
||||
|
||||
if (expr instanceof IdentifierTree) {
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder();
|
||||
String replacement =
|
||||
SuggestedFixes.qualifyStaticImport(
|
||||
Comparator.class.getCanonicalName() + '.' + preferredMethodName, fix, state);
|
||||
return fix.replace(expr, replacement).build();
|
||||
}
|
||||
|
||||
if (expr instanceof MemberSelectTree memberSelect) {
|
||||
return SuggestedFix.replace(
|
||||
memberSelect,
|
||||
SourceCode.treeToString(memberSelect.getExpression(), state) + '.' + preferredMethodName);
|
||||
}
|
||||
|
||||
throw new VerifyException("Unexpected type of expression: " + expr.getKind());
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static java.util.function.Predicate.not;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.VerifyException;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.Fix;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.LambdaExpressionTree;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Symtab;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import com.sun.tools.javac.code.Types;
|
||||
import com.sun.tools.javac.tree.JCTree.JCMemberReference;
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} which flags {@code Comparator#comparing*} invocations that can be replaced
|
||||
* with an equivalent alternative so as to avoid unnecessary (un)boxing.
|
||||
*/
|
||||
// XXX: Add more documentation. Explain how this is useful in the face of refactoring to more
|
||||
// specific types.
|
||||
// XXX: Change this checker's name?
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "PrimitiveComparison",
|
||||
summary =
|
||||
"Ensure invocations of `Comparator#comparing{,Double,Int,Long}` match the return type"
|
||||
+ " of the provided function",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.WARNING,
|
||||
tags = StandardTags.PERFORMANCE)
|
||||
public final class PrimitiveComparisonCheck extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> STATIC_COMPARISON_METHOD =
|
||||
anyOf(
|
||||
staticMethod()
|
||||
.onClass(Comparator.class.getName())
|
||||
.namedAnyOf("comparingInt", "comparingLong", "comparingDouble"),
|
||||
staticMethod()
|
||||
.onClass(Comparator.class.getName())
|
||||
.named("comparing")
|
||||
.withParameters(Function.class.getName()));
|
||||
private static final Matcher<ExpressionTree> INSTANCE_COMPARISON_METHOD =
|
||||
anyOf(
|
||||
instanceMethod()
|
||||
.onDescendantOf(Comparator.class.getName())
|
||||
.namedAnyOf("thenComparingInt", "thenComparingLong", "thenComparingDouble"),
|
||||
instanceMethod()
|
||||
.onDescendantOf(Comparator.class.getName())
|
||||
.named("thenComparing")
|
||||
.withParameters(Function.class.getName()));
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
boolean isStatic = STATIC_COMPARISON_METHOD.matches(tree, state);
|
||||
if (!isStatic && !INSTANCE_COMPARISON_METHOD.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return getPotentiallyBoxedReturnType(tree.getArguments().get(0))
|
||||
.flatMap(cmpType -> tryFix(tree, state, cmpType, isStatic))
|
||||
.map(fix -> describeMatch(tree, fix))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
private static Optional<Fix> tryFix(
|
||||
MethodInvocationTree tree, VisitorState state, Type cmpType, boolean isStatic) {
|
||||
return Optional.ofNullable(ASTHelpers.getSymbol(tree))
|
||||
.map(methodSymbol -> methodSymbol.getSimpleName().toString())
|
||||
.flatMap(
|
||||
actualMethodName ->
|
||||
Optional.of(getPreferredMethod(state, cmpType, isStatic))
|
||||
.filter(not(actualMethodName::equals)))
|
||||
.map(preferredMethodName -> suggestFix(tree, preferredMethodName, state));
|
||||
}
|
||||
|
||||
private static String getPreferredMethod(VisitorState state, Type cmpType, boolean isStatic) {
|
||||
Types types = state.getTypes();
|
||||
Symtab symtab = state.getSymtab();
|
||||
|
||||
if (types.isSubtype(cmpType, symtab.intType)) {
|
||||
return isStatic ? "comparingInt" : "thenComparingInt";
|
||||
}
|
||||
|
||||
if (types.isSubtype(cmpType, symtab.longType)) {
|
||||
return isStatic ? "comparingLong" : "thenComparingLong";
|
||||
}
|
||||
|
||||
if (types.isSubtype(cmpType, symtab.doubleType)) {
|
||||
return isStatic ? "comparingDouble" : "thenComparingDouble";
|
||||
}
|
||||
|
||||
return isStatic ? "comparing" : "thenComparing";
|
||||
}
|
||||
|
||||
private static Optional<Type> getPotentiallyBoxedReturnType(ExpressionTree tree) {
|
||||
switch (tree.getKind()) {
|
||||
case LAMBDA_EXPRESSION:
|
||||
/* Return the lambda expression's actual return type. */
|
||||
return Optional.ofNullable(ASTHelpers.getType(((LambdaExpressionTree) tree).getBody()));
|
||||
case MEMBER_REFERENCE:
|
||||
/* Return the method's declared return type. */
|
||||
// XXX: Very fragile. Do better.
|
||||
Type subType2 = ((JCMemberReference) tree).referentType;
|
||||
return Optional.of(subType2.getReturnType());
|
||||
default:
|
||||
/* This appears to be a genuine `{,ToInt,ToLong,ToDouble}Function`. */
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: We drop explicitly specified generic type information. In case the number of type
|
||||
// arguments before and after doesn't match, that's for the better. But if we e.g. replace
|
||||
// `comparingLong` with `comparingInt`, then we should retain it.
|
||||
private static Fix suggestFix(
|
||||
MethodInvocationTree tree, String preferredMethodName, VisitorState state) {
|
||||
ExpressionTree expr = tree.getMethodSelect();
|
||||
switch (expr.getKind()) {
|
||||
case IDENTIFIER:
|
||||
return SuggestedFix.builder()
|
||||
.addStaticImport(Comparator.class.getName() + '.' + preferredMethodName)
|
||||
.replace(expr, preferredMethodName)
|
||||
.build();
|
||||
case MEMBER_SELECT:
|
||||
MemberSelectTree ms = (MemberSelectTree) tree.getMethodSelect();
|
||||
return SuggestedFix.replace(
|
||||
ms, Util.treeToString(ms.getExpression(), state) + '.' + preferredMethodName);
|
||||
default:
|
||||
throw new VerifyException("Unexpected type of expression: " + expr.getKind());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,29 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anyMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anything;
|
||||
import static com.google.errorprone.matchers.Matchers.argumentCount;
|
||||
import static com.google.errorprone.matchers.Matchers.isNonNullUsingDataflow;
|
||||
import static com.google.errorprone.matchers.Matchers.isSameType;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Verify;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.primitives.Primitives;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.ErrorProneFlags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
@@ -24,6 +33,7 @@ import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Suppliers;
|
||||
import com.sun.source.tree.BinaryTree;
|
||||
import com.sun.source.tree.CompoundAssignmentTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
@@ -41,25 +51,32 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import javax.inject.Inject;
|
||||
import tech.picnic.errorprone.utils.Flags;
|
||||
import tech.picnic.errorprone.utils.MethodMatcherFactory;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/** A {@link BugChecker} which flags redundant explicit string conversions. */
|
||||
/** A {@link BugChecker} that flags redundant explicit string conversions. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "RedundantStringConversion",
|
||||
summary = "Avoid redundant string conversions when possible",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class RedundantStringConversionCheck extends BugChecker
|
||||
link = BUG_PATTERNS_BASE_URL + "RedundantStringConversion",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
@SuppressWarnings({
|
||||
"java:S1192" /* Factoring out repeated method names impacts readability. */,
|
||||
"java:S2160" /* Super class equality definition suffices. */,
|
||||
"key-to-resolve-AnnotationUseStyle-and-TrailingComment-check-conflict"
|
||||
})
|
||||
public final class RedundantStringConversion extends BugChecker
|
||||
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";
|
||||
|
||||
@SuppressWarnings("UnnecessaryLambda")
|
||||
private static final Matcher<ExpressionTree> ANY_EXPR = (t, s) -> true;
|
||||
"RedundantStringConversion:ExtraConversionMethods";
|
||||
|
||||
private static final Matcher<ExpressionTree> ANY_EXPR = anything();
|
||||
private static final Matcher<ExpressionTree> LOCALE = isSameType(Locale.class);
|
||||
private static final Matcher<ExpressionTree> MARKER = isSubtypeOf("org.slf4j.Marker");
|
||||
private static final Matcher<ExpressionTree> STRING = isSameType(String.class);
|
||||
@@ -68,106 +85,83 @@ public final class RedundantStringConversionCheck extends BugChecker
|
||||
allOf(STRING, isNonNullUsingDataflow());
|
||||
private static final Matcher<ExpressionTree> NOT_FORMATTABLE =
|
||||
not(isSubtypeOf(Formattable.class));
|
||||
private static final Matcher<ExpressionTree> WELL_KNOWN_STRING_CONVERSION_METHODS =
|
||||
private static final Matcher<MethodInvocationTree> WELL_KNOWN_STRING_CONVERSION_METHODS =
|
||||
anyOf(
|
||||
instanceMethod().onDescendantOfAny(Object.class.getName()).named("toString"),
|
||||
staticMethod()
|
||||
.onClass(Objects.class.getName())
|
||||
instanceMethod()
|
||||
.onDescendantOfAny(Object.class.getCanonicalName())
|
||||
.named("toString")
|
||||
.withParameters(Object.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(String.class.getName())
|
||||
.named("valueOf")
|
||||
.withParameters(Object.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(String.class.getName())
|
||||
.named("valueOf")
|
||||
.withParameters(String.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Byte.class.getName())
|
||||
.named("toString")
|
||||
.withParameters(byte.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Character.class.getName())
|
||||
.named("toString")
|
||||
.withParameters(char.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Short.class.getName())
|
||||
.named("toString")
|
||||
.withParameters(short.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Integer.class.getName())
|
||||
.named("toString")
|
||||
.withParameters(int.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Long.class.getName())
|
||||
.named("toString")
|
||||
.withParameters(long.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Float.class.getName())
|
||||
.named("toString")
|
||||
.withParameters(float.class.getName()),
|
||||
staticMethod()
|
||||
.onClass(Double.class.getName())
|
||||
.named("toString")
|
||||
.withParameters(double.class.getName()));
|
||||
.withNoParameters(),
|
||||
allOf(
|
||||
argumentCount(1),
|
||||
anyOf(
|
||||
staticMethod()
|
||||
.onClassAny(
|
||||
Stream.concat(
|
||||
Primitives.allWrapperTypes().stream(), Stream.of(Objects.class))
|
||||
.map(Class::getName)
|
||||
.collect(toImmutableSet()))
|
||||
.named("toString"),
|
||||
allOf(
|
||||
staticMethod().onClass(String.class.getCanonicalName()).named("valueOf"),
|
||||
not(
|
||||
anyMethod()
|
||||
.anyClass()
|
||||
.withAnyName()
|
||||
.withParametersOfType(
|
||||
ImmutableList.of(Suppliers.arrayOf(Suppliers.CHAR_TYPE))))))));
|
||||
private static final Matcher<ExpressionTree> STRINGBUILDER_APPEND_INVOCATION =
|
||||
instanceMethod()
|
||||
.onDescendantOf(StringBuilder.class.getName())
|
||||
.onDescendantOf(StringBuilder.class.getCanonicalName())
|
||||
.named("append")
|
||||
.withParameters(String.class.getName());
|
||||
.withParameters(String.class.getCanonicalName());
|
||||
private static final Matcher<ExpressionTree> STRINGBUILDER_INSERT_INVOCATION =
|
||||
instanceMethod()
|
||||
.onDescendantOf(StringBuilder.class.getName())
|
||||
.onDescendantOf(StringBuilder.class.getCanonicalName())
|
||||
.named("insert")
|
||||
.withParameters(int.class.getName(), String.class.getName());
|
||||
.withParameters(int.class.getCanonicalName(), String.class.getCanonicalName());
|
||||
private static final Matcher<ExpressionTree> FORMATTER_INVOCATION =
|
||||
anyOf(
|
||||
staticMethod().onClass(String.class.getName()).named("format"),
|
||||
instanceMethod().onDescendantOf(Formatter.class.getName()).named("format"),
|
||||
staticMethod().onClass(String.class.getCanonicalName()).named("format"),
|
||||
instanceMethod().onDescendantOf(Formatter.class.getCanonicalName()).named("format"),
|
||||
instanceMethod()
|
||||
.onDescendantOf(PrintStream.class.getName())
|
||||
.onDescendantOfAny(
|
||||
PrintStream.class.getCanonicalName(), PrintWriter.class.getCanonicalName())
|
||||
.namedAnyOf("format", "printf"),
|
||||
instanceMethod()
|
||||
.onDescendantOf(PrintStream.class.getName())
|
||||
.onDescendantOfAny(
|
||||
PrintStream.class.getCanonicalName(), PrintWriter.class.getCanonicalName())
|
||||
.namedAnyOf("print", "println")
|
||||
.withParameters(Object.class.getName()),
|
||||
instanceMethod()
|
||||
.onDescendantOf(PrintWriter.class.getName())
|
||||
.namedAnyOf("format", "printf"),
|
||||
instanceMethod()
|
||||
.onDescendantOf(PrintWriter.class.getName())
|
||||
.namedAnyOf("print", "println")
|
||||
.withParameters(Object.class.getName()),
|
||||
.withParameters(Object.class.getCanonicalName()),
|
||||
staticMethod()
|
||||
.onClass(Console.class.getName())
|
||||
.onClass(Console.class.getCanonicalName())
|
||||
.namedAnyOf("format", "printf", "readline", "readPassword"));
|
||||
private static final Matcher<ExpressionTree> GUAVA_GUARD_INVOCATION =
|
||||
anyOf(
|
||||
staticMethod()
|
||||
.onClass("com.google.common.base.Preconditions")
|
||||
.onClass(Preconditions.class.getCanonicalName())
|
||||
.namedAnyOf("checkArgument", "checkState", "checkNotNull"),
|
||||
staticMethod()
|
||||
.onClass("com.google.common.base.Verify")
|
||||
.onClass(Verify.class.getCanonicalName())
|
||||
.namedAnyOf("verify", "verifyNotNull"));
|
||||
private static final Matcher<ExpressionTree> SLF4J_LOGGER_INVOCATION =
|
||||
instanceMethod()
|
||||
.onDescendantOf("org.slf4j.Logger")
|
||||
.namedAnyOf("trace", "debug", "info", "warn", "error");
|
||||
|
||||
private final Matcher<ExpressionTree> conversionMethodMatcher;
|
||||
private final Matcher<MethodInvocationTree> conversionMethodMatcher;
|
||||
|
||||
/** Instantiates the default {@link RedundantStringConversionCheck}. */
|
||||
public RedundantStringConversionCheck() {
|
||||
/** Instantiates a default {@link RedundantStringConversion} instance. */
|
||||
public RedundantStringConversion() {
|
||||
this(ErrorProneFlags.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a customized {@link RedundantStringConversionCheck}.
|
||||
* Instantiates a customized {@link RedundantStringConversion}.
|
||||
*
|
||||
* @param flags Any provided command line flags.
|
||||
*/
|
||||
public RedundantStringConversionCheck(ErrorProneFlags flags) {
|
||||
@Inject
|
||||
RedundantStringConversion(ErrorProneFlags flags) {
|
||||
conversionMethodMatcher = createConversionMethodMatcher(flags);
|
||||
}
|
||||
|
||||
@@ -180,12 +174,12 @@ public final class RedundantStringConversionCheck extends BugChecker
|
||||
ExpressionTree lhs = tree.getLeftOperand();
|
||||
ExpressionTree rhs = tree.getRightOperand();
|
||||
if (!STRING.matches(lhs, state)) {
|
||||
return finalize(tree, tryFix(rhs, state, STRING));
|
||||
return createDescription(tree, tryFix(rhs, state, STRING));
|
||||
}
|
||||
|
||||
List<SuggestedFix.Builder> fixes = new ArrayList<>();
|
||||
|
||||
// XXX: Not so nice: we try to simplify the RHS twice.
|
||||
// XXX: Avoid trying to simplify the RHS twice.
|
||||
ExpressionTree preferredRhs = trySimplify(rhs, state).orElse(rhs);
|
||||
if (STRING.matches(preferredRhs, state)) {
|
||||
tryFix(lhs, state, ANY_EXPR).ifPresent(fixes::add);
|
||||
@@ -194,7 +188,7 @@ public final class RedundantStringConversionCheck extends BugChecker
|
||||
}
|
||||
tryFix(rhs, state, ANY_EXPR).ifPresent(fixes::add);
|
||||
|
||||
return finalize(tree, fixes.stream().reduce(SuggestedFix.Builder::merge));
|
||||
return createDescription(tree, fixes.stream().reduce(SuggestedFix.Builder::merge));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -203,36 +197,36 @@ public final class RedundantStringConversionCheck extends BugChecker
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return finalize(tree, tryFix(tree.getExpression(), state, ANY_EXPR));
|
||||
return createDescription(tree, tryFix(tree.getExpression(), state, ANY_EXPR));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (STRINGBUILDER_APPEND_INVOCATION.matches(tree, state)) {
|
||||
return finalize(tree, tryFixPositionalConverter(tree.getArguments(), state, 0));
|
||||
return createDescription(tree, tryFixPositionalConverter(tree.getArguments(), state, 0));
|
||||
}
|
||||
|
||||
if (STRINGBUILDER_INSERT_INVOCATION.matches(tree, state)) {
|
||||
return finalize(tree, tryFixPositionalConverter(tree.getArguments(), state, 1));
|
||||
return createDescription(tree, tryFixPositionalConverter(tree.getArguments(), state, 1));
|
||||
}
|
||||
|
||||
if (FORMATTER_INVOCATION.matches(tree, state)) {
|
||||
return finalize(tree, tryFixFormatter(tree.getArguments(), state));
|
||||
return createDescription(tree, tryFixFormatter(tree.getArguments(), state));
|
||||
}
|
||||
|
||||
if (GUAVA_GUARD_INVOCATION.matches(tree, state)) {
|
||||
return finalize(tree, tryFixGuavaGuard(tree.getArguments(), state));
|
||||
return createDescription(tree, tryFixGuavaGuard(tree.getArguments(), state));
|
||||
}
|
||||
|
||||
if (SLF4J_LOGGER_INVOCATION.matches(tree, state)) {
|
||||
return finalize(tree, tryFixSlf4jLogger(tree.getArguments(), state));
|
||||
return createDescription(tree, tryFixSlf4jLogger(tree.getArguments(), state));
|
||||
}
|
||||
|
||||
if (instanceMethod().matches(tree, state)) {
|
||||
return finalize(tree, tryFix(tree, state, STRING));
|
||||
return createDescription(tree, tryFix(tree, state, STRING));
|
||||
}
|
||||
|
||||
return finalize(tree, tryFix(tree, state, NON_NULL_STRING));
|
||||
return createDescription(tree, tryFix(tree, state, NON_NULL_STRING));
|
||||
}
|
||||
|
||||
private Optional<SuggestedFix.Builder> tryFixPositionalConverter(
|
||||
@@ -242,7 +236,7 @@ public final class RedundantStringConversionCheck extends BugChecker
|
||||
.flatMap(args -> tryFix(args.get(index), state, ANY_EXPR));
|
||||
}
|
||||
|
||||
// XXX: Write another check which checks that Formatter patterns don't use `{}` and have a
|
||||
// XXX: Write another check that checks that Formatter patterns don't use `{}` and have a
|
||||
// matching number of arguments of the appropriate type. Also flag explicit conversions from
|
||||
// `Formattable` to string.
|
||||
private Optional<SuggestedFix.Builder> tryFixFormatter(
|
||||
@@ -273,17 +267,17 @@ public final class RedundantStringConversionCheck extends BugChecker
|
||||
return tryFixFormatterArguments(arguments, state, ANY_EXPR, ANY_EXPR);
|
||||
}
|
||||
|
||||
// XXX: Write another check which checks that SLF4J patterns don't use `%s` and have a matching
|
||||
// XXX: Write another check that checks that SLF4J patterns don't use `%s` and have a matching
|
||||
// number of arguments of the appropriate type. Also flag explicit conversions from `Throwable` to
|
||||
// string as the last logger argument. Suggests either dropping the converison or going with
|
||||
// string as the last logger argument. Suggests either dropping the conversion or going with
|
||||
// `Throwable#getMessage()` instead.
|
||||
private Optional<SuggestedFix.Builder> tryFixSlf4jLogger(
|
||||
List<? extends ExpressionTree> arguments, VisitorState state) {
|
||||
/*
|
||||
* SLF4J treats the final argument to a log statement specially if it is a `Throwabe`: it
|
||||
* SLF4J treats the final argument to a log statement specially if it is a `Throwable`: it
|
||||
* will always choose to render the associated stacktrace, even if the argument has a
|
||||
* matching `{}` placeholder. (In this case the `{}` will simply be logged verbatim.) So if
|
||||
* a log statement's final argument is the string representation of a `Throwble`, then we
|
||||
* a log statement's final argument is the string representation of a `Throwable`, then we
|
||||
* must not strip this explicit string conversion, as that would change the statement's
|
||||
* semantics.
|
||||
*/
|
||||
@@ -315,7 +309,7 @@ public final class RedundantStringConversionCheck extends BugChecker
|
||||
|
||||
/* Simplify the values to be plugged into the format pattern, if possible. */
|
||||
return arguments.stream()
|
||||
.skip(patternIndex + 1)
|
||||
.skip(patternIndex + 1L)
|
||||
.map(arg -> tryFix(arg, state, remainingArgFilter))
|
||||
.flatMap(Optional::stream)
|
||||
.reduce(SuggestedFix.Builder::merge);
|
||||
@@ -326,7 +320,7 @@ public final class RedundantStringConversionCheck extends BugChecker
|
||||
return trySimplify(tree, state, filter)
|
||||
.map(
|
||||
replacement ->
|
||||
SuggestedFix.builder().replace(tree, Util.treeToString(replacement, state)));
|
||||
SuggestedFix.builder().replace(tree, SourceCode.treeToString(replacement, state)));
|
||||
}
|
||||
|
||||
private Optional<ExpressionTree> trySimplify(
|
||||
@@ -337,33 +331,33 @@ public final class RedundantStringConversionCheck extends BugChecker
|
||||
}
|
||||
|
||||
private Optional<ExpressionTree> trySimplify(ExpressionTree tree, VisitorState state) {
|
||||
if (tree.getKind() != Kind.METHOD_INVOCATION || !conversionMethodMatcher.matches(tree, state)) {
|
||||
if (!(tree instanceof MethodInvocationTree methodInvocation)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
MethodInvocationTree methodInvocation = (MethodInvocationTree) tree;
|
||||
switch (methodInvocation.getArguments().size()) {
|
||||
case 0:
|
||||
return trySimplifyNullaryMethod(methodInvocation, state);
|
||||
case 1:
|
||||
return trySimplifyUnaryMethod(methodInvocation, state);
|
||||
default:
|
||||
throw new IllegalStateException(
|
||||
"Cannot simplify method call with two or more arguments: "
|
||||
+ Util.treeToString(tree, state));
|
||||
if (!conversionMethodMatcher.matches(methodInvocation, state)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return switch (methodInvocation.getArguments().size()) {
|
||||
case 0 -> trySimplifyNullaryMethod(methodInvocation, state);
|
||||
case 1 -> trySimplifyUnaryMethod(methodInvocation, state);
|
||||
default ->
|
||||
throw new IllegalStateException(
|
||||
"Cannot simplify method call with two or more arguments: "
|
||||
+ SourceCode.treeToString(tree, state));
|
||||
};
|
||||
}
|
||||
|
||||
private static Optional<ExpressionTree> trySimplifyNullaryMethod(
|
||||
MethodInvocationTree methodInvocation, VisitorState state) {
|
||||
if (!instanceMethod().matches(methodInvocation, state)) {
|
||||
if (!instanceMethod().matches(methodInvocation, state)
|
||||
|| !(methodInvocation.getMethodSelect() instanceof MemberSelectTree memberSelect)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(methodInvocation.getMethodSelect())
|
||||
.filter(methodSelect -> methodSelect.getKind() == Kind.MEMBER_SELECT)
|
||||
.map(methodSelect -> ((MemberSelectTree) methodSelect).getExpression())
|
||||
.filter(expr -> !"super".equals(Util.treeToString(expr, state)));
|
||||
return Optional.of(memberSelect.getExpression())
|
||||
.filter(expr -> !"super".equals(SourceCode.treeToString(expr, state)));
|
||||
}
|
||||
|
||||
private static Optional<ExpressionTree> trySimplifyUnaryMethod(
|
||||
@@ -375,20 +369,20 @@ public final class RedundantStringConversionCheck extends BugChecker
|
||||
return Optional.of(Iterables.getOnlyElement(methodInvocation.getArguments()));
|
||||
}
|
||||
|
||||
private Description finalize(Tree tree, Optional<SuggestedFix.Builder> fixes) {
|
||||
private Description createDescription(Tree tree, Optional<SuggestedFix.Builder> fixes) {
|
||||
return fixes
|
||||
.map(SuggestedFix.Builder::build)
|
||||
.map(fix -> describeMatch(tree, fix))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
private static Matcher<ExpressionTree> createConversionMethodMatcher(ErrorProneFlags flags) {
|
||||
private static Matcher<MethodInvocationTree> createConversionMethodMatcher(
|
||||
ErrorProneFlags flags) {
|
||||
// XXX: ErrorProneFlags#getList splits by comma, but method signatures may also contain commas.
|
||||
// For this class methods accepting more than one argument are not valid, but still: not nice.
|
||||
return flags
|
||||
.getList(EXTRA_STRING_CONVERSION_METHODS_FLAG)
|
||||
.map(new MethodMatcherFactory()::create)
|
||||
.map(m -> anyOf(WELL_KNOWN_STRING_CONVERSION_METHODS, m))
|
||||
.orElse(WELL_KNOWN_STRING_CONVERSION_METHODS);
|
||||
return anyOf(
|
||||
WELL_KNOWN_STRING_CONVERSION_METHODS,
|
||||
new MethodMatcherFactory()
|
||||
.create(Flags.getList(flags, EXTRA_STRING_CONVERSION_METHODS_FLAG)));
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} which 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.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "RefasterAnyOfUsage",
|
||||
summary = "`Refaster#anyOf` should be passed at least two parameters",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class RefasterAnyOfUsageCheck extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> REFASTER_ANY_OF =
|
||||
staticMethod().onClass(Refaster.class.getName()).named("anyOf");
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (REFASTER_ANY_OF.matches(tree, state)) {
|
||||
switch (tree.getArguments().size()) {
|
||||
case 0:
|
||||
// We can't safely fix this case; dropping the expression may produce non-compilable code.
|
||||
return describeMatch(tree);
|
||||
case 1:
|
||||
return describeMatch(
|
||||
tree,
|
||||
SuggestedFix.replace(tree, Util.treeToString(tree.getArguments().get(0), state)));
|
||||
default:
|
||||
/* Handled below. */
|
||||
}
|
||||
}
|
||||
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.ALL;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isSameType;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static com.google.errorprone.matchers.Matchers.methodHasParameters;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
@@ -22,21 +22,25 @@ import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import java.io.InputStream;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} which flags {@code @RequestMapping} methods that have one or more parameters
|
||||
* A {@link BugChecker} that flags {@code @RequestMapping} methods that have one or more parameters
|
||||
* that appear to lack a relevant annotation.
|
||||
*
|
||||
* <p>Matched mappings are {@code @{Delete,Get,Patch,Post,Put,Request}Mapping}.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "RequestMappingAnnotation",
|
||||
summary = "Make sure all `@RequestMapping` method parameters are annotated",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.WARNING,
|
||||
tags = StandardTags.LIKELY_ERROR)
|
||||
public final class RequestMappingAnnotationCheck extends BugChecker implements MethodTreeMatcher {
|
||||
link = BUG_PATTERNS_BASE_URL + "RequestMappingAnnotation",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class RequestMappingAnnotation extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String ANN_PACKAGE_PREFIX = "org.springframework.web.bind.annotation.";
|
||||
// XXX: Generalize this logic to fully support Spring meta-annotations, then update the class
|
||||
@@ -51,8 +55,11 @@ public final class RequestMappingAnnotationCheck extends BugChecker implements M
|
||||
isType(ANN_PACKAGE_PREFIX + "PostMapping"),
|
||||
isType(ANN_PACKAGE_PREFIX + "PutMapping"),
|
||||
isType(ANN_PACKAGE_PREFIX + "RequestMapping")));
|
||||
// XXX: Add other parameters as necessary. See
|
||||
// https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-arguments.
|
||||
// XXX: Add other parameters as necessary. Also consider whether it makes sense to have WebMVC-
|
||||
// and WebFlux-specific logic. See
|
||||
// https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-arguments
|
||||
// and
|
||||
// https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-ann-arguments.
|
||||
private static final Matcher<MethodTree> LACKS_PARAMETER_ANNOTATION =
|
||||
not(
|
||||
methodHasParameters(
|
||||
@@ -62,14 +69,32 @@ public final class RequestMappingAnnotationCheck extends BugChecker implements M
|
||||
AT_LEAST_ONE,
|
||||
anyOf(
|
||||
isType(ANN_PACKAGE_PREFIX + "PathVariable"),
|
||||
isType(ANN_PACKAGE_PREFIX + "RequestAttribute"),
|
||||
isType(ANN_PACKAGE_PREFIX + "RequestBody"),
|
||||
isType(ANN_PACKAGE_PREFIX + "RequestHeader"),
|
||||
isType(ANN_PACKAGE_PREFIX + "RequestParam"))),
|
||||
isSameType("java.io.InputStream"),
|
||||
isType(ANN_PACKAGE_PREFIX + "RequestParam"),
|
||||
isType(ANN_PACKAGE_PREFIX + "RequestPart"),
|
||||
isType(
|
||||
"org.springframework.security.core.annotation.CurrentSecurityContext"))),
|
||||
isSameType(InputStream.class.getCanonicalName()),
|
||||
isSameType(Locale.class.getCanonicalName()),
|
||||
isSameType(TimeZone.class.getCanonicalName()),
|
||||
isSameType(ZoneId.class.getCanonicalName()),
|
||||
isSameType("jakarta.servlet.http.HttpServletRequest"),
|
||||
isSameType("jakarta.servlet.http.HttpServletResponse"),
|
||||
isSameType("javax.servlet.http.HttpServletRequest"),
|
||||
isSameType("javax.servlet.http.HttpServletResponse"),
|
||||
isSameType("org.springframework.http.HttpMethod"),
|
||||
isSubtypeOf("org.springframework.web.context.request.WebRequest"))));
|
||||
isSameType("org.springframework.ui.Model"),
|
||||
isSameType("org.springframework.validation.BindingResult"),
|
||||
isSameType("org.springframework.web.context.request.NativeWebRequest"),
|
||||
isSameType("org.springframework.web.context.request.WebRequest"),
|
||||
isSameType("org.springframework.web.server.ServerWebExchange"),
|
||||
isSameType("org.springframework.web.util.UriBuilder"),
|
||||
isSameType("org.springframework.web.util.UriComponentsBuilder"))));
|
||||
|
||||
/** Instantiates a new {@link RequestMappingAnnotation} instance. */
|
||||
public RequestMappingAnnotation() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
@@ -80,8 +105,10 @@ public final class RequestMappingAnnotationCheck extends BugChecker implements M
|
||||
&& LACKS_PARAMETER_ANNOTATION.matches(tree, state)
|
||||
? buildDescription(tree)
|
||||
.setMessage(
|
||||
"Not all parameters of this request mapping method are annotated; this may be a mistake. "
|
||||
+ "If the unannotated parameters represent query string parameters, annotate them with `@RequestParam`.")
|
||||
"""
|
||||
Not all parameters of this request mapping method are annotated; this may be a \
|
||||
mistake. If the unannotated parameters represent query string parameters, annotate \
|
||||
them with `@RequestParam`.""")
|
||||
.build()
|
||||
: Description.NO_MATCH;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.ErrorProneFlags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Suppliers;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import javax.inject.Inject;
|
||||
import tech.picnic.errorprone.utils.Flags;
|
||||
|
||||
/** A {@link BugChecker} that flags {@code @RequestParam} parameters with an unsupported type. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary =
|
||||
"""
|
||||
By default, `@RequestParam` does not support `ImmutableCollection` and `ImmutableMap` \
|
||||
subtypes""",
|
||||
link = BUG_PATTERNS_BASE_URL + "RequestParamType",
|
||||
linkType = CUSTOM,
|
||||
severity = ERROR,
|
||||
tags = LIKELY_ERROR)
|
||||
@SuppressWarnings("java:S2160" /* Super class equality definition suffices. */)
|
||||
public final class RequestParamType extends BugChecker implements VariableTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String SUPPORTED_CUSTOM_TYPES_FLAG = "RequestParamType:SupportedCustomTypes";
|
||||
|
||||
private final Matcher<VariableTree> hasUnsupportedRequestParamType;
|
||||
|
||||
/** Instantiates a default {@link RequestParamType} instance. */
|
||||
public RequestParamType() {
|
||||
this(ErrorProneFlags.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a customized {@link RequestParamType} instance.
|
||||
*
|
||||
* @param flags Any provided command line flags.
|
||||
*/
|
||||
@Inject
|
||||
RequestParamType(ErrorProneFlags flags) {
|
||||
hasUnsupportedRequestParamType = hasUnsupportedRequestParamType(flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Description matchVariable(VariableTree tree, VisitorState state) {
|
||||
return hasUnsupportedRequestParamType.matches(tree, state)
|
||||
? describeMatch(tree)
|
||||
: Description.NO_MATCH;
|
||||
}
|
||||
|
||||
private static Matcher<VariableTree> hasUnsupportedRequestParamType(ErrorProneFlags flags) {
|
||||
return allOf(
|
||||
annotations(AT_LEAST_ONE, isType("org.springframework.web.bind.annotation.RequestParam")),
|
||||
anyOf(isSubtypeOf(ImmutableCollection.class), isSubtypeOf(ImmutableMap.class)),
|
||||
not(isSubtypeOfAny(Flags.getList(flags, SUPPORTED_CUSTOM_TYPES_FLAG))));
|
||||
}
|
||||
|
||||
private static Matcher<Tree> isSubtypeOfAny(ImmutableList<String> inclusions) {
|
||||
return anyOf(
|
||||
inclusions.stream()
|
||||
.map(inclusion -> isSubtypeOf(Suppliers.typeFromString(inclusion)))
|
||||
.collect(toImmutableList()));
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
|
||||
import static com.google.errorprone.matchers.Matchers.isType;
|
||||
|
||||
import com.google.auto.common.AnnotationMirrors;
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.matchers.MultiMatcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} which flags methods with Spring's {@code @Scheduled} annotation that lack
|
||||
* New Relic Agent's {@code @Trace(dispatcher = true)}.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "ScheduledTransactionTrace",
|
||||
summary = "Scheduled operation must start a new New Relic transaction",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.ERROR,
|
||||
tags = StandardTags.LIKELY_ERROR)
|
||||
public final class ScheduledTransactionTraceCheck extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String TRACE_ANNOTATION_FQCN = "com.newrelic.api.agent.Trace";
|
||||
private static final Matcher<Tree> IS_SCHEDULED =
|
||||
hasAnnotation("org.springframework.scheduling.annotation.Scheduled");
|
||||
private static final MultiMatcher<Tree, AnnotationTree> TRACE_ANNOTATION =
|
||||
annotations(AT_LEAST_ONE, isType(TRACE_ANNOTATION_FQCN));
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (!IS_SCHEDULED.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
ImmutableList<AnnotationTree> traceAnnotations =
|
||||
TRACE_ANNOTATION.multiMatchResult(tree, state).matchingNodes();
|
||||
if (traceAnnotations.isEmpty()) {
|
||||
/* This method completely lacks the `@Trace` annotation; add it. */
|
||||
return describeMatch(
|
||||
tree,
|
||||
SuggestedFix.builder()
|
||||
.addImport(TRACE_ANNOTATION_FQCN)
|
||||
.prefixWith(tree, "@Trace(dispatcher = true)")
|
||||
.build());
|
||||
}
|
||||
|
||||
AnnotationTree traceAnnotation = Iterables.getOnlyElement(traceAnnotations);
|
||||
if (isCorrectAnnotation(traceAnnotation)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
/*
|
||||
* The `@Trace` annotation is present but does not specify `dispatcher = true`. Add or update
|
||||
* the `dispatcher` annotation element.
|
||||
*/
|
||||
return describeMatch(
|
||||
traceAnnotation,
|
||||
SuggestedFixes.updateAnnotationArgumentValues(
|
||||
traceAnnotation, "dispatcher", ImmutableList.of("true"))
|
||||
.build());
|
||||
}
|
||||
|
||||
private static boolean isCorrectAnnotation(AnnotationTree traceAnnotation) {
|
||||
return Boolean.TRUE.equals(
|
||||
AnnotationMirrors.getAnnotationValue(
|
||||
ASTHelpers.getAnnotationMirror(traceAnnotation), "dispatcher")
|
||||
.getValue());
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
@@ -22,22 +23,23 @@ import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/** A {@link BugChecker} which flags SLF4J usages that are likely to be in error. */
|
||||
/** A {@link BugChecker} that flags SLF4J usages that are likely to be in error. */
|
||||
// XXX: The special-casing of Throwable applies only to SLF4J 1.6.0+; see
|
||||
// https://www.slf4j.org/faq.html#paramException. That should be documented.
|
||||
// XXX: Also simplify `LOG.error(String.format("Something %s", arg), throwable)`.
|
||||
// XXX: Also simplify `LOG.error(String.join("sep", arg1, arg2), throwable)`? Perhaps too obscure.
|
||||
// XXX: Write a similar checker for Spring RestTemplates, String.format and friends, Guava
|
||||
// preconditions, ...
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "Slf4jLogStatement",
|
||||
summary = "Make sure SLF4J log statements contain proper placeholders with matching arguments",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.WARNING,
|
||||
tags = StandardTags.LIKELY_ERROR)
|
||||
public final class Slf4jLogStatementCheck extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
link = BUG_PATTERNS_BASE_URL + "Slf4jLogStatement",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = LIKELY_ERROR)
|
||||
public final class Slf4jLogStatement extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> MARKER = isSubtypeOf("org.slf4j.Marker");
|
||||
private static final Matcher<ExpressionTree> THROWABLE = isSubtypeOf(Throwable.class);
|
||||
@@ -46,6 +48,9 @@ public final class Slf4jLogStatementCheck extends BugChecker
|
||||
.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)) {
|
||||
@@ -114,7 +119,7 @@ public final class Slf4jLogStatementCheck extends BugChecker
|
||||
* replaced at this usage site.
|
||||
*/
|
||||
description.addFix(
|
||||
SuggestedFix.replace(tree, Util.treeToString(tree, state).replace("%s", "{}")));
|
||||
SuggestedFix.replace(tree, SourceCode.treeToString(tree, state).replace("%s", "{}")));
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -1,44 +1,46 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static java.util.function.Predicate.not;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.VerifyException;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.AnnotationTreeMatcher;
|
||||
import com.google.errorprone.fixes.Fix;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.AssignmentTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.NewArrayTree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import java.util.Optional;
|
||||
import tech.picnic.errorprone.utils.AnnotationAttributeMatcher;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} which flags {@code @RequestMapping} annotations that can be written more
|
||||
* A {@link BugChecker} that flags {@code @RequestMapping} annotations that can be written more
|
||||
* concisely.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "SpringMvcAnnotation",
|
||||
summary =
|
||||
"Prefer the conciseness of `@{Get,Put,Post,Delete,Patch}Mapping` over `@RequestMapping`",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class SpringMvcAnnotationCheck extends BugChecker implements AnnotationTreeMatcher {
|
||||
link = BUG_PATTERNS_BASE_URL + "SpringMvcAnnotation",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class SpringMvcAnnotation extends BugChecker implements AnnotationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String ANN_PACKAGE_PREFIX = "org.springframework.web.bind.annotation.";
|
||||
private static final AnnotationAttributeMatcher ARGUMENT_SELECTOR =
|
||||
@@ -54,6 +56,9 @@ public final class SpringMvcAnnotationCheck extends BugChecker implements Annota
|
||||
.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.
|
||||
@@ -73,31 +78,25 @@ public final class SpringMvcAnnotationCheck extends BugChecker implements Annota
|
||||
}
|
||||
|
||||
private static Optional<String> extractUniqueMethod(ExpressionTree arg, VisitorState state) {
|
||||
verify(
|
||||
arg.getKind() == Kind.ASSIGNMENT,
|
||||
"Annotation attribute is not an assignment: %s",
|
||||
arg.getKind());
|
||||
|
||||
ExpressionTree expr = ((AssignmentTree) arg).getExpression();
|
||||
if (expr.getKind() != Kind.NEW_ARRAY) {
|
||||
return Optional.of(extractMethod(expr, state));
|
||||
if (!(arg instanceof AssignmentTree assignment)) {
|
||||
throw new VerifyException("Annotation attribute is not an assignment:" + arg.getKind());
|
||||
}
|
||||
|
||||
NewArrayTree newArray = (NewArrayTree) expr;
|
||||
return Optional.of(newArray.getInitializers())
|
||||
.filter(args -> args.size() == 1)
|
||||
.map(args -> extractMethod(args.get(0), state));
|
||||
ExpressionTree expr = assignment.getExpression();
|
||||
return expr instanceof NewArrayTree newArray
|
||||
? Optional.of(newArray.getInitializers())
|
||||
.filter(args -> args.size() == 1)
|
||||
.map(args -> extractMethod(args.get(0), state))
|
||||
: Optional.of(extractMethod(expr, state));
|
||||
}
|
||||
|
||||
// XXX: Use switch pattern matching once the targeted JDK supports this.
|
||||
private static String extractMethod(ExpressionTree expr, VisitorState state) {
|
||||
switch (expr.getKind()) {
|
||||
case IDENTIFIER:
|
||||
return Util.treeToString(expr, state);
|
||||
case MEMBER_SELECT:
|
||||
return ((MemberSelectTree) expr).getIdentifier().toString();
|
||||
default:
|
||||
throw new VerifyException("Unexpected type of expression: " + expr.getKind());
|
||||
}
|
||||
return switch (expr.getKind()) {
|
||||
case IDENTIFIER -> SourceCode.treeToString(expr, state);
|
||||
case MEMBER_SELECT -> ((MemberSelectTree) expr).getIdentifier().toString();
|
||||
default -> throw new VerifyException("Unexpected type of expression: " + expr.getKind());
|
||||
};
|
||||
}
|
||||
|
||||
private static Fix replaceAnnotation(
|
||||
@@ -105,12 +104,11 @@ public final class SpringMvcAnnotationCheck extends BugChecker implements Annota
|
||||
String newArguments =
|
||||
tree.getArguments().stream()
|
||||
.filter(not(argToRemove::equals))
|
||||
.map(arg -> Util.treeToString(arg, state))
|
||||
.map(arg -> SourceCode.treeToString(arg, state))
|
||||
.collect(joining(", "));
|
||||
|
||||
return SuggestedFix.builder()
|
||||
.addImport(ANN_PACKAGE_PREFIX + newAnnotation)
|
||||
.replace(tree, String.format("@%s(%s)", newAnnotation, newArguments))
|
||||
.build();
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder();
|
||||
String annotation = SuggestedFixes.qualifyType(state, fix, ANN_PACKAGE_PREFIX + newAnnotation);
|
||||
return fix.replace(tree, String.format("@%s(%s)", annotation, newArguments)).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static tech.picnic.errorprone.bugpatterns.NonStaticImport.NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS;
|
||||
import static tech.picnic.errorprone.bugpatterns.NonStaticImport.NON_STATIC_IMPORT_CANDIDATE_MEMBERS;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Functions;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.base.Verify;
|
||||
import com.google.common.collect.Comparators;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultiset;
|
||||
import com.google.common.collect.ImmutableRangeSet;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSetMultimap;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.ImmutableSortedMultiset;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.collect.ImmutableTable;
|
||||
import com.google.common.collect.MoreCollectors;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MemberSelectTreeMatcher;
|
||||
import com.google.errorprone.bugpatterns.StaticImports;
|
||||
import com.google.errorprone.bugpatterns.StaticImports.StaticImportInfo;
|
||||
import com.google.errorprone.fixes.Fix;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matchers;
|
||||
import com.google.errorprone.refaster.ImportPolicy;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** A {@link BugChecker} that flags type members that can and should be statically imported. */
|
||||
// XXX: This check is closely linked to `NonStaticImport`. Consider merging the two.
|
||||
// XXX: Tricky cases:
|
||||
// - `org.springframework.http.HttpStatus` (not always an improvement, and `valueOf` must
|
||||
// certainly be excluded)
|
||||
// - `com.google.common.collect.Tables`
|
||||
// - `ch.qos.logback.classic.Level.{DEBUG, ERROR, INFO, TRACE, WARN}`
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Identifier should be statically imported",
|
||||
link = BUG_PATTERNS_BASE_URL + "StaticImport",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = STYLE)
|
||||
public final class StaticImport extends BugChecker implements MemberSelectTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Types whose members should be statically imported, unless exempted by {@link
|
||||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_MEMBERS} or {@link
|
||||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS}.
|
||||
*
|
||||
* <p>Types listed here should be mutually exclusive with {@link
|
||||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_TYPES}.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final ImmutableSet<String> STATIC_IMPORT_CANDIDATE_TYPES =
|
||||
ImmutableSet.of(
|
||||
BugPattern.LinkType.class.getCanonicalName(),
|
||||
BugPattern.SeverityLevel.class.getCanonicalName(),
|
||||
BugPattern.StandardTags.class.getCanonicalName(),
|
||||
Collections.class.getCanonicalName(),
|
||||
Collectors.class.getCanonicalName(),
|
||||
Comparator.class.getCanonicalName(),
|
||||
ImportPolicy.class.getCanonicalName(),
|
||||
Map.Entry.class.getCanonicalName(),
|
||||
Matchers.class.getCanonicalName(),
|
||||
MoreCollectors.class.getCanonicalName(),
|
||||
Pattern.class.getCanonicalName(),
|
||||
Preconditions.class.getCanonicalName(),
|
||||
Predicates.class.getCanonicalName(),
|
||||
StandardCharsets.class.getCanonicalName(),
|
||||
Verify.class.getCanonicalName(),
|
||||
"com.fasterxml.jackson.annotation.JsonCreator.Mode",
|
||||
"com.fasterxml.jackson.annotation.JsonFormat.Shape",
|
||||
"com.fasterxml.jackson.annotation.JsonInclude.Include",
|
||||
"com.fasterxml.jackson.annotation.JsonProperty.Access",
|
||||
"com.mongodb.client.model.Accumulators",
|
||||
"com.mongodb.client.model.Aggregates",
|
||||
"com.mongodb.client.model.Filters",
|
||||
"com.mongodb.client.model.Indexes",
|
||||
"com.mongodb.client.model.Projections",
|
||||
"com.mongodb.client.model.Sorts",
|
||||
"com.mongodb.client.model.Updates",
|
||||
"org.assertj.core.api.Assertions",
|
||||
"org.assertj.core.api.InstanceOfAssertFactories",
|
||||
"org.assertj.core.api.SoftAssertions",
|
||||
"org.assertj.core.data.Offset",
|
||||
"org.assertj.core.groups.Tuple",
|
||||
"org.hamcrest.Matchers",
|
||||
"org.hamcrest.text.MatchesPattern",
|
||||
"org.hibernate.validator.testutil.ConstraintViolationAssert",
|
||||
"org.junit.jupiter.api.Assertions",
|
||||
"org.mockito.AdditionalAnswers",
|
||||
"org.mockito.Answers",
|
||||
"org.mockito.ArgumentMatchers",
|
||||
"org.mockito.Mockito",
|
||||
"org.springframework.boot.test.context.SpringBootTest.WebEnvironment",
|
||||
"org.springframework.format.annotation.DateTimeFormat.ISO",
|
||||
"org.springframework.http.HttpHeaders",
|
||||
"org.springframework.http.HttpMethod",
|
||||
"org.springframework.http.MediaType",
|
||||
"org.testng.Assert",
|
||||
"reactor.function.TupleUtils",
|
||||
"tech.picnic.errorprone.utils.MoreTypes");
|
||||
|
||||
/**
|
||||
* Type members that should be statically imported.
|
||||
*
|
||||
* <p>Please note that:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Types listed by {@link #STATIC_IMPORT_CANDIDATE_TYPES} should be omitted from this
|
||||
* collection.
|
||||
* <li>This collection should be mutually exclusive with {@link
|
||||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_MEMBERS}.
|
||||
* <li>This collection should not list members contained in {@link
|
||||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS}.
|
||||
* </ul>
|
||||
*/
|
||||
static final ImmutableSetMultimap<String, String> STATIC_IMPORT_CANDIDATE_MEMBERS =
|
||||
ImmutableSetMultimap.<String, String>builder()
|
||||
.putAll(Comparators.class.getCanonicalName(), "emptiesFirst", "emptiesLast")
|
||||
.put(Function.class.getCanonicalName(), "identity")
|
||||
.put(Functions.class.getCanonicalName(), "identity")
|
||||
.put(ImmutableList.class.getCanonicalName(), "toImmutableList")
|
||||
.putAll(
|
||||
ImmutableListMultimap.class.getCanonicalName(),
|
||||
"flatteningToImmutableListMultimap",
|
||||
"toImmutableListMultimap")
|
||||
.put(ImmutableMap.class.getCanonicalName(), "toImmutableMap")
|
||||
.put(ImmutableMultiset.class.getCanonicalName(), "toImmutableMultiset")
|
||||
.put(ImmutableRangeSet.class.getCanonicalName(), "toImmutableRangeSet")
|
||||
.put(ImmutableSet.class.getCanonicalName(), "toImmutableSet")
|
||||
.putAll(
|
||||
ImmutableSetMultimap.class.getCanonicalName(),
|
||||
"flatteningToImmutableSetMultimap",
|
||||
"toImmutableSetMultimap")
|
||||
.put(ImmutableSortedMap.class.getCanonicalName(), "toImmutableSortedMap")
|
||||
.put(ImmutableSortedMultiset.class.getCanonicalName(), "toImmutableSortedMultiset")
|
||||
.put(ImmutableSortedSet.class.getCanonicalName(), "toImmutableSortedSet")
|
||||
.put(ImmutableTable.class.getCanonicalName(), "toImmutableTable")
|
||||
.putAll(
|
||||
Objects.class.getCanonicalName(),
|
||||
"checkIndex",
|
||||
"checkFromIndexSize",
|
||||
"checkFromToIndex",
|
||||
"requireNonNull",
|
||||
"requireNonNullElse",
|
||||
"requireNonNullElseGet")
|
||||
.put(Predicate.class.getCanonicalName(), "not")
|
||||
.put(Sets.class.getCanonicalName(), "toImmutableEnumSet")
|
||||
.put(UUID.class.getCanonicalName(), "randomUUID")
|
||||
.put(ZoneOffset.class.getCanonicalName(), "UTC")
|
||||
.put("org.junit.jupiter.params.provider.Arguments", "arguments")
|
||||
.build();
|
||||
|
||||
/** Instantiates a new {@link StaticImport} instance. */
|
||||
public StaticImport() {}
|
||||
|
||||
@Override
|
||||
public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) {
|
||||
if (!isCandidateContext(state) || !isCandidate(tree)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
StaticImportInfo importInfo = StaticImports.tryCreate(tree, state);
|
||||
if (importInfo == null) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return getCandidateSimpleName(importInfo)
|
||||
.flatMap(n -> tryStaticImport(tree, importInfo.canonicalName() + '.' + n, n, state))
|
||||
.map(fix -> describeMatch(tree, fix))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
private static boolean isCandidateContext(VisitorState state) {
|
||||
Tree parentTree =
|
||||
requireNonNull(state.getPath().getParentPath(), "MemberSelectTree lacks enclosing node")
|
||||
.getLeaf();
|
||||
|
||||
return parentTree instanceof MethodInvocationTree methodInvocation
|
||||
? methodInvocation.getTypeArguments().isEmpty()
|
||||
: (parentTree.getKind() != Kind.IMPORT && parentTree.getKind() != Kind.MEMBER_SELECT);
|
||||
}
|
||||
|
||||
private static boolean isCandidate(MemberSelectTree tree) {
|
||||
String identifier = tree.getIdentifier().toString();
|
||||
if (NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS.contains(identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Type type = ASTHelpers.getType(tree.getExpression());
|
||||
return type != null
|
||||
&& !NON_STATIC_IMPORT_CANDIDATE_MEMBERS.containsEntry(type.toString(), identifier);
|
||||
}
|
||||
|
||||
private static Optional<String> getCandidateSimpleName(StaticImportInfo importInfo) {
|
||||
String canonicalName = importInfo.canonicalName();
|
||||
return importInfo
|
||||
.simpleName()
|
||||
.toJavaUtil()
|
||||
.filter(
|
||||
name ->
|
||||
STATIC_IMPORT_CANDIDATE_TYPES.contains(canonicalName)
|
||||
|| STATIC_IMPORT_CANDIDATE_MEMBERS.containsEntry(canonicalName, name));
|
||||
}
|
||||
|
||||
private static Optional<Fix> tryStaticImport(
|
||||
MemberSelectTree tree, String fullyQualifiedName, String simpleName, VisitorState state) {
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder().replace(tree, simpleName);
|
||||
|
||||
if (!simpleName.equals(SuggestedFixes.qualifyStaticImport(fullyQualifiedName, fix, state))) {
|
||||
/* Statically importing this symbol would clash with an existing import. */
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(fix.build());
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSetMultimap;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MemberSelectTreeMatcher;
|
||||
import com.google.errorprone.bugpatterns.StaticImports;
|
||||
import com.google.errorprone.bugpatterns.StaticImports.StaticImportInfo;
|
||||
import com.google.errorprone.fixes.Fix;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} which flags methods and constants that can and should be statically
|
||||
* imported.
|
||||
*/
|
||||
// XXX: Tricky cases:
|
||||
// - `org.springframework.http.HttpStatus` (not always an improvement, and `valueOf` must
|
||||
// certainly be excluded)
|
||||
// - `com.google.common.collect.Tables`
|
||||
// - `ch.qos.logback.classic.Level.{DEBUG, ERROR, INFO, TRACE, WARN"}`
|
||||
// XXX: Also introduce a check which disallows static imports of certain methods. Candidates:
|
||||
// - `com.google.common.base.Strings`
|
||||
// - `java.util.Optional.empty`
|
||||
// - `java.util.Locale.ROOT`
|
||||
// - `ZoneOffset.ofHours` and other `ofXXX`-style methods.
|
||||
// - `java.time.Clock`.
|
||||
// - Several other `java.time` classes.
|
||||
// - Likely any of `*.{ZERO, ONE, MIX, MAX, MIN_VALUE, MAX_VALUE}`.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "StaticImport",
|
||||
summary = "Identifier should be statically imported",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.SUGGESTION,
|
||||
tags = StandardTags.SIMPLIFICATION)
|
||||
public final class StaticImportCheck extends BugChecker implements MemberSelectTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Types whose members should be statically imported, unless exempted by {@link
|
||||
* #STATIC_IMPORT_EXEMPTED_MEMBERS} or {@link #STATIC_IMPORT_EXEMPTED_IDENTIFIERS}.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final ImmutableSet<String> STATIC_IMPORT_CANDIDATE_TYPES =
|
||||
ImmutableSet.of(
|
||||
"com.google.common.base.Preconditions",
|
||||
"com.google.common.base.Predicates",
|
||||
"com.google.common.base.Verify",
|
||||
"com.google.common.collect.MoreCollectors",
|
||||
"com.mongodb.client.model.Accumulators",
|
||||
"com.mongodb.client.model.Aggregates",
|
||||
"com.mongodb.client.model.Filters",
|
||||
"com.mongodb.client.model.Indexes",
|
||||
"com.mongodb.client.model.Projections",
|
||||
"com.mongodb.client.model.Sorts",
|
||||
"com.mongodb.client.model.Updates",
|
||||
"java.nio.charset.StandardCharsets",
|
||||
"java.util.Comparator",
|
||||
"java.util.Map.Entry",
|
||||
"java.util.stream.Collectors",
|
||||
"org.assertj.core.api.Assertions",
|
||||
"org.assertj.core.api.InstanceOfAssertFactories",
|
||||
"org.assertj.core.api.SoftAssertions",
|
||||
"org.assertj.core.data.Offset",
|
||||
"org.assertj.core.groups.Tuple",
|
||||
"org.hamcrest.Matchers",
|
||||
"org.hamcrest.text.MatchesPattern",
|
||||
"org.hibernate.validator.testutil.ConstraintViolationAssert",
|
||||
"org.junit.jupiter.api.Assertions",
|
||||
"org.mockito.AdditionalAnswers",
|
||||
"org.mockito.Answers",
|
||||
"org.mockito.ArgumentMatchers",
|
||||
"org.mockito.Mockito",
|
||||
"org.springframework.boot.test.context.SpringBootTest.WebEnvironment",
|
||||
"org.springframework.format.annotation.DateTimeFormat.ISO",
|
||||
"org.springframework.http.HttpHeaders",
|
||||
"org.springframework.http.HttpMethod",
|
||||
"org.springframework.http.MediaType",
|
||||
"org.testng.Assert",
|
||||
"reactor.function.TupleUtils");
|
||||
|
||||
/** Type members that should be statically imported. */
|
||||
@VisibleForTesting
|
||||
static final ImmutableSetMultimap<String, String> STATIC_IMPORT_CANDIDATE_MEMBERS =
|
||||
ImmutableSetMultimap.<String, String>builder()
|
||||
.putAll(
|
||||
"com.google.common.collect.ImmutableListMultimap",
|
||||
"flatteningToImmutableListMultimap",
|
||||
"toImmutableListMultimap")
|
||||
.put("com.google.common.collect.ImmutableList", "toImmutableList")
|
||||
.put("com.google.common.collect.ImmutableMap", "toImmutableMap")
|
||||
.put("com.google.common.collect.ImmutableMultiset", "toImmutableMultiset")
|
||||
.put("com.google.common.collect.ImmutableRangeSet", "toImmutableRangeSet")
|
||||
.putAll(
|
||||
"com.google.common.collect.ImmutableSetMultimap",
|
||||
"flatteningToImmutableSetMultimap",
|
||||
"toImmutableSetMultimap")
|
||||
.put("com.google.common.collect.ImmutableSet", "toImmutableSet")
|
||||
.put("com.google.common.collect.ImmutableSortedMap", "toImmutableSortedMap")
|
||||
.put("com.google.common.collect.ImmutableSortedMultiset", "toImmutableSortedMultiset")
|
||||
.put("com.google.common.collect.ImmutableSortedSet", "toImmutableSortedSet")
|
||||
.put("com.google.common.collect.ImmutableTable", "toImmutableTable")
|
||||
.put("com.google.common.collect.Sets", "toImmutableEnumSet")
|
||||
.put("com.google.common.base.Functions", "identity")
|
||||
.put("java.util.function.Function", "identity")
|
||||
.put("java.util.function.Predicate", "not")
|
||||
.put("org.junit.jupiter.params.provider.Arguments", "arguments")
|
||||
.putAll(
|
||||
"java.util.Objects",
|
||||
"checkIndex",
|
||||
"checkFromIndexSize",
|
||||
"checkFromToIndex",
|
||||
"requireNonNull",
|
||||
"requireNonNullElse",
|
||||
"requireNonNullElseGet")
|
||||
.putAll("com.google.common.collect.Comparators", "emptiesFirst", "emptiesLast")
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Type members that should never be statically imported.
|
||||
*
|
||||
* <p>Identifiers listed by {@link #STATIC_IMPORT_EXEMPTED_IDENTIFIERS} should be omitted from
|
||||
* this collection.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final ImmutableSetMultimap<String, String> STATIC_IMPORT_EXEMPTED_MEMBERS =
|
||||
ImmutableSetMultimap.<String, String>builder()
|
||||
.put("com.mongodb.client.model.Filters", "empty")
|
||||
.put("org.springframework.http.MediaType", "ALL")
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Identifiers that should never be statically imported.
|
||||
*
|
||||
* <p>This should be a superset of the identifiers flagged by {@link
|
||||
* com.google.errorprone.bugpatterns.BadImport}.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final ImmutableSet<String> STATIC_IMPORT_EXEMPTED_IDENTIFIERS =
|
||||
ImmutableSet.of(
|
||||
"builder",
|
||||
"create",
|
||||
"copyOf",
|
||||
"from",
|
||||
"getDefaultInstance",
|
||||
"INSTANCE",
|
||||
"newBuilder",
|
||||
"of",
|
||||
"valueOf");
|
||||
|
||||
@Override
|
||||
public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) {
|
||||
if (!isCandidateContext(state) || !isCandidate(tree)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
StaticImportInfo importInfo = StaticImports.tryCreate(tree, state);
|
||||
if (importInfo == null) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return getCandidateSimpleName(importInfo)
|
||||
.flatMap(n -> tryStaticImport(tree, importInfo.canonicalName() + '.' + n, n, state))
|
||||
.map(fix -> describeMatch(tree, fix))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
private static boolean isCandidateContext(VisitorState state) {
|
||||
Tree parentTree =
|
||||
requireNonNull(state.getPath().getParentPath(), "MemberSelectTree lacks enclosing node")
|
||||
.getLeaf();
|
||||
switch (parentTree.getKind()) {
|
||||
case IMPORT:
|
||||
case MEMBER_SELECT:
|
||||
return false;
|
||||
case METHOD_INVOCATION:
|
||||
return ((MethodInvocationTree) parentTree).getTypeArguments().isEmpty();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isCandidate(MemberSelectTree tree) {
|
||||
String identifier = tree.getIdentifier().toString();
|
||||
if (STATIC_IMPORT_EXEMPTED_IDENTIFIERS.contains(identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Type type = ASTHelpers.getType(tree.getExpression());
|
||||
return type != null
|
||||
&& !STATIC_IMPORT_EXEMPTED_MEMBERS.containsEntry(type.toString(), identifier);
|
||||
}
|
||||
|
||||
private static Optional<String> getCandidateSimpleName(StaticImportInfo importInfo) {
|
||||
String canonicalName = importInfo.canonicalName();
|
||||
return importInfo
|
||||
.simpleName()
|
||||
.toJavaUtil()
|
||||
.filter(
|
||||
name ->
|
||||
STATIC_IMPORT_CANDIDATE_TYPES.contains(canonicalName)
|
||||
|| STATIC_IMPORT_CANDIDATE_MEMBERS.containsEntry(canonicalName, name));
|
||||
}
|
||||
|
||||
private static Optional<Fix> tryStaticImport(
|
||||
MemberSelectTree tree, String fullyQualifiedName, String simpleName, VisitorState state) {
|
||||
SuggestedFix.Builder fix = SuggestedFix.builder().replace(tree, simpleName);
|
||||
|
||||
if (!simpleName.equals(SuggestedFixes.qualifyStaticImport(fullyQualifiedName, fix, state))) {
|
||||
/* Statically importing this symbol would clash with an existing import. */
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(fix.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Supplier;
|
||||
import com.google.errorprone.suppliers.Suppliers;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import com.sun.tools.javac.util.Constants;
|
||||
import java.util.Formattable;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.utils.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@link String#format(String, Object...)} invocations which can be
|
||||
* replaced with a {@link String#join(CharSequence, CharSequence...)} or even a {@link
|
||||
* String#valueOf} invocation.
|
||||
*/
|
||||
// XXX: What about `v1 + "sep" + v2` and similar expressions? Do we want to rewrite those to
|
||||
// `String.join`, or should some `String.join` invocations be rewritten to use the `+` operator?
|
||||
// (The latter suggestion would conflict with the `FormatStringConcatenation` check.)
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Prefer `String#join` over `String#format`",
|
||||
link = BUG_PATTERNS_BASE_URL + "StringJoin",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class StringJoin extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Splitter FORMAT_SPECIFIER_SPLITTER = Splitter.on("%s");
|
||||
private static final Matcher<ExpressionTree> STRING_FORMAT_INVOCATION =
|
||||
staticMethod().onClass(String.class.getCanonicalName()).named("format");
|
||||
private static final Supplier<Type> CHAR_SEQUENCE_TYPE =
|
||||
Suppliers.typeFromClass(CharSequence.class);
|
||||
private static final Supplier<Type> FORMATTABLE_TYPE = Suppliers.typeFromClass(Formattable.class);
|
||||
|
||||
/** Instantiates a new {@link StringJoin} instance. */
|
||||
public StringJoin() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!STRING_FORMAT_INVOCATION.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
// XXX: This check assumes that if the first argument to `String#format` is a `Locale`, that
|
||||
// this argument is not vacuous, and that as a result the expression cannot be simplified using
|
||||
// `#valueOf` or `#join`. Implement a separate check that identifies and drops redundant
|
||||
// `Locale` arguments. See also a related comment in `FormatStringConcatenation`.
|
||||
String formatString = ASTHelpers.constValue(tree.getArguments().get(0), String.class);
|
||||
if (formatString == null) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
List<String> separators = FORMAT_SPECIFIER_SPLITTER.splitToList(formatString);
|
||||
if (separators.size() < 2) {
|
||||
/* The format string does not contain `%s` format specifiers. */
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (separators.size() != tree.getArguments().size()) {
|
||||
/* The number of arguments does not match the number of `%s` format specifiers. */
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
int lastIndex = separators.size() - 1;
|
||||
if (!separators.get(0).isEmpty() || !separators.get(lastIndex).isEmpty()) {
|
||||
/* The format string contains leading or trailing characters. */
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
ImmutableSet<String> innerSeparators = ImmutableSet.copyOf(separators.subList(1, lastIndex));
|
||||
if (innerSeparators.size() > 1) {
|
||||
/* The `%s` format specifiers are not uniformly separated. */
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (innerSeparators.isEmpty()) {
|
||||
/*
|
||||
* This `String#format` invocation performs a straightforward string conversion; use
|
||||
* `String#valueOf` instead.
|
||||
*/
|
||||
return trySuggestExplicitStringConversion(tree, state);
|
||||
}
|
||||
|
||||
String separator = Iterables.getOnlyElement(innerSeparators);
|
||||
if (separator.indexOf('%') >= 0) {
|
||||
/* The `%s` format specifiers are separated by another format specifier. */
|
||||
// XXX: Strictly speaking we could support `%%` by mapping it to a literal `%`, but that
|
||||
// doesn't seem worth the trouble.
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return trySuggestExplicitJoin(tree, separator, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* If guaranteed to be behavior preserving, suggests replacing {@code String.format("%s", arg)}
|
||||
* with {@code String.valueOf(arg)}.
|
||||
*
|
||||
* <p>If {@code arg} is already a string then the resultant conversion is vacuous. The {@link
|
||||
* IdentityConversion} check will subsequently drop it.
|
||||
*/
|
||||
private Description trySuggestExplicitStringConversion(
|
||||
MethodInvocationTree tree, VisitorState state) {
|
||||
ExpressionTree argument = tree.getArguments().get(1);
|
||||
if (isSubtype(ASTHelpers.getType(argument), FORMATTABLE_TYPE, state)) {
|
||||
/*
|
||||
* `Formattable` arguments are handled specially; `String#valueOf` is not a suitable
|
||||
* alternative.
|
||||
*/
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
return buildDescription(tree)
|
||||
.setMessage("Prefer `String#valueOf` over `String#format`")
|
||||
.addFix(SuggestedFix.replace(tree, withStringConversionExpression(argument, state)))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unless the given {@code String.format} expression includes {@link Formattable} arguments,
|
||||
* suggests replacing it with a {@code String.join} expression using the specified argument
|
||||
* separator.
|
||||
*/
|
||||
private Description trySuggestExplicitJoin(
|
||||
MethodInvocationTree tree, String separator, VisitorState state) {
|
||||
Iterator<? extends ExpressionTree> arguments = tree.getArguments().iterator();
|
||||
|
||||
SuggestedFix.Builder fix =
|
||||
SuggestedFix.builder()
|
||||
.replace(tree.getMethodSelect(), "String.join")
|
||||
.replace(arguments.next(), Constants.format(separator));
|
||||
|
||||
while (arguments.hasNext()) {
|
||||
ExpressionTree argument = arguments.next();
|
||||
Type argumentType = ASTHelpers.getType(argument);
|
||||
if (isSubtype(argumentType, FORMATTABLE_TYPE, state)) {
|
||||
/*
|
||||
* `Formattable` arguments are handled specially; `String#join` is not a suitable
|
||||
* alternative.
|
||||
*/
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
if (!isSubtype(argumentType, CHAR_SEQUENCE_TYPE, state)) {
|
||||
/*
|
||||
* The argument was previously implicitly converted to a string; now this must happen
|
||||
* explicitly.
|
||||
*/
|
||||
fix.replace(argument, withStringConversionExpression(argument, state));
|
||||
}
|
||||
}
|
||||
|
||||
return describeMatch(tree, fix.build());
|
||||
}
|
||||
|
||||
private static boolean isSubtype(
|
||||
@Nullable Type subType, Supplier<Type> superType, VisitorState state) {
|
||||
return ASTHelpers.isSubtype(subType, superType.get(state), state);
|
||||
}
|
||||
|
||||
private static String withStringConversionExpression(
|
||||
ExpressionTree argument, VisitorState state) {
|
||||
return String.format("String.valueOf(%s)", SourceCode.treeToString(argument, state));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.enclosingClass;
|
||||
@@ -7,12 +10,10 @@ import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.LinkType;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.BugPattern.StandardTags;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
|
||||
@@ -25,27 +26,32 @@ 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} which flags illegal time-zone related operations. */
|
||||
/** A {@link BugChecker} that flags illegal time-zone related operations. */
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
name = "TimeZoneUsage",
|
||||
summary =
|
||||
"Derive the current time from an existing `Clock` Spring bean, and don't rely on a `Clock`'s time zone",
|
||||
linkType = LinkType.NONE,
|
||||
severity = SeverityLevel.WARNING,
|
||||
tags = StandardTags.FRAGILE_CODE)
|
||||
public final class TimeZoneUsageCheck extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
"""
|
||||
Derive the current time from an existing `Clock` Spring bean, and don't rely on a \
|
||||
`Clock`'s time zone""",
|
||||
link = BUG_PATTERNS_BASE_URL + "TimeZoneUsage",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = FRAGILE_CODE)
|
||||
public final class TimeZoneUsage extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> BANNED_TIME_METHOD =
|
||||
anyOf(
|
||||
allOf(
|
||||
instanceMethod()
|
||||
.onDescendantOf(Clock.class.getName())
|
||||
.onDescendantOf(Clock.class.getCanonicalName())
|
||||
.namedAnyOf("getZone", "withZone"),
|
||||
not(enclosingClass(isSubtypeOf(Clock.class)))),
|
||||
staticMethod()
|
||||
.onClass(Clock.class.getName())
|
||||
.onClass(Clock.class.getCanonicalName())
|
||||
.namedAnyOf(
|
||||
"system",
|
||||
"systemDefaultZone",
|
||||
@@ -55,11 +61,20 @@ public final class TimeZoneUsageCheck extends BugChecker implements MethodInvoca
|
||||
"tickSeconds"),
|
||||
staticMethod()
|
||||
.onClassAny(
|
||||
LocalDate.class.getName(),
|
||||
LocalDateTime.class.getName(),
|
||||
LocalTime.class.getName())
|
||||
LocalDate.class.getCanonicalName(),
|
||||
LocalDateTime.class.getCanonicalName(),
|
||||
LocalTime.class.getCanonicalName(),
|
||||
OffsetDateTime.class.getCanonicalName(),
|
||||
OffsetTime.class.getCanonicalName(),
|
||||
ZonedDateTime.class.getCanonicalName())
|
||||
.named("now"),
|
||||
staticMethod().onClassAny(Instant.class.getName()).named("now").withParameters());
|
||||
staticMethod()
|
||||
.onClassAny(Instant.class.getCanonicalName())
|
||||
.named("now")
|
||||
.withNoParameters());
|
||||
|
||||
/** Instantiates a new {@link TimeZoneUsage} instance. */
|
||||
public TimeZoneUsage() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
@@ -1,20 +0,0 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.sun.source.tree.Tree;
|
||||
|
||||
// XXX: Can we locate this code in a better place? Maybe contribute it upstream?
|
||||
final class Util {
|
||||
private Util() {}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the given {@link Tree}, preferring the original source code
|
||||
* (if available) over its prettified representation.
|
||||
*
|
||||
* @return A non-{@code null} string.
|
||||
*/
|
||||
static String treeToString(Tree tree, VisitorState state) {
|
||||
String src = state.getSourceForNode(tree);
|
||||
return src != null ? src : tree.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,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,55 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
import static org.assertj.core.data.Percentage.withPercentage;
|
||||
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import java.math.BigDecimal;
|
||||
import org.assertj.core.api.AbstractBigDecimalAssert;
|
||||
import org.assertj.core.api.BigDecimalAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/**
|
||||
* Refaster rules related to AssertJ assertions over {@link BigDecimal}s.
|
||||
*
|
||||
* <p>Note that, contrary to collections of Refaster rules for other {@link
|
||||
* org.assertj.core.api.NumberAssert} subtypes, these rules do not rewrite to/from {@link
|
||||
* BigDecimalAssert#isEqualTo(Object)} and {@link BigDecimalAssert#isNotEqualTo(Object)}. This is
|
||||
* because {@link BigDecimal#equals(Object)} considers not only the numeric value of compared
|
||||
* instances, but also their scale. As a result various seemingly straightforward transformations
|
||||
* would actually subtly change the assertion's semantics.
|
||||
*/
|
||||
@OnlineDocumentation
|
||||
final class AssertJBigDecimalRules {
|
||||
private AssertJBigDecimalRules() {}
|
||||
|
||||
static final class AbstractBigDecimalAssertIsEqualByComparingTo {
|
||||
@BeforeTemplate
|
||||
AbstractBigDecimalAssert<?> before(AbstractBigDecimalAssert<?> bigDecimalAssert, BigDecimal n) {
|
||||
return Refaster.anyOf(
|
||||
bigDecimalAssert.isCloseTo(n, offset(BigDecimal.ZERO)),
|
||||
bigDecimalAssert.isCloseTo(n, withPercentage(0)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractBigDecimalAssert<?> after(AbstractBigDecimalAssert<?> bigDecimalAssert, BigDecimal n) {
|
||||
return bigDecimalAssert.isEqualByComparingTo(n);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AbstractBigDecimalAssertIsNotEqualByComparingTo {
|
||||
@BeforeTemplate
|
||||
AbstractBigDecimalAssert<?> before(AbstractBigDecimalAssert<?> bigDecimalAssert, BigDecimal n) {
|
||||
return Refaster.anyOf(
|
||||
bigDecimalAssert.isNotCloseTo(n, offset(BigDecimal.ZERO)),
|
||||
bigDecimalAssert.isNotCloseTo(n, withPercentage(0)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractBigDecimalAssert<?> after(AbstractBigDecimalAssert<?> bigDecimalAssert, BigDecimal n) {
|
||||
return bigDecimalAssert.isNotEqualByComparingTo(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,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 which drops unnecessary `L` suffixes from literal longs, then the `0L`/`1L`
|
||||
// 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,16 +1,18 @@
|
||||
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.errorprone.refaster.ImportPolicy;
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import org.assertj.core.api.AbstractBooleanAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
final class AssertJBooleanTemplates {
|
||||
private AssertJBooleanTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJBooleanRules {
|
||||
private AssertJBooleanRules() {}
|
||||
|
||||
static final class AbstractBooleanAssertIsEqualTo {
|
||||
@BeforeTemplate
|
||||
@@ -59,7 +61,7 @@ final class AssertJBooleanTemplates {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractBooleanAssert<?> after(boolean b) {
|
||||
return assertThat(b).isTrue();
|
||||
}
|
||||
@@ -88,7 +90,7 @@ final class AssertJBooleanTemplates {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractBooleanAssert<?> after(boolean b) {
|
||||
return assertThat(b).isFalse();
|
||||
}
|
||||
@@ -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,19 +7,17 @@ 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
|
||||
AbstractByteAssert<?> before(AbstractByteAssert<?> byteAssert, byte n) {
|
||||
return Refaster.anyOf(
|
||||
byteAssert.isCloseTo(n, offset((byte) 0)),
|
||||
byteAssert.isCloseTo(Byte.valueOf(n), offset((byte) 0)),
|
||||
byteAssert.isCloseTo(n, withPercentage(0)),
|
||||
byteAssert.isCloseTo(Byte.valueOf(n), withPercentage(0)),
|
||||
byteAssert.isEqualTo(Byte.valueOf(n)));
|
||||
byteAssert.isCloseTo(n, offset((byte) 0)), byteAssert.isCloseTo(n, withPercentage(0)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -33,10 +31,7 @@ final class AssertJByteTemplates {
|
||||
AbstractByteAssert<?> before(AbstractByteAssert<?> byteAssert, byte n) {
|
||||
return Refaster.anyOf(
|
||||
byteAssert.isNotCloseTo(n, offset((byte) 0)),
|
||||
byteAssert.isNotCloseTo(Byte.valueOf(n), offset((byte) 0)),
|
||||
byteAssert.isNotCloseTo(n, withPercentage(0)),
|
||||
byteAssert.isNotCloseTo(Byte.valueOf(n), withPercentage(0)),
|
||||
byteAssert.isNotEqualTo(Byte.valueOf(n)));
|
||||
byteAssert.isNotCloseTo(n, withPercentage(0)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -1,16 +1,18 @@
|
||||
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.errorprone.refaster.ImportPolicy;
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
final class AssertJCharSequenceTemplates {
|
||||
private AssertJCharSequenceTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJCharSequenceRules {
|
||||
private AssertJCharSequenceRules() {}
|
||||
|
||||
static final class AssertThatCharSequenceIsEmpty {
|
||||
@BeforeTemplate
|
||||
@@ -21,7 +23,7 @@ final class AssertJCharSequenceTemplates {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
void after(CharSequence charSequence) {
|
||||
assertThat(charSequence).isEmpty();
|
||||
}
|
||||
@@ -36,7 +38,7 @@ final class AssertJCharSequenceTemplates {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractAssert<?, ?> after(CharSequence charSequence) {
|
||||
return assertThat(charSequence).isNotEmpty();
|
||||
}
|
||||
@@ -49,7 +51,7 @@ final class AssertJCharSequenceTemplates {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractAssert<?, ?> after(CharSequence charSequence, int length) {
|
||||
return assertThat(charSequence).hasSize(length);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import org.assertj.core.api.AbstractComparableAssert;
|
||||
import org.assertj.core.api.AbstractIntegerAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
@OnlineDocumentation
|
||||
final class AssertJComparableRules {
|
||||
private AssertJComparableRules() {}
|
||||
|
||||
static final class AssertThatIsEqualByComparingTo<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(T actual, T expected) {
|
||||
return assertThat(actual.compareTo(expected)).isEqualTo(0);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractComparableAssert<?, ?> after(T actual, T expected) {
|
||||
return assertThat(actual).isEqualByComparingTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsNotEqualByComparingTo<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(T actual, T expected) {
|
||||
return assertThat(actual.compareTo(expected)).isNotEqualTo(0);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractComparableAssert<?, ?> after(T actual, T expected) {
|
||||
return assertThat(actual).isNotEqualByComparingTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsLessThan<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(T actual, T expected) {
|
||||
return assertThat(actual.compareTo(expected)).isNegative();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractComparableAssert<?, ?> after(T actual, T expected) {
|
||||
return assertThat(actual).isLessThan(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsLessThanOrEqualTo<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(T actual, T expected) {
|
||||
return assertThat(actual.compareTo(expected)).isNotPositive();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractComparableAssert<?, ?> after(T actual, T expected) {
|
||||
return assertThat(actual).isLessThanOrEqualTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsGreaterThan<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(T actual, T expected) {
|
||||
return assertThat(actual.compareTo(expected)).isPositive();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractComparableAssert<?, ?> after(T actual, T expected) {
|
||||
return assertThat(actual).isGreaterThan(expected);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatIsGreaterThanOrEqualTo<T extends Comparable<? super T>> {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(T actual, T expected) {
|
||||
return assertThat(actual.compareTo(expected)).isNotNegative();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractComparableAssert<?, ?> after(T actual, T expected) {
|
||||
return assertThat(actual).isGreaterThanOrEqualTo(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,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
|
||||
@@ -36,11 +38,7 @@ final class AssertJDoubleTemplates {
|
||||
@BeforeTemplate
|
||||
AbstractDoubleAssert<?> before(AbstractDoubleAssert<?> doubleAssert, double n) {
|
||||
return Refaster.anyOf(
|
||||
doubleAssert.isCloseTo(n, offset(0.0)),
|
||||
doubleAssert.isCloseTo(Double.valueOf(n), offset(0.0)),
|
||||
doubleAssert.isCloseTo(n, withPercentage(0.0)),
|
||||
doubleAssert.isCloseTo(Double.valueOf(n), withPercentage(0.0)),
|
||||
doubleAssert.isEqualTo(Double.valueOf(n)));
|
||||
doubleAssert.isCloseTo(n, offset(0.0)), doubleAssert.isCloseTo(n, withPercentage(0.0)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -54,10 +52,7 @@ final class AssertJDoubleTemplates {
|
||||
AbstractDoubleAssert<?> before(AbstractDoubleAssert<?> doubleAssert, double n) {
|
||||
return Refaster.anyOf(
|
||||
doubleAssert.isNotCloseTo(n, offset(0.0)),
|
||||
doubleAssert.isNotCloseTo(Double.valueOf(n), offset(0.0)),
|
||||
doubleAssert.isNotCloseTo(n, withPercentage(0.0)),
|
||||
doubleAssert.isNotCloseTo(Double.valueOf(n), withPercentage(0.0)),
|
||||
doubleAssert.isNotEqualTo(Double.valueOf(n)));
|
||||
doubleAssert.isNotCloseTo(n, withPercentage(0.0)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -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
|
||||
@@ -36,11 +38,7 @@ final class AssertJFloatTemplates {
|
||||
@BeforeTemplate
|
||||
AbstractFloatAssert<?> before(AbstractFloatAssert<?> floatAssert, float n) {
|
||||
return Refaster.anyOf(
|
||||
floatAssert.isCloseTo(n, offset(0F)),
|
||||
floatAssert.isCloseTo(Float.valueOf(n), offset(0F)),
|
||||
floatAssert.isCloseTo(n, withPercentage(0)),
|
||||
floatAssert.isCloseTo(Float.valueOf(n), withPercentage(0)),
|
||||
floatAssert.isEqualTo(Float.valueOf(n)));
|
||||
floatAssert.isCloseTo(n, offset(0F)), floatAssert.isCloseTo(n, withPercentage(0)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -53,11 +51,7 @@ final class AssertJFloatTemplates {
|
||||
@BeforeTemplate
|
||||
AbstractFloatAssert<?> before(AbstractFloatAssert<?> floatAssert, float n) {
|
||||
return Refaster.anyOf(
|
||||
floatAssert.isNotCloseTo(n, offset(0F)),
|
||||
floatAssert.isNotCloseTo(Float.valueOf(n), offset(0F)),
|
||||
floatAssert.isNotCloseTo(n, withPercentage(0)),
|
||||
floatAssert.isNotCloseTo(Float.valueOf(n), withPercentage(0)),
|
||||
floatAssert.isNotEqualTo(Float.valueOf(n)));
|
||||
floatAssert.isNotCloseTo(n, offset(0F)), floatAssert.isNotCloseTo(n, withPercentage(0)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -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,19 +7,17 @@ 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
|
||||
AbstractIntegerAssert<?> before(AbstractIntegerAssert<?> intAssert, int n) {
|
||||
return Refaster.anyOf(
|
||||
intAssert.isCloseTo(n, offset(0)),
|
||||
intAssert.isCloseTo(Integer.valueOf(n), offset(0)),
|
||||
intAssert.isCloseTo(n, withPercentage(0)),
|
||||
intAssert.isCloseTo(Integer.valueOf(n), withPercentage(0)),
|
||||
intAssert.isEqualTo(Integer.valueOf(n)));
|
||||
intAssert.isCloseTo(n, offset(0)), intAssert.isCloseTo(n, withPercentage(0)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -32,11 +30,7 @@ final class AssertJIntegerTemplates {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(AbstractIntegerAssert<?> intAssert, int n) {
|
||||
return Refaster.anyOf(
|
||||
intAssert.isNotCloseTo(n, offset(0)),
|
||||
intAssert.isNotCloseTo(Integer.valueOf(n), offset(0)),
|
||||
intAssert.isNotCloseTo(n, withPercentage(0)),
|
||||
intAssert.isNotCloseTo(Integer.valueOf(n), withPercentage(0)),
|
||||
intAssert.isNotEqualTo(Integer.valueOf(n)));
|
||||
intAssert.isNotCloseTo(n, offset(0)), intAssert.isNotCloseTo(n, withPercentage(0)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -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,19 +7,17 @@ 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
|
||||
AbstractLongAssert<?> before(AbstractLongAssert<?> longAssert, long n) {
|
||||
return Refaster.anyOf(
|
||||
longAssert.isCloseTo(n, offset(0L)),
|
||||
longAssert.isCloseTo(Long.valueOf(n), offset(0L)),
|
||||
longAssert.isCloseTo(n, withPercentage(0)),
|
||||
longAssert.isCloseTo(Long.valueOf(n), withPercentage(0)),
|
||||
longAssert.isEqualTo(Long.valueOf(n)));
|
||||
longAssert.isCloseTo(n, offset(0L)), longAssert.isCloseTo(n, withPercentage(0)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -32,11 +30,7 @@ final class AssertJLongTemplates {
|
||||
@BeforeTemplate
|
||||
AbstractLongAssert<?> before(AbstractLongAssert<?> longAssert, long n) {
|
||||
return Refaster.anyOf(
|
||||
longAssert.isNotCloseTo(n, offset(0L)),
|
||||
longAssert.isNotCloseTo(Long.valueOf(n), offset(0L)),
|
||||
longAssert.isNotCloseTo(n, withPercentage(0)),
|
||||
longAssert.isNotCloseTo(Long.valueOf(n), withPercentage(0)),
|
||||
longAssert.isNotEqualTo(Long.valueOf(n)));
|
||||
longAssert.isNotCloseTo(n, offset(0L)), longAssert.isNotCloseTo(n, withPercentage(0)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -0,0 +1,221 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.Matches;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
import org.assertj.core.api.AbstractBooleanAssert;
|
||||
import org.assertj.core.api.AbstractCollectionAssert;
|
||||
import org.assertj.core.api.AbstractMapAssert;
|
||||
import org.assertj.core.api.MapAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
import tech.picnic.errorprone.refaster.matchers.IsEmpty;
|
||||
|
||||
@OnlineDocumentation
|
||||
final class AssertJMapRules {
|
||||
private AssertJMapRules() {}
|
||||
|
||||
static final class AbstractMapAssertIsEmpty<K, V> {
|
||||
@BeforeTemplate
|
||||
void before(
|
||||
AbstractMapAssert<?, ?, K, V> mapAssert,
|
||||
@Matches(IsEmpty.class) Map<? extends K, ? extends V> wellTypedMap,
|
||||
@Matches(IsEmpty.class) Map<?, ?> arbitrarilyTypedMap,
|
||||
@Matches(IsEmpty.class) Iterable<? extends K> keys) {
|
||||
Refaster.anyOf(
|
||||
mapAssert.containsExactlyEntriesOf(wellTypedMap),
|
||||
mapAssert.containsExactlyInAnyOrderEntriesOf(wellTypedMap),
|
||||
mapAssert.hasSameSizeAs(arbitrarilyTypedMap),
|
||||
mapAssert.isEqualTo(arbitrarilyTypedMap),
|
||||
mapAssert.containsOnlyKeys(keys),
|
||||
mapAssert.containsExactly(),
|
||||
mapAssert.containsOnly(),
|
||||
mapAssert.containsOnlyKeys());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
void after(AbstractMapAssert<?, ?, K, V> mapAssert) {
|
||||
mapAssert.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapIsEmpty<K, V> {
|
||||
@BeforeTemplate
|
||||
void before(Map<K, V> map) {
|
||||
Refaster.anyOf(
|
||||
assertThat(map).hasSize(0),
|
||||
assertThat(map.isEmpty()).isTrue(),
|
||||
assertThat(map.size()).isEqualTo(0L),
|
||||
assertThat(map.size()).isNotPositive());
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
void before2(Map<K, V> map) {
|
||||
assertThat(Refaster.anyOf(map.keySet(), map.values(), map.entrySet())).isEmpty();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
void after(Map<K, V> map) {
|
||||
assertThat(map).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
static final class AbstractMapAssertIsNotEmpty<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractMapAssert<?, ?, K, V> before(
|
||||
AbstractMapAssert<?, ?, K, V> mapAssert, @Matches(IsEmpty.class) Map<?, ?> map) {
|
||||
return mapAssert.isNotEqualTo(map);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert) {
|
||||
return mapAssert.isNotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapIsNotEmpty<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractAssert<?, ?> before(Map<K, V> map) {
|
||||
return Refaster.anyOf(
|
||||
assertThat(map.isEmpty()).isFalse(),
|
||||
assertThat(map.size()).isNotEqualTo(0),
|
||||
assertThat(map.size()).isPositive(),
|
||||
assertThat(Refaster.anyOf(map.keySet(), map.values(), map.entrySet())).isNotEmpty());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map) {
|
||||
return assertThat(map).isNotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
static final class AbstractMapAssertContainsExactlyInAnyOrderEntriesOf<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractMapAssert<?, ?, K, V> before(
|
||||
AbstractMapAssert<?, ?, K, V> mapAssert, Map<? extends K, ? extends V> map) {
|
||||
return mapAssert.isEqualTo(map);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractMapAssert<?, ?, K, V> after(
|
||||
AbstractMapAssert<?, ?, K, V> mapAssert, Map<? extends K, ? extends V> map) {
|
||||
return mapAssert.containsExactlyInAnyOrderEntriesOf(map);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AbstractMapAssertContainsExactlyEntriesOf<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractMapAssert<?, ?, K, V> before(AbstractMapAssert<?, ?, K, V> mapAssert, K key, V value) {
|
||||
return mapAssert.containsExactlyInAnyOrderEntriesOf(ImmutableMap.of(key, value));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert, K key, V value) {
|
||||
return mapAssert.containsExactlyEntriesOf(ImmutableMap.of(key, value));
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapHasSize<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractAssert<?, ?> before(Map<K, V> map, int length) {
|
||||
return Refaster.anyOf(
|
||||
assertThat(map.size()).isEqualTo(length),
|
||||
assertThat(Refaster.anyOf(map.keySet(), map.values(), map.entrySet())).hasSize(length));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, int length) {
|
||||
return assertThat(map).hasSize(length);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AbstractMapAssertHasSameSizeAs<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractMapAssert<?, ?, K, V> before(AbstractMapAssert<?, ?, K, V> mapAssert, Map<?, ?> map) {
|
||||
return mapAssert.hasSize(map.size());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
AbstractMapAssert<?, ?, K, V> after(AbstractMapAssert<?, ?, K, V> mapAssert, Map<?, ?> map) {
|
||||
return mapAssert.hasSameSizeAs(map);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapContainsKey<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(Map<K, V> map, K key) {
|
||||
return assertThat(map.containsKey(key)).isTrue();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, K key) {
|
||||
return assertThat(map).containsKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapDoesNotContainKey<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(Map<K, V> map, K key) {
|
||||
return assertThat(map.containsKey(key)).isFalse();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, K key) {
|
||||
return assertThat(map).doesNotContainKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapContainsOnlyKeys<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractCollectionAssert<?, Collection<? extends K>, K, ?> before(
|
||||
Map<K, V> map, Set<? extends K> keys) {
|
||||
return assertThat(map.keySet()).hasSameElementsAs(keys);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, Set<? extends K> keys) {
|
||||
return assertThat(map).containsOnlyKeys(keys);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapContainsValue<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(Map<K, V> map, V value) {
|
||||
return assertThat(map.containsValue(value)).isTrue();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, V value) {
|
||||
return assertThat(map).containsValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatMapDoesNotContainValue<K, V> {
|
||||
@BeforeTemplate
|
||||
AbstractBooleanAssert<?> before(Map<K, V> map, V value) {
|
||||
return assertThat(map.containsValue(value)).isFalse();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
MapAssert<K, V> after(Map<K, V> map, V value) {
|
||||
return assertThat(map).doesNotContainValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
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.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.NotMatches;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import org.assertj.core.api.AbstractBigDecimalAssert;
|
||||
@@ -14,11 +19,14 @@ 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.annotation.OnlineDocumentation;
|
||||
import tech.picnic.errorprone.refaster.matchers.IsCharacter;
|
||||
|
||||
final class AssertJNumberTemplates {
|
||||
private AssertJNumberTemplates() {}
|
||||
@OnlineDocumentation
|
||||
final class AssertJNumberRules {
|
||||
private AssertJNumberRules() {}
|
||||
|
||||
static final class NumberIsPositive {
|
||||
static final class NumberAssertIsPositive {
|
||||
@BeforeTemplate
|
||||
AbstractByteAssert<?> before(AbstractByteAssert<?> numberAssert) {
|
||||
return Refaster.anyOf(
|
||||
@@ -69,7 +77,7 @@ final class AssertJNumberTemplates {
|
||||
}
|
||||
}
|
||||
|
||||
static final class NumberIsNotPositive {
|
||||
static final class NumberAssertIsNotPositive {
|
||||
@BeforeTemplate
|
||||
AbstractByteAssert<?> before(AbstractByteAssert<?> numberAssert) {
|
||||
return Refaster.anyOf(
|
||||
@@ -120,7 +128,7 @@ final class AssertJNumberTemplates {
|
||||
}
|
||||
}
|
||||
|
||||
static final class NumberIsNegative {
|
||||
static final class NumberAssertIsNegative {
|
||||
@BeforeTemplate
|
||||
AbstractByteAssert<?> before(AbstractByteAssert<?> numberAssert) {
|
||||
return Refaster.anyOf(
|
||||
@@ -171,7 +179,7 @@ final class AssertJNumberTemplates {
|
||||
}
|
||||
}
|
||||
|
||||
static final class NumberIsNotNegative {
|
||||
static final class NumberAssertIsNotNegative {
|
||||
@BeforeTemplate
|
||||
AbstractByteAssert<?> before(AbstractByteAssert<?> numberAssert) {
|
||||
return Refaster.anyOf(
|
||||
@@ -221,4 +229,54 @@ final class AssertJNumberTemplates {
|
||||
return numberAssert.isNotNegative();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link AbstractLongAssert#isOdd()} (and similar methods for other {@link NumberAssert}
|
||||
* subtypes) over alternatives with less informative error messages.
|
||||
*
|
||||
* <p>Note that {@link org.assertj.core.api.AbstractCharacterAssert} does not implement {@link
|
||||
* NumberAssert} and does not provide an {@code isOdd} test.
|
||||
*/
|
||||
static final class AssertThatIsOdd {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(@NotMatches(IsCharacter.class) int number) {
|
||||
return assertThat(number % 2).isEqualTo(1);
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
AbstractLongAssert<?> before(long number) {
|
||||
return assertThat(number % 2).isEqualTo(1);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
NumberAssert<?, ?> after(long number) {
|
||||
return assertThat(number).isOdd();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link AbstractLongAssert#isEven()} (and similar methods for other {@link NumberAssert}
|
||||
* subtypes) over alternatives with less informative error messages.
|
||||
*
|
||||
* <p>Note that {@link org.assertj.core.api.AbstractCharacterAssert} does not implement {@link
|
||||
* NumberAssert} and does not provide an {@code isEven} test.
|
||||
*/
|
||||
static final class AssertThatIsEven {
|
||||
@BeforeTemplate
|
||||
AbstractIntegerAssert<?> before(@NotMatches(IsCharacter.class) int number) {
|
||||
return assertThat(number % 2).isEqualTo(0);
|
||||
}
|
||||
|
||||
@BeforeTemplate
|
||||
AbstractLongAssert<?> before(long number) {
|
||||
return assertThat(number % 2).isEqualTo(0);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
NumberAssert<?, ?> after(long number) {
|
||||
return assertThat(number).isEven();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
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.errorprone.refaster.ImportPolicy;
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
@@ -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
|
||||
@@ -21,7 +23,7 @@ final class AssertJObjectTemplates {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
ObjectAssert<S> after(S object) {
|
||||
return assertThat(object).isInstanceOf(Refaster.<T>clazz());
|
||||
}
|
||||
@@ -34,7 +36,7 @@ final class AssertJObjectTemplates {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
ObjectAssert<S> after(S object) {
|
||||
return assertThat(object).isNotInstanceOf(Refaster.<T>clazz());
|
||||
}
|
||||
@@ -47,7 +49,7 @@ final class AssertJObjectTemplates {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
ObjectAssert<S> after(S object1, T object2) {
|
||||
return assertThat(object1).isEqualTo(object2);
|
||||
}
|
||||
@@ -60,7 +62,7 @@ final class AssertJObjectTemplates {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
ObjectAssert<S> after(S object1, T object2) {
|
||||
return assertThat(object1).isNotEqualTo(object2);
|
||||
}
|
||||
@@ -73,7 +75,7 @@ final class AssertJObjectTemplates {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
ObjectAssert<T> after(T object, String str) {
|
||||
return assertThat(object).hasToString(str);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
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.errorprone.refaster.ImportPolicy;
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
@@ -14,19 +14,20 @@ 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());
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, T> after(Optional<T> optional) {
|
||||
return assertThat(optional).get();
|
||||
}
|
||||
@@ -53,7 +54,7 @@ final class AssertJOptionalTemplates {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
OptionalAssert<T> after(Optional<T> optional) {
|
||||
return assertThat(optional).isPresent();
|
||||
}
|
||||
@@ -80,7 +81,7 @@ final class AssertJOptionalTemplates {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
OptionalAssert<T> after(Optional<T> optional) {
|
||||
return assertThat(optional).isEmpty();
|
||||
}
|
||||
@@ -122,7 +123,7 @@ final class AssertJOptionalTemplates {
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractObjectAssert<?, T> after(Optional<T> optional, Predicate<? super T> predicate) {
|
||||
return assertThat(optional).get().matches(predicate);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user