Compare commits
1398 Commits
mf-operato
...
main
Author | SHA1 | Date |
---|---|---|
![]() |
3ec9bc85bc | |
![]() |
f5ea2492dc | |
![]() |
b89e43a799 | |
![]() |
c8cd311615 | |
![]() |
ba42a6aa36 | |
![]() |
83ab977a1b | |
![]() |
f12b8d66d3 | |
![]() |
e29de7a99f | |
![]() |
d32bf2b879 | |
![]() |
e5cf99088b | |
![]() |
7462187191 | |
![]() |
23326fc433 | |
![]() |
156f6aabc8 | |
![]() |
43190834c5 | |
![]() |
f1d5a55658 | |
![]() |
c735e6320f | |
![]() |
2f9997e2cc | |
![]() |
8f3eea06d3 | |
![]() |
a53a1f052b | |
![]() |
704c9ae116 | |
![]() |
521df18fdb | |
![]() |
a2ba0a0626 | |
![]() |
1094a3b70e | |
![]() |
754127924f | |
![]() |
3f039f26d5 | |
![]() |
671589b1d4 | |
![]() |
d938b2d3c3 | |
![]() |
7fd22e0d6e | |
![]() |
07740506b5 | |
![]() |
26d06bcebb | |
![]() |
fe312a06fe | |
![]() |
d73970e9d7 | |
![]() |
c0ea9d1925 | |
![]() |
405113f793 | |
![]() |
f4987071f0 | |
![]() |
306dec0fcc | |
![]() |
4d4a330d46 | |
![]() |
1e73835e83 | |
![]() |
866001db2f | |
![]() |
34f5ffa7f7 | |
![]() |
1b1b19a902 | |
![]() |
15a18fd4e8 | |
![]() |
5802fd99b1 | |
![]() |
e7bf813f15 | |
![]() |
3cf9585b2e | |
![]() |
8827fca693 | |
![]() |
9f7c57b719 | |
![]() |
73acbaf6d5 | |
![]() |
5616d858bc | |
![]() |
6cdb0aa689 | |
![]() |
7483e61615 | |
![]() |
87e230ce87 | |
![]() |
7f3b93147a | |
![]() |
7501d44b69 | |
![]() |
0ffe0c5911 | |
![]() |
c99a4286b8 | |
![]() |
8d5700afca | |
![]() |
1fa8e484c4 | |
![]() |
9e15e37255 | |
![]() |
97259e24d0 | |
![]() |
eb6d50a04c | |
![]() |
9c1678968f | |
![]() |
fa32fbc4f0 | |
![]() |
8822d40687 | |
![]() |
c0cf1bf5c9 | |
![]() |
444653bfd5 | |
![]() |
9138213121 | |
![]() |
6fd5f98d38 | |
![]() |
bc18ff14e3 | |
![]() |
5e15039554 | |
![]() |
7756793356 | |
![]() |
f0138ea1df | |
![]() |
6a2e973de3 | |
![]() |
e86c06c390 | |
![]() |
6b094dd711 | |
![]() |
9054ec0658 | |
![]() |
71b89e66de | |
![]() |
a7bc9e20c7 | |
![]() |
eaf34d7204 | |
![]() |
86d60400c1 | |
![]() |
740572f049 | |
![]() |
3541ef4d02 | |
![]() |
39a05f2c35 | |
![]() |
711d92e26f | |
![]() |
d8dfc3c937 | |
![]() |
3a2bb15ca3 | |
![]() |
a7e3909c6a | |
![]() |
72c2a5488d | |
![]() |
ea56405983 | |
![]() |
9c414932d8 | |
![]() |
8a21549ca9 | |
![]() |
4abcf5f0dd | |
![]() |
b2a4c9fcb9 | |
![]() |
603fff9a82 | |
![]() |
2544dc79d3 | |
![]() |
ea6b3b4d64 | |
![]() |
172b8b8a94 | |
![]() |
4c04cdafa7 | |
![]() |
165172e0fa | |
![]() |
3f52acd0a2 | |
![]() |
e14a73438f | |
![]() |
817de197d5 | |
![]() |
214a749ace | |
![]() |
fd4f9afb08 | |
![]() |
c241935635 | |
![]() |
46ff727a13 | |
![]() |
dc89109622 | |
![]() |
9c2525139a | |
![]() |
d601917e40 | |
![]() |
498b4c8fbe | |
![]() |
69fadb6918 | |
![]() |
f866ec32b1 | |
![]() |
6d51459323 | |
![]() |
21dc766c93 | |
![]() |
0f802f766a | |
![]() |
fbbccf9db8 | |
![]() |
8b72eb0406 | |
![]() |
c22d60fac0 | |
![]() |
1892c846b0 | |
![]() |
41290a23d3 | |
![]() |
5814ec1a1a | |
![]() |
f9b0e9f0af | |
![]() |
3ef44cf742 | |
![]() |
5aed7ce95c | |
![]() |
97fd216455 | |
![]() |
f127ba14dd | |
![]() |
74b82daba0 | |
![]() |
8c617d7412 | |
![]() |
a4f4680442 | |
![]() |
62b2914271 | |
![]() |
1b0f37c6f6 | |
![]() |
5226725689 | |
![]() |
e361e14c90 | |
![]() |
70a56a1420 | |
![]() |
409f117da9 | |
![]() |
7fe7bb28d9 | |
![]() |
bb11e6a0a7 | |
![]() |
eee97a7b2b | |
![]() |
cf1e2e27dc | |
![]() |
a2facce70c | |
![]() |
bd444fcd77 | |
![]() |
ee849bcb10 | |
![]() |
6d4bc78cb4 | |
![]() |
16e2bb0f18 | |
![]() |
b0cbb440c3 | |
![]() |
58a07eb452 | |
![]() |
82491e6f84 | |
![]() |
3d15419adb | |
![]() |
6a09af169e | |
![]() |
7dad240ea7 | |
![]() |
ca43d2359b | |
![]() |
da27f1c7fd | |
![]() |
eb85125a5f | |
![]() |
62b9c2de7a | |
![]() |
aeee6325af | |
![]() |
5a91b1e392 | |
![]() |
efc5afd5d5 | |
![]() |
f2d15355be | |
![]() |
a840058cf5 | |
![]() |
358bdc89a5 | |
![]() |
a81a532091 | |
![]() |
6983c813c8 | |
![]() |
fb89ab2fb5 | |
![]() |
7ac128c83d | |
![]() |
06578e5d91 | |
![]() |
1940e0e2d2 | |
![]() |
bd77cbcf6e | |
![]() |
1bf2f25e40 | |
![]() |
7d7bee5eee | |
![]() |
31510b662e | |
![]() |
0bd8a7aba6 | |
![]() |
084ad9dfd4 | |
![]() |
228d53787b | |
![]() |
4fee8e6a0c | |
![]() |
4f9a608cf4 | |
![]() |
1c3c62e422 | |
![]() |
d6e3bbb64d | |
![]() |
128f37a6b8 | |
![]() |
cfd9a26e33 | |
![]() |
a71f588637 | |
![]() |
d73d87ac97 | |
![]() |
04791929a7 | |
![]() |
c9b1b961f5 | |
![]() |
613e916c39 | |
![]() |
e2f3f3ad45 | |
![]() |
5c94a43352 | |
![]() |
183c0aa784 | |
![]() |
ed6793f8f1 | |
![]() |
1790ad56dd | |
![]() |
2f0e537f9b | |
![]() |
393318d903 | |
![]() |
f3e5557de9 | |
![]() |
352ffdfc57 | |
![]() |
6a9e6776a9 | |
![]() |
ec38c244fd | |
![]() |
ad29864d7f | |
![]() |
b9e5cfb202 | |
![]() |
9c216baf20 | |
![]() |
7eb479d546 | |
![]() |
aafc574e90 | |
![]() |
5af4291b53 | |
![]() |
d3ebfc5567 | |
![]() |
60d0dd8a05 | |
![]() |
0163ffd328 | |
![]() |
eb5712582f | |
![]() |
6ea0397620 | |
![]() |
aba0f63704 | |
![]() |
993f34a96c | |
![]() |
eaacd9873e | |
![]() |
843198d241 | |
![]() |
325d0ee1e4 | |
![]() |
5eed8fe91b | |
![]() |
60610cef84 | |
![]() |
ca09bc5a22 | |
![]() |
0796236031 | |
![]() |
651b00eb70 | |
![]() |
500f143c7d | |
![]() |
81f1dbfd1b | |
![]() |
0750d5d465 | |
![]() |
84c6d200b6 | |
![]() |
9ac13f078a | |
![]() |
c84124d8bc | |
![]() |
3907772163 | |
![]() |
d98fd5386a | |
![]() |
36c24c822c | |
![]() |
e1fdf17d1b | |
![]() |
23e4db3e5f | |
![]() |
b4641e7e60 | |
![]() |
341db66f3c | |
![]() |
c9791783ba | |
![]() |
dc228d57ac | |
![]() |
fbd9f16955 | |
![]() |
d120f41181 | |
![]() |
db6aea5d07 | |
![]() |
876d8fc872 | |
![]() |
45c5f06754 | |
![]() |
040096a641 | |
![]() |
f91a2d5310 | |
![]() |
f8f2317bdb | |
![]() |
82249f5ed4 | |
![]() |
78c9ad6f33 | |
![]() |
693e504258 | |
![]() |
b66dd13e2f | |
![]() |
5ec6112ba1 | |
![]() |
74dbd52add | |
![]() |
cc44c989b7 | |
![]() |
e64e82d80d | |
![]() |
ea8b17dd9c | |
![]() |
672e19651f | |
![]() |
67eb330f1c | |
![]() |
e6d4ad653c | |
![]() |
100b3eec2a | |
![]() |
bcfc2c4b6c | |
![]() |
68dc0f58d2 | |
![]() |
d6ff2a7f37 | |
![]() |
64d9619a8a | |
![]() |
470d471e51 | |
![]() |
5ff40867b3 | |
![]() |
58611e6718 | |
![]() |
c740da48d5 | |
![]() |
eda0d92f44 | |
![]() |
d76daf5f62 | |
![]() |
1a39194f65 | |
![]() |
0e53e19cb1 | |
![]() |
3cba460f9a | |
![]() |
d3928cb8e8 | |
![]() |
a6c3cec8c9 | |
![]() |
7bd8362dae | |
![]() |
e1cddb710d | |
![]() |
4a6387da67 | |
![]() |
83ee9a89dd | |
![]() |
540003e2a1 | |
![]() |
3745704c03 | |
![]() |
33fa42becb | |
![]() |
25c3fb161d | |
![]() |
53ee4a955d | |
![]() |
40fb3796cb | |
![]() |
a876e860ee | |
![]() |
4c406459bd | |
![]() |
4ddeb22b6d | |
![]() |
ff06e0ed00 | |
![]() |
6734a64554 | |
![]() |
73a64d674c | |
![]() |
7f8eb9de77 | |
![]() |
609418f702 | |
![]() |
4adabd8e4b | |
![]() |
93d0d8fa7f | |
![]() |
4dfef014ab | |
![]() |
95d56e4130 | |
![]() |
7f9dd69c62 | |
![]() |
ac44fe23b6 | |
![]() |
60128ab196 | |
![]() |
7a8b2d1dab | |
![]() |
92304cdd98 | |
![]() |
ab143685a4 | |
![]() |
9cb1069090 | |
![]() |
e405d3d583 | |
![]() |
97ba82d740 | |
![]() |
d8d5e5bb43 | |
![]() |
28a4aa2195 | |
![]() |
28e2a1b022 | |
![]() |
c1c5e5f722 | |
![]() |
60ad710b7f | |
![]() |
5a7d3ade02 | |
![]() |
12c8bb369e | |
![]() |
f088bbde15 | |
![]() |
e9c1128b86 | |
![]() |
4ccf9eebb4 | |
![]() |
f6de471b5f | |
![]() |
0f68a0a0cb | |
![]() |
ac21584224 | |
![]() |
907c80f84c | |
![]() |
fd502b4526 | |
![]() |
a6c4ea9614 | |
![]() |
571ad33ceb | |
![]() |
6af9175386 | |
![]() |
fe4ec3521e | |
![]() |
1dbde3d9c3 | |
![]() |
cc49e82348 | |
![]() |
4408250920 | |
![]() |
85e59c706a | |
![]() |
cdd891a4a2 | |
![]() |
e5b1944ae6 | |
![]() |
cd291b043a | |
![]() |
8cdbb50cbf | |
![]() |
ba8899714d | |
![]() |
4f652a68e7 | |
![]() |
953ee620f7 | |
![]() |
18f9a0e3a6 | |
![]() |
0362cd07b8 | |
![]() |
9d2ec3dc8d | |
![]() |
04d1184904 | |
![]() |
4bd7da32ea | |
![]() |
901f05cd4f | |
![]() |
ca0813c605 | |
![]() |
ac7833aaf4 | |
![]() |
5fceb213d9 | |
![]() |
2928dab4ec | |
![]() |
0c828e35fb | |
![]() |
3864ecc12f | |
![]() |
fe141cd3b6 | |
![]() |
eaf7db1250 | |
![]() |
6c7e2107ae | |
![]() |
1edef5ebdc | |
![]() |
7e8a3ae2ac | |
![]() |
45ac3dcdff | |
![]() |
1c675bc993 | |
![]() |
fc0c0c7f5b | |
![]() |
22522254df | |
![]() |
9a706ba371 | |
![]() |
d4ea19e434 | |
![]() |
eb3d502ad7 | |
![]() |
65874dc40f | |
![]() |
759408fdb5 | |
![]() |
ec5124d939 | |
![]() |
05e06d1945 | |
![]() |
6fcdd8077f | |
![]() |
32152646f3 | |
![]() |
e97119efec | |
![]() |
fce8283843 | |
![]() |
f1f6a3c4a0 | |
![]() |
c56e19a0c2 | |
![]() |
3800ad32bf | |
![]() |
79e3c8bd4a | |
![]() |
6ad4a704f5 | |
![]() |
20bfe264f5 | |
![]() |
697eaa73dd | |
![]() |
5144cccb57 | |
![]() |
98e9c2f1f4 | |
![]() |
63c043664d | |
![]() |
a18436d4d6 | |
![]() |
a5c44605af | |
![]() |
b7f621c239 | |
![]() |
d551cb8c16 | |
![]() |
1b59a3f168 | |
![]() |
6b5352feac | |
![]() |
9a305dd2e6 | |
![]() |
7738f0c0a5 | |
![]() |
374130bb56 | |
![]() |
d37465ad37 | |
![]() |
50c85ef5c1 | |
![]() |
fa6db3cca7 | |
![]() |
96925d480e | |
![]() |
31ea58381c | |
![]() |
95838aa230 | |
![]() |
f8d505a225 | |
![]() |
7a8de1c966 | |
![]() |
7a3d0d55a0 | |
![]() |
1c6b6c67e0 | |
![]() |
87bebb6744 | |
![]() |
7f5b189c1f | |
![]() |
c1f2b615e2 | |
![]() |
5d1906456b | |
![]() |
53752f58ad | |
![]() |
c98ffcc940 | |
![]() |
351c8be2e0 | |
![]() |
cb79584c7d | |
![]() |
def404259f | |
![]() |
de7996f5c7 | |
![]() |
b4f45aa55e | |
![]() |
f0f46a45eb | |
![]() |
1ee5154687 | |
![]() |
401d0f7929 | |
![]() |
25a04e8eeb | |
![]() |
0bd9e4e65f | |
![]() |
60054da0f7 | |
![]() |
35fcf55198 | |
![]() |
cee4af098f | |
![]() |
9c8708bc01 | |
![]() |
7b1099a33d | |
![]() |
359699cea7 | |
![]() |
4589161742 | |
![]() |
5af8e3dd68 | |
![]() |
1fb04902c0 | |
![]() |
0dfc7a85b5 | |
![]() |
a4fbec355c | |
![]() |
8f3a23e1a9 | |
![]() |
ead4ca9b56 | |
![]() |
9f4b2c37ba | |
![]() |
f509998ec1 | |
![]() |
2d76e43068 | |
![]() |
e84430f222 | |
![]() |
469a3aa87a | |
![]() |
59ad84f6e9 | |
![]() |
fd722892e5 | |
![]() |
c46fd55711 | |
![]() |
2929e35183 | |
![]() |
533917dced | |
![]() |
25c9d34f5a | |
![]() |
d11dc2b321 | |
![]() |
62c8985a3e | |
![]() |
230688848e | |
![]() |
eb8971a933 | |
![]() |
016831d471 | |
![]() |
93b016bca3 | |
![]() |
bf716d7089 | |
![]() |
d3274565a1 | |
![]() |
04d7ce05ca | |
![]() |
bda8487ed2 | |
![]() |
a8bc9e9b42 | |
![]() |
286e59a0e6 | |
![]() |
db910946dc | |
![]() |
1837ae59cb | |
![]() |
03848ce583 | |
![]() |
e9f2248072 | |
![]() |
9a9629c1e8 | |
![]() |
1b86aa7eeb | |
![]() |
8ec6e0eefc | |
![]() |
937228d1ba | |
![]() |
0504d2c3f9 | |
![]() |
07be57027a | |
![]() |
844b7c02dd | |
![]() |
b4ec2d360d | |
![]() |
89c227ecaa | |
![]() |
6f4b75b0f6 | |
![]() |
4522005ab4 | |
![]() |
56a23448c3 | |
![]() |
bb1de21059 | |
![]() |
89c24cd52d | |
![]() |
0fbd03cf2b | |
![]() |
8b96aa0585 | |
![]() |
7ff3094821 | |
![]() |
f8e5339c69 | |
![]() |
9959f971f9 | |
![]() |
dcf03f5987 | |
![]() |
860b027005 | |
![]() |
4aa9588454 | |
![]() |
37bf2c9168 | |
![]() |
bd8c9e5bcb | |
![]() |
510cb39b11 | |
![]() |
c9ef157c29 | |
![]() |
e74e11533c | |
![]() |
a7ff8a8d76 | |
![]() |
7585aef910 | |
![]() |
f04e28cbf8 | |
![]() |
39bb05f7d1 | |
![]() |
7624059caa | |
![]() |
fa6bf50a22 | |
![]() |
5c3d4c1dab | |
![]() |
02b5a034d2 | |
![]() |
48fde2321f | |
![]() |
8d29b21ecb | |
![]() |
3bae16bee5 | |
![]() |
33f6ee1f36 | |
![]() |
de0b91cd5a | |
![]() |
4e3c750086 | |
![]() |
2f300502c0 | |
![]() |
9db88947b9 | |
![]() |
99436906cd | |
![]() |
7653b2d635 | |
![]() |
b4e01ba1b4 | |
![]() |
b9653e3e1b | |
![]() |
9aaeff67d0 | |
![]() |
1a7f03bbc5 | |
![]() |
b849234572 | |
![]() |
0282bf1923 | |
![]() |
4b04e528af | |
![]() |
602070164b | |
![]() |
87e9757af2 | |
![]() |
81ee297908 | |
![]() |
016c7b6abd | |
![]() |
6998d8af23 | |
![]() |
4da2ff1118 | |
![]() |
05d98b84cf | |
![]() |
ec05864880 | |
![]() |
b938a4e07d | |
![]() |
ca7510a394 | |
![]() |
04bd091810 | |
![]() |
82e916db15 | |
![]() |
957208c44b | |
![]() |
fe6a930bd4 | |
![]() |
8bcf8713e9 | |
![]() |
d913c8e63b | |
![]() |
b01e1504a1 | |
![]() |
3ac518ce69 | |
![]() |
0c06b7f65f | |
![]() |
1a76a882ca | |
![]() |
f34169575c | |
![]() |
9c53c94736 | |
![]() |
515587fb9f | |
![]() |
f174a55c94 | |
![]() |
c0a4dd1be7 | |
![]() |
1533e72fd4 | |
![]() |
549c71a27c | |
![]() |
367389a489 | |
![]() |
ef248f6529 | |
![]() |
a6c90dd942 | |
![]() |
2a95f1bde9 | |
![]() |
5ba9d60f8d | |
![]() |
8345545865 | |
![]() |
d6fd754679 | |
![]() |
a8d3813cc7 | |
![]() |
5ae1d98596 | |
![]() |
d3fd234883 | |
![]() |
503f8c506b | |
![]() |
8470b65ae9 | |
![]() |
4552facb7e | |
![]() |
c29e1673dd | |
![]() |
cfabd8ec99 | |
![]() |
91d4a42687 | |
![]() |
7af26f0f77 | |
![]() |
c087e79437 | |
![]() |
54ec0c9b28 | |
![]() |
3834e52ac4 | |
![]() |
5d9a72b2a5 | |
![]() |
af222a5754 | |
![]() |
54ccc39e72 | |
![]() |
8b578ebf36 | |
![]() |
2eb764d0c9 | |
![]() |
13c45a5c15 | |
![]() |
3fd1573c57 | |
![]() |
6d75645f67 | |
![]() |
8eec5e6997 | |
![]() |
44382ea397 | |
![]() |
41b8834a13 | |
![]() |
a40cc70478 | |
![]() |
1cbf3caae1 | |
![]() |
0f9705901b | |
![]() |
129a55fb74 | |
![]() |
c70510c3ea | |
![]() |
6bfecb8fcc | |
![]() |
db20dbb4a3 | |
![]() |
46649c8662 | |
![]() |
1662a38922 | |
![]() |
18ab68859c | |
![]() |
ab3f070222 | |
![]() |
e3f1b56129 | |
![]() |
26ca731cf0 | |
![]() |
8926596da0 | |
![]() |
647819eedc | |
![]() |
f37ea8acc4 | |
![]() |
ac91f7bfa0 | |
![]() |
462f741065 | |
![]() |
268415893d | |
![]() |
2388e49190 | |
![]() |
b2caef7202 | |
![]() |
2efd538d37 | |
![]() |
0fe3404494 | |
![]() |
da75cdca40 | |
![]() |
86f6023d13 | |
![]() |
afb46dc76d | |
![]() |
a40488feaf | |
![]() |
d2c638b5e4 | |
![]() |
86a176d4c6 | |
![]() |
73def99b49 | |
![]() |
fd7afedfcf | |
![]() |
c6eec8072c | |
![]() |
a4c46f624d | |
![]() |
734eceada0 | |
![]() |
4fb5ebe19b | |
![]() |
bb309de9df | |
![]() |
7be6f866fe | |
![]() |
0db26db975 | |
![]() |
f8a4276aba | |
![]() |
95ffaed655 | |
![]() |
7b1de2f9cd | |
![]() |
49665a9ec7 | |
![]() |
0ea26edd4f | |
![]() |
2efa085dd4 | |
![]() |
f3d367f667 | |
![]() |
345a904055 | |
![]() |
0808f2508b | |
![]() |
8d500d1a63 | |
![]() |
992cc9afe3 | |
![]() |
49ace1729f | |
![]() |
38e71f7a10 | |
![]() |
a9ec894caf | |
![]() |
9db303e3a3 | |
![]() |
4b1bb38fc3 | |
![]() |
623bc8ea73 | |
![]() |
baf5647e2b | |
![]() |
ea574bd640 | |
![]() |
837f46479f | |
![]() |
c6f3d0a6af | |
![]() |
8c5d3cbfc8 | |
![]() |
311a724c3c | |
![]() |
6b393b1b53 | |
![]() |
4234664671 | |
![]() |
8cd025112d | |
![]() |
efbe4d9f0f | |
![]() |
4a239a695e | |
![]() |
bc7792ecd6 | |
![]() |
958cc20a9f | |
![]() |
117ef7f0e2 | |
![]() |
0d03221281 | |
![]() |
80c8e5d37a | |
![]() |
9060d3f8e8 | |
![]() |
eb30be3826 | |
![]() |
2a8d536857 | |
![]() |
c34955ca64 | |
![]() |
e459c4ae9b | |
![]() |
75a6d14788 | |
![]() |
e2900c4a7e | |
![]() |
dd94d4ec53 | |
![]() |
1e7e0d6301 | |
![]() |
4b8a68072b | |
![]() |
26465f21ce | |
![]() |
b29c07e07e | |
![]() |
622d48bd0a | |
![]() |
b9e6a9aed8 | |
![]() |
6c0413077c | |
![]() |
772e5f5806 | |
![]() |
c7c4e0a1f3 | |
![]() |
ca2d3a874f | |
![]() |
79347a1728 | |
![]() |
c1650e6918 | |
![]() |
c26c40d31f | |
![]() |
93158a609e | |
![]() |
a3b70a0ffb | |
![]() |
e381a7d684 | |
![]() |
abad253310 | |
![]() |
57dc1c9532 | |
![]() |
d72877a34f | |
![]() |
ea41b9fd09 | |
![]() |
e0f23fa8e9 | |
![]() |
4bc6588f38 | |
![]() |
d14cf598fd | |
![]() |
9979c4fd27 | |
![]() |
f032c82b59 | |
![]() |
c8a66e7070 | |
![]() |
f32bc1f337 | |
![]() |
fc3b143973 | |
![]() |
4119c27857 | |
![]() |
a9e5a644bb | |
![]() |
501142d3e7 | |
![]() |
ea6ca3e8dd | |
![]() |
0fd9a112b4 | |
![]() |
eb14f5283c | |
![]() |
cd9bd5f9fd | |
![]() |
9875ab904b | |
![]() |
7412205b6c | |
![]() |
8dbc0e4cbf | |
![]() |
ccac242f3a | |
![]() |
dcb5bf438d | |
![]() |
fdcdcf76d1 | |
![]() |
05461c8525 | |
![]() |
848ec452c3 | |
![]() |
bf9bf83143 | |
![]() |
d9ec625601 | |
![]() |
ef36201e6b | |
![]() |
5a30991fa4 | |
![]() |
aebded15ba | |
![]() |
7dfadc205a | |
![]() |
9cfd3dc978 | |
![]() |
c73f68683a | |
![]() |
cf764e7b90 | |
![]() |
83bf37e642 | |
![]() |
7259c02ee5 | |
![]() |
68983d7da2 | |
![]() |
2960ae6a7a | |
![]() |
69f3a3b4b7 | |
![]() |
f3e2c0b7bb | |
![]() |
e8f738b39c | |
![]() |
6dc2ef937e | |
![]() |
0e3f3de17f | |
![]() |
dd55f59207 | |
![]() |
c1de2ef087 | |
![]() |
78949c1b99 | |
![]() |
1ef15f3bb7 | |
![]() |
3aa249b8a1 | |
![]() |
c2be5b18b4 | |
![]() |
f6fc4727b6 | |
![]() |
73c88c3af0 | |
![]() |
ac23f2e89a | |
![]() |
781034bd78 | |
![]() |
13703c4466 | |
![]() |
0a3ebdbd37 | |
![]() |
408ba29951 | |
![]() |
3037946bbc | |
![]() |
d1a1485af6 | |
![]() |
ef515ac45e | |
![]() |
15970311e1 | |
![]() |
35df4ba6a1 | |
![]() |
6f03036591 | |
![]() |
dec9ecbf0b | |
![]() |
5e1eaea148 | |
![]() |
21ba7c8280 | |
![]() |
a163638214 | |
![]() |
9eb6dd016c | |
![]() |
557c5b5948 | |
![]() |
c1b74cd752 | |
![]() |
a02d4a76cb | |
![]() |
dd4aefd23a | |
![]() |
73f4c3e991 | |
![]() |
d730e0b3fa | |
![]() |
828f36958f | |
![]() |
adec1c76ee | |
![]() |
8f620ccd0b | |
![]() |
481da28f1e | |
![]() |
f8e1b178d6 | |
![]() |
436a6f3484 | |
![]() |
449190d324 | |
![]() |
1ac0419eb6 | |
![]() |
a97718f4c6 | |
![]() |
ec8ba17388 | |
![]() |
2b00164081 | |
![]() |
f39323160c | |
![]() |
235264bb7e | |
![]() |
cc7c400e81 | |
![]() |
8b8b3d0738 | |
![]() |
0ded5859a1 | |
![]() |
cdb87e3869 | |
![]() |
c50b42c183 | |
![]() |
ee655a920c | |
![]() |
19eda14df1 | |
![]() |
54a8435479 | |
![]() |
1dab8181b8 | |
![]() |
8b90623b38 | |
![]() |
b70c92c34c | |
![]() |
340fce43c8 | |
![]() |
6c5f53ef66 | |
![]() |
949ab6289a | |
![]() |
4d8abec16e | |
![]() |
68d88ded61 | |
![]() |
9e485266be | |
![]() |
3a64518119 | |
![]() |
29cd97e59c | |
![]() |
c0f9f2175b | |
![]() |
7155af1981 | |
![]() |
ffc6379828 | |
![]() |
fe652e8c8a | |
![]() |
8ee7485c8a | |
![]() |
24735ef4d3 | |
![]() |
05ac3c9d75 | |
![]() |
09837b7bab | |
![]() |
22fb9eb9e5 | |
![]() |
617f83c81d | |
![]() |
f81972e98c | |
![]() |
c53dc0ce17 | |
![]() |
1c7b8b7dcb | |
![]() |
4aa38957c3 | |
![]() |
c5aa8065d1 | |
![]() |
c95670edd2 | |
![]() |
69dc18135a | |
![]() |
a51be7bcce | |
![]() |
37f95d996e | |
![]() |
e34f49695b | |
![]() |
25ed3d2d26 | |
![]() |
45a03dec20 | |
![]() |
a19ffddf66 | |
![]() |
1faea36a22 | |
![]() |
a20a75d422 | |
![]() |
04972a39da | |
![]() |
634406a30a | |
![]() |
0d070a8c8c | |
![]() |
71db511003 | |
![]() |
86e765c418 | |
![]() |
64f656cb46 | |
![]() |
323a249f70 | |
![]() |
4382ef49b9 | |
![]() |
809dcc6bdc | |
![]() |
ca9217d5e1 | |
![]() |
f880b66cbf | |
![]() |
ea6cc50890 | |
![]() |
ebc77391bc | |
![]() |
bc7917be04 | |
![]() |
3a4afce623 | |
![]() |
a27c7da7e2 | |
![]() |
113b2985de | |
![]() |
e497f1f5b1 | |
![]() |
11dff81247 | |
![]() |
f0d6c54574 | |
![]() |
43c84146db | |
![]() |
5f44d3f306 | |
![]() |
1752587b7b | |
![]() |
a786e310a3 | |
![]() |
291b500269 | |
![]() |
564c6ddb55 | |
![]() |
870a7be2f4 | |
![]() |
bdb548e1c0 | |
![]() |
23fe6f4a39 | |
![]() |
9beef23175 | |
![]() |
42a2932883 | |
![]() |
5d7394be0e | |
![]() |
0fbf010ea4 | |
![]() |
cb5fa0425d | |
![]() |
5897598ba8 | |
![]() |
e92a3aeae9 | |
![]() |
4c14678d77 | |
![]() |
a28516d5e8 | |
![]() |
62cc0f1a32 | |
![]() |
89ebac11d5 | |
![]() |
fa2a0bab6c | |
![]() |
a773c3ec21 | |
![]() |
9ace4bbb73 | |
![]() |
a801bbc83b | |
![]() |
83a86870bb | |
![]() |
d756541204 | |
![]() |
206ff10e6e | |
![]() |
e5791ec16c | |
![]() |
1616023b63 | |
![]() |
702b36a781 | |
![]() |
e06da98b6b | |
![]() |
a59be8092d | |
![]() |
91b58c6cbe | |
![]() |
12cda68ce2 | |
![]() |
24bf129268 | |
![]() |
a256fa2c8c | |
![]() |
21bb280068 | |
![]() |
c44a3f4ff9 | |
![]() |
402fe69f59 | |
![]() |
87d21e44e6 | |
![]() |
a129163053 | |
![]() |
de3569cb3b | |
![]() |
ca2712aaee | |
![]() |
0e5d838765 | |
![]() |
84bc2174af | |
![]() |
db2721f78e | |
![]() |
c664c4f9e6 | |
![]() |
1abc50304c | |
![]() |
2ae22d06a2 | |
![]() |
92e199649d | |
![]() |
3273cef42b | |
![]() |
eeba297f33 | |
![]() |
b619154905 | |
![]() |
811ca32c6d | |
![]() |
0691a2e7e4 | |
![]() |
ebaa6d2182 | |
![]() |
0f3de846d8 | |
![]() |
6fa8ff6070 | |
![]() |
f9b9c35c1f | |
![]() |
2ae6a55ba4 | |
![]() |
28dd673c48 | |
![]() |
0830eca24c | |
![]() |
678c3483ed | |
![]() |
3f24998be5 | |
![]() |
f19555f707 | |
![]() |
a43e52aa91 | |
![]() |
be1f8596e4 | |
![]() |
d84be10e66 | |
![]() |
f3712b2b93 | |
![]() |
ee7eee32b5 | |
![]() |
ff4249856e | |
![]() |
02675a9c09 | |
![]() |
7b7754ad3b | |
![]() |
7f9f41aa36 | |
![]() |
54a24638a2 | |
![]() |
d77d23cbeb | |
![]() |
0e7b588d50 | |
![]() |
e6c92f5958 | |
![]() |
9c81e1a93a | |
![]() |
932e8f4996 | |
![]() |
867fc1a34c | |
![]() |
8d0aeb995d | |
![]() |
51d97a2037 | |
![]() |
fe905f8ae9 | |
![]() |
c09b6fe3ad | |
![]() |
1864d77f37 | |
![]() |
f9be00421e | |
![]() |
2f2a4ad680 | |
![]() |
f8909b5c0f | |
![]() |
b4a54f32df | |
![]() |
23b5c2b0ec | |
![]() |
09f6f1bc83 | |
![]() |
b107180cf5 | |
![]() |
d65a708f29 | |
![]() |
068f6037fe | |
![]() |
c5ea00c2d7 | |
![]() |
4525e9999b | |
![]() |
006bb2a85e | |
![]() |
83560fffa7 | |
![]() |
0c731e72da | |
![]() |
2bd54cf3ce | |
![]() |
4c3bb24a04 | |
![]() |
db3422b69a | |
![]() |
6ce4766517 | |
![]() |
a94d120839 | |
![]() |
0f55447956 | |
![]() |
b91b56c14d | |
![]() |
61c5351978 | |
![]() |
31872aaa3f | |
![]() |
17779cbfd4 | |
![]() |
73e6cef0c2 | |
![]() |
d99be668c5 | |
![]() |
3d1821d3c0 | |
![]() |
76dae19b1c | |
![]() |
11e86babc7 | |
![]() |
fb7755adf5 | |
![]() |
e3f232ec28 | |
![]() |
d534a30a78 | |
![]() |
341ec2a907 | |
![]() |
aff3536bb8 | |
![]() |
c52d4cde8d | |
![]() |
640efba68d | |
![]() |
af59581b66 | |
![]() |
3d7775de11 | |
![]() |
5fd8ef38c1 | |
![]() |
bb37a253e2 | |
![]() |
f38083998d | |
![]() |
a2ada88fd1 | |
![]() |
9e37d0fa44 | |
![]() |
09d912ca37 | |
![]() |
c90c329f12 | |
![]() |
2408839be3 | |
![]() |
027bdcdc95 | |
![]() |
559685823e | |
![]() |
95349376fd | |
![]() |
4f82adf018 | |
![]() |
3da6502bac | |
![]() |
ed79195cf5 | |
![]() |
050473a0e1 | |
![]() |
5429ff642c | |
![]() |
210874f4d8 | |
![]() |
d97bf0951e | |
![]() |
119f9d7f91 | |
![]() |
2544d7b945 | |
![]() |
5a98458d94 | |
![]() |
292642a815 | |
![]() |
99465e659f | |
![]() |
8c3b51ea75 | |
![]() |
b519753fd6 | |
![]() |
13e1bf709d | |
![]() |
945efde5f5 | |
![]() |
a87aee1771 | |
![]() |
383e5b5bc2 | |
![]() |
b621fb128f | |
![]() |
67114bb729 | |
![]() |
6b8c4442b0 | |
![]() |
e4ff164f64 | |
![]() |
b63a60df14 | |
![]() |
4052c43f16 | |
![]() |
cfa3dff7fe | |
![]() |
2674d367f9 | |
![]() |
eb3a405930 | |
![]() |
68bfc13941 | |
![]() |
00799cc782 | |
![]() |
db229aa221 | |
![]() |
67ba092912 | |
![]() |
4d3217f959 | |
![]() |
888619912f | |
![]() |
f9be1e1ac3 | |
![]() |
e820e750b0 | |
![]() |
9c9e3f54ab | |
![]() |
986d98c475 | |
![]() |
e6301c0d66 | |
![]() |
7420248c0f | |
![]() |
1f44d56357 | |
![]() |
6953fe268e | |
![]() |
55b62cccb3 | |
![]() |
4a293632c2 | |
![]() |
b1307de785 | |
![]() |
50b6992852 | |
![]() |
cc79edd4af | |
![]() |
9e3f0a8ff8 | |
![]() |
1401d56195 | |
![]() |
01b9488c16 | |
![]() |
83e1bee22b | |
![]() |
9aff1e0a8e | |
![]() |
bc2563175c | |
![]() |
81eb0d9b1f | |
![]() |
9939c536d5 | |
![]() |
300bf77967 | |
![]() |
147f43c1bf | |
![]() |
6367ff20c8 | |
![]() |
50169fb556 | |
![]() |
92f76057cf | |
![]() |
9c5d3b3c6f | |
![]() |
3536a86ba1 | |
![]() |
881bd244da | |
![]() |
91b125b001 | |
![]() |
d6223893ed | |
![]() |
d7aeea39d9 | |
![]() |
0dd96d249e | |
![]() |
538764278b | |
![]() |
c1d1390f8f | |
![]() |
050a10bbd9 | |
![]() |
80ca9a7e37 | |
![]() |
7ea514b77a | |
![]() |
f0c707f477 | |
![]() |
fa06f10c04 | |
![]() |
04a41bdb2f | |
![]() |
14ab9fe775 | |
![]() |
07c8e52fc8 | |
![]() |
242151776f | |
![]() |
625cd180ae | |
![]() |
dd672b4b0c | |
![]() |
ebb51ed6f7 | |
![]() |
4e3c87e2f3 | |
![]() |
a10158c475 | |
![]() |
5a3844401b | |
![]() |
d712e77bab | |
![]() |
7976b74615 | |
![]() |
deb7ffb355 | |
![]() |
86b6392aa2 | |
![]() |
4039eff984 | |
![]() |
60ec029e1b | |
![]() |
c1b77f8e0a | |
![]() |
058869f446 | |
![]() |
ca559c70df | |
![]() |
da2ca76953 | |
![]() |
e47fb60c7a | |
![]() |
92061e84b5 | |
![]() |
aa32742abf | |
![]() |
027f130e8c | |
![]() |
8b145d4ba4 | |
![]() |
7f7fc03a8f | |
![]() |
eec4c58e1f | |
![]() |
87d4accb39 | |
![]() |
fa1154c9a8 | |
![]() |
0a0fb1e71b | |
![]() |
fd97e2259c | |
![]() |
24122e1325 | |
![]() |
a6a5e633d0 | |
![]() |
7c1b5ff757 | |
![]() |
a4820f49c2 | |
![]() |
3fbc482a2e | |
![]() |
cdd877c289 | |
![]() |
bd4dcd5b23 | |
![]() |
9d02cec17f | |
![]() |
3d0c87738c | |
![]() |
43083f6537 | |
![]() |
fed8644e01 | |
![]() |
2009b00366 | |
![]() |
3e04acd97d | |
![]() |
9182eda98c | |
![]() |
8c29a89888 | |
![]() |
706abfe9e3 | |
![]() |
5b1d737683 | |
![]() |
4e2a569aac | |
![]() |
8b6167c7f2 | |
![]() |
721b077608 | |
![]() |
b66f7c7992 | |
![]() |
235f598ac7 | |
![]() |
0dfb04b25a | |
![]() |
520d224719 | |
![]() |
8326624491 | |
![]() |
62d558eb53 | |
![]() |
21cf0b77db | |
![]() |
4266ea9d60 | |
![]() |
5eb00b79cc | |
![]() |
559470a245 | |
![]() |
2f6fd1e95f | |
![]() |
8f216b8398 | |
![]() |
3643fea111 | |
![]() |
402e76b33d | |
![]() |
eca9ea4bb8 | |
![]() |
4e75099e16 | |
![]() |
cfd67b9534 | |
![]() |
08fbd043c0 | |
![]() |
6b51c3ebc5 | |
![]() |
60242fd599 | |
![]() |
ff1ca8fd5a | |
![]() |
8248178213 | |
![]() |
46777539ad | |
![]() |
01a0f82e99 | |
![]() |
7ea9b733ff | |
![]() |
f66c51b9f2 | |
![]() |
bbf1ad4a18 | |
![]() |
59eb887cfa | |
![]() |
6a38b15657 | |
![]() |
6de5771899 | |
![]() |
537e53f6b3 | |
![]() |
22d25da7fc | |
![]() |
2d17045b11 | |
![]() |
a39e72ef13 | |
![]() |
56e4f175ac | |
![]() |
5bffc77af6 | |
![]() |
e3e169b4b9 | |
![]() |
256288ab88 | |
![]() |
ba49f7d309 | |
![]() |
d10b80d49f | |
![]() |
0a0e3f50eb | |
![]() |
81b12739df | |
![]() |
dacfdcef40 | |
![]() |
6dba929b4f | |
![]() |
5134545093 | |
![]() |
7351648590 | |
![]() |
6bad2eccdf | |
![]() |
4a94f6a894 | |
![]() |
fa55508668 | |
![]() |
82b00bf4b5 | |
![]() |
e7f4a8fd67 | |
![]() |
2436f669fe | |
![]() |
d53fc2664d | |
![]() |
40cf2ddef2 | |
![]() |
ab6aeed5b1 | |
![]() |
9c672c774c | |
![]() |
ebca5238a7 | |
![]() |
f8bc10aaaa | |
![]() |
01b1001a81 | |
![]() |
cb3eed544c | |
![]() |
12cea6b9c7 | |
![]() |
ab700a3262 | |
![]() |
d75b9c45df | |
![]() |
35d75809c9 | |
![]() |
2b50902d1a | |
![]() |
d010569f1d | |
![]() |
800030ad11 | |
![]() |
82e5870a16 | |
![]() |
71449e76b7 | |
![]() |
22ee449971 | |
![]() |
c521e54e7e | |
![]() |
4357eed219 | |
![]() |
103cf7504a | |
![]() |
62298de662 | |
![]() |
a84aee23df | |
![]() |
4e61c8436c | |
![]() |
03f13e5bb7 | |
![]() |
0c5136d725 | |
![]() |
f926cfaabd | |
![]() |
0019c61de7 | |
![]() |
b73673ab01 | |
![]() |
231ef2b5db | |
![]() |
be21dc3b4b | |
![]() |
f0f0086aca | |
![]() |
b8f5b6a30e | |
![]() |
26aae94f1b | |
![]() |
28cb43afa9 | |
![]() |
715a8db2db | |
![]() |
495d742f65 | |
![]() |
4c5a3f0577 | |
![]() |
69250c9cd5 | |
![]() |
f45f381320 | |
![]() |
e143a18e5c | |
![]() |
8bf8ca482a | |
![]() |
ba5e6d2e1c | |
![]() |
cd9cf682e7 | |
![]() |
36263cd0b9 | |
![]() |
5a077d9f3f | |
![]() |
42373a94b6 | |
![]() |
5f057d5ebf | |
![]() |
88d77a3211 | |
![]() |
b484d439b8 | |
![]() |
97052faf18 | |
![]() |
e50e34a60a | |
![]() |
b7ec75b68b | |
![]() |
866102c3fc | |
![]() |
3151e6c45f | |
![]() |
07f559549a | |
![]() |
874cacbd1f | |
![]() |
999bfbdefa | |
![]() |
8a0eb67561 | |
![]() |
2e26e9cd93 | |
![]() |
6d4487c517 | |
![]() |
bf2790df54 | |
![]() |
869fe868b3 | |
![]() |
30db88dde3 | |
![]() |
5185afc4a4 | |
![]() |
5108233ac5 | |
![]() |
6bd38e8607 | |
![]() |
442eac2556 | |
![]() |
3508ef8e1e | |
![]() |
1a104e4dc4 | |
![]() |
dd5c0d3599 | |
![]() |
eb0c1b271f | |
![]() |
d9b5df2d72 | |
![]() |
ddfba6ea5d | |
![]() |
d319ad1c8b | |
![]() |
6c370caa77 | |
![]() |
46ed5e2ece | |
![]() |
aa02a8aec0 | |
![]() |
0788cd237c | |
![]() |
b9841595a5 | |
![]() |
474cb2bd29 | |
![]() |
5e23fbdd3e | |
![]() |
d1d42a9a35 | |
![]() |
7b1175511e | |
![]() |
b679b8d75c | |
![]() |
fced10f61b | |
![]() |
f365dfd1d0 | |
![]() |
76708d88f7 | |
![]() |
b2b95cf403 | |
![]() |
2950826bb1 | |
![]() |
2d94feee86 | |
![]() |
45f7aaa961 | |
![]() |
f2c9a55811 | |
![]() |
d91c2179bb | |
![]() |
41c99048bf | |
![]() |
ba58d571d8 | |
![]() |
67165f7c64 | |
![]() |
0e29701aba | |
![]() |
f2e12c3db5 | |
![]() |
551cf2da3c | |
![]() |
7ccda8eb23 | |
![]() |
1172ab5730 | |
![]() |
c8596a647a | |
![]() |
6d77deb359 | |
![]() |
1b3e9945af | |
![]() |
e316bd693d | |
![]() |
6c5a850c25 | |
![]() |
d305e03905 | |
![]() |
812e9dd950 | |
![]() |
0465d3984a | |
![]() |
87bf6c6a6b | |
![]() |
bed8591e65 | |
![]() |
390e9a2423 | |
![]() |
693ee2cae9 | |
![]() |
580314d21e | |
![]() |
c8d8d913b9 | |
![]() |
865b59f0c8 | |
![]() |
35ad3f941c | |
![]() |
2dcce6b184 | |
![]() |
ac2c5e4054 | |
![]() |
81942449d3 | |
![]() |
b005920cc5 | |
![]() |
fc0092dc8c | |
![]() |
d628c41665 | |
![]() |
05344f5fed | |
![]() |
51084adf82 | |
![]() |
e6df1d32e1 | |
![]() |
1950abb3b7 | |
![]() |
5308e0d167 | |
![]() |
9afc3bbfd9 | |
![]() |
662db7be39 | |
![]() |
5d6e25ae5f | |
![]() |
da408b5901 | |
![]() |
15c25abc47 | |
![]() |
2731f994d8 | |
![]() |
1ff293888b | |
![]() |
4f8b7a5f48 | |
![]() |
64b309e19e | |
![]() |
494796b526 | |
![]() |
a67b0f26e7 | |
![]() |
774267c436 | |
![]() |
ea06b79e0d | |
![]() |
7db31658cb | |
![]() |
f71394a372 | |
![]() |
59d6c2ef52 | |
![]() |
0ecf9a945b | |
![]() |
5e537fa821 | |
![]() |
6d2e8cfc06 | |
![]() |
9b93b3efb8 | |
![]() |
ea311bab23 | |
![]() |
d9ce579b38 | |
![]() |
9ab72060b6 | |
![]() |
7009463876 | |
![]() |
51544bb4cc | |
![]() |
4083f16885 | |
![]() |
89981cc984 | |
![]() |
96c8918982 | |
![]() |
8945038087 | |
![]() |
d0d9ad575f | |
![]() |
2ea422b52a | |
![]() |
1a1db6c706 | |
![]() |
ff0e9f46c0 | |
![]() |
81e8501dd5 | |
![]() |
e677737fe6 | |
![]() |
eb841988f9 | |
![]() |
32579f457a | |
![]() |
e90d2e2005 | |
![]() |
5a1693c6aa | |
![]() |
93527bc9b8 | |
![]() |
af2837e3e5 | |
![]() |
c3b5bc186e | |
![]() |
7feb50f0df | |
![]() |
1b95821205 | |
![]() |
776b7d74e2 | |
![]() |
2418534ce5 | |
![]() |
0e8ad1d169 | |
![]() |
da66a81710 | |
![]() |
83f52704f4 | |
![]() |
5e1c13be52 | |
![]() |
6a8f413bd7 | |
![]() |
6cf3534db9 | |
![]() |
5ae6a32944 | |
![]() |
bf834b6ff4 | |
![]() |
1dbe1f23f0 | |
![]() |
4756ce6b69 | |
![]() |
bb372c2146 | |
![]() |
a61ae0e656 | |
![]() |
124ed9564b | |
![]() |
20092640fe | |
![]() |
e6d9f72cad | |
![]() |
6c0f8365b3 | |
![]() |
d0328b3990 | |
![]() |
3d05146c3a | |
![]() |
6e237f77b8 | |
![]() |
623e69e623 | |
![]() |
6dbc0523fa | |
![]() |
2b7230709d | |
![]() |
c156af86bc | |
![]() |
a5dbe93023 | |
![]() |
2bb7e842b5 | |
![]() |
aae9730ee1 | |
![]() |
244f41c2dd | |
![]() |
30c7add095 | |
![]() |
09a0a216d1 | |
![]() |
63bd8ce7a5 | |
![]() |
89523a92a9 | |
![]() |
c9d96ff5c7 | |
![]() |
733db8496b | |
![]() |
6b3e4e64a0 | |
![]() |
f3802a3a75 | |
![]() |
b33b86acd9 | |
![]() |
0cabed81d7 | |
![]() |
2d67213741 | |
![]() |
caedfcb62b | |
![]() |
8638d23aec | |
![]() |
f8ef7d649c | |
![]() |
5a817b0139 | |
![]() |
a05a87e7f2 | |
![]() |
6ec5806a93 | |
![]() |
ed37784cd6 | |
![]() |
b1a776116e | |
![]() |
d36743b58a | |
![]() |
5ad5bf2dae | |
![]() |
b541d253c6 | |
![]() |
1279523798 | |
![]() |
934a208df7 | |
![]() |
0a592a34c9 | |
![]() |
cf94d5d8ea | |
![]() |
9e5557fbc5 | |
![]() |
6d2360a2f4 | |
![]() |
2d7702fc25 | |
![]() |
a4a0481a50 | |
![]() |
28bec96b3e | |
![]() |
20b86147d9 | |
![]() |
dc1b60742e | |
![]() |
b64d02c829 | |
![]() |
5f8f9a5a3d | |
![]() |
e238587588 | |
![]() |
0219584605 | |
![]() |
240e25232f | |
![]() |
a23d4e1c34 | |
![]() |
5242d6b442 | |
![]() |
063cd3e56c | |
![]() |
990c2808ef | |
![]() |
a2de4a0585 | |
![]() |
a718ffae35 | |
![]() |
d09a1411c9 | |
![]() |
1f5ee4d9fd | |
![]() |
d08ea47920 | |
![]() |
0dd3dac82c | |
![]() |
e367a11175 | |
![]() |
46b99892d2 | |
![]() |
660920a5f8 | |
![]() |
075312af6e | |
![]() |
eaff622233 | |
![]() |
a91c987552 | |
![]() |
11b13da9ad | |
![]() |
d1b1393748 | |
![]() |
bda1cd56e0 | |
![]() |
8ed7c31161 | |
![]() |
9f05692d12 | |
![]() |
7da113e932 | |
![]() |
e3ba417303 | |
![]() |
345933ebb1 | |
![]() |
52cd6ebe06 | |
![]() |
bf3da85d8d | |
![]() |
5a22b94ec6 | |
![]() |
889dc49bb6 | |
![]() |
ef4a37ee1e | |
![]() |
1e1807b88b | |
![]() |
b1c72069ca | |
![]() |
bc4b7d2962 | |
![]() |
da3e1a793b | |
![]() |
85d3425210 | |
![]() |
e6ecd6b8ce | |
![]() |
0bbbcb0a56 | |
![]() |
39ee6fe34e | |
![]() |
c201ae43e6 | |
![]() |
302f377a4b | |
![]() |
d10ccacb45 | |
![]() |
8beb7144f0 | |
![]() |
8f22943ca1 | |
![]() |
b744cf08f1 | |
![]() |
05a8a854f5 | |
![]() |
5a4c3f143d |
|
@ -0,0 +1 @@
|
||||||
|
.build
|
|
@ -0,0 +1,15 @@
|
||||||
|
common --enable_bzlmod
|
||||||
|
|
||||||
|
try-import %workspace%/ci.bazelrc
|
||||||
|
try-import %workspace%/user.bazelrc
|
||||||
|
|
||||||
|
build --macos_minimum_os=12.0 --host_macos_minimum_os=12.0
|
||||||
|
build --disk_cache=~/.bazel_cache
|
||||||
|
build --experimental_remote_cache_compression
|
||||||
|
build --experimental_remote_build_event_upload=minimal
|
||||||
|
build --nolegacy_important_outputs
|
||||||
|
build --swiftcopt=-warnings-as-errors
|
||||||
|
|
||||||
|
build:release \
|
||||||
|
--compilation_mode=opt \
|
||||||
|
--features=swift.opt_uses_wmo
|
|
@ -0,0 +1 @@
|
||||||
|
6.2.0
|
|
@ -0,0 +1,3 @@
|
||||||
|
fixedReleaser:
|
||||||
|
login: jpsim
|
||||||
|
email: jp@jpsim.com
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"homepage": "https://github.com/realm/SwiftLint",
|
||||||
|
"maintainers": [
|
||||||
|
{
|
||||||
|
"email": "jp@jpsim.com",
|
||||||
|
"github": "jpsim",
|
||||||
|
"name": "JP Simard"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repository": [
|
||||||
|
"github:realm/SwiftLint"
|
||||||
|
],
|
||||||
|
"versions": [],
|
||||||
|
"yanked_versions": {}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
shell_commands: &shell_commands
|
||||||
|
- "echo --- Downloading and extracting Swift $SWIFT_VERSION to $SWIFT_HOME"
|
||||||
|
- "mkdir $SWIFT_HOME"
|
||||||
|
- "curl https://download.swift.org/swift-${SWIFT_VERSION}-release/ubuntu2004/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE-ubuntu20.04.tar.gz | tar xvz --strip-components=1 -C $SWIFT_HOME"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
verify_targets_linux:
|
||||||
|
name: Verify targets (Linux)
|
||||||
|
platform: ubuntu2004
|
||||||
|
environment:
|
||||||
|
CC: "clang"
|
||||||
|
SWIFT_VERSION: "5.7.2"
|
||||||
|
SWIFT_HOME: "$HOME/swift-$SWIFT_VERSION"
|
||||||
|
PATH: "$PATH:$SWIFT_HOME/usr/bin"
|
||||||
|
shell_commands: *shell_commands
|
||||||
|
build_flags:
|
||||||
|
- "--action_env=PATH"
|
||||||
|
build_targets:
|
||||||
|
- '@SwiftLint//:swiftlint'
|
||||||
|
verify_targets_macos:
|
||||||
|
name: Verify targets (macOS)
|
||||||
|
platform: macos
|
||||||
|
build_targets:
|
||||||
|
- '@SwiftLint//:swiftlint'
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"url": "https://github.com/realm/SwiftLint/releases/download/{TAG}/bazel.tar.gz",
|
||||||
|
"integrity": "",
|
||||||
|
"strip_prefix": ""
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
steps:
|
||||||
|
- label: "Bazel"
|
||||||
|
commands:
|
||||||
|
- echo "+++ Build"
|
||||||
|
- bazel build :swiftlint
|
||||||
|
- echo "+++ Test"
|
||||||
|
- bazel test --test_output=errors //Tests/...
|
||||||
|
- label: "SwiftPM"
|
||||||
|
commands:
|
||||||
|
- echo "+++ Test"
|
||||||
|
- swift test --parallel -Xswiftc -DDISABLE_FOCUSED_EXAMPLES
|
||||||
|
- label: "Danger"
|
||||||
|
commands:
|
||||||
|
- echo "--- Install Bundler"
|
||||||
|
- gem install bundler
|
||||||
|
- echo "--- Bundle Install"
|
||||||
|
- bundle install
|
||||||
|
- echo "+++ Run Danger"
|
||||||
|
- bundle exec danger --verbose
|
||||||
|
- label: "TSan Tests"
|
||||||
|
commands:
|
||||||
|
- echo "+++ Test"
|
||||||
|
- bazel test --test_output=errors --build_tests_only --features=tsan --test_timeout=1000 //Tests/...
|
||||||
|
- label: "Sourcery"
|
||||||
|
commands:
|
||||||
|
- echo "+++ Run Sourcery"
|
||||||
|
- make --always-make sourcery
|
||||||
|
- echo "+++ Diff Files"
|
||||||
|
- git diff --quiet HEAD
|
|
@ -0,0 +1,5 @@
|
||||||
|
*
|
||||||
|
!Plugins
|
||||||
|
!Source
|
||||||
|
!Tests
|
||||||
|
!Package.*
|
|
@ -0,0 +1,46 @@
|
||||||
|
name: docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Extract DOCKER_TAG using tag name
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
run: |
|
||||||
|
echo "DOCKER_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Use default DOCKER_TAG
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') != true
|
||||||
|
run: |
|
||||||
|
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set lowercase repository name
|
||||||
|
run: |
|
||||||
|
echo "REPOSITORY_LC=${REPOSITORY,,}" >>${GITHUB_ENV}
|
||||||
|
env:
|
||||||
|
REPOSITORY: '${{ github.repository }}'
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Login to Github registry
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
registry: ghcr.io
|
||||||
|
|
||||||
|
- uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
tags: ghcr.io/${{ env.REPOSITORY_LC }}:${{ env.DOCKER_TAG }}
|
|
@ -1,6 +1,5 @@
|
||||||
# Xcode
|
# Xcode
|
||||||
#
|
#
|
||||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
|
||||||
|
|
||||||
## Build generated
|
## Build generated
|
||||||
build/
|
build/
|
||||||
|
@ -22,6 +21,7 @@ xcuserdata
|
||||||
*.moved-aside
|
*.moved-aside
|
||||||
*.xcuserstate
|
*.xcuserstate
|
||||||
*.xcscmblueprint
|
*.xcscmblueprint
|
||||||
|
default.profraw
|
||||||
|
|
||||||
## Obj-C/Swift specific
|
## Obj-C/Swift specific
|
||||||
*.hmap
|
*.hmap
|
||||||
|
@ -35,22 +35,19 @@ xcuserdata
|
||||||
#
|
#
|
||||||
#Pods/
|
#Pods/
|
||||||
|
|
||||||
# Carthage
|
|
||||||
#
|
|
||||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
|
||||||
# Carthage/Checkouts
|
|
||||||
|
|
||||||
Carthage/Build
|
|
||||||
|
|
||||||
# SwiftLint
|
# SwiftLint
|
||||||
|
|
||||||
|
SwiftLint.xcodeproj
|
||||||
SwiftLint.pkg
|
SwiftLint.pkg
|
||||||
SwiftLintFramework.framework.zip
|
*.zip
|
||||||
benchmark_*
|
benchmark_*
|
||||||
portable_swiftlint.zip
|
|
||||||
osscheck/
|
osscheck/
|
||||||
docs/
|
docs/
|
||||||
rule_docs/
|
rule_docs/
|
||||||
|
bazel.tar.gz
|
||||||
|
bazel.tar.gz.sha256
|
||||||
|
ci.bazelrc
|
||||||
|
user.bazelrc
|
||||||
|
|
||||||
# Swift Package Manager
|
# Swift Package Manager
|
||||||
#
|
#
|
||||||
|
@ -65,3 +62,6 @@ Packages/
|
||||||
# Bundler
|
# Bundler
|
||||||
.bundle/
|
.bundle/
|
||||||
bundle/
|
bundle/
|
||||||
|
|
||||||
|
# Bazel
|
||||||
|
bazel-*
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
[submodule "Carthage/Checkouts/SWXMLHash"]
|
|
||||||
path = Carthage/Checkouts/SWXMLHash
|
|
||||||
url = https://github.com/drmohundro/SWXMLHash.git
|
|
||||||
[submodule "Carthage/Checkouts/xcconfigs"]
|
|
||||||
path = Carthage/Checkouts/xcconfigs
|
|
||||||
url = https://github.com/jspahrsummers/xcconfigs.git
|
|
||||||
[submodule "Carthage/Checkouts/Commandant"]
|
|
||||||
path = Carthage/Checkouts/Commandant
|
|
||||||
url = https://github.com/Carthage/Commandant.git
|
|
||||||
[submodule "Carthage/Checkouts/SourceKitten"]
|
|
||||||
path = Carthage/Checkouts/SourceKitten
|
|
||||||
url = https://github.com/jpsim/SourceKitten.git
|
|
||||||
[submodule "Carthage/Checkouts/SwiftyTextTable"]
|
|
||||||
path = Carthage/Checkouts/SwiftyTextTable
|
|
||||||
url = https://github.com/scottrhoyt/SwiftyTextTable.git
|
|
||||||
[submodule "Carthage/Checkouts/Yams"]
|
|
||||||
path = Carthage/Checkouts/Yams
|
|
||||||
url = https://github.com/jpsim/Yams.git
|
|
||||||
[submodule "SwiftSyntax"]
|
|
||||||
path = SwiftSyntax
|
|
||||||
url = https://github.com/apple/swift-syntax.git
|
|
10
.jazzy.yaml
10
.jazzy.yaml
|
@ -1,19 +1,20 @@
|
||||||
module: SwiftLintFramework
|
module: SwiftLintCore
|
||||||
author: JP Simard, SwiftLint Contributors
|
author: JP Simard, SwiftLint Contributors
|
||||||
author_url: https://jpsim.com
|
author_url: https://jpsim.com
|
||||||
root_url: https://realm.github.io/SwiftLint/
|
root_url: https://realm.github.io/SwiftLint/
|
||||||
github_url: https://github.com/realm/SwiftLint
|
github_url: https://github.com/realm/SwiftLint
|
||||||
github_file_prefix: https://github.com/realm/SwiftLint/tree/master
|
github_file_prefix: https://github.com/realm/SwiftLint/tree/main
|
||||||
swift_build_tool: spm
|
swift_build_tool: spm
|
||||||
theme: fullwidth
|
theme: fullwidth
|
||||||
clean: true
|
clean: true
|
||||||
copyright: '© 2020 [JP Simard](https://jpsim.com) under MIT.'
|
copyright: '© 2023 [JP Simard](https://jpsim.com) under MIT.'
|
||||||
|
|
||||||
documentation: rule_docs/*.md
|
documentation: rule_docs/*.md
|
||||||
hide_unlisted_documentation: true
|
hide_unlisted_documentation: true
|
||||||
custom_categories_unlisted_prefix: ''
|
custom_categories_unlisted_prefix: ''
|
||||||
exclude:
|
exclude:
|
||||||
- Source/SwiftLintFramework/Rules/**/*.swift
|
# TODO: Document extensions
|
||||||
|
- Source/SwiftLintCore/Extensions/*.swift
|
||||||
custom_categories:
|
custom_categories:
|
||||||
- name: Rules
|
- name: Rules
|
||||||
children:
|
children:
|
||||||
|
@ -22,6 +23,7 @@ custom_categories:
|
||||||
children:
|
children:
|
||||||
- CSVReporter
|
- CSVReporter
|
||||||
- CheckstyleReporter
|
- CheckstyleReporter
|
||||||
|
- CodeClimateReporter
|
||||||
- EmojiReporter
|
- EmojiReporter
|
||||||
- GitHubActionsLoggingReporter
|
- GitHubActionsLoggingReporter
|
||||||
- HTMLReporter
|
- HTMLReporter
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
- id: swiftlint
|
||||||
|
name: SwiftLint
|
||||||
|
description: "Check Swift files for issues with SwiftLint"
|
||||||
|
entry: "swiftlint --quiet"
|
||||||
|
language: swift
|
||||||
|
types: [swift]
|
|
@ -1,14 +0,0 @@
|
||||||
import SwiftLintFramework
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
// swiftlint:disable file_length single_test_class type_name
|
|
||||||
|
|
||||||
{% for rule in types.structs|based:"AutomaticTestableRule" %}
|
|
||||||
class {{ rule.name }}Tests: XCTestCase {
|
|
||||||
func testWithDefaultConfiguration() {
|
|
||||||
verifyRule({{ rule.name }}.description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{% if not forloop.last %}
|
|
||||||
|
|
||||||
{% endif %}{% endfor %}
|
|
|
@ -1,4 +1,5 @@
|
||||||
|
|
||||||
/// The rule list containing all available rules built into SwiftLint.
|
/// The rule list containing all available rules built into SwiftLint.
|
||||||
public let masterRuleList = RuleList(rules: [
|
public let builtInRules: [Rule.Type] = [
|
||||||
{% for rule in types.structs where rule.name|hasSuffix:"Rule" or rule.name|hasSuffix:"Rules" %} {{ rule.name }}.self{% if not forloop.last %},{% endif %}
|
{% for rule in types.structs where rule.name|hasSuffix:"Rule" or rule.name|hasSuffix:"Rules" %} {{ rule.name }}.self{% if not forloop.last %},{% endif %}
|
||||||
{% endfor %}])
|
{% endfor %}]
|
|
@ -0,0 +1,20 @@
|
||||||
|
@testable import SwiftLintBuiltInRules
|
||||||
|
@_spi(TestHelper)
|
||||||
|
@testable import SwiftLintCore
|
||||||
|
import SwiftLintTestHelpers
|
||||||
|
|
||||||
|
// swiftlint:disable:next blanket_disable_command
|
||||||
|
// swiftlint:disable file_length single_test_class type_name
|
||||||
|
|
||||||
|
{% for rule in types.structs %}
|
||||||
|
{% if rule.name|hasSuffix:"Rule" %}
|
||||||
|
class {{ rule.name }}GeneratedTests: SwiftLintTestCase {
|
||||||
|
func testWithDefaultConfiguration() {
|
||||||
|
verifyRule({{ rule.name }}.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% if not forloop.last %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
|
@ -1,16 +0,0 @@
|
||||||
@testable import SwiftLintFrameworkTests
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
// swiftlint:disable line_length file_length
|
|
||||||
|
|
||||||
{% for type in types.classes|based:"XCTestCase" %}
|
|
||||||
extension {{ type.name }} {
|
|
||||||
static var allTests: [(String, ({{ type.name }}) -> () throws -> Void)] = [
|
|
||||||
{% for method in type.methods where method.parameters.count == 0 and method.shortName|hasPrefix:"test" and method|!annotated:"skipTestOnLinux" %} ("{{ method.shortName }}", {{ method.shortName }}){% if not forloop.last %},{% endif %}
|
|
||||||
{% endfor %}]
|
|
||||||
}
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
XCTMain([
|
|
||||||
{% for type in types.classes|based:"XCTestCase" %} testCase({{ type.name }}.allTests){% if not forloop.last %},{% endif %}
|
|
||||||
{% endfor %}])
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
/// The reporters list containing all the reporters built into SwiftLint.
|
||||||
|
public let reportersList: [Reporter.Type] = [
|
||||||
|
{% for reporter in types.structs where reporter.name|hasSuffix:"Reporter" %}
|
||||||
|
{{ reporter.name }}.self{% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
]
|
|
@ -1 +0,0 @@
|
||||||
5.0
|
|
143
.swiftlint.yml
143
.swiftlint.yml
|
@ -1,4 +1,5 @@
|
||||||
included:
|
included:
|
||||||
|
- Plugins
|
||||||
- Source
|
- Source
|
||||||
- Tests
|
- Tests
|
||||||
excluded:
|
excluded:
|
||||||
|
@ -7,95 +8,84 @@ analyzer_rules:
|
||||||
- unused_declaration
|
- unused_declaration
|
||||||
- unused_import
|
- unused_import
|
||||||
opt_in_rules:
|
opt_in_rules:
|
||||||
|
- all
|
||||||
|
disabled_rules:
|
||||||
|
- anonymous_argument_in_multiline_closure
|
||||||
- anyobject_protocol
|
- anyobject_protocol
|
||||||
- array_init
|
- closure_body_length
|
||||||
- attributes
|
- conditional_returns_on_newline
|
||||||
- closure_end_indentation
|
- convenience_type
|
||||||
- closure_spacing
|
- discouraged_optional_collection
|
||||||
- collection_alignment
|
- explicit_acl
|
||||||
- contains_over_filter_count
|
- explicit_enum_raw_value
|
||||||
- contains_over_filter_is_empty
|
- explicit_top_level_acl
|
||||||
- contains_over_first_not_nil
|
- explicit_type_interface
|
||||||
- contains_over_range_nil_comparison
|
- file_types_order
|
||||||
- discouraged_object_literal
|
- force_unwrapping
|
||||||
- empty_collection_literal
|
- function_default_parameter_at_end
|
||||||
- empty_count
|
- implicit_return
|
||||||
- empty_string
|
- implicitly_unwrapped_optional
|
||||||
- empty_xctest_method
|
- indentation_width
|
||||||
- enum_case_associated_values_count
|
- inert_defer
|
||||||
- explicit_init
|
- missing_docs
|
||||||
- extension_access_modifier
|
- multiline_arguments
|
||||||
- fallthrough
|
- multiline_arguments_brackets
|
||||||
- fatal_error_message
|
- multiline_function_chains
|
||||||
- file_header
|
- multiline_literal_brackets
|
||||||
- file_name
|
- multiline_parameters
|
||||||
- first_where
|
- multiline_parameters_brackets
|
||||||
- flatmap_over_map_reduce
|
- no_extension_access_modifier
|
||||||
- identical_operands
|
- no_fallthrough_only
|
||||||
- joined_default_parameter
|
- no_grouping_extension
|
||||||
- legacy_random
|
- no_magic_numbers
|
||||||
- let_var_whitespace
|
- prefer_nimble
|
||||||
- last_where
|
- prefer_self_in_static_references
|
||||||
- literal_expression_end_indentation
|
- prefixed_toplevel_constant
|
||||||
- lower_acl_than_parent
|
- redundant_self_in_closure
|
||||||
- modifier_order
|
- required_deinit
|
||||||
- nimble_operator
|
- self_binding
|
||||||
- nslocalizedstring_key
|
- sorted_enum_cases
|
||||||
- number_separator
|
- strict_fileprivate
|
||||||
- object_literal
|
- superfluous_else
|
||||||
- operator_usage_whitespace
|
- switch_case_on_newline
|
||||||
- overridden_super_call
|
- todo
|
||||||
- override_in_extension
|
- trailing_closure
|
||||||
- pattern_matching_keywords
|
- type_contents_order
|
||||||
- private_action
|
- unused_capture_list
|
||||||
- private_outlet
|
- vertical_whitespace_between_cases
|
||||||
- prohibited_interface_builder
|
|
||||||
- prohibited_nan_comparison
|
|
||||||
- prohibited_super_call
|
|
||||||
- quick_discouraged_call
|
|
||||||
- quick_discouraged_focused_test
|
|
||||||
- quick_discouraged_pending_test
|
|
||||||
- reduce_into
|
|
||||||
- redundant_nil_coalescing
|
|
||||||
- redundant_type_annotation
|
|
||||||
- return_value_from_void_function
|
|
||||||
- single_test_class
|
|
||||||
- sorted_first_last
|
|
||||||
- sorted_imports
|
|
||||||
- static_operator
|
|
||||||
- strong_iboutlet
|
|
||||||
- toggle_bool
|
|
||||||
- tuple_pattern
|
|
||||||
- unavailable_function
|
|
||||||
- unneeded_parentheses_in_closure_argument
|
|
||||||
- unowned_variable_capture
|
|
||||||
- untyped_error_in_catch
|
|
||||||
- vertical_parameter_alignment_on_call
|
|
||||||
- vertical_whitespace_closing_braces
|
|
||||||
- vertical_whitespace_opening_braces
|
|
||||||
- void_function_in_ternary
|
|
||||||
- xct_specific_matcher
|
|
||||||
- yoda_condition
|
|
||||||
|
|
||||||
|
attributes:
|
||||||
|
always_on_line_above:
|
||||||
|
- "@OptionGroup"
|
||||||
identifier_name:
|
identifier_name:
|
||||||
excluded:
|
excluded:
|
||||||
- id
|
- id
|
||||||
|
large_tuple: 3
|
||||||
number_separator:
|
number_separator:
|
||||||
minimum_length: 5
|
minimum_length: 5
|
||||||
file_name:
|
file_name:
|
||||||
excluded:
|
excluded:
|
||||||
- main.swift
|
- Exports.swift
|
||||||
- LinuxMain.swift
|
- GeneratedTests.swift
|
||||||
|
- SwiftSyntax+SwiftLint.swift
|
||||||
- TestHelpers.swift
|
- TestHelpers.swift
|
||||||
- shim.swift
|
|
||||||
- AutomaticRuleTests.generated.swift
|
balanced_xctest_lifecycle: &unit_test_configuration
|
||||||
|
test_parent_classes:
|
||||||
|
- SwiftLintTestCase
|
||||||
|
- XCTestCase
|
||||||
|
empty_xctest_method: *unit_test_configuration
|
||||||
|
single_test_class: *unit_test_configuration
|
||||||
|
|
||||||
|
function_body_length: 60
|
||||||
|
type_body_length: 400
|
||||||
|
|
||||||
custom_rules:
|
custom_rules:
|
||||||
rule_id:
|
rule_id:
|
||||||
included: Source/SwiftLintFramework/Rules/.+/\w+\.swift
|
included: Source/SwiftLintBuiltInRules/Rules/.+/\w+\.swift
|
||||||
name: Rule ID
|
name: Rule ID
|
||||||
message: Rule IDs must be all lowercase, snake case and not end with `rule`
|
message: Rule IDs must be all lowercase, snake case and not end with `rule`
|
||||||
regex: identifier:\s*("\w+_rule"|"\S*[^a-z_]\S*")
|
regex: ^\s+identifier:\s*("\w+_rule"|"\S*[^a-z_]\S*")
|
||||||
severity: error
|
severity: error
|
||||||
fatal_error:
|
fatal_error:
|
||||||
name: Fatal Error
|
name: Fatal Error
|
||||||
|
@ -111,3 +101,8 @@ custom_rules:
|
||||||
message: Rule Test Function mustn't end with `rule`
|
message: Rule Test Function mustn't end with `rule`
|
||||||
regex: func\s*test\w+(r|R)ule\(\)
|
regex: func\s*test\w+(r|R)ule\(\)
|
||||||
severity: error
|
severity: error
|
||||||
|
|
||||||
|
unused_import:
|
||||||
|
always_keep_imports:
|
||||||
|
- SwiftSyntaxBuilder # we can't detect uses of string interpolation of swift syntax nodes
|
||||||
|
- SwiftLintFramework # now that this is a wrapper around other modules, don't treat as unused
|
||||||
|
|
|
@ -0,0 +1,211 @@
|
||||||
|
load("@build_bazel_rules_apple//apple:apple.bzl", "apple_universal_binary")
|
||||||
|
load(
|
||||||
|
"@build_bazel_rules_swift//swift:swift.bzl",
|
||||||
|
"swift_binary",
|
||||||
|
"swift_library",
|
||||||
|
)
|
||||||
|
load(
|
||||||
|
"@rules_xcodeproj//xcodeproj:defs.bzl",
|
||||||
|
"xcode_schemes",
|
||||||
|
"xcodeproj",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Targets
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "SwiftLintCore",
|
||||||
|
srcs = glob(["Source/SwiftLintCore/**/*.swift"]),
|
||||||
|
module_name = "SwiftLintCore",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"@SwiftSyntax//:SwiftIDEUtils_opt",
|
||||||
|
"@SwiftSyntax//:SwiftOperators_opt",
|
||||||
|
"@SwiftSyntax//:SwiftParserDiagnostics_opt",
|
||||||
|
"@SwiftSyntax//:SwiftSyntaxBuilder_opt",
|
||||||
|
"@SwiftSyntax//:SwiftSyntax_opt",
|
||||||
|
"@com_github_jpsim_sourcekitten//:SourceKittenFramework",
|
||||||
|
"@sourcekitten_com_github_jpsim_yams//:Yams",
|
||||||
|
"@swiftlint_com_github_scottrhoyt_swifty_text_table//:SwiftyTextTable",
|
||||||
|
] + select({
|
||||||
|
"@platforms//os:linux": ["@com_github_krzyzanowskim_cryptoswift//:CryptoSwift"],
|
||||||
|
"//conditions:default": [":DyldWarningWorkaround"],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "SwiftLintBuiltInRules",
|
||||||
|
srcs = glob(["Source/SwiftLintBuiltInRules/**/*.swift"]),
|
||||||
|
module_name = "SwiftLintBuiltInRules",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
":SwiftLintCore",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "SwiftLintExtraRules",
|
||||||
|
srcs = [
|
||||||
|
"Source/SwiftLintExtraRules/Exports.swift",
|
||||||
|
"@swiftlint_extra_rules//:extra_rules",
|
||||||
|
],
|
||||||
|
module_name = "SwiftLintExtraRules",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
":SwiftLintCore",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "SwiftLintFramework",
|
||||||
|
srcs = glob(
|
||||||
|
["Source/SwiftLintFramework/**/*.swift"],
|
||||||
|
),
|
||||||
|
module_name = "SwiftLintFramework",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
":SwiftLintBuiltInRules",
|
||||||
|
":SwiftLintCore",
|
||||||
|
":SwiftLintExtraRules",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "swiftlint.library",
|
||||||
|
srcs = glob(["Source/swiftlint/**/*.swift"]),
|
||||||
|
module_name = "swiftlint",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
":SwiftLintFramework",
|
||||||
|
"@com_github_johnsundell_collectionconcurrencykit//:CollectionConcurrencyKit",
|
||||||
|
"@sourcekitten_com_github_apple_swift_argument_parser//:ArgumentParser",
|
||||||
|
"@swiftlint_com_github_scottrhoyt_swifty_text_table//:SwiftyTextTable",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
swift_binary(
|
||||||
|
name = "swiftlint",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
":swiftlint.library",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
apple_universal_binary(
|
||||||
|
name = "universal_swiftlint",
|
||||||
|
binary = ":swiftlint",
|
||||||
|
forced_cpus = [
|
||||||
|
"x86_64",
|
||||||
|
"arm64",
|
||||||
|
],
|
||||||
|
minimum_os_version = "12.0",
|
||||||
|
platform_type = "macos",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "DyldWarningWorkaroundSources",
|
||||||
|
srcs = [
|
||||||
|
"Source/DyldWarningWorkaround/DyldWarningWorkaround.c",
|
||||||
|
"Source/DyldWarningWorkaround/include/objc_dupclass.h",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_library(
|
||||||
|
name = "DyldWarningWorkaround",
|
||||||
|
srcs = ["//:DyldWarningWorkaroundSources"],
|
||||||
|
includes = ["Source/DyldWarningWorkaround/include"],
|
||||||
|
alwayslink = True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "LintInputs",
|
||||||
|
srcs = glob(["Source/**/*.swift"]) + [
|
||||||
|
".swiftlint.yml",
|
||||||
|
"//Tests:SwiftLintFrameworkTestsData",
|
||||||
|
],
|
||||||
|
visibility = ["//Tests:__subpackages__"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Release
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "release_files",
|
||||||
|
srcs = [
|
||||||
|
"BUILD",
|
||||||
|
"LICENSE",
|
||||||
|
"MODULE.bazel",
|
||||||
|
"//:DyldWarningWorkaroundSources",
|
||||||
|
"//:LintInputs",
|
||||||
|
"//Tests:BUILD",
|
||||||
|
"//bazel:release_files",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: Use rules_pkg
|
||||||
|
genrule(
|
||||||
|
name = "release",
|
||||||
|
srcs = [":release_files"],
|
||||||
|
outs = [
|
||||||
|
"bazel.tar.gz",
|
||||||
|
"bazel.tar.gz.sha256",
|
||||||
|
],
|
||||||
|
cmd = """\
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
outs=($(OUTS))
|
||||||
|
|
||||||
|
COPYFILE_DISABLE=1 tar czvfh "$${outs[0]}" \
|
||||||
|
--exclude ^bazel-out/ \
|
||||||
|
--exclude ^external/ \
|
||||||
|
*
|
||||||
|
shasum -a 256 "$${outs[0]}" > "$${outs[1]}"
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Xcode Integration
|
||||||
|
|
||||||
|
xcodeproj(
|
||||||
|
name = "xcodeproj",
|
||||||
|
project_name = "SwiftLint",
|
||||||
|
schemes = [
|
||||||
|
xcode_schemes.scheme(
|
||||||
|
name = "SwiftLint",
|
||||||
|
launch_action = xcode_schemes.launch_action(
|
||||||
|
"swiftlint",
|
||||||
|
args = [
|
||||||
|
"--progress",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
test_action = xcode_schemes.test_action([
|
||||||
|
"//Tests:CLITests",
|
||||||
|
"//Tests:SwiftLintFrameworkTests",
|
||||||
|
"//Tests:GeneratedTests",
|
||||||
|
"//Tests:IntegrationTests",
|
||||||
|
"//Tests:ExtraRulesTests",
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
top_level_targets = [
|
||||||
|
"//:swiftlint",
|
||||||
|
"//Tests:CLITests",
|
||||||
|
"//Tests:SwiftLintFrameworkTests",
|
||||||
|
"//Tests:GeneratedTests",
|
||||||
|
"//Tests:IntegrationTests",
|
||||||
|
"//Tests:ExtraRulesTests",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Analyze
|
||||||
|
|
||||||
|
sh_test(
|
||||||
|
name = "analyze",
|
||||||
|
srcs = ["//tools:test-analyze.sh"],
|
||||||
|
data = [
|
||||||
|
"Package.resolved",
|
||||||
|
"Package.swift",
|
||||||
|
":LintInputs",
|
||||||
|
":swiftlint",
|
||||||
|
],
|
||||||
|
)
|
2368
CHANGELOG.md
2368
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
|
@ -1,21 +1,47 @@
|
||||||
|
## Tutorial
|
||||||
|
|
||||||
|
If you'd like to write a SwiftLint rule but aren't sure how to start,
|
||||||
|
please watch and follow along with
|
||||||
|
[this video tutorial](https://vimeo.com/819268038).
|
||||||
|
|
||||||
## Pull Requests
|
## Pull Requests
|
||||||
|
|
||||||
All changes, no matter how trivial, must be done via pull request. Commits
|
All changes, no matter how trivial, must be done via pull request. Commits
|
||||||
should never be made directly on the `master` branch. Prefer rebasing over
|
should never be made directly on the `main` branch. Prefer rebasing over
|
||||||
merging `master` into your PR branch to update it and resolve conflicts.
|
merging `main` into your PR branch to update it and resolve conflicts.
|
||||||
|
|
||||||
_If you have commit access to SwiftLint and believe your change to be trivial
|
_If you have commit access to SwiftLint and believe your change to be trivial
|
||||||
and not worth waiting for review, you may open a pull request and merge
|
and not worth waiting for review, you may open a pull request and merge
|
||||||
immediately, but this should be the exception, not the norm._
|
immediately, but this should be the exception, not the norm._
|
||||||
|
|
||||||
### Submodules
|
### Building And Running Locally
|
||||||
|
|
||||||
This SwiftLint repository uses submodules for its dependencies.
|
#### Using Xcode
|
||||||
This means that if you decide to fork this repository to contribute to SwiftLint,
|
|
||||||
don't forget to checkout the submodules as well when cloning, by running
|
|
||||||
`git submodule update --init --recursive` after cloning.
|
|
||||||
|
|
||||||
See more info [in the README](https://github.com/realm/SwiftLint#installation).
|
1. `git clone https://github.com/realm/SwiftLint.git`
|
||||||
|
1. `cd SwiftLint`
|
||||||
|
1. `xed .`
|
||||||
|
1. Select the "swiftlint" scheme
|
||||||
|
1. `cmd-opt-r` open the scheme options
|
||||||
|
1. Set the "Arguments Passed On Launch" you want in the "Arguments" tab. See
|
||||||
|
available arguments [in the README](https://github.com/realm/SwiftLint#command-line).
|
||||||
|
1. Set the "Working Directory" in the "Options" tab to the path where you would like
|
||||||
|
to execute SwiftLint. A folder that contains swift source files.
|
||||||
|
1. Hit "Run"
|
||||||
|
|
||||||
|
|Arguments|Options|
|
||||||
|
|-|-|
|
||||||
|
|||
|
||||||
|
|
||||||
|
Then you can use the full power of Xcode/LLDB/Instruments to develop and debug your changes to SwiftLint.
|
||||||
|
|
||||||
|
#### Using the command line
|
||||||
|
|
||||||
|
1. `git clone https://github.com/realm/SwiftLint.git`
|
||||||
|
1. `cd SwiftLint`
|
||||||
|
1. `swift build [-c release]`
|
||||||
|
1. Use the produced `swiftlint` binary from the command line, either by running `swift run [-c release] [swiftlint] [arguments]` or by invoking the binary directly at `.build/[release|debug]/swiftlint`
|
||||||
|
1. [Optional] Attach LLDB: `lldb -- .build/[release|debug]/swiftlint [arguments]`
|
||||||
|
|
||||||
### Code Generation
|
### Code Generation
|
||||||
|
|
||||||
|
@ -31,14 +57,14 @@ with Swift Package Manager on Linux. When contributing code changes, please
|
||||||
ensure that all three supported build methods continue to work and pass tests.
|
ensure that all three supported build methods continue to work and pass tests.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ script/cibuild
|
$ xcodebuild -scheme swiftlint test
|
||||||
$ swift test
|
$ swift test
|
||||||
$ make docker_test
|
$ make docker_test
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
New rules should be added in the `Source/SwiftLintFramework/Rules` directory.
|
New rules should be added in the `Source/SwiftLintBuiltInRules/Rules` directory.
|
||||||
|
|
||||||
Rules should conform to either the `Rule` or `ASTRule` protocols.
|
Rules should conform to either the `Rule` or `ASTRule` protocols.
|
||||||
|
|
||||||
|
@ -52,6 +78,19 @@ over time. This way adding a unit test for your new Rule is just a matter of
|
||||||
adding a test case in `RulesTests.swift` which simply calls
|
adding a test case in `RulesTests.swift` which simply calls
|
||||||
`verifyRule(YourNewRule.description)`.
|
`verifyRule(YourNewRule.description)`.
|
||||||
|
|
||||||
|
For debugging purposes examples can be marked as `focused`. If there are any
|
||||||
|
focused examples found, then only those will be run when running tests for that rule.
|
||||||
|
```
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("let x: [Int]"),
|
||||||
|
Example("let x: [Int: String]").focused() // only this one will be run in tests
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("let x: ↓Array<String>"),
|
||||||
|
Example("let x: ↓Dictionary<Int, String>")
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
### `ConfigurationProviderRule`
|
### `ConfigurationProviderRule`
|
||||||
|
|
||||||
If your rule supports user-configurable options via `.swiftlint.yml`, you can
|
If your rule supports user-configurable options via `.swiftlint.yml`, you can
|
||||||
|
@ -65,11 +104,11 @@ configuration object via the `configuration` property:
|
||||||
* If none of the provided `RuleConfiguration`s are applicable, you can create one
|
* If none of the provided `RuleConfiguration`s are applicable, you can create one
|
||||||
specifically for your rule.
|
specifically for your rule.
|
||||||
|
|
||||||
See [`ForceCastRule`](https://github.com/realm/SwiftLint/blob/master/Source/SwiftLintFramework/Rules/ForceCastRule.swift)
|
See [`ForceCastRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForceCastRule.swift)
|
||||||
for a rule that allows severity configuration,
|
for a rule that allows severity configuration,
|
||||||
[`FileLengthRule`](https://github.com/realm/SwiftLint/blob/master/Source/SwiftLintFramework/Rules/FileLengthRule.swift)
|
[`FileLengthRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift)
|
||||||
for a rule that has multiple severity levels,
|
for a rule that has multiple severity levels,
|
||||||
[`IdentifierNameRule`](https://github.com/realm/SwiftLint/blob/master/Source/SwiftLintFramework/Rules/IdentifierNameRule.swift)
|
[`IdentifierNameRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRule.swift)
|
||||||
for a rule that allows name evaluation configuration:
|
for a rule that allows name evaluation configuration:
|
||||||
|
|
||||||
``` yaml
|
``` yaml
|
||||||
|
@ -106,16 +145,40 @@ If your rule is configurable, but does not fit the pattern of
|
||||||
|
|
||||||
All changes should be made via pull requests on GitHub.
|
All changes should be made via pull requests on GitHub.
|
||||||
|
|
||||||
When issuing a pull request, please add a summary of your changes to
|
When issuing a pull request with user-facing changes, please add a
|
||||||
the `CHANGELOG.md` file.
|
summary of your changes to the `CHANGELOG.md` file.
|
||||||
|
|
||||||
We follow the same syntax as CocoaPods' CHANGELOG.md:
|
We follow the same syntax as CocoaPods' CHANGELOG.md:
|
||||||
|
|
||||||
1. One Markdown unnumbered list item describing the change.
|
1. One Markdown unnumbered list item describing the change.
|
||||||
2. 2 trailing spaces on the last line describing the change.
|
2. 2 trailing spaces on the last line describing the change (so that Markdown renders each change [on its own line](https://daringfireball.net/projects/markdown/syntax#p)).
|
||||||
3. A list of Markdown hyperlinks to the contributors to the change. One entry
|
3. A list of Markdown hyperlinks to the contributors to the change. One entry
|
||||||
per line. Usually just one.
|
per line. Usually just one.
|
||||||
4. A list of Markdown hyperlinks to the issues the change addresses. One entry
|
4. A list of Markdown hyperlinks to the issues the change addresses. One entry
|
||||||
per line. Usually just one. If there was no issue tracking this change,
|
per line. Usually just one. If there was no issue tracking this change,
|
||||||
you may instead link to the change's pull request.
|
you may instead link to the change's pull request.
|
||||||
5. All CHANGELOG.md content is hard-wrapped at 80 characters.
|
5. All CHANGELOG.md content is hard-wrapped at 80 characters.
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
SwiftLint uses Azure Pipelines for most of its CI jobs, primarily because
|
||||||
|
they're the only CI provider to have a free tier with 10x concurrency on macOS.
|
||||||
|
|
||||||
|
Some CI jobs run in GitHub Actions (e.g. Docker).
|
||||||
|
|
||||||
|
Some CI jobs run on Buildkite using Mac Minis from MacStadium. These are jobs
|
||||||
|
that benefit from being run on the latest Xcode & macOS versions on bare metal.
|
||||||
|
|
||||||
|
### Buildkite Setup
|
||||||
|
|
||||||
|
To bring up a new Buildkite worker from MacStadium:
|
||||||
|
|
||||||
|
1. Change account password
|
||||||
|
1. Update macOS to the latest version
|
||||||
|
1. Install Homebrew: https://brew.sh
|
||||||
|
1. Install Buildkite agent and other tools via Homebrew:
|
||||||
|
`brew install aria2 bazelisk htop buildkite/buildkite/buildkite-agent robotsandpencils/made/xcodes`
|
||||||
|
1. Install latest Xcode version: `xcodes update && xcodes install 14.0.0`
|
||||||
|
1. Add `DANGER_GITHUB_API_TOKEN` and `HOME` to `/opt/homebrew/etc/buildkite-agent/hooks/environment`
|
||||||
|
1. Configure and launch buildkite agent: `brew info buildkite-agent` /
|
||||||
|
https://buildkite.com/organizations/swiftlint/agents#setup-macos
|
||||||
|
|
2
Cartfile
2
Cartfile
|
@ -1,2 +0,0 @@
|
||||||
github "jpsim/SourceKitten" ~> 0.29.0
|
|
||||||
github "scottrhoyt/SwiftyTextTable" ~> 0.9.0
|
|
|
@ -1,3 +0,0 @@
|
||||||
github "Carthage/Commandant" ~> 0.17.0
|
|
||||||
github "jpsim/Yams" ~> 2.0.0
|
|
||||||
github "jspahrsummers/xcconfigs" ~> 0.12.0
|
|
|
@ -1,6 +0,0 @@
|
||||||
github "Carthage/Commandant" "0.17.0"
|
|
||||||
github "drmohundro/SWXMLHash" "5.0.1"
|
|
||||||
github "jpsim/SourceKitten" "0.29.0"
|
|
||||||
github "jpsim/Yams" "2.0.0"
|
|
||||||
github "jspahrsummers/xcconfigs" "0.12"
|
|
||||||
github "scottrhoyt/SwiftyTextTable" "0.9.0"
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit ab68611013dec67413628ac87c1f29e8427bc8e4
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit a4931e5c3bafbedeb1601d3bb76bbe835c6d475a
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 77a4dbbb477a8110eb8765e3c44c70fb4929098f
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit c6df6cf533d120716bff38f8ff9885e1ce2a4ac3
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit c947a306d2e80ecb2c0859047b35c73b8e1ca27f
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit bb795558a76e5daf3688500055bbcfe243bffa8d
|
|
11
Dangerfile
11
Dangerfile
|
@ -14,11 +14,13 @@ modified_files = git.modified_files + git.added_files
|
||||||
# including in a CHANGELOG for example
|
# including in a CHANGELOG for example
|
||||||
has_app_changes = !modified_files.grep(/Source/).empty?
|
has_app_changes = !modified_files.grep(/Source/).empty?
|
||||||
has_test_changes = !modified_files.grep(/Tests/).empty?
|
has_test_changes = !modified_files.grep(/Tests/).empty?
|
||||||
has_danger_changes = !modified_files.grep(/Dangerfile|script\/oss-check|Gemfile/).empty?
|
has_danger_changes = !modified_files.grep(/Dangerfile|tools\/oss-check|Gemfile/).empty?
|
||||||
|
has_package_changes = !modified_files.grep(/Package\.swift/).empty?
|
||||||
|
has_bazel_changes = !modified_files.grep(/\.bazelrc|\.bazelversion|WORKSPACE|bazel\/|BUILD|MODULE\.bazel/).empty?
|
||||||
|
|
||||||
# Add a CHANGELOG entry for app changes
|
# Add a CHANGELOG entry for app changes
|
||||||
if !modified_files.include?('CHANGELOG.md') && has_app_changes
|
if !modified_files.include?('CHANGELOG.md') && has_app_changes
|
||||||
warn("Please include a CHANGELOG entry to credit yourself! \nYou can find it at [CHANGELOG.md](https://github.com/realm/SwiftLint/blob/master/CHANGELOG.md).")
|
warn("If this is a user-facing change, please include a CHANGELOG entry to credit yourself! \nYou can find it at [CHANGELOG.md](https://github.com/realm/SwiftLint/blob/main/CHANGELOG.md).")
|
||||||
markdown <<-MARKDOWN
|
markdown <<-MARKDOWN
|
||||||
Here's an example of your CHANGELOG entry:
|
Here's an example of your CHANGELOG entry:
|
||||||
```markdown
|
```markdown
|
||||||
|
@ -30,7 +32,7 @@ Here's an example of your CHANGELOG entry:
|
||||||
MARKDOWN
|
MARKDOWN
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless has_app_changes || has_danger_changes
|
return unless has_app_changes || has_danger_changes || has_package_changes || has_bazel_changes
|
||||||
|
|
||||||
# Non-trivial amounts of app changes without tests
|
# Non-trivial amounts of app changes without tests
|
||||||
if git.lines_of_code > 50 && has_app_changes && !has_test_changes
|
if git.lines_of_code > 50 && has_app_changes && !has_test_changes
|
||||||
|
@ -49,7 +51,8 @@ end
|
||||||
|
|
||||||
file = Tempfile.new('violations')
|
file = Tempfile.new('violations')
|
||||||
|
|
||||||
Open3.popen3("script/oss-check -v 2> #{file.path}") do |_, stdout, _, _|
|
force_flag = has_danger_changes ? "--force" : ""
|
||||||
|
Open3.popen3("tools/oss-check -v #{force_flag} 2> #{file.path}") do |_, stdout, _, _|
|
||||||
while char = stdout.getc
|
while char = stdout.getc
|
||||||
print char
|
print char
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Explicitly specify `jammy` to keep the Swift & Ubuntu images in sync.
|
||||||
|
ARG BUILDER_IMAGE=swift:jammy
|
||||||
|
ARG RUNTIME_IMAGE=ubuntu:jammy
|
||||||
|
|
||||||
|
# builder image
|
||||||
|
FROM ${BUILDER_IMAGE} AS builder
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
&& rm -r /var/lib/apt/lists/*
|
||||||
|
WORKDIR /workdir/
|
||||||
|
COPY Plugins Plugins/
|
||||||
|
COPY Source Source/
|
||||||
|
COPY Tests Tests/
|
||||||
|
COPY Package.* ./
|
||||||
|
|
||||||
|
RUN swift package update
|
||||||
|
ARG SWIFT_FLAGS="-c release -Xswiftc -static-stdlib -Xlinker -lCFURLSessionInterface -Xlinker -lCFXMLInterface -Xlinker -lcurl -Xlinker -lxml2 -Xswiftc -I. -Xlinker -fuse-ld=lld -Xlinker -L/usr/lib/swift/linux"
|
||||||
|
RUN swift build $SWIFT_FLAGS --product swiftlint
|
||||||
|
RUN mkdir -p /executables
|
||||||
|
RUN install -v `swift build $SWIFT_FLAGS --show-bin-path`/swiftlint /executables
|
||||||
|
|
||||||
|
# runtime image
|
||||||
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
LABEL org.opencontainers.image.source https://github.com/realm/SwiftLint
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libcurl4 \
|
||||||
|
libxml2 \
|
||||||
|
&& rm -r /var/lib/apt/lists/*
|
||||||
|
COPY --from=builder /usr/lib/libsourcekitdInProc.so /usr/lib
|
||||||
|
COPY --from=builder /usr/lib/swift/linux/libBlocksRuntime.so /usr/lib
|
||||||
|
COPY --from=builder /usr/lib/swift/linux/libdispatch.so /usr/lib
|
||||||
|
COPY --from=builder /usr/lib/swift/linux/libswiftCore.so /usr/lib
|
||||||
|
COPY --from=builder /executables/* /usr/bin
|
||||||
|
|
||||||
|
RUN swiftlint version
|
||||||
|
RUN echo "_ = 0" | swiftlint --use-stdin
|
||||||
|
|
||||||
|
CMD ["swiftlint"]
|
156
Gemfile.lock
156
Gemfile.lock
|
@ -1,141 +1,157 @@
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
CFPropertyList (3.0.2)
|
CFPropertyList (3.0.6)
|
||||||
activesupport (4.2.11.1)
|
rexml
|
||||||
i18n (~> 0.7)
|
activesupport (7.0.4.3)
|
||||||
minitest (~> 5.1)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
thread_safe (~> 0.3, >= 0.3.4)
|
i18n (>= 1.6, < 2)
|
||||||
tzinfo (~> 1.1)
|
minitest (>= 5.1)
|
||||||
addressable (2.7.0)
|
tzinfo (~> 2.0)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
addressable (2.8.4)
|
||||||
algoliasearch (1.27.1)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
|
algoliasearch (1.27.5)
|
||||||
httpclient (~> 2.8, >= 2.8.3)
|
httpclient (~> 2.8, >= 2.8.3)
|
||||||
json (>= 1.5.1)
|
json (>= 1.5.1)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
claide (1.0.3)
|
claide (1.1.0)
|
||||||
claide-plugins (0.9.2)
|
claide-plugins (0.9.2)
|
||||||
cork
|
cork
|
||||||
nap
|
nap
|
||||||
open4 (~> 1.3)
|
open4 (~> 1.3)
|
||||||
cocoapods (1.8.4)
|
cocoapods (1.12.1)
|
||||||
activesupport (>= 4.0.2, < 5)
|
addressable (~> 2.8)
|
||||||
claide (>= 1.0.2, < 2.0)
|
claide (>= 1.0.2, < 2.0)
|
||||||
cocoapods-core (= 1.8.4)
|
cocoapods-core (= 1.12.1)
|
||||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||||
cocoapods-downloader (>= 1.2.2, < 2.0)
|
cocoapods-downloader (>= 1.6.0, < 2.0)
|
||||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||||
cocoapods-search (>= 1.0.0, < 2.0)
|
cocoapods-search (>= 1.0.0, < 2.0)
|
||||||
cocoapods-stats (>= 1.0.0, < 2.0)
|
cocoapods-trunk (>= 1.6.0, < 2.0)
|
||||||
cocoapods-trunk (>= 1.4.0, < 2.0)
|
|
||||||
cocoapods-try (>= 1.1.0, < 2.0)
|
cocoapods-try (>= 1.1.0, < 2.0)
|
||||||
colored2 (~> 3.1)
|
colored2 (~> 3.1)
|
||||||
escape (~> 0.0.4)
|
escape (~> 0.0.4)
|
||||||
fourflusher (>= 2.3.0, < 3.0)
|
fourflusher (>= 2.3.0, < 3.0)
|
||||||
gh_inspector (~> 1.0)
|
gh_inspector (~> 1.0)
|
||||||
molinillo (~> 0.6.6)
|
molinillo (~> 0.8.0)
|
||||||
nap (~> 1.0)
|
nap (~> 1.0)
|
||||||
ruby-macho (~> 1.4)
|
ruby-macho (>= 2.3.0, < 3.0)
|
||||||
xcodeproj (>= 1.11.1, < 2.0)
|
xcodeproj (>= 1.21.0, < 2.0)
|
||||||
cocoapods-core (1.8.4)
|
cocoapods-core (1.12.1)
|
||||||
activesupport (>= 4.0.2, < 6)
|
activesupport (>= 5.0, < 8)
|
||||||
|
addressable (~> 2.8)
|
||||||
algoliasearch (~> 1.0)
|
algoliasearch (~> 1.0)
|
||||||
concurrent-ruby (~> 1.1)
|
concurrent-ruby (~> 1.1)
|
||||||
fuzzy_match (~> 2.0.4)
|
fuzzy_match (~> 2.0.4)
|
||||||
nap (~> 1.0)
|
nap (~> 1.0)
|
||||||
cocoapods-deintegrate (1.0.4)
|
netrc (~> 0.11)
|
||||||
cocoapods-downloader (1.3.0)
|
public_suffix (~> 4.0)
|
||||||
|
typhoeus (~> 1.0)
|
||||||
|
cocoapods-deintegrate (1.0.5)
|
||||||
|
cocoapods-downloader (1.6.3)
|
||||||
cocoapods-plugins (1.0.0)
|
cocoapods-plugins (1.0.0)
|
||||||
nap
|
nap
|
||||||
cocoapods-search (1.0.0)
|
cocoapods-search (1.0.1)
|
||||||
cocoapods-stats (1.1.0)
|
cocoapods-trunk (1.6.0)
|
||||||
cocoapods-trunk (1.4.1)
|
|
||||||
nap (>= 0.8, < 2.0)
|
nap (>= 0.8, < 2.0)
|
||||||
netrc (~> 0.11)
|
netrc (~> 0.11)
|
||||||
cocoapods-try (1.1.0)
|
cocoapods-try (1.2.0)
|
||||||
colored2 (3.1.2)
|
colored2 (3.1.2)
|
||||||
concurrent-ruby (1.1.5)
|
concurrent-ruby (1.2.2)
|
||||||
cork (0.3.0)
|
cork (0.3.0)
|
||||||
colored2 (~> 3.1)
|
colored2 (~> 3.1)
|
||||||
danger (6.1.0)
|
danger (9.2.0)
|
||||||
claide (~> 1.0)
|
claide (~> 1.0)
|
||||||
claide-plugins (>= 0.9.2)
|
claide-plugins (>= 0.9.2)
|
||||||
colored2 (~> 3.1)
|
colored2 (~> 3.1)
|
||||||
cork (~> 0.1)
|
cork (~> 0.1)
|
||||||
faraday (~> 0.9)
|
faraday (>= 0.9.0, < 3.0)
|
||||||
faraday-http-cache (~> 2.0)
|
faraday-http-cache (~> 2.0)
|
||||||
git (~> 1.5)
|
git (~> 1.7)
|
||||||
kramdown (~> 2.0)
|
kramdown (~> 2.3)
|
||||||
kramdown-parser-gfm (~> 1.0)
|
kramdown-parser-gfm (~> 1.0)
|
||||||
no_proxy_fix
|
no_proxy_fix
|
||||||
octokit (~> 4.7)
|
octokit (~> 5.0)
|
||||||
terminal-table (~> 1)
|
terminal-table (>= 1, < 4)
|
||||||
escape (0.0.4)
|
escape (0.0.4)
|
||||||
faraday (0.17.3)
|
ethon (0.16.0)
|
||||||
multipart-post (>= 1.2, < 3)
|
ffi (>= 1.15.0)
|
||||||
faraday-http-cache (2.0.0)
|
faraday (2.7.4)
|
||||||
faraday (~> 0.8)
|
faraday-net_http (>= 2.0, < 3.1)
|
||||||
ffi (1.11.3)
|
ruby2_keywords (>= 0.0.4)
|
||||||
|
faraday-http-cache (2.4.1)
|
||||||
|
faraday (>= 0.8)
|
||||||
|
faraday-net_http (3.0.2)
|
||||||
|
ffi (1.15.5)
|
||||||
fourflusher (2.3.1)
|
fourflusher (2.3.1)
|
||||||
fuzzy_match (2.0.4)
|
fuzzy_match (2.0.4)
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
git (1.5.0)
|
git (1.18.0)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
rchardet (~> 1.8)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (0.9.5)
|
i18n (1.12.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
jazzy (0.13.1)
|
jazzy (0.14.3)
|
||||||
cocoapods (~> 1.5)
|
cocoapods (~> 1.5)
|
||||||
mustache (~> 1.1)
|
mustache (~> 1.1)
|
||||||
open4
|
open4 (~> 1.3)
|
||||||
redcarpet (~> 3.4)
|
redcarpet (~> 3.4)
|
||||||
|
rexml (~> 3.2)
|
||||||
rouge (>= 2.0.6, < 4.0)
|
rouge (>= 2.0.6, < 4.0)
|
||||||
sassc (~> 2.1)
|
sassc (~> 2.1)
|
||||||
sqlite3 (~> 1.3)
|
sqlite3 (~> 1.3)
|
||||||
xcinvoke (~> 0.3.0)
|
xcinvoke (~> 0.3.0)
|
||||||
json (2.3.0)
|
json (2.6.3)
|
||||||
kramdown (2.1.0)
|
kramdown (2.4.0)
|
||||||
|
rexml
|
||||||
kramdown-parser-gfm (1.1.0)
|
kramdown-parser-gfm (1.1.0)
|
||||||
kramdown (~> 2.0)
|
kramdown (~> 2.0)
|
||||||
liferaft (0.0.6)
|
liferaft (0.0.6)
|
||||||
minitest (5.13.0)
|
minitest (5.18.0)
|
||||||
molinillo (0.6.6)
|
molinillo (0.8.0)
|
||||||
multipart-post (2.1.1)
|
|
||||||
mustache (1.1.1)
|
mustache (1.1.1)
|
||||||
nanaimo (0.2.6)
|
nanaimo (0.3.0)
|
||||||
nap (1.1.0)
|
nap (1.1.0)
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
no_proxy_fix (0.1.2)
|
no_proxy_fix (0.1.2)
|
||||||
octokit (4.15.0)
|
octokit (5.6.1)
|
||||||
faraday (>= 0.9)
|
faraday (>= 1, < 3)
|
||||||
sawyer (~> 0.8.0, >= 0.5.3)
|
sawyer (~> 0.9)
|
||||||
open4 (1.3.4)
|
open4 (1.3.4)
|
||||||
public_suffix (4.0.3)
|
public_suffix (4.0.7)
|
||||||
redcarpet (3.5.0)
|
rchardet (1.8.0)
|
||||||
rouge (3.14.0)
|
redcarpet (3.6.0)
|
||||||
ruby-macho (1.4.0)
|
rexml (3.2.5)
|
||||||
sassc (2.2.1)
|
rouge (3.30.0)
|
||||||
|
ruby-macho (2.5.1)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
|
sassc (2.4.0)
|
||||||
ffi (~> 1.9)
|
ffi (~> 1.9)
|
||||||
sawyer (0.8.2)
|
sawyer (0.9.2)
|
||||||
addressable (>= 2.3.5)
|
addressable (>= 2.3.5)
|
||||||
faraday (> 0.8, < 2.0)
|
faraday (>= 0.17.3, < 3)
|
||||||
sqlite3 (1.4.2)
|
sqlite3 (1.6.2-arm64-darwin)
|
||||||
terminal-table (1.8.0)
|
terminal-table (3.0.2)
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
thread_safe (0.3.6)
|
typhoeus (1.4.0)
|
||||||
tzinfo (1.2.6)
|
ethon (>= 0.9.0)
|
||||||
thread_safe (~> 0.1)
|
tzinfo (2.0.6)
|
||||||
unicode-display_width (1.6.0)
|
concurrent-ruby (~> 1.0)
|
||||||
|
unicode-display_width (2.4.2)
|
||||||
xcinvoke (0.3.0)
|
xcinvoke (0.3.0)
|
||||||
liferaft (~> 0.0.6)
|
liferaft (~> 0.0.6)
|
||||||
xcodeproj (1.14.0)
|
xcodeproj (1.22.0)
|
||||||
CFPropertyList (>= 2.3.3, < 4.0)
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
atomos (~> 0.1.3)
|
atomos (~> 0.1.3)
|
||||||
claide (>= 1.0.2, < 2.0)
|
claide (>= 1.0.2, < 2.0)
|
||||||
colored2 (~> 3.1)
|
colored2 (~> 3.1)
|
||||||
nanaimo (~> 0.2.6)
|
nanaimo (~> 0.3.0)
|
||||||
|
rexml (~> 3.2.4)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
arm64-darwin-21
|
||||||
|
arm64-darwin-22
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
cocoapods
|
cocoapods
|
||||||
|
@ -143,4 +159,4 @@ DEPENDENCIES
|
||||||
jazzy
|
jazzy
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.0.2
|
2.4.12
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
module(
|
||||||
|
name = "swiftlint",
|
||||||
|
version = "0.52.2",
|
||||||
|
compatibility_level = 1,
|
||||||
|
repo_name = "SwiftLint",
|
||||||
|
)
|
||||||
|
|
||||||
|
bazel_dep(name = "bazel_skylib", version = "1.4.1", dev_dependency = True)
|
||||||
|
bazel_dep(name = "platforms", version = "0.0.6")
|
||||||
|
bazel_dep(name = "rules_apple", version = "2.3.0", repo_name = "build_bazel_rules_apple")
|
||||||
|
bazel_dep(name = "rules_swift", version = "1.8.0", repo_name = "build_bazel_rules_swift")
|
||||||
|
bazel_dep(name = "rules_xcodeproj", version = "1.7.0")
|
||||||
|
bazel_dep(name = "sourcekitten", version = "0.34.1", repo_name = "com_github_jpsim_sourcekitten")
|
||||||
|
bazel_dep(name = "swift_argument_parser", version = "1.2.1", repo_name = "sourcekitten_com_github_apple_swift_argument_parser")
|
||||||
|
bazel_dep(name = "yams", version = "5.0.5", repo_name = "sourcekitten_com_github_jpsim_yams")
|
||||||
|
|
||||||
|
swiftlint_repos = use_extension("//bazel:repos.bzl", "swiftlint_repos_bzlmod")
|
||||||
|
use_repo(
|
||||||
|
swiftlint_repos,
|
||||||
|
"SwiftSyntax",
|
||||||
|
"com_github_johnsundell_collectionconcurrencykit",
|
||||||
|
"com_github_krzyzanowskim_cryptoswift",
|
||||||
|
"swiftlint_com_github_scottrhoyt_swifty_text_table",
|
||||||
|
)
|
||||||
|
|
||||||
|
extra_rules = use_extension("//bazel:extensions.bzl", "extra_rules")
|
||||||
|
use_repo(extra_rules, "swiftlint_extra_rules")
|
133
Makefile
133
Makefile
|
@ -2,21 +2,16 @@ TEMPORARY_FOLDER?=/tmp/SwiftLint.dst
|
||||||
PREFIX?=/usr/local
|
PREFIX?=/usr/local
|
||||||
BUILD_TOOL?=xcodebuild
|
BUILD_TOOL?=xcodebuild
|
||||||
|
|
||||||
XCODEFLAGS=-workspace 'SwiftLint.xcworkspace' \
|
XCODEFLAGS=-scheme 'swiftlint' \
|
||||||
-scheme 'swiftlint' \
|
|
||||||
DSTROOT=$(TEMPORARY_FOLDER) \
|
DSTROOT=$(TEMPORARY_FOLDER) \
|
||||||
OTHER_LDFLAGS=-Wl,-headerpad_max_install_names
|
OTHER_LDFLAGS=-Wl,-headerpad_max_install_names
|
||||||
|
|
||||||
SWIFT_BUILD_FLAGS=--configuration release
|
SWIFT_BUILD_FLAGS=--configuration release -Xlinker -dead_strip
|
||||||
UNAME=$(shell uname)
|
|
||||||
ifeq ($(UNAME), Darwin)
|
|
||||||
USE_SWIFT_STATIC_STDLIB:=$(shell test -d $$(dirname $$(xcrun --find swift))/../lib/swift_static/macosx && echo yes)
|
|
||||||
ifeq ($(USE_SWIFT_STATIC_STDLIB), yes)
|
|
||||||
SWIFT_BUILD_FLAGS+= -Xswiftc -static-stdlib
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
|
|
||||||
SWIFTLINT_EXECUTABLE=$(shell swift build $(SWIFT_BUILD_FLAGS) --show-bin-path)/swiftlint
|
SWIFTLINT_EXECUTABLE_PARENT=.build/universal
|
||||||
|
SWIFTLINT_EXECUTABLE=$(SWIFTLINT_EXECUTABLE_PARENT)/swiftlint
|
||||||
|
|
||||||
|
ARTIFACT_BUNDLE_PATH=$(TEMPORARY_FOLDER)/SwiftLintBinary.artifactbundle
|
||||||
|
|
||||||
TSAN_LIB=$(subst bin/swift,lib/swift/clang/lib/darwin/libclang_rt.tsan_osx_dynamic.dylib,$(shell xcrun --find swift))
|
TSAN_LIB=$(subst bin/swift,lib/swift/clang/lib/darwin/libclang_rt.tsan_osx_dynamic.dylib,$(shell xcrun --find swift))
|
||||||
TSAN_SWIFT_BUILD_FLAGS=-Xswiftc -sanitize=thread
|
TSAN_SWIFT_BUILD_FLAGS=-Xswiftc -sanitize=thread
|
||||||
|
@ -29,41 +24,35 @@ LICENSE_PATH="$(shell pwd)/LICENSE"
|
||||||
|
|
||||||
OUTPUT_PACKAGE=SwiftLint.pkg
|
OUTPUT_PACKAGE=SwiftLint.pkg
|
||||||
|
|
||||||
SWIFTLINT_PLIST=Source/swiftlint/Supporting Files/Info.plist
|
VERSION_STRING=$(shell ./tools/get-version)
|
||||||
SWIFTLINTFRAMEWORK_PLIST=Source/SwiftLintFramework/Supporting Files/Info.plist
|
|
||||||
|
|
||||||
VERSION_STRING=$(shell /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$(SWIFTLINT_PLIST)")
|
.PHONY: all clean build install package test uninstall docs
|
||||||
|
|
||||||
.PHONY: all bootstrap clean build install package test uninstall docs
|
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
sourcery: Source/SwiftLintFramework/Models/MasterRuleList.swift Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift Tests/LinuxMain.swift
|
sourcery: Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift Source/SwiftLintCore/Models/ReportersList.swift Tests/GeneratedTests/GeneratedTests.swift
|
||||||
|
|
||||||
Tests/LinuxMain.swift: Tests/*/*.swift .sourcery/LinuxMain.stencil
|
Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift: Source/SwiftLintBuiltInRules/Rules/**/*.swift .sourcery/BuiltInRules.stencil
|
||||||
sourcery --sources Tests --exclude-sources Tests/SwiftLintFrameworkTests/Resources --templates .sourcery/LinuxMain.stencil --output .sourcery --force-parse generated
|
./tools/sourcery --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/BuiltInRules.stencil --output .sourcery
|
||||||
mv .sourcery/LinuxMain.generated.swift Tests/LinuxMain.swift
|
mv .sourcery/BuiltInRules.generated.swift Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift
|
||||||
|
|
||||||
Source/SwiftLintFramework/Models/MasterRuleList.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/MasterRuleList.stencil
|
Source/SwiftLintCore/Models/ReportersList.swift: Source/SwiftLintCore/Reporters/*.swift .sourcery/ReportersList.stencil
|
||||||
sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/MasterRuleList.stencil --output .sourcery
|
./tools/sourcery --sources Source/SwiftLintCore/Reporters --templates .sourcery/ReportersList.stencil --output .sourcery
|
||||||
mv .sourcery/MasterRuleList.generated.swift Source/SwiftLintFramework/Models/MasterRuleList.swift
|
mv .sourcery/ReportersList.generated.swift Source/SwiftLintCore/Models/ReportersList.swift
|
||||||
|
|
||||||
Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/AutomaticRuleTests.stencil
|
Tests/GeneratedTests/GeneratedTests.swift: Source/SwiftLint*/Rules/**/*.swift .sourcery/GeneratedTests.stencil
|
||||||
sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/AutomaticRuleTests.stencil --output .sourcery
|
./tools/sourcery --sources Source/SwiftLintCore/Rules --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/GeneratedTests.stencil --output .sourcery
|
||||||
mv .sourcery/AutomaticRuleTests.generated.swift Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift
|
mv .sourcery/GeneratedTests.generated.swift Tests/GeneratedTests/GeneratedTests.swift
|
||||||
|
|
||||||
bootstrap:
|
test: clean_xcode
|
||||||
script/bootstrap
|
|
||||||
|
|
||||||
test: clean_xcode bootstrap
|
|
||||||
$(BUILD_TOOL) $(XCODEFLAGS) test
|
$(BUILD_TOOL) $(XCODEFLAGS) test
|
||||||
|
|
||||||
test_tsan:
|
test_tsan:
|
||||||
swift build --build-tests $(TSAN_SWIFT_BUILD_FLAGS)
|
swift build --build-tests $(TSAN_SWIFT_BUILD_FLAGS)
|
||||||
DYLD_INSERT_LIBRARIES=$(TSAN_LIB) $(TSAN_XCTEST) $(TSAN_TEST_BUNDLE)
|
DYLD_INSERT_LIBRARIES=$(TSAN_LIB) $(TSAN_XCTEST) $(TSAN_TEST_BUNDLE)
|
||||||
|
|
||||||
write_xcodebuild_log: bootstrap
|
write_xcodebuild_log:
|
||||||
xcodebuild -workspace SwiftLint.xcworkspace -scheme swiftlint clean build-for-testing > xcodebuild.log
|
xcodebuild -scheme swiftlint clean build-for-testing -destination "platform=macOS" > xcodebuild.log
|
||||||
|
|
||||||
analyze: write_xcodebuild_log
|
analyze: write_xcodebuild_log
|
||||||
swift run -c release swiftlint analyze --strict --compiler-log-path xcodebuild.log
|
swift run -c release swiftlint analyze --strict --compiler-log-path xcodebuild.log
|
||||||
|
@ -74,14 +63,19 @@ analyze_autocorrect: write_xcodebuild_log
|
||||||
clean:
|
clean:
|
||||||
rm -f "$(OUTPUT_PACKAGE)"
|
rm -f "$(OUTPUT_PACKAGE)"
|
||||||
rm -rf "$(TEMPORARY_FOLDER)"
|
rm -rf "$(TEMPORARY_FOLDER)"
|
||||||
rm -f "./portable_swiftlint.zip"
|
rm -f "./*.zip" "bazel.tar.gz" "bazel.tar.gz.sha256"
|
||||||
swift package clean
|
swift package clean
|
||||||
|
|
||||||
clean_xcode:
|
clean_xcode:
|
||||||
$(BUILD_TOOL) $(XCODEFLAGS) -configuration Test clean
|
$(BUILD_TOOL) $(XCODEFLAGS) -configuration Test clean
|
||||||
|
|
||||||
build:
|
build:
|
||||||
swift build $(SWIFT_BUILD_FLAGS)
|
mkdir -p "$(SWIFTLINT_EXECUTABLE_PARENT)"
|
||||||
|
bazel build --config release universal_swiftlint
|
||||||
|
$(eval SWIFTLINT_BINARY := $(shell bazel cquery --config release --output=files universal_swiftlint))
|
||||||
|
mv "$(SWIFTLINT_BINARY)" "$(SWIFTLINT_EXECUTABLE)"
|
||||||
|
chmod +w "$(SWIFTLINT_EXECUTABLE)"
|
||||||
|
strip -rSTX "$(SWIFTLINT_EXECUTABLE)"
|
||||||
|
|
||||||
build_with_disable_sandbox:
|
build_with_disable_sandbox:
|
||||||
swift build --disable-sandbox $(SWIFT_BUILD_FLAGS)
|
swift build --disable-sandbox $(SWIFT_BUILD_FLAGS)
|
||||||
|
@ -107,25 +101,50 @@ portable_zip: installables
|
||||||
cp -f "$(LICENSE_PATH)" "$(TEMPORARY_FOLDER)"
|
cp -f "$(LICENSE_PATH)" "$(TEMPORARY_FOLDER)"
|
||||||
(cd "$(TEMPORARY_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./portable_swiftlint.zip"
|
(cd "$(TEMPORARY_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./portable_swiftlint.zip"
|
||||||
|
|
||||||
package: installables
|
spm_artifactbundle_macos: installables
|
||||||
|
mkdir -p "$(ARTIFACT_BUNDLE_PATH)/swiftlint-$(VERSION_STRING)-macos/bin"
|
||||||
|
sed 's/__VERSION__/$(VERSION_STRING)/g' tools/info-macos.json.template > "$(ARTIFACT_BUNDLE_PATH)/info.json"
|
||||||
|
cp -f "$(SWIFTLINT_EXECUTABLE)" "$(ARTIFACT_BUNDLE_PATH)/swiftlint-$(VERSION_STRING)-macos/bin"
|
||||||
|
cp -f "$(LICENSE_PATH)" "$(ARTIFACT_BUNDLE_PATH)"
|
||||||
|
(cd "$(TEMPORARY_FOLDER)"; zip -yr - "SwiftLintBinary.artifactbundle") > "./SwiftLintBinary-macos.artifactbundle.zip"
|
||||||
|
|
||||||
|
zip_linux: docker_image
|
||||||
|
$(eval TMP_FOLDER := $(shell mktemp -d))
|
||||||
|
docker run swiftlint cat /usr/bin/swiftlint > "$(TMP_FOLDER)/swiftlint"
|
||||||
|
chmod +x "$(TMP_FOLDER)/swiftlint"
|
||||||
|
cp -f "$(LICENSE_PATH)" "$(TMP_FOLDER)"
|
||||||
|
(cd "$(TMP_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./swiftlint_linux.zip"
|
||||||
|
|
||||||
|
zip_linux_release:
|
||||||
|
$(eval TMP_FOLDER := $(shell mktemp -d))
|
||||||
|
docker run --platform linux/amd64 "ghcr.io/realm/swiftlint:$(VERSION_STRING)" cat /usr/bin/swiftlint > "$(TMP_FOLDER)/swiftlint"
|
||||||
|
chmod +x "$(TMP_FOLDER)/swiftlint"
|
||||||
|
cp -f "$(LICENSE_PATH)" "$(TMP_FOLDER)"
|
||||||
|
(cd "$(TMP_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./swiftlint_linux.zip"
|
||||||
|
gh release upload "$(VERSION_STRING)" swiftlint_linux.zip
|
||||||
|
|
||||||
|
package: build
|
||||||
|
$(eval PACKAGE_ROOT := $(shell mktemp -d))
|
||||||
|
cp "$(SWIFTLINT_EXECUTABLE)" "$(PACKAGE_ROOT)"
|
||||||
pkgbuild \
|
pkgbuild \
|
||||||
--identifier "io.realm.swiftlint" \
|
--identifier "io.realm.swiftlint" \
|
||||||
--install-location "/" \
|
--install-location "/usr/local/bin" \
|
||||||
--root "$(TEMPORARY_FOLDER)" \
|
--root "$(PACKAGE_ROOT)" \
|
||||||
--version "$(VERSION_STRING)" \
|
--version "$(VERSION_STRING)" \
|
||||||
"$(OUTPUT_PACKAGE)"
|
"$(OUTPUT_PACKAGE)"
|
||||||
|
|
||||||
archive:
|
bazel_release:
|
||||||
carthage build --no-skip-current --platform mac
|
bazel build :release
|
||||||
carthage archive SwiftLintFramework
|
mv bazel-bin/bazel.tar.gz bazel-bin/bazel.tar.gz.sha256 .
|
||||||
|
|
||||||
release: package archive portable_zip
|
docker_image:
|
||||||
|
docker build --platform linux/amd64 --force-rm --tag swiftlint .
|
||||||
|
|
||||||
docker_test:
|
docker_test:
|
||||||
docker run -v `pwd`:`pwd` -w `pwd` --name swiftlint --rm norionomura/swift:51 swift test --parallel
|
docker run -v `pwd`:`pwd` -w `pwd` --name swiftlint --rm swift:5.7-focal swift test --parallel
|
||||||
|
|
||||||
docker_htop:
|
docker_htop:
|
||||||
docker run -it --rm --pid=container:swiftlint terencewestphal/htop || reset
|
docker run --platform linux/amd64 -it --rm --pid=container:swiftlint terencewestphal/htop || reset
|
||||||
|
|
||||||
# https://irace.me/swift-profiling
|
# https://irace.me/swift-profiling
|
||||||
display_compilation_time:
|
display_compilation_time:
|
||||||
|
@ -133,8 +152,8 @@ display_compilation_time:
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
brew update && brew bump-formula-pr --tag=$(shell git describe --tags) --revision=$(shell git rev-parse HEAD) swiftlint
|
brew update && brew bump-formula-pr --tag=$(shell git describe --tags) --revision=$(shell git rev-parse HEAD) swiftlint
|
||||||
pod trunk push SwiftLintFramework.podspec
|
bundle install
|
||||||
pod trunk push SwiftLint.podspec
|
bundle exec pod trunk push SwiftLint.podspec
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
swift run swiftlint generate-docs
|
swift run swiftlint generate-docs
|
||||||
|
@ -142,22 +161,32 @@ docs:
|
||||||
bundle exec jazzy
|
bundle exec jazzy
|
||||||
|
|
||||||
get_version:
|
get_version:
|
||||||
@echo $(VERSION_STRING)
|
@echo "$(VERSION_STRING)"
|
||||||
|
|
||||||
push_version:
|
release:
|
||||||
ifneq ($(strip $(shell git status --untracked-files=no --porcelain 2>/dev/null)),)
|
ifneq ($(strip $(shell git status --untracked-files=no --porcelain 2>/dev/null)),)
|
||||||
$(error git state is not clean)
|
$(error git state is not clean)
|
||||||
endif
|
endif
|
||||||
$(eval NEW_VERSION_AND_NAME := $(filter-out $@,$(MAKECMDGOALS)))
|
$(eval NEW_VERSION_AND_NAME := $(filter-out $@,$(MAKECMDGOALS)))
|
||||||
$(eval NEW_VERSION := $(shell echo $(NEW_VERSION_AND_NAME) | sed 's/:.*//' ))
|
$(eval NEW_VERSION := $(shell echo $(NEW_VERSION_AND_NAME) | sed 's/:.*//' ))
|
||||||
@sed -i '' 's/## Master/## $(NEW_VERSION_AND_NAME)/g' CHANGELOG.md
|
@sed -i '' 's/## Main/## $(NEW_VERSION_AND_NAME)/g' CHANGELOG.md
|
||||||
@sed 's/__VERSION__/$(NEW_VERSION)/g' script/Version.swift.template > Source/SwiftLintFramework/Models/Version.swift
|
@sed 's/__VERSION__/$(NEW_VERSION)/g' tools/Version.swift.template > Source/SwiftLintCore/Models/Version.swift
|
||||||
@/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $(NEW_VERSION)" "$(SWIFTLINTFRAMEWORK_PLIST)"
|
@sed -e '3s/.*/ version = "$(NEW_VERSION)",/' -i '' MODULE.bazel
|
||||||
@/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $(NEW_VERSION)" "$(SWIFTLINT_PLIST)"
|
make clean
|
||||||
|
make package
|
||||||
|
make bazel_release
|
||||||
|
make portable_zip
|
||||||
|
make spm_artifactbundle_macos
|
||||||
|
./tools/update-artifact-bundle.sh "$(NEW_VERSION)"
|
||||||
git commit -a -m "release $(NEW_VERSION)"
|
git commit -a -m "release $(NEW_VERSION)"
|
||||||
git tag -a $(NEW_VERSION) -m "$(NEW_VERSION_AND_NAME)"
|
git tag -a $(NEW_VERSION) -m "$(NEW_VERSION_AND_NAME)"
|
||||||
git push origin master
|
git push origin HEAD
|
||||||
git push origin $(NEW_VERSION)
|
git push origin $(NEW_VERSION)
|
||||||
|
./tools/create-github-release.sh "$(NEW_VERSION)"
|
||||||
|
make publish
|
||||||
|
./tools/add-new-changelog-section.sh
|
||||||
|
git commit -a -m "Add new changelog section"
|
||||||
|
git push origin HEAD
|
||||||
|
|
||||||
%:
|
%:
|
||||||
@:
|
@:
|
||||||
|
|
168
Package.resolved
168
Package.resolved
|
@ -1,97 +1,77 @@
|
||||||
{
|
{
|
||||||
"object": {
|
"pins" : [
|
||||||
"pins": [
|
{
|
||||||
{
|
"identity" : "collectionconcurrencykit",
|
||||||
"package": "Commandant",
|
"kind" : "remoteSourceControl",
|
||||||
"repositoryURL": "https://github.com/Carthage/Commandant.git",
|
"location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git",
|
||||||
"state": {
|
"state" : {
|
||||||
"branch": null,
|
"revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95",
|
||||||
"revision": "ab68611013dec67413628ac87c1f29e8427bc8e4",
|
"version" : "0.2.0"
|
||||||
"version": "0.17.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "CwlCatchException",
|
|
||||||
"repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "7cd2f8cacc4d22f21bc0b2309c3b18acf7957b66",
|
|
||||||
"version": "1.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "CwlPreconditionTesting",
|
|
||||||
"repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "c228db5d2ad1b01ebc84435e823e6cca4e3db98b",
|
|
||||||
"version": "1.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "Nimble",
|
|
||||||
"repositoryURL": "https://github.com/Quick/Nimble.git",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "b02b00b30b6353632aa4a5fb6124f8147f7140c0",
|
|
||||||
"version": "8.0.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "Quick",
|
|
||||||
"repositoryURL": "https://github.com/Quick/Quick.git",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "33682c2f6230c60614861dfc61df267e11a1602f",
|
|
||||||
"version": "2.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "SourceKitten",
|
|
||||||
"repositoryURL": "https://github.com/jpsim/SourceKitten.git",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "77a4dbbb477a8110eb8765e3c44c70fb4929098f",
|
|
||||||
"version": "0.29.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "SwiftSyntax",
|
|
||||||
"repositoryURL": "https://github.com/apple/swift-syntax.git",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "3e3eb191fcdbecc6031522660c4ed6ce25282c25",
|
|
||||||
"version": "0.50100.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "SwiftyTextTable",
|
|
||||||
"repositoryURL": "https://github.com/scottrhoyt/SwiftyTextTable.git",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3",
|
|
||||||
"version": "0.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "SWXMLHash",
|
|
||||||
"repositoryURL": "https://github.com/drmohundro/SWXMLHash.git",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "a4931e5c3bafbedeb1601d3bb76bbe835c6d475a",
|
|
||||||
"version": "5.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "Yams",
|
|
||||||
"repositoryURL": "https://github.com/jpsim/Yams.git",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "c947a306d2e80ecb2c0859047b35c73b8e1ca27f",
|
|
||||||
"version": "2.0.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
},
|
{
|
||||||
"version": 1
|
"identity" : "cryptoswift",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "32f641cf24fc7abc1c591a2025e9f2f572648b0f",
|
||||||
|
"version" : "1.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sourcekitten",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/jpsim/SourceKitten.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56",
|
||||||
|
"version" : "0.34.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-argument-parser",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-argument-parser.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4ad606ba5d7673ea60679a61ff867cc1ff8c8e86",
|
||||||
|
"version" : "1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-syntax",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-syntax.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "165fc6d22394c1168ff76ab5d951245971ef07e5",
|
||||||
|
"version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-05-a"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftytexttable",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/scottrhoyt/SwiftyTextTable.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3",
|
||||||
|
"version" : "0.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swxmlhash",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/drmohundro/SWXMLHash.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4d0f62f561458cbe1f732171e625f03195151b60",
|
||||||
|
"version" : "7.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "yams",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/jpsim/Yams.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "f47ba4838c30dbd59998a4e4c87ab620ff959e8a",
|
||||||
|
"version" : "5.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
}
|
}
|
||||||
|
|
120
Package.swift
120
Package.swift
|
@ -1,59 +1,121 @@
|
||||||
// swift-tools-version:5.0
|
// swift-tools-version:5.7
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
#if canImport(CommonCrypto)
|
|
||||||
private let addCryptoSwift = false
|
|
||||||
#else
|
|
||||||
private let addCryptoSwift = true
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if compiler(>=5.1.0)
|
|
||||||
private let addSwiftSyntax = true
|
|
||||||
#else
|
|
||||||
private let addSwiftSyntax = false
|
|
||||||
#endif
|
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "SwiftLint",
|
name: "SwiftLint",
|
||||||
platforms: [
|
platforms: [.macOS(.v12)],
|
||||||
.macOS(.v10_12)
|
|
||||||
],
|
|
||||||
products: [
|
products: [
|
||||||
.executable(name: "swiftlint", targets: ["swiftlint"]),
|
.executable(name: "swiftlint", targets: ["swiftlint"]),
|
||||||
.library(name: "SwiftLintFramework", targets: ["SwiftLintFramework"])
|
.library(name: "SwiftLintFramework", targets: ["SwiftLintFramework"]),
|
||||||
|
.plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"])
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/Carthage/Commandant.git", .upToNextMinor(from: "0.17.0")),
|
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.2.1")),
|
||||||
.package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMinor(from: "0.29.0")),
|
.package(url: "https://github.com/apple/swift-syntax.git", exact: "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-05-a"),
|
||||||
.package(url: "https://github.com/jpsim/Yams.git", from: "2.0.0"),
|
.package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMinor(from: "0.34.1")),
|
||||||
|
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.5"),
|
||||||
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
|
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
|
||||||
] + (addCryptoSwift ? [.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.0.0"))] : []) +
|
.package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"),
|
||||||
(addSwiftSyntax ? [.package(url: "https://github.com/apple/swift-syntax.git", .exact("0.50100.0"))] : []),
|
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.7.2"))
|
||||||
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.plugin(
|
||||||
|
name: "SwiftLintPlugin",
|
||||||
|
capability: .buildTool(),
|
||||||
|
dependencies: [
|
||||||
|
.target(name: "SwiftLintBinary", condition: .when(platforms: [.macOS])),
|
||||||
|
.target(name: "swiftlint", condition: .when(platforms: [.linux]))
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.executableTarget(
|
||||||
name: "swiftlint",
|
name: "swiftlint",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"Commandant",
|
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||||
|
"CollectionConcurrencyKit",
|
||||||
"SwiftLintFramework",
|
"SwiftLintFramework",
|
||||||
"SwiftyTextTable",
|
"SwiftyTextTable",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "CLITests",
|
||||||
|
dependencies: [
|
||||||
|
"swiftlint"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "SwiftLintCore",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "CryptoSwift", package: "CryptoSwift", condition: .when(platforms: [.linux])),
|
||||||
|
.target(name: "DyldWarningWorkaround", condition: .when(platforms: [.macOS])),
|
||||||
|
.product(name: "SourceKittenFramework", package: "SourceKitten"),
|
||||||
|
.product(name: "SwiftIDEUtils", package: "swift-syntax"),
|
||||||
|
.product(name: "SwiftOperators", package: "swift-syntax"),
|
||||||
|
.product(name: "SwiftParser", package: "swift-syntax"),
|
||||||
|
.product(name: "SwiftSyntax", package: "swift-syntax"),
|
||||||
|
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
|
||||||
|
.product(name: "SwiftyTextTable", package: "SwiftyTextTable"),
|
||||||
|
.product(name: "Yams", package: "Yams"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "SwiftLintBuiltInRules",
|
||||||
|
dependencies: ["SwiftLintCore"]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "SwiftLintExtraRules",
|
||||||
|
dependencies: ["SwiftLintCore"]
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SwiftLintFramework",
|
name: "SwiftLintFramework",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"SourceKittenFramework",
|
"SwiftLintBuiltInRules",
|
||||||
"Yams",
|
"SwiftLintCore",
|
||||||
] + (addCryptoSwift ? ["CryptoSwift"] : []) +
|
"SwiftLintExtraRules"
|
||||||
(addSwiftSyntax ? ["SwiftSyntax"] : [])
|
]
|
||||||
|
),
|
||||||
|
.target(name: "DyldWarningWorkaround"),
|
||||||
|
.target(
|
||||||
|
name: "SwiftLintTestHelpers",
|
||||||
|
dependencies: [
|
||||||
|
"SwiftLintFramework"
|
||||||
|
],
|
||||||
|
path: "Tests/SwiftLintTestHelpers"
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "SwiftLintFrameworkTests",
|
name: "SwiftLintFrameworkTests",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"SwiftLintFramework"
|
"SwiftLintFramework",
|
||||||
|
"SwiftLintTestHelpers"
|
||||||
],
|
],
|
||||||
exclude: [
|
exclude: [
|
||||||
"Resources",
|
"Resources",
|
||||||
]
|
]
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "GeneratedTests",
|
||||||
|
dependencies: [
|
||||||
|
"SwiftLintFramework",
|
||||||
|
"SwiftLintTestHelpers"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "IntegrationTests",
|
||||||
|
dependencies: [
|
||||||
|
"SwiftLintFramework",
|
||||||
|
"SwiftLintTestHelpers"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "ExtraRulesTests",
|
||||||
|
dependencies: [
|
||||||
|
"SwiftLintFramework",
|
||||||
|
"SwiftLintTestHelpers"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.binaryTarget(
|
||||||
|
name: "SwiftLintBinary",
|
||||||
|
url: "https://github.com/realm/SwiftLint/releases/download/0.52.2/SwiftLintBinary-macos.artifactbundle.zip",
|
||||||
|
checksum: "89651e1c87fb62faf076ef785a5b1af7f43570b2b74c6773526e0d5114e0578e"
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import Foundation
|
||||||
|
import PackagePlugin
|
||||||
|
|
||||||
|
#if os(Linux)
|
||||||
|
import Glibc
|
||||||
|
#else
|
||||||
|
import Darwin
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extension Path {
|
||||||
|
/// Scans the receiver, then all of its parents looking for a configuration file with the name ".swiftlint.yml".
|
||||||
|
///
|
||||||
|
/// - returns: Path to the configuration file, or nil if one cannot be found.
|
||||||
|
func firstConfigurationFileInParentDirectories() -> Path? {
|
||||||
|
let defaultConfigurationFileName = ".swiftlint.yml"
|
||||||
|
let proposedDirectory = sequence(
|
||||||
|
first: self,
|
||||||
|
next: { path in
|
||||||
|
guard path.stem.count > 1 else {
|
||||||
|
// Check we're not at the root of this filesystem, as `removingLastComponent()`
|
||||||
|
// will continually return the root from itself.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.removingLastComponent()
|
||||||
|
}
|
||||||
|
).first { path in
|
||||||
|
let potentialConfigurationFile = path.appending(subpath: defaultConfigurationFileName)
|
||||||
|
return potentialConfigurationFile.isAccessible()
|
||||||
|
}
|
||||||
|
return proposedDirectory?.appending(subpath: defaultConfigurationFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe way to check if the file is accessible from within the current process sandbox.
|
||||||
|
private func isAccessible() -> Bool {
|
||||||
|
let result = string.withCString { pointer in
|
||||||
|
access(pointer, R_OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result == 0
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import Foundation
|
||||||
|
import PackagePlugin
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct SwiftLintPlugin: BuildToolPlugin {
|
||||||
|
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
|
||||||
|
guard let sourceTarget = target as? SourceModuleTarget else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return createBuildCommands(
|
||||||
|
inputFiles: sourceTarget.sourceFiles(withSuffix: "swift").map(\.path),
|
||||||
|
packageDirectory: context.package.directory,
|
||||||
|
workingDirectory: context.pluginWorkDirectory,
|
||||||
|
tool: try context.tool(named: "swiftlint")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createBuildCommands(
|
||||||
|
inputFiles: [Path],
|
||||||
|
packageDirectory: Path,
|
||||||
|
workingDirectory: Path,
|
||||||
|
tool: PluginContext.Tool
|
||||||
|
) -> [Command] {
|
||||||
|
if inputFiles.isEmpty {
|
||||||
|
// Don't lint anything if there are no Swift source files in this target
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
var arguments = [
|
||||||
|
"lint",
|
||||||
|
"--quiet",
|
||||||
|
// We always pass all of the Swift source files in the target to the tool,
|
||||||
|
// so we need to ensure that any exclusion rules in the configuration are
|
||||||
|
// respected.
|
||||||
|
"--force-exclude",
|
||||||
|
"--cache-path", "\(workingDirectory)"
|
||||||
|
]
|
||||||
|
|
||||||
|
// Manually look for configuration files, to avoid issues when the plugin does not execute our tool from the
|
||||||
|
// package source directory.
|
||||||
|
if let configuration = packageDirectory.firstConfigurationFileInParentDirectories() {
|
||||||
|
arguments.append(contentsOf: ["--config", "\(configuration.string)"])
|
||||||
|
}
|
||||||
|
arguments += inputFiles.map(\.string)
|
||||||
|
|
||||||
|
// We are not producing output files and this is needed only to not include cache files into bundle
|
||||||
|
let outputFilesDirectory = workingDirectory.appending("Output")
|
||||||
|
|
||||||
|
return [
|
||||||
|
.prebuildCommand(
|
||||||
|
displayName: "SwiftLint",
|
||||||
|
executable: tool.path,
|
||||||
|
arguments: arguments,
|
||||||
|
outputFilesDirectory: outputFilesDirectory
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(XcodeProjectPlugin)
|
||||||
|
import XcodeProjectPlugin
|
||||||
|
|
||||||
|
extension SwiftLintPlugin: XcodeBuildToolPlugin {
|
||||||
|
func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] {
|
||||||
|
let inputFilePaths = target.inputFiles
|
||||||
|
.filter { $0.type == .source && $0.path.extension == "swift" }
|
||||||
|
.map(\.path)
|
||||||
|
return createBuildCommands(
|
||||||
|
inputFiles: inputFilePaths,
|
||||||
|
packageDirectory: context.xcodeProject.directory,
|
||||||
|
workingDirectory: context.pluginWorkDirectory,
|
||||||
|
tool: try context.tool(named: "swiftlint")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
534
README.md
534
README.md
|
@ -1,15 +1,14 @@
|
||||||
# SwiftLint
|
# SwiftLint
|
||||||
|
|
||||||
A tool to enforce Swift style and conventions, loosely based on
|
A tool to enforce Swift style and conventions, loosely based on the now archived [GitHub Swift Style Guide](https://github.com/github/swift-style-guide). SwiftLint enforces the style guide rules that are generally accepted by the Swift community. These rules are well described in popular style guides like [Kodeco's Swift Style Guide](https://github.com/kodecocodes/swift-style-guide).
|
||||||
[GitHub's Swift Style Guide](https://github.com/github/swift-style-guide).
|
|
||||||
|
|
||||||
SwiftLint hooks into [Clang](http://clang.llvm.org) and
|
SwiftLint hooks into [Clang](http://clang.llvm.org) and
|
||||||
[SourceKit](http://www.jpsim.com/uncovering-sourcekit) to use the
|
[SourceKit](http://www.jpsim.com/uncovering-sourcekit) to use the
|
||||||
[AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) representation
|
[AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) representation
|
||||||
of your source files for more accurate results.
|
of your source files for more accurate results.
|
||||||
|
|
||||||
[](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=master)
|
[](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=main)
|
||||||
[](https://codecov.io/github/realm/SwiftLint?branch=master)
|
[](https://codecov.io/github/realm/SwiftLint?branch=main)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -17,7 +16,7 @@ This project adheres to the [Contributor Covenant Code of Conduct](https://realm
|
||||||
By participating, you are expected to uphold this code. Please report
|
By participating, you are expected to uphold this code. Please report
|
||||||
unacceptable behavior to [info@realm.io](mailto:info@realm.io).
|
unacceptable behavior to [info@realm.io](mailto:info@realm.io).
|
||||||
|
|
||||||
> Language Switch: [中文](https://github.com/realm/SwiftLint/blob/master/README_CN.md), [한국어](https://github.com/realm/SwiftLint/blob/master/README_KR.md).
|
> Language Switch: [中文](https://github.com/realm/SwiftLint/blob/main/README_CN.md), [한국어](https://github.com/realm/SwiftLint/blob/main/README_KR.md).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -42,7 +41,7 @@ in your Script Build Phases.
|
||||||
This is the recommended way to install a specific version of SwiftLint since it supports
|
This is the recommended way to install a specific version of SwiftLint since it supports
|
||||||
installing a pinned version rather than simply the latest (which is the case with Homebrew).
|
installing a pinned version rather than simply the latest (which is the case with Homebrew).
|
||||||
|
|
||||||
Note that this will add the SwiftLint binaries, its dependencies' binaries and the Swift binary
|
Note that this will add the SwiftLint binaries, its dependencies' binaries, and the Swift binary
|
||||||
library distribution to the `Pods/` directory, so checking in this directory to SCM such as
|
library distribution to the `Pods/` directory, so checking in this directory to SCM such as
|
||||||
git is discouraged.
|
git is discouraged.
|
||||||
|
|
||||||
|
@ -58,22 +57,77 @@ You can also install SwiftLint by downloading `SwiftLint.pkg` from the
|
||||||
[latest GitHub release](https://github.com/realm/SwiftLint/releases/latest) and
|
[latest GitHub release](https://github.com/realm/SwiftLint/releases/latest) and
|
||||||
running it.
|
running it.
|
||||||
|
|
||||||
### Compiling from source:
|
### Installing from source:
|
||||||
|
|
||||||
You can also build from source by cloning this project and running
|
You can also build and install from source by cloning this project and running
|
||||||
`script/bootstrap; make install` (Xcode 11.0 or later).
|
`make install` (Xcode 13.3 or later).
|
||||||
|
|
||||||
### Known Installation Issues On MacOS Before 10.14.4
|
### Using Bazel
|
||||||
|
|
||||||
Starting with [SwiftLint 0.32.0](https://github.com/realm/SwiftLint/releases/tag/0.32.0), if you get
|
Put this in your `MODULE.bazel`:
|
||||||
an error similar to `dyld: Symbol not found: _$s11SubSequenceSlTl` when running SwiftLint,
|
|
||||||
you'll need to install the [Swift 5 Runtime Support for Command Line Tools](https://support.apple.com/kb/DL1998).
|
|
||||||
|
|
||||||
Alternatively, you can:
|
```bzl
|
||||||
|
bazel_dep(name = "swiftlint", version = "0.50.4", repo_name = "SwiftLint")
|
||||||
|
```
|
||||||
|
|
||||||
* Update to macOS 10.14.4 or later
|
Or put this in your `WORKSPACE`:
|
||||||
* Install Xcode 10.2 or later at `/Applications/Xcode.app`
|
|
||||||
* Rebuild SwiftLint from source using Xcode 10.2 or later
|
<details>
|
||||||
|
|
||||||
|
<summary>WORKSPACE</summary>
|
||||||
|
|
||||||
|
```bzl
|
||||||
|
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||||
|
|
||||||
|
http_archive(
|
||||||
|
name = "build_bazel_rules_apple",
|
||||||
|
sha256 = "f94e6dddf74739ef5cb30f000e13a2a613f6ebfa5e63588305a71fce8a8a9911",
|
||||||
|
url = "https://github.com/bazelbuild/rules_apple/releases/download/1.1.3/rules_apple.1.1.3.tar.gz",
|
||||||
|
)
|
||||||
|
|
||||||
|
load(
|
||||||
|
"@build_bazel_rules_apple//apple:repositories.bzl",
|
||||||
|
"apple_rules_dependencies",
|
||||||
|
)
|
||||||
|
|
||||||
|
apple_rules_dependencies()
|
||||||
|
|
||||||
|
load(
|
||||||
|
"@build_bazel_rules_swift//swift:repositories.bzl",
|
||||||
|
"swift_rules_dependencies",
|
||||||
|
)
|
||||||
|
|
||||||
|
swift_rules_dependencies()
|
||||||
|
|
||||||
|
load(
|
||||||
|
"@build_bazel_rules_swift//swift:extras.bzl",
|
||||||
|
"swift_rules_extra_dependencies",
|
||||||
|
)
|
||||||
|
|
||||||
|
swift_rules_extra_dependencies()
|
||||||
|
|
||||||
|
http_archive(
|
||||||
|
name = "SwiftLint",
|
||||||
|
sha256 = "7c454ff4abeeecdd9513f6293238a6d9f803b587eb93de147f9aa1be0d8337c4",
|
||||||
|
url = "https://github.com/realm/SwiftLint/releases/download/0.49.1/bazel.tar.gz",
|
||||||
|
)
|
||||||
|
|
||||||
|
load("@SwiftLint//bazel:repos.bzl", "swiftlint_repos")
|
||||||
|
|
||||||
|
swiftlint_repos()
|
||||||
|
|
||||||
|
load("@SwiftLint//bazel:deps.bzl", "swiftlint_deps")
|
||||||
|
|
||||||
|
swiftlint_deps()
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
Then you can run SwiftLint in the current directory with this command:
|
||||||
|
|
||||||
|
```console
|
||||||
|
bazel run -c opt @SwiftLint//:swiftlint
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
@ -82,49 +136,113 @@ Alternatively, you can:
|
||||||
To get a high-level overview of recommended ways to integrate SwiftLint into your project,
|
To get a high-level overview of recommended ways to integrate SwiftLint into your project,
|
||||||
we encourage you to watch this presentation or read the transcript:
|
we encourage you to watch this presentation or read the transcript:
|
||||||
|
|
||||||
[](https://academy.realm.io/posts/slug-jp-simard-swiftlint/)
|
[](https://youtu.be/9Z1nTMTejqU)
|
||||||
|
|
||||||
### Xcode
|
### Xcode
|
||||||
|
|
||||||
Integrate SwiftLint into an Xcode scheme to get warnings and errors displayed
|
Integrate SwiftLint into your Xcode project to get warnings and errors displayed
|
||||||
in the IDE. Just add a new "Run Script Phase" with:
|
in the issue navigator.
|
||||||
|
|
||||||
|
To do this select the project in the file navigator, then select the primary app
|
||||||
|
target, and go to Build Phases. Click the + and select "New Run Script Phase".
|
||||||
|
Insert the following as the script:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If you installed SwiftLint via Homebrew on Apple Silicon, you might experience this warning:
|
||||||
|
|
||||||
|
> warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint
|
||||||
|
|
||||||
|
That is because Homebrew on Apple Silicon installs the binaries into the `/opt/homebrew/bin`
|
||||||
|
folder by default. To instruct Xcode where to find SwiftLint, you can either add
|
||||||
|
`/opt/homebrew/bin` to the `PATH` environment variable in your build phase
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
if which swiftlint >/dev/null; then
|
if [[ "$(uname -m)" == arm64 ]]; then
|
||||||
|
export PATH="/opt/homebrew/bin:$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if which swiftlint > /dev/null; then
|
||||||
swiftlint
|
swiftlint
|
||||||
else
|
else
|
||||||
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
|
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
|
||||||
fi
|
fi
|
||||||
```
|
```
|
||||||
|
|
||||||

|
or you can create a symbolic link in `/usr/local/bin` pointing to the actual binary:
|
||||||
|
|
||||||
Alternatively, if you've installed SwiftLint via CocoaPods the script should look like this:
|
```bash
|
||||||
|
ln -s /opt/homebrew/bin/swiftlint /usr/local/bin/swiftlint
|
||||||
|
```
|
||||||
|
|
||||||
|
You might want to move your SwiftLint phase directly before the 'Compile Sources'
|
||||||
|
step to detect errors quickly before compiling. However, SwiftLint is designed
|
||||||
|
to run on valid Swift code that cleanly completes the compiler's parsing stage.
|
||||||
|
So running SwiftLint before 'Compile Sources' might yield some incorrect
|
||||||
|
results.
|
||||||
|
|
||||||
|
If you wish to fix violations as well, your script could run
|
||||||
|
`swiftlint --fix && swiftlint` instead of just `swiftlint`. This will mean
|
||||||
|
that all correctable violations are fixed while ensuring warnings show up in
|
||||||
|
your project for remaining violations.
|
||||||
|
|
||||||
|
If you've installed SwiftLint via CocoaPods the script should look like this:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
"${PODS_ROOT}/SwiftLint/swiftlint"
|
"${PODS_ROOT}/SwiftLint/swiftlint"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Format on Save Xcode Plugin
|
### Plug-in Support
|
||||||
|
|
||||||
To run `swiftlint autocorrect` on save in Xcode, install the
|
SwiftLint can be used as a build tool plug-in for both Xcode projects as well as
|
||||||
[SwiftLintXcode](https://github.com/ypresto/SwiftLintXcode) plugin from Alcatraz.
|
Swift packages.
|
||||||
|
|
||||||
⚠️This plugin will not work with Xcode 8 or later without disabling SIP.
|
> Due to limitations with Swift Package Manager Plug-ins this is only
|
||||||
This is not recommended.
|
recommended for projects that have a SwiftLint configuration in their root directory as
|
||||||
|
there is currently no way to pass any additional options to the SwiftLint executable.
|
||||||
|
|
||||||
### AppCode
|
#### Xcode
|
||||||
|
|
||||||
To integrate SwiftLint with AppCode, install
|
You can integrate SwiftLint as an Xcode Build Tool Plug-in if you're working
|
||||||
[this plugin](https://plugins.jetbrains.com/plugin/9175) and configure
|
with a project in Xcode.
|
||||||
SwiftLint's installed path in the plugin's preferences.
|
|
||||||
The `autocorrect` action is available via `⌥⏎`.
|
|
||||||
|
|
||||||
### Atom
|
Add SwiftLint as a package dependency to your project without linking any of the
|
||||||
|
products.
|
||||||
|
|
||||||
To integrate SwiftLint with [Atom](https://atom.io/), install the
|
Select the target you want to add linting to and open the `Build Phases` inspector.
|
||||||
[`linter-swiftlint`](https://atom.io/packages/linter-swiftlint) package from
|
Open `Run Build Tool Plug-ins` and select the `+` button.
|
||||||
APM.
|
Select `SwiftLintPlugin` from the list and add it to the project.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
For unattended use (e.g. on CI), you can disable the package validation dialog by
|
||||||
|
|
||||||
|
* individually passing `-skipPackagePluginValidation` to `xcodebuild` or
|
||||||
|
* globally setting `defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES`
|
||||||
|
for that user.
|
||||||
|
|
||||||
|
_Note: This implicitly trusts all Xcode package plugins and bypasses Xcode's package validation
|
||||||
|
dialogs, which has security implications._
|
||||||
|
|
||||||
|
#### Swift Package
|
||||||
|
|
||||||
|
You can integrate SwiftLint as a Swift Package Manager Plug-in if you're working with
|
||||||
|
a Swift Package with a `Package.swift` manifest.
|
||||||
|
|
||||||
|
Add SwiftLint as a package dependency to your `Package.swift` file.
|
||||||
|
Add SwiftLint to a target using the `plugins` parameter.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
.target(
|
||||||
|
...
|
||||||
|
plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")]
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Studio Code
|
||||||
|
|
||||||
|
To integrate SwiftLint with [vscode](https://code.visualstudio.com), install the
|
||||||
|
[`vscode-swiftlint`](https://marketplace.visualstudio.com/items?itemName=vknabel.vscode-swiftlint) extension from the marketplace.
|
||||||
|
|
||||||
### fastlane
|
### fastlane
|
||||||
|
|
||||||
|
@ -148,32 +266,65 @@ swiftlint(
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
`swiftlint` is also available as a [Docker](https://www.docker.com/) image using `Ubuntu`.
|
||||||
|
So just the first time you need to pull the docker image using the next command:
|
||||||
|
```bash
|
||||||
|
docker pull ghcr.io/realm/swiftlint:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Then following times, you just run `swiftlint` inside of the docker like:
|
||||||
|
```bash
|
||||||
|
docker run -it -v `pwd`:`pwd` -w `pwd` ghcr.io/realm/swiftlint:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
This will execute `swiftlint` in the folder where you are right now (`pwd`), showing an output like:
|
||||||
|
```bash
|
||||||
|
$ docker run -it -v `pwd`:`pwd` -w `pwd` ghcr.io/realm/swiftlint:latest
|
||||||
|
Linting Swift files in current working directory
|
||||||
|
Linting 'RuleDocumentation.swift' (1/490)
|
||||||
|
...
|
||||||
|
Linting 'YamlSwiftLintTests.swift' (490/490)
|
||||||
|
Done linting! Found 0 violations, 0 serious in 490 files.
|
||||||
|
```
|
||||||
|
|
||||||
|
Here you have more documentation about the usage of [Docker Images](https://docs.docker.com/).
|
||||||
|
|
||||||
### Command Line
|
### Command Line
|
||||||
|
|
||||||
```
|
```
|
||||||
$ swiftlint help
|
$ swiftlint help
|
||||||
Available commands:
|
OVERVIEW: A tool to enforce Swift style and conventions.
|
||||||
|
|
||||||
analyze [Experimental] Run analysis rules
|
USAGE: swiftlint <subcommand>
|
||||||
autocorrect Automatically correct warnings and errors
|
|
||||||
generate-docs Generates markdown documentation for all rules
|
OPTIONS:
|
||||||
help Display general or command-specific help
|
--version Show the version.
|
||||||
lint Print lint warnings and errors (default command)
|
-h, --help Show help information.
|
||||||
rules Display the list of rules and their identifiers
|
|
||||||
version Display the current version of SwiftLint
|
SUBCOMMANDS:
|
||||||
|
analyze Run analysis rules
|
||||||
|
docs Open SwiftLint documentation website in the default web browser
|
||||||
|
generate-docs Generates markdown documentation for all rules
|
||||||
|
lint (default) Print lint warnings and errors
|
||||||
|
reporters Display the list of reporters and their identifiers
|
||||||
|
rules Display the list of rules and their identifiers
|
||||||
|
version Display the current version of SwiftLint
|
||||||
|
|
||||||
|
See 'swiftlint help <subcommand>' for detailed help.
|
||||||
```
|
```
|
||||||
|
|
||||||
Run `swiftlint` in the directory containing the Swift files to lint. Directories
|
Run `swiftlint` in the directory containing the Swift files to lint. Directories
|
||||||
will be searched recursively.
|
will be searched recursively.
|
||||||
|
|
||||||
To specify a list of files when using `lint`, `autocorrect` or `analyze`
|
To specify a list of files when using `lint` or `analyze`
|
||||||
(like the list of files modified by Xcode specified by the
|
(like the list of files modified by Xcode specified by the
|
||||||
[`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) Xcode
|
[`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) Xcode
|
||||||
plugin, or modified files in the working tree based on `git ls-files -m`), you
|
plugin, or modified files in the working tree based on `git ls-files -m`), you
|
||||||
can do so by passing the option `--use-script-input-files` and setting the
|
can do so by passing the option `--use-script-input-files` and setting the
|
||||||
following instance variables: `SCRIPT_INPUT_FILE_COUNT` and
|
following instance variables: `SCRIPT_INPUT_FILE_COUNT` and
|
||||||
`SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`...`SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT}`.
|
`SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`...`SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT - 1}`.
|
||||||
|
|
||||||
These are same environment variables set for input files to
|
These are same environment variables set for input files to
|
||||||
[custom Xcode script phases](http://indiestack.com/2014/12/speeding-up-custom-script-phases/).
|
[custom Xcode script phases](http://indiestack.com/2014/12/speeding-up-custom-script-phases/).
|
||||||
|
@ -209,36 +360,48 @@ You may also set the `TOOLCHAINS` environment variable to the reverse-DNS
|
||||||
notation that identifies a Swift toolchain version:
|
notation that identifies a Swift toolchain version:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3 swiftlint autocorrect
|
$ TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3 swiftlint --fix
|
||||||
```
|
```
|
||||||
|
|
||||||
On Linux, SourceKit is expected to be located in
|
On Linux, SourceKit is expected to be located in
|
||||||
`/usr/lib/libsourcekitdInProc.so` or specified by the `LINUX_SOURCEKIT_LIB_PATH`
|
`/usr/lib/libsourcekitdInProc.so` or specified by the `LINUX_SOURCEKIT_LIB_PATH`
|
||||||
environment variable.
|
environment variable.
|
||||||
|
|
||||||
### Swift Version Support
|
### pre-commit
|
||||||
|
|
||||||
Here's a reference of which SwiftLint version to use for a given Swift version.
|
SwiftLint can be run as a [pre-commit](https://pre-commit.com/) hook.
|
||||||
|
Once [installed](https://pre-commit.com/#install), add this to the
|
||||||
|
`.pre-commit-config.yaml` in the root of your repository:
|
||||||
|
|
||||||
| Swift version | Last supported SwiftLint release |
|
```yaml
|
||||||
|:----------------|:---------------------------------|
|
repos:
|
||||||
| Swift 1.x | SwiftLint 0.1.2 |
|
- repo: https://github.com/realm/SwiftLint
|
||||||
| Swift 2.x | SwiftLint 0.18.1 |
|
rev: 0.50.3
|
||||||
| Swift 3.x | SwiftLint 0.25.1 |
|
hooks:
|
||||||
| Swift 4.0-4.1.x | SwiftLint 0.28.2 |
|
- id: swiftlint
|
||||||
| Swift 4.2.x | SwiftLint 0.35.0 |
|
```
|
||||||
| Swift 5.x | Latest |
|
|
||||||
|
Adjust `rev` to the SwiftLint version of your choice. `pre-commit autoupdate` can be used to update to the current version.
|
||||||
|
|
||||||
|
SwiftLint can be configured using `entry` to apply fixes and fail on errors:
|
||||||
|
```yaml
|
||||||
|
- repo: https://github.com/realm/SwiftLint
|
||||||
|
rev: 0.50.3
|
||||||
|
hooks:
|
||||||
|
- id: swiftlint
|
||||||
|
entry: swiftlint --fix --strict
|
||||||
|
```
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
Over 75 rules are included in SwiftLint and the Swift community (that's you!)
|
Over 200 rules are included in SwiftLint and the Swift community (that's you!)
|
||||||
continues to contribute more over time.
|
continues to contribute more over time.
|
||||||
[Pull requests](CONTRIBUTING.md) are encouraged.
|
[Pull requests](CONTRIBUTING.md) are encouraged.
|
||||||
|
|
||||||
You can find an updated list of rules and more information about them
|
You can find an updated list of rules and more information about them
|
||||||
[here](https://realm.github.io/SwiftLint/rule-directory.html).
|
[here](https://realm.github.io/SwiftLint/rule-directory.html).
|
||||||
|
|
||||||
You can also check [Source/SwiftLintFramework/Rules](Source/SwiftLintFramework/Rules)
|
You can also check [Source/SwiftLintBuiltInRules/Rules](https://github.com/realm/SwiftLint/tree/main/Source/SwiftLintBuiltInRules/Rules)
|
||||||
directory to see their implementation.
|
directory to see their implementation.
|
||||||
|
|
||||||
### Opt-In Rules
|
### Opt-In Rules
|
||||||
|
@ -316,23 +479,33 @@ run SwiftLint from. The following parameters can be configured:
|
||||||
Rule inclusion:
|
Rule inclusion:
|
||||||
|
|
||||||
* `disabled_rules`: Disable rules from the default enabled set.
|
* `disabled_rules`: Disable rules from the default enabled set.
|
||||||
* `opt_in_rules`: Enable rules not from the default set.
|
* `opt_in_rules`: Enable rules that are not part of the default set. The
|
||||||
* `whitelist_rules`: Acts as a whitelist, only the rules specified in this list
|
special `all` identifier will enable all opt in linter rules, except the ones
|
||||||
will be enabled. Can not be specified alongside `disabled_rules` or
|
listed in `disabled_rules`.
|
||||||
`opt_in_rules`.
|
* `only_rules`: Only the rules specified in this list will be enabled.
|
||||||
|
Cannot be specified alongside `disabled_rules` or `opt_in_rules`.
|
||||||
* `analyzer_rules`: This is an entirely separate list of rules that are only
|
* `analyzer_rules`: This is an entirely separate list of rules that are only
|
||||||
run by the `analyze` command. All analyzer rules are opt-in, so this is the
|
run by the `analyze` command. All analyzer rules are opt-in, so this is the
|
||||||
only configurable rule list (there is no disabled/whitelist equivalent).
|
only configurable rule list, there are no equivalents for `disabled_rules`
|
||||||
|
`only_rules`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
disabled_rules: # rule identifiers to exclude from running
|
# By default, SwiftLint uses a set of sensible default rules you can adjust:
|
||||||
|
disabled_rules: # rule identifiers turned on by default to exclude from running
|
||||||
- colon
|
- colon
|
||||||
- comma
|
- comma
|
||||||
- control_statement
|
- control_statement
|
||||||
opt_in_rules: # some rules are only opt-in
|
opt_in_rules: # some rules are turned off by default, so you need to opt-in
|
||||||
- empty_count
|
- empty_count # Find all the available rules by running: `swiftlint rules`
|
||||||
# Find all the available rules by running:
|
|
||||||
# swiftlint rules
|
# Alternatively, specify all rules explicitly by uncommenting this option:
|
||||||
|
# only_rules: # delete `disabled_rules` & `opt_in_rules` if using this
|
||||||
|
# - empty_parameters
|
||||||
|
# - vertical_whitespace
|
||||||
|
|
||||||
|
analyzer_rules: # Rules run by `swiftlint analyze`
|
||||||
|
- explicit_self
|
||||||
|
|
||||||
included: # paths to include during linting. `--path` is ignored if present.
|
included: # paths to include during linting. `--path` is ignored if present.
|
||||||
- Source
|
- Source
|
||||||
excluded: # paths to ignore during linting. Takes precedence over `included`.
|
excluded: # paths to ignore during linting. Takes precedence over `included`.
|
||||||
|
@ -341,8 +514,9 @@ excluded: # paths to ignore during linting. Takes precedence over `included`.
|
||||||
- Source/ExcludedFolder
|
- Source/ExcludedFolder
|
||||||
- Source/ExcludedFile.swift
|
- Source/ExcludedFile.swift
|
||||||
- Source/*/ExcludedFile.swift # Exclude files with a wildcard
|
- Source/*/ExcludedFile.swift # Exclude files with a wildcard
|
||||||
analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
|
|
||||||
- explicit_self
|
# If true, SwiftLint will not fail if no lintable files are found.
|
||||||
|
allow_zero_lintable_files: false
|
||||||
|
|
||||||
# configurable rules can be customized from this configuration file
|
# configurable rules can be customized from this configuration file
|
||||||
# binary rules can set their severity level
|
# binary rules can set their severity level
|
||||||
|
@ -376,13 +550,29 @@ identifier_name:
|
||||||
- id
|
- id
|
||||||
- URL
|
- URL
|
||||||
- GlobalAPIKey
|
- GlobalAPIKey
|
||||||
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown)
|
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary)
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also use environment variables in your configuration file,
|
You can also use environment variables in your configuration file,
|
||||||
by using `${SOME_VARIABLE}` in a string.
|
by using `${SOME_VARIABLE}` in a string.
|
||||||
|
|
||||||
#### Defining Custom Rules
|
### Defining Custom Rules
|
||||||
|
|
||||||
|
In addition to the rules that the main SwiftLint project ships with, SwiftLint
|
||||||
|
can also run two types of custom rules that you can define yourself in your own
|
||||||
|
projects:
|
||||||
|
|
||||||
|
#### 1. Swift Custom Rules
|
||||||
|
|
||||||
|
These rules are written the same way as the Swift-based rules that ship with
|
||||||
|
SwiftLint so they're fast, accurate, can leverage SwiftSyntax, can be unit
|
||||||
|
tested, and more.
|
||||||
|
|
||||||
|
Using these requires building SwiftLint with Bazel as described in
|
||||||
|
[this video](https://vimeo.com/820572803) or its associated code in
|
||||||
|
[github.com/jpsim/swiftlint-bazel-example](https://github.com/jpsim/swiftlint-bazel-example).
|
||||||
|
|
||||||
|
#### 2. Regex Custom Rules
|
||||||
|
|
||||||
You can define custom regex-based rules in your configuration file using the
|
You can define custom regex-based rules in your configuration file using the
|
||||||
following syntax:
|
following syntax:
|
||||||
|
@ -390,10 +580,12 @@ following syntax:
|
||||||
```yaml
|
```yaml
|
||||||
custom_rules:
|
custom_rules:
|
||||||
pirates_beat_ninjas: # rule identifier
|
pirates_beat_ninjas: # rule identifier
|
||||||
included: ".*\\.swift" # regex that defines paths to include during linting. optional.
|
included:
|
||||||
excluded: ".*Test\\.swift" # regex that defines paths to exclude during linting. optional
|
- ".*\\.swift" # regex that defines paths to include during linting. optional.
|
||||||
|
excluded:
|
||||||
|
- ".*Test\\.swift" # regex that defines paths to exclude during linting. optional
|
||||||
name: "Pirates Beat Ninjas" # rule name. optional.
|
name: "Pirates Beat Ninjas" # rule name. optional.
|
||||||
regex: "([n,N]inja)" # matching pattern
|
regex: "([nN]inja)" # matching pattern
|
||||||
capture_group: 0 # number of regex capture group to highlight the rule violation at. optional.
|
capture_group: 0 # number of regex capture group to highlight the rule violation at. optional.
|
||||||
match_kinds: # SyntaxKinds to match. optional.
|
match_kinds: # SyntaxKinds to match. optional.
|
||||||
- comment
|
- comment
|
||||||
|
@ -401,7 +593,7 @@ custom_rules:
|
||||||
message: "Pirates are better than ninjas." # violation message. optional.
|
message: "Pirates are better than ninjas." # violation message. optional.
|
||||||
severity: error # violation severity. optional.
|
severity: error # violation severity. optional.
|
||||||
no_hiding_in_strings:
|
no_hiding_in_strings:
|
||||||
regex: "([n,N]inja)"
|
regex: "([nN]inja)"
|
||||||
match_kinds: string
|
match_kinds: string
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -413,39 +605,40 @@ You can filter the matches by providing one or more `match_kinds`, which will
|
||||||
reject matches that include syntax kinds that are not present in this list. Here
|
reject matches that include syntax kinds that are not present in this list. Here
|
||||||
are all the possible syntax kinds:
|
are all the possible syntax kinds:
|
||||||
|
|
||||||
* argument
|
* `argument`
|
||||||
* attribute.builtin
|
* `attribute.builtin`
|
||||||
* attribute.id
|
* `attribute.id`
|
||||||
* buildconfig.id
|
* `buildconfig.id`
|
||||||
* buildconfig.keyword
|
* `buildconfig.keyword`
|
||||||
* comment
|
* `comment`
|
||||||
* comment.mark
|
* `comment.mark`
|
||||||
* comment.url
|
* `comment.url`
|
||||||
* doccomment
|
* `doccomment`
|
||||||
* doccomment.field
|
* `doccomment.field`
|
||||||
* identifier
|
* `identifier`
|
||||||
* keyword
|
* `keyword`
|
||||||
* number
|
* `number`
|
||||||
* objectliteral
|
* `objectliteral`
|
||||||
* parameter
|
* `parameter`
|
||||||
* placeholder
|
* `placeholder`
|
||||||
* string
|
* `string`
|
||||||
* string_interpolation_anchor
|
* `string_interpolation_anchor`
|
||||||
* typeidentifier
|
* `typeidentifier`
|
||||||
|
|
||||||
If using custom rules alongside a whitelist, make sure to add `custom_rules` as an item under `whitelist_rules`.
|
All syntax kinds used in a snippet of Swift code can be extracted asking
|
||||||
|
[SourceKitten](https://github.com/jpsim/SourceKitten). For example,
|
||||||
|
`sourcekitten syntax --text "struct S {}"` delivers
|
||||||
|
|
||||||
#### Nested Configurations
|
* `source.lang.swift.syntaxtype.keyword` for the `struct` keyword and
|
||||||
|
* `source.lang.swift.syntaxtype.identifier` for its name `S`
|
||||||
|
|
||||||
SwiftLint supports nesting configuration files for more granular control over
|
which match to `keyword` and `identifier` in the above list.
|
||||||
the linting process.
|
|
||||||
|
|
||||||
* Include additional `.swiftlint.yml` files where necessary in your directory
|
If using custom rules in combination with `only_rules`, make sure to add
|
||||||
structure.
|
`custom_rules` as an item under `only_rules`.
|
||||||
* Each file will be linted using the configuration file that is in its
|
|
||||||
directory or at the deepest level of its parent directories. Otherwise the
|
Unlike Swift custom rules, you can use official SwiftLint builds
|
||||||
root configuration will be used.
|
(e.g. from Homebrew) to run regex custom rules.
|
||||||
* `included` is ignored for nested configurations.
|
|
||||||
|
|
||||||
### Auto-correct
|
### Auto-correct
|
||||||
|
|
||||||
|
@ -453,27 +646,136 @@ SwiftLint can automatically correct certain violations. Files on disk are
|
||||||
overwritten with a corrected version.
|
overwritten with a corrected version.
|
||||||
|
|
||||||
Please make sure to have backups of these files before running
|
Please make sure to have backups of these files before running
|
||||||
`swiftlint autocorrect`, otherwise important data may be lost.
|
`swiftlint --fix`, otherwise important data may be lost.
|
||||||
|
|
||||||
Standard linting is disabled while correcting because of the high likelihood of
|
Standard linting is disabled while correcting because of the high likelihood of
|
||||||
violations (or their offsets) being incorrect after modifying a file while
|
violations (or their offsets) being incorrect after modifying a file while
|
||||||
applying corrections.
|
applying corrections.
|
||||||
|
|
||||||
### Analyze (experimental)
|
### Analyze
|
||||||
|
|
||||||
The _experimental_ `swiftlint analyze` command can lint Swift files using the
|
The `swiftlint analyze` command can lint Swift files using the
|
||||||
full type-checked AST. The compiler log path containing the clean `swiftc` build
|
full type-checked AST. The compiler log path containing the clean `swiftc` build
|
||||||
command invocation (incremental builds will fail) must be passed to `analyze`
|
command invocation (incremental builds will fail) must be passed to `analyze`
|
||||||
via the `--compiler-log-path` flag.
|
via the `--compiler-log-path` flag.
|
||||||
e.g. `--compiler-log-path /path/to/xcodebuild.log`
|
e.g. `--compiler-log-path /path/to/xcodebuild.log`
|
||||||
|
|
||||||
This can be obtained by running
|
This can be obtained by
|
||||||
`xcodebuild -workspace {WORKSPACE}.xcworkspace -scheme {SCHEME} > xcodebuild.log`
|
|
||||||
with a clean `DerivedData` folder.
|
|
||||||
|
|
||||||
This command and related code in SwiftLint is subject to substantial changes at
|
1. Cleaning DerivedData (incremental builds won't work with analyze)
|
||||||
any time while this feature is marked as experimental. Analyzer rules also tend
|
2. Running `xcodebuild -workspace {WORKSPACE}.xcworkspace -scheme {SCHEME} > xcodebuild.log`
|
||||||
to be considerably slower than lint rules.
|
3. Running `swiftlint analyze --compiler-log-path xcodebuild.log`
|
||||||
|
|
||||||
|
Analyzer rules tend to be considerably slower than lint rules.
|
||||||
|
|
||||||
|
## Using Multiple Configuration Files
|
||||||
|
|
||||||
|
SwiftLint offers a variety of ways to include multiple configuration files.
|
||||||
|
Multiple configuration files get merged into one single configuration that is then applied
|
||||||
|
just as a single configuration file would get applied.
|
||||||
|
|
||||||
|
There are quite a lot of use cases where using multiple configuration files could be helpful:
|
||||||
|
|
||||||
|
For instance, one could use a team-wide shared SwiftLint configuration while allowing overrides
|
||||||
|
in each project via a child configuration file.
|
||||||
|
|
||||||
|
Team-Wide Configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
disabled_rules:
|
||||||
|
- force_cast
|
||||||
|
```
|
||||||
|
|
||||||
|
Project-Specific Configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
opt_in_rules:
|
||||||
|
- force_cast
|
||||||
|
```
|
||||||
|
|
||||||
|
### Child / Parent Configs (Locally)
|
||||||
|
|
||||||
|
You can specify a `child_config` and / or a `parent_config` reference within a configuration file.
|
||||||
|
These references should be local paths relative to the folder of the configuration file they are specified in.
|
||||||
|
This even works recursively, as long as there are no cycles and no ambiguities.
|
||||||
|
|
||||||
|
**A child config is treated as a refinement and therefore has a higher priority**,
|
||||||
|
while a parent config is considered a base with lower priority in case of conflicts.
|
||||||
|
|
||||||
|
Here's an example, assuming you have the following file structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
ProjectRoot
|
||||||
|
|_ .swiftlint.yml
|
||||||
|
|_ .swiftlint_refinement.yml
|
||||||
|
|_ Base
|
||||||
|
|_ .swiftlint_base.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
To include both the refinement and the base file, your `.swiftlint.yml` should look like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
child_config: .swiftlint_refinement.yml
|
||||||
|
parent_config: Base/.swiftlint_base.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
When merging parent and child configs, `included` and `excluded` configurations
|
||||||
|
are processed carefully to account for differences in the directory location
|
||||||
|
of the containing configuration files.
|
||||||
|
|
||||||
|
### Child / Parent Configs (Remote)
|
||||||
|
|
||||||
|
Just as you can provide local `child_config` / `parent_config` references, instead of
|
||||||
|
referencing local paths, you can just put urls that lead to configuration files.
|
||||||
|
In order for SwiftLint to detect these remote references, they must start with `http://` or `https://`.
|
||||||
|
|
||||||
|
The referenced remote configuration files may even recursively reference other
|
||||||
|
remote configuration files, but aren't allowed to include local references.
|
||||||
|
|
||||||
|
Using a remote reference, your `.swiftlint.yml` could look like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
parent_config: https://myteamserver.com/our-base-swiftlint-config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Every time you run SwiftLint and have an Internet connection, SwiftLint tries to get a new version of
|
||||||
|
every remote configuration that is referenced. If this request times out, a cached version is
|
||||||
|
used if available. If there is no cached version available, SwiftLint fails – but no worries, a cached version
|
||||||
|
should be there once SwiftLint has run successfully at least once.
|
||||||
|
|
||||||
|
If needed, the timeouts for the remote configuration fetching can be specified manually via the
|
||||||
|
configuration file(s) using the `remote_timeout` / `remote_timeout_if_cached` specifiers.
|
||||||
|
These values default to 2 / 1 second(s).
|
||||||
|
|
||||||
|
### Command Line
|
||||||
|
|
||||||
|
Instead of just providing one configuration file when running SwiftLint via the command line,
|
||||||
|
you can also pass a hierarchy, where the first configuration is treated as a parent,
|
||||||
|
while the last one is treated as the highest-priority child.
|
||||||
|
|
||||||
|
A simple example including just two configuration files looks like this:
|
||||||
|
|
||||||
|
`swiftlint --config .swiftlint.yml --config .swiftlint_child.yml`
|
||||||
|
|
||||||
|
### Nested Configurations
|
||||||
|
|
||||||
|
In addition to a main configuration (the `.swiftlint.yml` file in the root folder),
|
||||||
|
you can put other configuration files named `.swiftlint.yml` into the directory structure
|
||||||
|
that then get merged as a child config, but only with an effect for those files
|
||||||
|
that are within the same directory as the config or in a deeper directory where
|
||||||
|
there isn't another configuration file. In other words: Nested configurations don't work
|
||||||
|
recursively – there's a maximum number of one nested configuration per file
|
||||||
|
that may be applied in addition to the main configuration.
|
||||||
|
|
||||||
|
`.swiftlint.yml` files are only considered as a nested configuration if they have not been
|
||||||
|
used to build the main configuration already (e. g. by having been referenced via something
|
||||||
|
like `child_config: Folder/.swiftlint.yml`). Also, `parent_config` / `child_config`
|
||||||
|
specifications of nested configurations are getting ignored because there's no sense to that.
|
||||||
|
|
||||||
|
If one (or more) SwiftLint file(s) are explicitly specified via the `--config` parameter,
|
||||||
|
that configuration will be treated as an override, no matter whether there exist
|
||||||
|
other `.swiftlint.yml` files somewhere within the directory. **So if you want to use
|
||||||
|
nested configurations, you can't use the `--config` parameter.**
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
35
README_CN.md
35
README_CN.md
|
@ -1,11 +1,11 @@
|
||||||
# SwiftLint
|
# SwiftLint
|
||||||
|
|
||||||
SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具,基本上以 [GitHub's Swift 代码风格指南](https://github.com/github/swift-style-guide)为基础。
|
SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具,基本上以 [Kodeco's Swift 代码风格指南](https://github.com/kodecocodes/swift-style-guide)为基础。
|
||||||
|
|
||||||
SwiftLint Hook 了 [Clang](http://clang.llvm.org) 和 [SourceKit](http://www.jpsim.com/uncovering-sourcekit) 从而能够使用 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 来表示源代码文件的更多精确结果。
|
SwiftLint Hook 了 [Clang](http://clang.llvm.org) 和 [SourceKit](http://www.jpsim.com/uncovering-sourcekit) 从而能够使用 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 来表示源代码文件的更多精确结果。
|
||||||
|
|
||||||
[](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=master)
|
[](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=main)
|
||||||
[](https://codecov.io/github/realm/SwiftLint?branch=master)
|
[](https://codecov.io/github/realm/SwiftLint?branch=main)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ $ mint install realm/SwiftLint
|
||||||
|
|
||||||
### 编译源代码:
|
### 编译源代码:
|
||||||
|
|
||||||
你也可以通过 Clone SwiftLint 的 Git 仓库到本地然后执行 `git submodule update --init --recursive; make install` (Xcode 10.2+) 编译源代码的方式来安装。
|
你也可以通过 Clone SwiftLint 的 Git 仓库到本地然后执行 `make install` (Xcode 12.5+) 编译源代码的方式来安装。
|
||||||
|
|
||||||
## 用法
|
## 用法
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ fi
|
||||||
|
|
||||||
#### 格式化保存 Xcode 插件
|
#### 格式化保存 Xcode 插件
|
||||||
|
|
||||||
在 XCode 中保存时执行 `swiftlint autocorrect`,需要从 Alcatraz 安装 [SwiftLintXcode](https://github.com/ypresto/SwiftLintXcode) 插件。
|
在 Xcode 中保存时执行 `swiftlint autocorrect`,需要从 Alcatraz 安装 [SwiftLintXcode](https://github.com/ypresto/SwiftLintXcode) 插件。
|
||||||
|
|
||||||
⚠ ️如果没有禁用 SIP 的话,这个插件在 Xcode 8 或者更新版本的 Xcode 上将不会工作。不推荐此操作。
|
⚠ ️如果没有禁用 SIP 的话,这个插件在 Xcode 8 或者更新版本的 Xcode 上将不会工作。不推荐此操作。
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ Available commands:
|
||||||
在包含有需要执行代码分析的 Swift 源码文件的目录下执行 `swiftlint` 命令,会对目录进行递归查找。
|
在包含有需要执行代码分析的 Swift 源码文件的目录下执行 `swiftlint` 命令,会对目录进行递归查找。
|
||||||
|
|
||||||
当使用 `lint` 或者 `autocorrect` 命令时,你可以通过添加 `--use-script-input-files` 选项并且设置以下实例变量:`SCRIPT_INPUT_FILE_COUNT` 和
|
当使用 `lint` 或者 `autocorrect` 命令时,你可以通过添加 `--use-script-input-files` 选项并且设置以下实例变量:`SCRIPT_INPUT_FILE_COUNT` 和
|
||||||
`SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`... `SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT}` 的方式来指定一个文件列表(就像被 Xcode 特别是 [`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) Xcode 插件修改的文件组成的列表,或者类似 Git 工作树中 `git ls-files -m` 命令显示的被修改的文件列表)。
|
`SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`... `SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT - 1}` 的方式来指定一个文件列表(就像被 Xcode 特别是 [`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) Xcode 插件修改的文件组成的列表,或者类似 Git 工作树中 `git ls-files -m` 命令显示的被修改的文件列表)。
|
||||||
|
|
||||||
也有类似的用来设置输入文件的环境变量以 [自定义 Xcode script phases](http://indiestack.com/2014/12/speeding-up-custom-script-phases/) 。
|
也有类似的用来设置输入文件的环境变量以 [自定义 Xcode script phases](http://indiestack.com/2014/12/speeding-up-custom-script-phases/) 。
|
||||||
|
|
||||||
|
@ -161,26 +161,13 @@ $ TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3 swiftlint autocorrect
|
||||||
|
|
||||||
在 Linux 上,SourceKit 默认需要位于 `/usr/lib/libsourcekitdInProc.so` 或者通过 `LINUX_SOURCEKIT_LIB_PATH` 环境变量进行指定。
|
在 Linux 上,SourceKit 默认需要位于 `/usr/lib/libsourcekitdInProc.so` 或者通过 `LINUX_SOURCEKIT_LIB_PATH` 环境变量进行指定。
|
||||||
|
|
||||||
### Swift Version Support
|
|
||||||
|
|
||||||
这里有一份 SwiftLint 版本和对应该 Swift 版本的对照表作为参考。
|
|
||||||
|
|
||||||
| Swift 版本 | 最后一个 SwiftLint 支持版本 |
|
|
||||||
|:----------------|:----------------------------|
|
|
||||||
| Swift 1.x | SwiftLint 0.1.2 |
|
|
||||||
| Swift 2.x | SwiftLint 0.18.1 |
|
|
||||||
| Swift 3.x | SwiftLint 0.25.1 |
|
|
||||||
| Swift 4.0-4.1.x | SwiftLint 0.28.2 |
|
|
||||||
| Swift 4.2.x | SwiftLint 0.35.0 |
|
|
||||||
| Swift 5.x | 最新的 |
|
|
||||||
|
|
||||||
## 规则
|
## 规则
|
||||||
|
|
||||||
SwiftLint 已经包含了超过 75 条规则,并且我们希望 Swift 社区(就是你!)会在以后有更多的贡献,我们鼓励提交 [Pull Requests](CONTRIBUTING.md)。
|
SwiftLint 已经包含了超过 75 条规则,并且我们希望 Swift 社区(就是你!)会在以后有更多的贡献,我们鼓励提交 [Pull Requests](CONTRIBUTING.md)。
|
||||||
|
|
||||||
你可以在 [Rule Directory](https://realm.github.io/SwiftLint/rule-directory.html) 找到规则的更新列表和更多信息。
|
你可以在 [Rule Directory](https://realm.github.io/SwiftLint/rule-directory.html) 找到规则的更新列表和更多信息。
|
||||||
|
|
||||||
你也可以检视 [Source/SwiftLintFramework/Rules](Source/SwiftLintFramework/Rules) 目录来查看它们的实现。
|
你也可以检视 [Source/SwiftLintBuiltInRules/Rules](Source/SwiftLintBuiltInRules/Rules) 目录来查看它们的实现。
|
||||||
|
|
||||||
`opt_in_rules` 默认是关闭的(即,你需要在你的配置文件中明确地打开它们)。
|
`opt_in_rules` 默认是关闭的(即,你需要在你的配置文件中明确地打开它们)。
|
||||||
|
|
||||||
|
@ -232,7 +219,7 @@ let noWarning3 = NSNumber() as! Int
|
||||||
|
|
||||||
* `disabled_rules`: 关闭某些默认开启的规则。
|
* `disabled_rules`: 关闭某些默认开启的规则。
|
||||||
* `opt_in_rules`: 一些规则是可选的。
|
* `opt_in_rules`: 一些规则是可选的。
|
||||||
* `whitelist_rules`: 不可以和 `disabled_rules` 或者 `opt_in_rules` 并列。类似一个白名单,只有在这个列表中的规则才是开启的。
|
* `only_rules`: 不可以和 `disabled_rules` 或者 `opt_in_rules` 并列。类似一个白名单,只有在这个列表中的规则才是开启的。
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
disabled_rules: # 执行时排除掉的规则
|
disabled_rules: # 执行时排除掉的规则
|
||||||
|
@ -283,7 +270,7 @@ identifier_name:
|
||||||
- id
|
- id
|
||||||
- URL
|
- URL
|
||||||
- GlobalAPIKey
|
- GlobalAPIKey
|
||||||
reporter: "xcode" # 报告类型 (xcode, json, csv, checkstyle, junit, html, emoji)
|
reporter: "xcode" # 报告类型 (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 定义自定义规则
|
#### 定义自定义规则
|
||||||
|
@ -294,14 +281,14 @@ reporter: "xcode" # 报告类型 (xcode, json, csv, checkstyle, junit, html, emo
|
||||||
custom_rules:
|
custom_rules:
|
||||||
pirates_beat_ninjas: # 规则标识符
|
pirates_beat_ninjas: # 规则标识符
|
||||||
name: "Pirates Beat Ninjas" # 规则名称,可选
|
name: "Pirates Beat Ninjas" # 规则名称,可选
|
||||||
regex: "([n,N]inja)" # 匹配的模式
|
regex: "([nN]inja)" # 匹配的模式
|
||||||
match_kinds: # 需要匹配的语法类型,可选
|
match_kinds: # 需要匹配的语法类型,可选
|
||||||
- comment
|
- comment
|
||||||
- identifier
|
- identifier
|
||||||
message: "Pirates are better than ninjas." # 提示信息,可选
|
message: "Pirates are better than ninjas." # 提示信息,可选
|
||||||
severity: error # 提示的级别,可选
|
severity: error # 提示的级别,可选
|
||||||
no_hiding_in_strings:
|
no_hiding_in_strings:
|
||||||
regex: "([n,N]inja)"
|
regex: "([nN]inja)"
|
||||||
match_kinds: string
|
match_kinds: string
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
80
README_KR.md
80
README_KR.md
|
@ -1,11 +1,11 @@
|
||||||
# SwiftLint
|
# SwiftLint
|
||||||
|
|
||||||
SwiftLint는 스위프트 스타일 및 컨벤션을 강제하기 위한 도구로, [GitHub 스위프트 스타일 가이드](https://github.com/github/swift-style-guide)에 대략적인 기반을 두고 있습니다.
|
SwiftLint는 스위프트 스타일 및 컨벤션을 강제하기 위한 도구로, [Kodeco 스위프트 스타일 가이드](https://github.com/kodecocodes/swift-style-guide)에 대략적인 기반을 두고 있습니다.
|
||||||
|
|
||||||
SwiftLint는 좀 더 정확한 결과를 위해 [Clang](http://clang.llvm.org)과 [SourceKit](http://www.jpsim.com/uncovering-sourcekit)에 연결하여 소스 파일의 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 표현을 사용합니다.
|
SwiftLint는 좀 더 정확한 결과를 위해 [Clang](http://clang.llvm.org)과 [SourceKit](http://www.jpsim.com/uncovering-sourcekit)에 연결하여 소스 파일의 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 표현을 사용합니다.
|
||||||
|
|
||||||
[](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=master)
|
[](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=main)
|
||||||
[](https://codecov.io/github/realm/SwiftLint?branch=master)
|
[](https://codecov.io/github/realm/SwiftLint?branch=main)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -27,11 +27,11 @@ Podfile에 아래 라인을 추가하기만 하면 됩니다.
|
||||||
pod 'SwiftLint'
|
pod 'SwiftLint'
|
||||||
```
|
```
|
||||||
|
|
||||||
이를 실행하면 다음번 `pod install` 실행시 SwiftLint 바이너리 및 `Pods/`에 있는 디펜던시들을 다운로드하고, Script Build Phases에서 `${PODS_ROOT}/SwiftLint/swiftlint` 명령을 사용할 수 있게 됩니다.
|
이를 실행하면 다음번 `pod install` 실행 시 SwiftLint 바이너리 및 `Pods/`에 있는 디펜던시들을 다운로드하고, Script Build Phases에서 `${PODS_ROOT}/SwiftLint/swiftlint` 명령을 사용할 수 있게 됩니다.
|
||||||
|
|
||||||
CocoaPods를 사용하면 최신 버전 외에도 SwiftLint의 특정 버전을 설치할 수 있기 때문에 이 방법을 권장합니다. (Homebrew는 최신 버전만 설치 가능)
|
CocoaPods를 사용하면 최신 버전 외에도 SwiftLint의 특정 버전을 설치할 수 있기 때문에 이 방법을 권장합니다. (Homebrew는 최신 버전만 설치 가능)
|
||||||
|
|
||||||
이렇게 했을때 SwiftLint 바이너리 및 그에 종속된 바이너리들과 스위프트 바이너리까지 `Pods/` 디렉토리에 추가되기 때문에, git 등의 SCM에 이런 디렉토리들을 체크인하는 것은 권장하지 않습니다.
|
이렇게 했을 때 SwiftLint 바이너리 및 그에 종속된 바이너리들과 스위프트 바이너리까지 `Pods/` 디렉터리에 추가되기 때문에, git 등의 SCM에 이런 디렉터리들을 체크인하는 것은 권장하지 않습니다.
|
||||||
|
|
||||||
### [Mint](https://github.com/yonaskolb/mint)를 사용하는 경우:
|
### [Mint](https://github.com/yonaskolb/mint)를 사용하는 경우:
|
||||||
```
|
```
|
||||||
|
@ -44,7 +44,7 @@ $ mint install realm/SwiftLint
|
||||||
|
|
||||||
### 소스를 직접 컴파일하는 경우:
|
### 소스를 직접 컴파일하는 경우:
|
||||||
|
|
||||||
본 프로젝트를 클론해서 빌드할 수도 있습니다. `git submodule update --init --recursive; make install` 명령을 사용합니다. (Xcode 10.2 이후 버전)
|
본 프로젝트를 클론해서 빌드할 수도 있습니다. `make install` 명령을 사용합니다. (Xcode 12.5 이후 버전)
|
||||||
|
|
||||||
## 사용 방법
|
## 사용 방법
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ $ mint install realm/SwiftLint
|
||||||
|
|
||||||
### Xcode
|
### Xcode
|
||||||
|
|
||||||
SwiftLint를 Xcode 스킴에 통합하여 IDE 상에 경고나 에러를 표시할 수 있습니다. "Run Script Phase"를 새로 만들고 아래 스크립트를 추가하기만 하면 됩니다.
|
SwiftLint를 Xcode 프로젝트에 통합하여 IDE 상에 경고나 에러를 표시할 수 있습니다. 프로젝트의 파일 내비게이터에서 타겟 앱을 선택 후 "Build Phases" 탭으로 이동합니다. + 버튼을 클릭한 후 "Run Script Phase"를 선택합니다. 그 후 아래 스크립트를 추가하기만 하면 됩니다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
if which swiftlint >/dev/null; then
|
if which swiftlint >/dev/null; then
|
||||||
|
@ -68,6 +68,34 @@ fi
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
만약, 애플 실리콘 환경에서 Homebrew를 통해 SwiftLint를 설치했다면, 아마도 다음과 같은 경고를 겪었을 것입니다.
|
||||||
|
|
||||||
|
> warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint
|
||||||
|
|
||||||
|
그 이유는, 애플 실리콘 기반 맥에서 Homebrew는 기본적으로 바이너리들을 `/opt/homebrew/bin`에 저장하기 때문입니다. SwiftLint가 어디 있는지 찾는 것을 Xcode에 알려주기 위해, build phase에서 `/opt/homebrew/bin`를 `PATH` 환경 변수에 동시에 추가하여야 합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [[ "$(uname -m)" == arm64 ]]; then
|
||||||
|
export PATH="/opt/homebrew/bin:$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if which swiftlint > /dev/null; then
|
||||||
|
swiftlint
|
||||||
|
else
|
||||||
|
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
혹은 아래와 같이 `/usr/local/bin`에 심볼릭 링크를 생성하여 실제 바이너리가 있는 곳으로 포인팅할 수 있습니다. :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -s /opt/homebrew/bin/swiftlint /usr/local/bin/swiftlint
|
||||||
|
```
|
||||||
|
|
||||||
|
당신은 SwiftLint phase를 'Compile Sources' 단계 직전으로 옮겨 컴파일 전에 에러를 빠르게 찾고 싶어 할 것입니다. 하지만, SwiftLint는 컴파일러의 구문 분석 단계를 완벽히 수행하는 유효한 Swift 코드를 실행하기 위해 설계되었습니다. 따라서, 'Compile Sources' 전에 SwiftLint를 실행하면 일부 부정확한 오류가 발생할 수도 있습니다.
|
||||||
|
|
||||||
|
만약 당신은 위반 사항(violations)을 동시에 수정하는 것을 원한다면, 스크립트에 `swiftlint` 대신 `swiftlint --fix && swiftlint`을 적어야 합니다. 이는 프로젝트의 수정 가능한 모든 위반 사항들이 수정되고 나머지 위반 사항에 대한 경고가 표시된다는 것을 의미합니다.
|
||||||
|
|
||||||
CocoaPods를 사용해서 설치한 경우는 아래 스크립트를 대신 사용합니다.
|
CocoaPods를 사용해서 설치한 경우는 아래 스크립트를 대신 사용합니다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -95,7 +123,7 @@ fastlane 과정에서 SwiftLint를 사용하려면 [공식적인 fastlane 액션
|
||||||
```ruby
|
```ruby
|
||||||
swiftlint(
|
swiftlint(
|
||||||
mode: :lint, # SwiftLint 모드: :lint (디폴트) 아니면 :autocorrect
|
mode: :lint, # SwiftLint 모드: :lint (디폴트) 아니면 :autocorrect
|
||||||
executable: "Pods/SwiftLint/swiftlint", # SwiftLint 바이너리 경로 (선택 가능). CocoaPods를 사용해서 설치한 경우는 이 옾션이 중요합니다
|
executable: "Pods/SwiftLint/swiftlint", # SwiftLint 바이너리 경로 (선택 가능). CocoaPods를 사용해서 설치한 경우는 이 옵션이 중요합니다
|
||||||
output_file: "swiftlint.result.json", # 결과 파일의 경로 (선택 가능)
|
output_file: "swiftlint.result.json", # 결과 파일의 경로 (선택 가능)
|
||||||
reporter: "json", # 보고 유형 (선택 가능)
|
reporter: "json", # 보고 유형 (선택 가능)
|
||||||
config_file: ".swiftlint-ci.yml", # 설정 파일의 경로 (선택 가능)
|
config_file: ".swiftlint-ci.yml", # 설정 파일의 경로 (선택 가능)
|
||||||
|
@ -116,16 +144,16 @@ Available commands:
|
||||||
version Display the current version of SwiftLint
|
version Display the current version of SwiftLint
|
||||||
```
|
```
|
||||||
|
|
||||||
스위프트 파일이 있는 디렉토리에서 `swiftlint`를 실행합니다. 디렉토리는 재귀적으로 탐색됩니다.
|
스위프트 파일이 있는 디렉터리에서 `swiftlint`를 실행합니다. 디렉터리는 재귀적으로 탐색됩니다.
|
||||||
|
|
||||||
`lint`나 `autocorrect`를 사용할 때 여러 파일(예를 들면, [`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) 플러그인에 의해 Xcode가 변경한 파일들 혹은 `git ls-files -m` 명령으로 인해 작업 트리에서 변경된 파일들)을 지정하려면 `--use-script-input-files` 옵션을 넘기고 다음 인스턴스 변수들을 설정하면 됩니다. `SCRIPT_INPUT_FILE_COUNT` and
|
`lint`나 `autocorrect`를 사용할 때 여러 파일(예를 들면, [`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) 플러그인에 의해 Xcode가 변경한 파일들 혹은 `git ls-files -m` 명령으로 인해 작업 트리에서 변경된 파일들)을 지정하려면 `--use-script-input-files` 옵션을 넘기고 다음 인스턴스 변수들을 설정하면 됩니다. `SCRIPT_INPUT_FILE_COUNT` and
|
||||||
`SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`...`SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT}`
|
`SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`...`SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT - 1}`
|
||||||
|
|
||||||
이는 [Xcode의 커스텀 스크립트 단계](http://indiestack.com/2014/12/speeding-up-custom-script-phases/)에 입력 파일로 환경 변수를 지정하는 것과 동일합니다.
|
이는 [Xcode의 커스텀 스크립트 단계](http://indiestack.com/2014/12/speeding-up-custom-script-phases/)에 입력 파일로 환경 변수를 지정하는 것과 동일합니다.
|
||||||
|
|
||||||
### 스위프트 여러 버전에 대한 대응
|
### 스위프트 여러 버전에 대한 대응
|
||||||
|
|
||||||
SwiftLint는 SourceKit에 연결되어 있으므로 스위프트 언어가 변화하더라도 이상없이 동작할 수 있습니다.
|
SwiftLint는 SourceKit에 연결되어 있으므로 스위프트 언어가 변화하더라도 이상 없이 동작할 수 있습니다.
|
||||||
|
|
||||||
이는 전체 스위프트 컴파일러가 포함되지 않아도 되므로 SwiftLint가 간결하게 유지될 수 있습니다. SwiftLint는 데스크탑에 이미 설치되어 있는 공식 스위프트 컴파일러와 통신하기만 하면 됩니다.
|
이는 전체 스위프트 컴파일러가 포함되지 않아도 되므로 SwiftLint가 간결하게 유지될 수 있습니다. SwiftLint는 데스크탑에 이미 설치되어 있는 공식 스위프트 컴파일러와 통신하기만 하면 됩니다.
|
||||||
|
|
||||||
|
@ -143,7 +171,7 @@ SwiftLint가 어느 스위프트 툴체인을 사용할지 결정하는 순서
|
||||||
* `~/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain`
|
* `~/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain`
|
||||||
* `~/Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain`
|
* `~/Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain`
|
||||||
|
|
||||||
`sourcekitd.framework`은 위에서 선택된 경로의 `usr/lib/` 하위 디렉토리에 존재해야 합니다.
|
`sourcekitd.framework`은 위에서 선택된 경로의 `usr/lib/` 하위 디렉터리에 존재해야 합니다.
|
||||||
|
|
||||||
`TOOLCHAINS` 환경 변수에 스위프트 툴체인 버전을 식별할 수 있는 값을 리버스 DNS 형식으로 지정할 수도 있습니다.
|
`TOOLCHAINS` 환경 변수에 스위프트 툴체인 버전을 식별할 수 있는 값을 리버스 DNS 형식으로 지정할 수도 있습니다.
|
||||||
|
|
||||||
|
@ -155,9 +183,9 @@ $ TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3 swiftlint autocorrect
|
||||||
|
|
||||||
## 룰
|
## 룰
|
||||||
|
|
||||||
SwiftLint에는 75개가 넘는 룰들이 있고, 스위프트 커뮤니티(바로 여러분들!)는 이를 지속적으로 발전시켜 가고 있습니다. [풀 리퀘스트](CONTRIBUTING.md)는 언제나 환영입니다.
|
SwiftLint에는 200개가 넘는 룰들이 있고, 스위프트 커뮤니티(바로 여러분들!)는 이를 지속적으로 발전시켜 가고 있습니다. [풀 리퀘스트](CONTRIBUTING.md)는 언제나 환영입니다.
|
||||||
|
|
||||||
현재 구현된 룰 전체를 확인하려면 [Source/SwiftLintFramework/Rules](Source/SwiftLintFramework/Rules)를 살펴보세요.
|
현재 구현된 룰 전체를 확인하려면 [Source/SwiftLintBuiltInRules/Rules](Source/SwiftLintBuiltInRules/Rules)를 살펴보세요.
|
||||||
|
|
||||||
`opt_in_rules`는 기본적으로 비활성화되어 있습니다. (즉, 설정 파일에서 명시적으로 해당 룰을 활성화해야 합니다.)
|
`opt_in_rules`는 기본적으로 비활성화되어 있습니다. (즉, 설정 파일에서 명시적으로 해당 룰을 활성화해야 합니다.)
|
||||||
|
|
||||||
|
@ -169,11 +197,11 @@ SwiftLint에는 75개가 넘는 룰들이 있고, 스위프트 커뮤니티(바
|
||||||
|
|
||||||
### 코드에서 룰 비활성화하기
|
### 코드에서 룰 비활성화하기
|
||||||
|
|
||||||
소스 파일에서 아래 형식의 주석을 사용하면 룰을 비활성화 할 수 있습니다.
|
소스 파일에서 아래 형식의 주석을 사용하면 룰을 비활성화할 수 있습니다.
|
||||||
|
|
||||||
`// swiftlint:disable <룰1> [<룰2> <룰3>...]`
|
`// swiftlint:disable <룰1> [<룰2> <룰3>...]`
|
||||||
|
|
||||||
비활성화 된 룰은 해당 파일의 마지막까지 적용되거나, 활성화 주석이 나타날 때까지 적용됩니다.
|
비활성화된 룰은 해당 파일의 마지막까지 적용되거나, 활성화 주석이 나타날 때까지 적용됩니다.
|
||||||
|
|
||||||
`// swiftlint:enable <룰1> [<룰2> <룰3>...]`
|
`// swiftlint:enable <룰1> [<룰2> <룰3>...]`
|
||||||
|
|
||||||
|
@ -203,13 +231,13 @@ let noWarning3 = NSNumber() as! Int
|
||||||
|
|
||||||
### 설정
|
### 설정
|
||||||
|
|
||||||
SwiftLint가 실행될 디렉토리에 `.swiftlint.yml` 파일을 추가해서 SwiftLint를 설정할 수 있습니다. 아래 파라미터들을 설정 가능합니다.
|
SwiftLint가 실행될 디렉터리에 `.swiftlint.yml` 파일을 추가해서 SwiftLint를 설정할 수 있습니다. 아래 파라미터들을 설정 가능합니다.
|
||||||
|
|
||||||
룰 적용여부 설정:
|
룰 적용여부 설정:
|
||||||
|
|
||||||
* `disabled_rules`: 기본 활성화된 룰 중에 비활성화할 룰들을 지정합니다.
|
* `disabled_rules`: 기본 활성화된 룰 중에 비활성화할 룰들을 지정합니다.
|
||||||
* `opt_in_rules`: 기본 룰이 아닌 룰들을 활성화합니다.
|
* `opt_in_rules`: 기본 룰이 아닌 룰들을 활성화합니다.
|
||||||
* `whitelist_rules`: 지정한 룰들만 활성화되도록 화이트리스트로 지정합니다. `disabled_rules` 및 `opt_in_rules`과는 같이 사용할 수 없습니다.
|
* `only_rules`: 지정한 룰들만 활성화되도록 화이트리스트로 지정합니다. `disabled_rules` 및 `opt_in_rules`과는 같이 사용할 수 없습니다.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
disabled_rules: # 실행에서 제외할 룰 식별자들
|
disabled_rules: # 실행에서 제외할 룰 식별자들
|
||||||
|
@ -258,7 +286,7 @@ identifier_name:
|
||||||
- id
|
- id
|
||||||
- URL
|
- URL
|
||||||
- GlobalAPIKey
|
- GlobalAPIKey
|
||||||
reporter: "xcode" # 보고 유형 (xcode, json, csv, checkstyle, junit, html, emoji, markdown)
|
reporter: "xcode" # 보고 유형 (xcode, json, csv, codeclimate, checkstyle, junit, html, emoji, sonarqube, markdown, github-actions-logging)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 커스텀 룰 정의
|
#### 커스텀 룰 정의
|
||||||
|
@ -268,16 +296,16 @@ reporter: "xcode" # 보고 유형 (xcode, json, csv, checkstyle, junit, html, em
|
||||||
```yaml
|
```yaml
|
||||||
custom_rules:
|
custom_rules:
|
||||||
pirates_beat_ninjas: # 룰 식별자
|
pirates_beat_ninjas: # 룰 식별자
|
||||||
included: ".*.swift" # 린트 실행시 포함할 경로를 정의하는 정규표현식. 선택 가능.
|
included: ".*.swift" # 린트 실행 시 포함할 경로를 정의하는 정규표현식. 선택 가능.
|
||||||
name: "Pirates Beat Ninjas" # 룰 이름. 선택 가능.
|
name: "Pirates Beat Ninjas" # 룰 이름. 선택 가능.
|
||||||
regex: "([n,N]inja)" # 패턴 매칭
|
regex: "([nN]inja)" # 패턴 매칭
|
||||||
match_kinds: # 매칭할 SyntaxKinds. 선택 가능.
|
match_kinds: # 매칭할 SyntaxKinds. 선택 가능.
|
||||||
- comment
|
- comment
|
||||||
- identifier
|
- identifier
|
||||||
message: "Pirates are better than ninjas." # 위반 메시지. 선택 가능.
|
message: "Pirates are better than ninjas." # 위반 메시지. 선택 가능.
|
||||||
severity: error # 위반 수준. 선택 가능.
|
severity: error # 위반 수준. 선택 가능.
|
||||||
no_hiding_in_strings:
|
no_hiding_in_strings:
|
||||||
regex: "([n,N]inja)"
|
regex: "([nN]inja)"
|
||||||
match_kinds: string
|
match_kinds: string
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -285,7 +313,7 @@ custom_rules:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
하나 이상의 `match_kinds`를 사용해서 매칭된 결과를 필터링 할 수 있습니다. 이 목록에 들어있지 않은 구문 유형이 포함된 결과는 매칭에서 제외됩니다. 사용 가능한 모든 구문 유형은 다음과 같습니다.
|
하나 이상의 `match_kinds`를 사용해서 매칭된 결과를 필터링할 수 있습니다. 이 목록에 들어있지 않은 구문 유형이 포함된 결과는 매칭에서 제외됩니다. 사용 가능한 모든 구문 유형은 다음과 같습니다.
|
||||||
|
|
||||||
* argument
|
* argument
|
||||||
* attribute.builtin
|
* attribute.builtin
|
||||||
|
@ -311,13 +339,13 @@ custom_rules:
|
||||||
|
|
||||||
SwiftLint는 설정 파일을 중첩되게 구성해서 린트 과정을 더욱 세밀하게 제어할 수 있습니다.
|
SwiftLint는 설정 파일을 중첩되게 구성해서 린트 과정을 더욱 세밀하게 제어할 수 있습니다.
|
||||||
|
|
||||||
* 디렉토리 구조에서 필요한 곳이면 어디든지 `.swiftlint.yml` 파일을 추가할 수 있습니다.
|
* 디렉터리 구조에서 필요한 곳이면 어디든지 `.swiftlint.yml` 파일을 추가할 수 있습니다.
|
||||||
* 각 파일은 자신의 디렉토리 내에 있는 설정 파일을 사용하거나, 계층구조 상 가장 가까운 부모 디렉토리에 있는 설정 파일을 사용해서 린트됩니다. 별도로 설정 파일이 존재하지 않으면 루트에 있는 설정 파일이 사용됩니다.
|
* 각 파일은 자신의 디렉터리 내에 있는 설정 파일을 사용하거나, 계층구조 상 가장 가까운 부모 디렉터리에 있는 설정 파일을 사용해서 린트됩니다. 별도로 설정 파일이 존재하지 않으면 루트에 있는 설정 파일이 사용됩니다.
|
||||||
* 중첩 구성에서 `excluded` 및 `included`는 무시됩니다.
|
* 중첩 구성에서 `excluded` 및 `included`는 무시됩니다.
|
||||||
|
|
||||||
### 자동 수정
|
### 자동 수정
|
||||||
|
|
||||||
SwiftLint는 일부 위반 사항들을 자동으로 수정할 수 있습니다. 디스크상의 파일들은 수정된 버전으로 덮어 쓰여지게 됩니다.
|
SwiftLint는 일부 위반 사항들을 자동으로 수정할 수 있습니다. 디스크 상의 파일들은 수정된 버전으로 덮어 쓰여지게 됩니다.
|
||||||
|
|
||||||
`swiftlint autocorrect`를 실행하기 전에 파일들을 백업해주세요. 그렇지 않으면 중요한 데이터가 유실될 수도 있습니다.
|
`swiftlint autocorrect`를 실행하기 전에 파일들을 백업해주세요. 그렇지 않으면 중요한 데이터가 유실될 수도 있습니다.
|
||||||
|
|
||||||
|
|
18
Releasing.md
18
Releasing.md
|
@ -7,16 +7,8 @@ For SwiftLint contributors, follow these steps to cut a release:
|
||||||
* FabricSoftenerRule
|
* FabricSoftenerRule
|
||||||
* Top Loading
|
* Top Loading
|
||||||
* Fresh Out Of The Dryer
|
* Fresh Out Of The Dryer
|
||||||
2. Push new version: `make push_version "0.2.0: Tumble Dry"`
|
1. Make sure you have the latest stable Xcode version installed and
|
||||||
3. Make sure you have the latest stable Xcode version installed and
|
`xcode-select`ed
|
||||||
`xcode-select`ed.
|
1. Release new version: `make release "0.2.0: Tumble Dry"`
|
||||||
4. Create the pkg installer, framework zip, and portable zip: `make release`
|
1. Wait for the Docker CI job to finish then run: `make zip_linux_release`
|
||||||
5. Create a GitHub release: https://github.com/realm/SwiftLint/releases/new
|
1. Celebrate. :tada:
|
||||||
* Specify the tag you just pushed from the dropdown.
|
|
||||||
* Set the release title to the new version number & release name.
|
|
||||||
* Add the changelog section to the release description text box.
|
|
||||||
* Upload the pkg installer, framework zip, and portable zip you just built
|
|
||||||
to the GitHub release binaries.
|
|
||||||
* Click "Publish release".
|
|
||||||
6. Publish to Homebrew and CocoaPods trunk: `make publish`
|
|
||||||
7. Celebrate. :tada:
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
#ifdef __APPLE__
|
||||||
|
|
||||||
|
#include "objc_dupclass.h"
|
||||||
|
|
||||||
|
OBJC_DUPCLASS(_TtC11SwiftSyntax11SyntaxArena);
|
||||||
|
OBJC_DUPCLASS(_TtC11SwiftSyntax13SyntaxVisitor);
|
||||||
|
OBJC_DUPCLASS(_TtC11SwiftSyntax14SyntaxRewriter);
|
||||||
|
OBJC_DUPCLASS(_TtC11SwiftSyntax16BumpPtrAllocator);
|
||||||
|
OBJC_DUPCLASS(_TtC11SwiftSyntax16SyntaxAnyVisitor);
|
||||||
|
OBJC_DUPCLASS(_TtC11SwiftSyntax18ParsingSyntaxArena);
|
||||||
|
OBJC_DUPCLASS(_TtC11SwiftSyntax23SourceLocationConverter);
|
||||||
|
OBJC_DUPCLASS(_TtC11SwiftSyntax26IncrementalParseTransition);
|
||||||
|
OBJC_DUPCLASS(_TtC11SwiftSyntax35IncrementalParseReusedNodeCollector);
|
||||||
|
|
||||||
|
#endif // __APPLE__
|
|
@ -0,0 +1,19 @@
|
||||||
|
// https://github.com/keith/objc_dupclass
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// TODO: This isn't entirely accurate, but I'm not sure how to more accurately determine
|
||||||
|
#if (defined(__arm64__) || defined(DUPCLASS_FORCE_DATA_CONST)) && !defined(DUPCLASS_FORCE_DATA)
|
||||||
|
#define SECTION "__DATA_CONST"
|
||||||
|
#else
|
||||||
|
#define SECTION "__DATA"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Struct layout from https://github.com/apple-oss-distributions/objc4/blob/8701d5672d3fd3cd817aeb84db1077aafe1a1604/runtime/objc-abi.h#L175-L183
|
||||||
|
#define OBJC_DUPCLASS(kclass) \
|
||||||
|
__attribute__((used)) __attribute__((visibility("hidden"))) \
|
||||||
|
static struct { uint32_t version; uint32_t flags; const char name[64]; } \
|
||||||
|
const __duplicate_class_##kclass = { 0, 0, #kclass }; \
|
||||||
|
\
|
||||||
|
__attribute__((used)) __attribute__((visibility("hidden"))) \
|
||||||
|
__attribute__((section (SECTION",__objc_dupclass"))) \
|
||||||
|
const void* __set___objc_dupclass_sym___duplicate_class_##kclass = &__duplicate_class_##kclass
|
|
@ -0,0 +1 @@
|
||||||
|
@_exported import SwiftLintCore
|
|
@ -0,0 +1,187 @@
|
||||||
|
import SourceKittenFramework
|
||||||
|
|
||||||
|
/// Struct to represent SwiftUI ViewModifiers for the purpose of finding modifiers in a substructure.
|
||||||
|
struct SwiftUIModifier {
|
||||||
|
/// Name of the modifier.
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
/// List of arguments to check for in the modifier.
|
||||||
|
let arguments: [Argument]
|
||||||
|
|
||||||
|
struct Argument {
|
||||||
|
/// Name of the argument we want to find. For single unnamed arguments, use the empty string.
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
/// Whether or not the argument is required. If the argument is present, value checks are enforced.
|
||||||
|
/// Allows for better handling of modifiers with default values for certain arguments where we want
|
||||||
|
/// to ensure that the default value is used.
|
||||||
|
let required: Bool
|
||||||
|
|
||||||
|
/// List of possible values for the argument. Typically should just be a list with a single element,
|
||||||
|
/// but allows for the flexibility of checking for multiple possible values. To only check for the presence
|
||||||
|
/// of the modifier and not enforce any certain values, pass an empty array. All values are parsed as
|
||||||
|
/// Strings; for other types (boolean, numeric, optional, etc) types you can check for "true", "5", "nil", etc.
|
||||||
|
let values: [String]
|
||||||
|
|
||||||
|
/// Success criteria used for matching values (prefix, suffix, substring, exact match, or none).
|
||||||
|
let matchType: MatchType
|
||||||
|
|
||||||
|
init(name: String, required: Bool = true, values: [String], matchType: MatchType = .exactMatch) {
|
||||||
|
self.name = name
|
||||||
|
self.required = required
|
||||||
|
self.values = values
|
||||||
|
self.matchType = matchType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MatchType {
|
||||||
|
case prefix, suffix, substring, exactMatch
|
||||||
|
|
||||||
|
/// Compares the parsed argument value to a target value for the given match type
|
||||||
|
/// and returns true is a match is found.
|
||||||
|
func matches(argumentValue: String, targetValue: String) -> Bool {
|
||||||
|
switch self {
|
||||||
|
case .prefix:
|
||||||
|
return argumentValue.hasPrefix(targetValue)
|
||||||
|
case .suffix:
|
||||||
|
return argumentValue.hasSuffix(targetValue)
|
||||||
|
case .substring:
|
||||||
|
return argumentValue.contains(targetValue)
|
||||||
|
case .exactMatch:
|
||||||
|
return argumentValue == targetValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extensions for recursively checking SwiftUI code for certain modifiers.
|
||||||
|
extension SourceKittenDictionary {
|
||||||
|
/// Call on a SwiftUI View to recursively check the substructure for a certain modifier with certain arguments.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - modifiers: A list of `SwiftUIModifier` structs to check for in the view's substructure.
|
||||||
|
/// In most cases, this can just be a single modifier, but since some modifiers have
|
||||||
|
/// multiple versions, this enables checking for any modifier from the list.
|
||||||
|
/// - file: The SwiftLintFile object for the current file, used to extract argument values.
|
||||||
|
/// - Returns: A boolean value representing whether or not the given modifier with the specified
|
||||||
|
/// arguments appears in the view's substructure.
|
||||||
|
func hasModifier(anyOf modifiers: [SwiftUIModifier], in file: SwiftLintFile) -> Bool {
|
||||||
|
// SwiftUI ViewModifiers are treated as `call` expressions, and we make sure we can get the expression's name.
|
||||||
|
guard expressionKind == .call, let name else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any modifier from the list matches, return true.
|
||||||
|
for modifier in modifiers {
|
||||||
|
// Check for the given modifier name
|
||||||
|
guard name.hasSuffix(modifier.name) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check arguments.
|
||||||
|
var matchesArgs = true
|
||||||
|
for argument in modifier.arguments {
|
||||||
|
var foundArg = false
|
||||||
|
var argValue: String?
|
||||||
|
|
||||||
|
// Check for single unnamed argument.
|
||||||
|
if argument.name.isEmpty {
|
||||||
|
foundArg = true
|
||||||
|
argValue = getSingleUnnamedArgumentValue(in: file)
|
||||||
|
} else if let parsedArgument = enclosedArguments.first(where: { $0.name == argument.name }) {
|
||||||
|
foundArg = true
|
||||||
|
argValue = parsedArgument.getArgumentValue(in: file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If argument is not required and we didn't find it, continue.
|
||||||
|
if !foundArg && !argument.required {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we must have found an argument with a non-nil value to continue.
|
||||||
|
guard foundArg, let argumentValue = argValue else {
|
||||||
|
matchesArgs = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Argument value can match any of the options given in the argument struct.
|
||||||
|
if argument.values.isEmpty || argument.values.contains(where: {
|
||||||
|
argument.matchType.matches(argumentValue: argumentValue, targetValue: $0)
|
||||||
|
}) {
|
||||||
|
// Found a match, continue to next argument.
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
// Did not find a match, exit loop over arguments.
|
||||||
|
matchesArgs = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return true if all arguments matched
|
||||||
|
if matchesArgs {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively check substructure.
|
||||||
|
// SwiftUI literal Views with modifiers will have a SourceKittenDictionary structure like:
|
||||||
|
// Image("myImage").resizable().accessibility(hidden: true).frame
|
||||||
|
// --> Image("myImage").resizable().accessibility
|
||||||
|
// --> Image("myImage").resizable
|
||||||
|
// --> Image
|
||||||
|
return substructure.contains(where: { $0.hasModifier(anyOf: modifiers, in: file) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Sample use cases of `hasModifier` that are used in multiple rules
|
||||||
|
|
||||||
|
/// Whether or not the dictionary represents a SwiftUI View with an `accesibilityHidden(true)`
|
||||||
|
/// or `accessibility(hidden: true)` modifier.
|
||||||
|
func hasAccessibilityHiddenModifier(in file: SwiftLintFile) -> Bool {
|
||||||
|
return hasModifier(
|
||||||
|
anyOf: [
|
||||||
|
SwiftUIModifier(
|
||||||
|
name: "accessibilityHidden",
|
||||||
|
arguments: [.init(name: "", values: ["true"])]
|
||||||
|
),
|
||||||
|
SwiftUIModifier(
|
||||||
|
name: "accessibility",
|
||||||
|
arguments: [.init(name: "hidden", values: ["true"])]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
in: file
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether or not the dictionary represents a SwiftUI View with an `accessibilityElement()` or
|
||||||
|
/// `accessibilityElement(children: .ignore)` modifier (`.ignore` is the default parameter value).
|
||||||
|
func hasAccessibilityElementChildrenIgnoreModifier(in file: SwiftLintFile) -> Bool {
|
||||||
|
return hasModifier(
|
||||||
|
anyOf: [
|
||||||
|
SwiftUIModifier(
|
||||||
|
name: "accessibilityElement",
|
||||||
|
arguments: [.init(name: "children", required: false, values: [".ignore"], matchType: .suffix)]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
in: file
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Helpers to extract argument values
|
||||||
|
|
||||||
|
/// Helper to get the value of an argument.
|
||||||
|
func getArgumentValue(in file: SwiftLintFile) -> String? {
|
||||||
|
guard expressionKind == .argument, let bodyByteRange else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.stringView.substringWithByteRange(bodyByteRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to get the value of a single unnamed argument to a function call.
|
||||||
|
func getSingleUnnamedArgumentValue(in file: SwiftLintFile) -> String? {
|
||||||
|
guard expressionKind == .call, let bodyByteRange else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.stringView.substringWithByteRange(bodyByteRange)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
import SwiftSyntaxBuilder
|
||||||
|
|
||||||
|
/// A helper to hold a visitor and rewriter that can lint and correct legacy NS/CG functions to a more modern syntax.
|
||||||
|
enum LegacyFunctionRuleHelper {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
private let legacyFunctions: [String: RewriteStrategy]
|
||||||
|
|
||||||
|
init(legacyFunctions: [String: RewriteStrategy]) {
|
||||||
|
self.legacyFunctions = legacyFunctions
|
||||||
|
super.init(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: FunctionCallExprSyntax) {
|
||||||
|
if node.isLegacyFunctionExpression(legacyFunctions: legacyFunctions) {
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RewriteStrategy {
|
||||||
|
case equal
|
||||||
|
case property(name: String)
|
||||||
|
case function(name: String, argumentLabels: [String], reversed: Bool = false)
|
||||||
|
|
||||||
|
var expectedInitialArguments: Int {
|
||||||
|
switch self {
|
||||||
|
case .equal:
|
||||||
|
return 2
|
||||||
|
case .property:
|
||||||
|
return 1
|
||||||
|
case .function(name: _, argumentLabels: let argumentLabels, reversed: _):
|
||||||
|
return argumentLabels.count + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
|
||||||
|
private(set) var correctionPositions: [AbsolutePosition] = []
|
||||||
|
private let locationConverter: SourceLocationConverter
|
||||||
|
private let disabledRegions: [SourceRange]
|
||||||
|
private let legacyFunctions: [String: RewriteStrategy]
|
||||||
|
|
||||||
|
init(
|
||||||
|
legacyFunctions: [String: RewriteStrategy],
|
||||||
|
locationConverter: SourceLocationConverter,
|
||||||
|
disabledRegions: [SourceRange]
|
||||||
|
) {
|
||||||
|
self.legacyFunctions = legacyFunctions
|
||||||
|
self.locationConverter = locationConverter
|
||||||
|
self.disabledRegions = disabledRegions
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
|
||||||
|
guard
|
||||||
|
node.isLegacyFunctionExpression(legacyFunctions: legacyFunctions),
|
||||||
|
let funcName = node.calledExpression.as(IdentifierExprSyntax.self)?.identifier.text,
|
||||||
|
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
|
||||||
|
else {
|
||||||
|
return super.visit(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
|
||||||
|
let trimmedArguments = node.argumentList.map { $0.trimmingTrailingComma() }
|
||||||
|
let rewriteStrategy = legacyFunctions[funcName]
|
||||||
|
|
||||||
|
let expr: ExprSyntax
|
||||||
|
switch rewriteStrategy {
|
||||||
|
case .equal:
|
||||||
|
expr = "\(trimmedArguments[0]) == \(trimmedArguments[1])"
|
||||||
|
case let .property(name: propertyName):
|
||||||
|
expr = "\(trimmedArguments[0]).\(raw: propertyName)"
|
||||||
|
case let .function(name: functionName, argumentLabels: argumentLabels, reversed: reversed):
|
||||||
|
let arguments = reversed ? trimmedArguments.reversed() : trimmedArguments
|
||||||
|
let params = zip(argumentLabels, arguments.dropFirst())
|
||||||
|
.map { $0.isEmpty ? "\($1)" : "\($0): \($1)" }
|
||||||
|
.joined(separator: ", ")
|
||||||
|
expr = "\(arguments[0]).\(raw: functionName)(\(raw: params))"
|
||||||
|
case .none:
|
||||||
|
return super.visit(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr
|
||||||
|
.with(\.leadingTrivia, node.leadingTrivia)
|
||||||
|
.with(\.trailingTrivia, node.trailingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FunctionCallExprSyntax {
|
||||||
|
func isLegacyFunctionExpression(legacyFunctions: [String: LegacyFunctionRuleHelper.RewriteStrategy]) -> Bool {
|
||||||
|
guard
|
||||||
|
let calledExpression = calledExpression.as(IdentifierExprSyntax.self),
|
||||||
|
let rewriteStrategy = legacyFunctions[calledExpression.identifier.text],
|
||||||
|
argumentList.count == rewriteStrategy.expectedInitialArguments
|
||||||
|
else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension TupleExprElementSyntax {
|
||||||
|
func trimmingTrailingComma() -> TupleExprElementSyntax {
|
||||||
|
self.trimmed.with(\.trailingComma, nil).trimmed
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,18 @@
|
||||||
// Generated using Sourcery 0.17.0 — https://github.com/krzysztofzablocki/Sourcery
|
// Generated using Sourcery 2.0.2 — https://github.com/krzysztofzablocki/Sourcery
|
||||||
// DO NOT EDIT
|
// DO NOT EDIT
|
||||||
|
|
||||||
/// The rule list containing all available rules built into SwiftLint.
|
/// The rule list containing all available rules built into SwiftLint.
|
||||||
public let masterRuleList = RuleList(rules: [
|
public let builtInRules: [Rule.Type] = [
|
||||||
|
AccessibilityLabelForImageRule.self,
|
||||||
|
AccessibilityTraitForButtonRule.self,
|
||||||
|
AnonymousArgumentInMultilineClosureRule.self,
|
||||||
AnyObjectProtocolRule.self,
|
AnyObjectProtocolRule.self,
|
||||||
ArrayInitRule.self,
|
ArrayInitRule.self,
|
||||||
AttributesRule.self,
|
AttributesRule.self,
|
||||||
|
BalancedXCTestLifecycleRule.self,
|
||||||
|
BlanketDisableCommandRule.self,
|
||||||
BlockBasedKVORule.self,
|
BlockBasedKVORule.self,
|
||||||
|
CaptureVariableRule.self,
|
||||||
ClassDelegateProtocolRule.self,
|
ClassDelegateProtocolRule.self,
|
||||||
ClosingBraceRule.self,
|
ClosingBraceRule.self,
|
||||||
ClosureBodyLengthRule.self,
|
ClosureBodyLengthRule.self,
|
||||||
|
@ -15,8 +21,11 @@ public let masterRuleList = RuleList(rules: [
|
||||||
ClosureSpacingRule.self,
|
ClosureSpacingRule.self,
|
||||||
CollectionAlignmentRule.self,
|
CollectionAlignmentRule.self,
|
||||||
ColonRule.self,
|
ColonRule.self,
|
||||||
|
CommaInheritanceRule.self,
|
||||||
CommaRule.self,
|
CommaRule.self,
|
||||||
|
CommentSpacingRule.self,
|
||||||
CompilerProtocolInitRule.self,
|
CompilerProtocolInitRule.self,
|
||||||
|
ComputedAccessorsOrderRule.self,
|
||||||
ConditionalReturnsOnNewlineRule.self,
|
ConditionalReturnsOnNewlineRule.self,
|
||||||
ContainsOverFilterCountRule.self,
|
ContainsOverFilterCountRule.self,
|
||||||
ContainsOverFilterIsEmptyRule.self,
|
ContainsOverFilterIsEmptyRule.self,
|
||||||
|
@ -24,16 +33,20 @@ public let masterRuleList = RuleList(rules: [
|
||||||
ContainsOverRangeNilComparisonRule.self,
|
ContainsOverRangeNilComparisonRule.self,
|
||||||
ControlStatementRule.self,
|
ControlStatementRule.self,
|
||||||
ConvenienceTypeRule.self,
|
ConvenienceTypeRule.self,
|
||||||
CustomRules.self,
|
|
||||||
CyclomaticComplexityRule.self,
|
CyclomaticComplexityRule.self,
|
||||||
DeploymentTargetRule.self,
|
DeploymentTargetRule.self,
|
||||||
|
DirectReturnRule.self,
|
||||||
DiscardedNotificationCenterObserverRule.self,
|
DiscardedNotificationCenterObserverRule.self,
|
||||||
|
DiscouragedAssertRule.self,
|
||||||
DiscouragedDirectInitRule.self,
|
DiscouragedDirectInitRule.self,
|
||||||
|
DiscouragedNoneNameRule.self,
|
||||||
DiscouragedObjectLiteralRule.self,
|
DiscouragedObjectLiteralRule.self,
|
||||||
DiscouragedOptionalBooleanRule.self,
|
DiscouragedOptionalBooleanRule.self,
|
||||||
DiscouragedOptionalCollectionRule.self,
|
DiscouragedOptionalCollectionRule.self,
|
||||||
|
DuplicateConditionsRule.self,
|
||||||
DuplicateEnumCasesRule.self,
|
DuplicateEnumCasesRule.self,
|
||||||
DuplicateImportsRule.self,
|
DuplicateImportsRule.self,
|
||||||
|
DuplicatedKeyInDictionaryLiteralRule.self,
|
||||||
DynamicInlineRule.self,
|
DynamicInlineRule.self,
|
||||||
EmptyCollectionLiteralRule.self,
|
EmptyCollectionLiteralRule.self,
|
||||||
EmptyCountRule.self,
|
EmptyCountRule.self,
|
||||||
|
@ -68,13 +81,16 @@ public let masterRuleList = RuleList(rules: [
|
||||||
FunctionDefaultParameterAtEndRule.self,
|
FunctionDefaultParameterAtEndRule.self,
|
||||||
FunctionParameterCountRule.self,
|
FunctionParameterCountRule.self,
|
||||||
GenericTypeNameRule.self,
|
GenericTypeNameRule.self,
|
||||||
|
IBInspectableInExtensionRule.self,
|
||||||
IdenticalOperandsRule.self,
|
IdenticalOperandsRule.self,
|
||||||
IdentifierNameRule.self,
|
IdentifierNameRule.self,
|
||||||
ImplicitGetterRule.self,
|
ImplicitGetterRule.self,
|
||||||
ImplicitReturnRule.self,
|
ImplicitReturnRule.self,
|
||||||
ImplicitlyUnwrappedOptionalRule.self,
|
ImplicitlyUnwrappedOptionalRule.self,
|
||||||
|
InclusiveLanguageRule.self,
|
||||||
IndentationWidthRule.self,
|
IndentationWidthRule.self,
|
||||||
InertDeferRule.self,
|
InertDeferRule.self,
|
||||||
|
InvalidSwiftLintCommandRule.self,
|
||||||
IsDisjointRule.self,
|
IsDisjointRule.self,
|
||||||
JoinedDefaultParameterRule.self,
|
JoinedDefaultParameterRule.self,
|
||||||
LargeTupleRule.self,
|
LargeTupleRule.self,
|
||||||
|
@ -86,10 +102,12 @@ public let masterRuleList = RuleList(rules: [
|
||||||
LegacyHashingRule.self,
|
LegacyHashingRule.self,
|
||||||
LegacyMultipleRule.self,
|
LegacyMultipleRule.self,
|
||||||
LegacyNSGeometryFunctionsRule.self,
|
LegacyNSGeometryFunctionsRule.self,
|
||||||
|
LegacyObjcTypeRule.self,
|
||||||
LegacyRandomRule.self,
|
LegacyRandomRule.self,
|
||||||
LetVarWhitespaceRule.self,
|
LetVarWhitespaceRule.self,
|
||||||
LineLengthRule.self,
|
LineLengthRule.self,
|
||||||
LiteralExpressionEndIdentationRule.self,
|
LiteralExpressionEndIndentationRule.self,
|
||||||
|
LocalDocCommentRule.self,
|
||||||
LowerACLThanParentRule.self,
|
LowerACLThanParentRule.self,
|
||||||
MarkRule.self,
|
MarkRule.self,
|
||||||
MissingDocsRule.self,
|
MissingDocsRule.self,
|
||||||
|
@ -103,12 +121,14 @@ public let masterRuleList = RuleList(rules: [
|
||||||
MultipleClosuresWithTrailingClosureRule.self,
|
MultipleClosuresWithTrailingClosureRule.self,
|
||||||
NSLocalizedStringKeyRule.self,
|
NSLocalizedStringKeyRule.self,
|
||||||
NSLocalizedStringRequireBundleRule.self,
|
NSLocalizedStringRequireBundleRule.self,
|
||||||
|
NSNumberInitAsFunctionReferenceRule.self,
|
||||||
NSObjectPreferIsEqualRule.self,
|
NSObjectPreferIsEqualRule.self,
|
||||||
NestingRule.self,
|
NestingRule.self,
|
||||||
NimbleOperatorRule.self,
|
NimbleOperatorRule.self,
|
||||||
NoExtensionAccessModifierRule.self,
|
NoExtensionAccessModifierRule.self,
|
||||||
NoFallthroughOnlyRule.self,
|
NoFallthroughOnlyRule.self,
|
||||||
NoGroupingExtensionRule.self,
|
NoGroupingExtensionRule.self,
|
||||||
|
NoMagicNumbersRule.self,
|
||||||
NoSpaceInMethodCallRule.self,
|
NoSpaceInMethodCallRule.self,
|
||||||
NotificationCenterDetachmentRule.self,
|
NotificationCenterDetachmentRule.self,
|
||||||
NumberSeparatorRule.self,
|
NumberSeparatorRule.self,
|
||||||
|
@ -121,12 +141,16 @@ public let masterRuleList = RuleList(rules: [
|
||||||
OverriddenSuperCallRule.self,
|
OverriddenSuperCallRule.self,
|
||||||
OverrideInExtensionRule.self,
|
OverrideInExtensionRule.self,
|
||||||
PatternMatchingKeywordsRule.self,
|
PatternMatchingKeywordsRule.self,
|
||||||
PhohibitedNaNComparisonRule.self,
|
PeriodSpacingRule.self,
|
||||||
|
PreferNimbleRule.self,
|
||||||
|
PreferSelfInStaticReferencesRule.self,
|
||||||
PreferSelfTypeOverTypeOfSelfRule.self,
|
PreferSelfTypeOverTypeOfSelfRule.self,
|
||||||
|
PreferZeroOverExplicitInitRule.self,
|
||||||
PrefixedTopLevelConstantRule.self,
|
PrefixedTopLevelConstantRule.self,
|
||||||
PrivateActionRule.self,
|
PrivateActionRule.self,
|
||||||
PrivateOutletRule.self,
|
PrivateOutletRule.self,
|
||||||
PrivateOverFilePrivateRule.self,
|
PrivateOverFilePrivateRule.self,
|
||||||
|
PrivateSubjectRule.self,
|
||||||
PrivateUnitTestRule.self,
|
PrivateUnitTestRule.self,
|
||||||
ProhibitedInterfaceBuilderRule.self,
|
ProhibitedInterfaceBuilderRule.self,
|
||||||
ProhibitedSuperRule.self,
|
ProhibitedSuperRule.self,
|
||||||
|
@ -141,6 +165,7 @@ public let masterRuleList = RuleList(rules: [
|
||||||
RedundantNilCoalescingRule.self,
|
RedundantNilCoalescingRule.self,
|
||||||
RedundantObjcAttributeRule.self,
|
RedundantObjcAttributeRule.self,
|
||||||
RedundantOptionalInitializationRule.self,
|
RedundantOptionalInitializationRule.self,
|
||||||
|
RedundantSelfInClosureRule.self,
|
||||||
RedundantSetAccessControlRule.self,
|
RedundantSetAccessControlRule.self,
|
||||||
RedundantStringEnumValueRule.self,
|
RedundantStringEnumValueRule.self,
|
||||||
RedundantTypeAnnotationRule.self,
|
RedundantTypeAnnotationRule.self,
|
||||||
|
@ -149,18 +174,23 @@ public let masterRuleList = RuleList(rules: [
|
||||||
RequiredEnumCaseRule.self,
|
RequiredEnumCaseRule.self,
|
||||||
ReturnArrowWhitespaceRule.self,
|
ReturnArrowWhitespaceRule.self,
|
||||||
ReturnValueFromVoidFunctionRule.self,
|
ReturnValueFromVoidFunctionRule.self,
|
||||||
|
SelfBindingRule.self,
|
||||||
|
SelfInPropertyInitializationRule.self,
|
||||||
ShorthandOperatorRule.self,
|
ShorthandOperatorRule.self,
|
||||||
|
ShorthandOptionalBindingRule.self,
|
||||||
SingleTestClassRule.self,
|
SingleTestClassRule.self,
|
||||||
|
SortedEnumCasesRule.self,
|
||||||
SortedFirstLastRule.self,
|
SortedFirstLastRule.self,
|
||||||
SortedImportsRule.self,
|
SortedImportsRule.self,
|
||||||
StatementPositionRule.self,
|
StatementPositionRule.self,
|
||||||
StaticOperatorRule.self,
|
StaticOperatorRule.self,
|
||||||
StrictFilePrivateRule.self,
|
StrictFilePrivateRule.self,
|
||||||
StrongIBOutletRule.self,
|
StrongIBOutletRule.self,
|
||||||
SuperfluousDisableCommandRule.self,
|
SuperfluousElseRule.self,
|
||||||
SwitchCaseAlignmentRule.self,
|
SwitchCaseAlignmentRule.self,
|
||||||
SwitchCaseOnNewlineRule.self,
|
SwitchCaseOnNewlineRule.self,
|
||||||
SyntacticSugarRule.self,
|
SyntacticSugarRule.self,
|
||||||
|
TestCaseAccessibilityRule.self,
|
||||||
TodoRule.self,
|
TodoRule.self,
|
||||||
ToggleBoolRule.self,
|
ToggleBoolRule.self,
|
||||||
TrailingClosureRule.self,
|
TrailingClosureRule.self,
|
||||||
|
@ -168,11 +198,13 @@ public let masterRuleList = RuleList(rules: [
|
||||||
TrailingNewlineRule.self,
|
TrailingNewlineRule.self,
|
||||||
TrailingSemicolonRule.self,
|
TrailingSemicolonRule.self,
|
||||||
TrailingWhitespaceRule.self,
|
TrailingWhitespaceRule.self,
|
||||||
TuplePatternRule.self,
|
|
||||||
TypeBodyLengthRule.self,
|
TypeBodyLengthRule.self,
|
||||||
TypeContentsOrderRule.self,
|
TypeContentsOrderRule.self,
|
||||||
TypeNameRule.self,
|
TypeNameRule.self,
|
||||||
|
TypesafeArrayInitRule.self,
|
||||||
|
UnavailableConditionRule.self,
|
||||||
UnavailableFunctionRule.self,
|
UnavailableFunctionRule.self,
|
||||||
|
UnhandledThrowingTaskRule.self,
|
||||||
UnneededBreakInSwitchRule.self,
|
UnneededBreakInSwitchRule.self,
|
||||||
UnneededParenthesesInClosureArgumentRule.self,
|
UnneededParenthesesInClosureArgumentRule.self,
|
||||||
UnownedVariableCaptureRule.self,
|
UnownedVariableCaptureRule.self,
|
||||||
|
@ -198,4 +230,4 @@ public let masterRuleList = RuleList(rules: [
|
||||||
XCTFailMessageRule.self,
|
XCTFailMessageRule.self,
|
||||||
XCTSpecificMatcherRule.self,
|
XCTSpecificMatcherRule.self,
|
||||||
YodaConditionRule.self
|
YodaConditionRule.self
|
||||||
])
|
]
|
|
@ -0,0 +1,29 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Represents unused or missing import statements.
|
||||||
|
enum ImportUsage {
|
||||||
|
/// The import is unused. Range is for the entire import statement.
|
||||||
|
case unused(module: String, range: NSRange)
|
||||||
|
/// The file is missing an explicit import of the `module`.
|
||||||
|
case missing(module: String)
|
||||||
|
|
||||||
|
/// The range where the violation for this import usage should be reported.
|
||||||
|
var violationRange: NSRange? {
|
||||||
|
switch self {
|
||||||
|
case .unused(_, let range):
|
||||||
|
return range
|
||||||
|
case .missing:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reason why this import usage is a violation.
|
||||||
|
var violationReason: String? {
|
||||||
|
switch self {
|
||||||
|
case .unused:
|
||||||
|
return nil
|
||||||
|
case .missing(let module):
|
||||||
|
return "Missing import for referenced module '\(module)'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct AnonymousArgumentInMultilineClosureRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "anonymous_argument_in_multiline_closure",
|
||||||
|
name: "Anonymous Argument in Multiline Closure",
|
||||||
|
description: "Use named arguments in multiline closures",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("closure { $0 }"),
|
||||||
|
Example("closure { print($0) }"),
|
||||||
|
Example("""
|
||||||
|
closure { arg in
|
||||||
|
print(arg)
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
closure { arg in
|
||||||
|
nestedClosure { $0 + arg }
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
closure {
|
||||||
|
print(↓$0)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(locationConverter: file.locationConverter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AnonymousArgumentInMultilineClosureRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
private let locationConverter: SourceLocationConverter
|
||||||
|
|
||||||
|
init(locationConverter: SourceLocationConverter) {
|
||||||
|
self.locationConverter = locationConverter
|
||||||
|
super.init(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
|
||||||
|
let startLocation = locationConverter.location(for: node.leftBrace.positionAfterSkippingLeadingTrivia)
|
||||||
|
let endLocation = locationConverter.location(for: node.rightBrace.endPositionBeforeTrailingTrivia)
|
||||||
|
return startLocation.line == endLocation.line ? .skipChildren : .visitChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: IdentifierExprSyntax) {
|
||||||
|
if case .dollarIdentifier = node.identifier.tokenKind {
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct BlockBasedKVORule: SwiftSyntaxRule, ConfigurationProviderRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "block_based_kvo",
|
||||||
|
name: "Block Based KVO",
|
||||||
|
description: "Prefer the new block based KVO API with keypaths when using Swift 3.2 or later",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example(#"""
|
||||||
|
let observer = foo.observe(\.value, options: [.new]) { (foo, change) in
|
||||||
|
print(change.newValue)
|
||||||
|
}
|
||||||
|
"""#)
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
class Foo: NSObject {
|
||||||
|
override ↓func observeValue(forKeyPath keyPath: String?, of object: Any?,
|
||||||
|
change: [NSKeyValueChangeKey : Any]?,
|
||||||
|
context: UnsafeMutableRawPointer?) {}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class Foo: NSObject {
|
||||||
|
override ↓func observeValue(forKeyPath keyPath: String?, of object: Any?,
|
||||||
|
change: Dictionary<NSKeyValueChangeKey, Any>?,
|
||||||
|
context: UnsafeMutableRawPointer?) {}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension BlockBasedKVORule {
|
||||||
|
private final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: FunctionDeclSyntax) {
|
||||||
|
guard node.modifiers.containsOverride,
|
||||||
|
case let parameterList = node.signature.input.parameterList,
|
||||||
|
parameterList.count == 4,
|
||||||
|
node.identifier.text == "observeValue",
|
||||||
|
parameterList.map(\.firstName.text) == ["forKeyPath", "of", "change", "context"]
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let types = parameterList
|
||||||
|
.map { $0.type.trimmedDescription.replacingOccurrences(of: " ", with: "") }
|
||||||
|
let firstTypes = ["String?", "Any?", "[NSKeyValueChangeKey:Any]?", "UnsafeMutableRawPointer?"]
|
||||||
|
let secondTypes = ["String?", "Any?", "Dictionary<NSKeyValueChangeKey,Any>?", "UnsafeMutableRawPointer?"]
|
||||||
|
if types == firstTypes || types == secondTypes {
|
||||||
|
violations.append(node.funcKeyword.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,232 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct ConvenienceTypeRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "convenience_type",
|
||||||
|
name: "Convenience Type",
|
||||||
|
description: "Types used for hosting only static members should be implemented as a caseless enum " +
|
||||||
|
"to avoid instantiation",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
enum Math { // enum
|
||||||
|
public static let pi = 3.14
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
// class with inheritance
|
||||||
|
class MathViewController: UIViewController {
|
||||||
|
public static let pi = 3.14
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@objc class Math: NSObject { // class visible to Obj-C
|
||||||
|
public static let pi = 3.14
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
struct Math { // type with non-static declarations
|
||||||
|
public static let pi = 3.14
|
||||||
|
public let randomNumber = 2
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("class DummyClass {}"),
|
||||||
|
Example("""
|
||||||
|
class Foo: NSObject { // class with Obj-C class property
|
||||||
|
class @objc let foo = 1
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class Foo: NSObject { // class with Obj-C static property
|
||||||
|
static @objc let foo = 1
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class Foo { // @objc class func can't exist on an enum
|
||||||
|
@objc class func foo() {}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class Foo { // @objc static func can't exist on an enum
|
||||||
|
@objc static func foo() {}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@objcMembers class Foo { // @objc static func can't exist on an enum
|
||||||
|
static func foo() {}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
final class Foo { // final class, but @objc class func can't exist on an enum
|
||||||
|
@objc class func foo() {}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
final class Foo { // final class, but @objc static func can't exist on an enum
|
||||||
|
@objc static func foo() {}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@globalActor actor MyActor {
|
||||||
|
static let shared = MyActor()
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
↓struct Math {
|
||||||
|
public static let pi = 3.14
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
↓struct Math {
|
||||||
|
public static let pi = 3.14
|
||||||
|
@available(*, unavailable) init() {}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
final ↓class Foo { // final class can't be inherited
|
||||||
|
class let foo = 1
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
|
||||||
|
// Intentional false positives. Non-final classes could be
|
||||||
|
// subclassed, but we figure it is probably rare enough that it is
|
||||||
|
// more important to catch these cases, and manually disable the
|
||||||
|
// rule if needed.
|
||||||
|
|
||||||
|
Example("""
|
||||||
|
↓class Foo {
|
||||||
|
class let foo = 1
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
↓class Foo {
|
||||||
|
final class let foo = 1
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
↓class SomeClass {
|
||||||
|
static func foo() {}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ConvenienceTypeRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] }
|
||||||
|
|
||||||
|
override func visitPost(_ node: StructDeclSyntax) {
|
||||||
|
if hasViolation(
|
||||||
|
inheritance: node.inheritanceClause,
|
||||||
|
attributes: node.attributes,
|
||||||
|
members: node.memberBlock
|
||||||
|
) {
|
||||||
|
violations.append(node.structKeyword.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: ClassDeclSyntax) {
|
||||||
|
if hasViolation(
|
||||||
|
inheritance: node.inheritanceClause,
|
||||||
|
attributes: node.attributes,
|
||||||
|
members: node.memberBlock
|
||||||
|
) {
|
||||||
|
violations.append(node.classKeyword.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hasViolation(inheritance: TypeInheritanceClauseSyntax?,
|
||||||
|
attributes: AttributeListSyntax?,
|
||||||
|
members: MemberDeclBlockSyntax) -> Bool {
|
||||||
|
guard inheritance.isNilOrEmpty,
|
||||||
|
!attributes.containsObjcMembers,
|
||||||
|
!attributes.containsObjc,
|
||||||
|
!members.members.isEmpty else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConvenienceTypeCheckVisitor(viewMode: .sourceAccurate)
|
||||||
|
.walk(tree: members, handler: \.canBeConvenienceType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ConvenienceTypeCheckVisitor: ViolationsSyntaxVisitor {
|
||||||
|
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .all }
|
||||||
|
|
||||||
|
private(set) var canBeConvenienceType = true
|
||||||
|
|
||||||
|
override func visitPost(_ node: VariableDeclSyntax) {
|
||||||
|
if node.isInstanceVariable {
|
||||||
|
canBeConvenienceType = false
|
||||||
|
} else if node.attributes.containsObjc {
|
||||||
|
canBeConvenienceType = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: FunctionDeclSyntax) {
|
||||||
|
if node.modifiers.containsStaticOrClass {
|
||||||
|
if node.attributes.containsObjc {
|
||||||
|
canBeConvenienceType = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
canBeConvenienceType = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: InitializerDeclSyntax) {
|
||||||
|
if !node.attributes.hasUnavailableAttribute {
|
||||||
|
canBeConvenienceType = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: SubscriptDeclSyntax) {
|
||||||
|
if !node.modifiers.containsStaticOrClass {
|
||||||
|
canBeConvenienceType = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension TypeInheritanceClauseSyntax? {
|
||||||
|
var isNilOrEmpty: Bool {
|
||||||
|
self?.inheritedTypeCollection.isEmpty ?? true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AttributeListSyntax? {
|
||||||
|
var containsObjcMembers: Bool {
|
||||||
|
contains(attributeNamed: "objcMembers")
|
||||||
|
}
|
||||||
|
|
||||||
|
var containsObjc: Bool {
|
||||||
|
contains(attributeNamed: "objc")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AttributeListSyntax? {
|
||||||
|
var hasUnavailableAttribute: Bool {
|
||||||
|
guard let attrs = self else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs.contains { elem in
|
||||||
|
guard let attr = elem.as(AttributeSyntax.self),
|
||||||
|
let arguments = attr.argument?.as(AvailabilitySpecListSyntax.self) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return attr.attributeNameText == "available" && arguments.contains { arg in
|
||||||
|
arg.entry.as(TokenSyntax.self)?.tokenKind.isUnavailableKeyword == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct DiscouragedAssertRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "discouraged_assert",
|
||||||
|
name: "Discouraged Assert",
|
||||||
|
description: "Prefer `assertionFailure()` and/or `preconditionFailure()` over `assert(false)`",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example(#"assert(true)"#),
|
||||||
|
Example(#"assert(true, "foobar")"#),
|
||||||
|
Example(#"assert(true, "foobar", file: "toto", line: 42)"#),
|
||||||
|
Example(#"assert(false || true)"#),
|
||||||
|
Example(#"XCTAssert(false)"#)
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example(#"↓assert(false)"#),
|
||||||
|
Example(#"↓assert(false, "foobar")"#),
|
||||||
|
Example(#"↓assert(false, "foobar", file: "toto", line: 42)"#),
|
||||||
|
Example(#"↓assert( false , "foobar")"#)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension DiscouragedAssertRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: FunctionCallExprSyntax) {
|
||||||
|
guard node.calledExpression.as(IdentifierExprSyntax.self)?.identifier.text == "assert",
|
||||||
|
let firstArg = node.argumentList.first,
|
||||||
|
firstArg.label == nil,
|
||||||
|
let boolExpr = firstArg.expression.as(BooleanLiteralExprSyntax.self),
|
||||||
|
boolExpr.booleanLiteral.tokenKind == .keyword(.false) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,237 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct DiscouragedNoneNameRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static var description = RuleDescription(
|
||||||
|
identifier: "discouraged_none_name",
|
||||||
|
name: "Discouraged None Name",
|
||||||
|
description: "Enum cases and static members named `none` are discouraged as they can conflict with " +
|
||||||
|
"`Optional<T>.none`.",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
// Should not trigger unless exactly matches "none"
|
||||||
|
Example("""
|
||||||
|
enum MyEnum {
|
||||||
|
case nOne
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum MyEnum {
|
||||||
|
case _none
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum MyEnum {
|
||||||
|
case none_
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum MyEnum {
|
||||||
|
case none(Any)
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum MyEnum {
|
||||||
|
case nonenone
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class MyClass {
|
||||||
|
class var nonenone: MyClass { MyClass() }
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class MyClass {
|
||||||
|
static var nonenone = MyClass()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class MyClass {
|
||||||
|
static let nonenone = MyClass()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
struct MyStruct {
|
||||||
|
static var nonenone = MyStruct()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
struct MyStruct {
|
||||||
|
static let nonenone = MyStruct()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
|
||||||
|
// Should not trigger if not an enum case or static/class member
|
||||||
|
Example("""
|
||||||
|
struct MyStruct {
|
||||||
|
let none = MyStruct()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
struct MyStruct {
|
||||||
|
var none = MyStruct()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class MyClass {
|
||||||
|
let none = MyClass()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class MyClass {
|
||||||
|
var none = MyClass()
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
enum MyEnum {
|
||||||
|
case ↓none
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum MyEnum {
|
||||||
|
case a, ↓none
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum MyEnum {
|
||||||
|
case ↓none, b
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum MyEnum {
|
||||||
|
case a, ↓none, b
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum MyEnum {
|
||||||
|
case a
|
||||||
|
case ↓none
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum MyEnum {
|
||||||
|
case ↓none
|
||||||
|
case b
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum MyEnum {
|
||||||
|
case a
|
||||||
|
case ↓none
|
||||||
|
case b
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class MyClass {
|
||||||
|
↓static let none = MyClass()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class MyClass {
|
||||||
|
↓static let none: MyClass = MyClass()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class MyClass {
|
||||||
|
↓static var none: MyClass = MyClass()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class MyClass {
|
||||||
|
↓class var none: MyClass { MyClass() }
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
struct MyStruct {
|
||||||
|
↓static var none = MyStruct()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
struct MyStruct {
|
||||||
|
↓static var none: MyStruct = MyStruct()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
struct MyStruct {
|
||||||
|
↓static var none = MyStruct()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
struct MyStruct {
|
||||||
|
↓static var none: MyStruct = MyStruct()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
struct MyStruct {
|
||||||
|
↓static var a = MyStruct(), none = MyStruct()
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
struct MyStruct {
|
||||||
|
↓static var none = MyStruct(), a = MyStruct()
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension DiscouragedNoneNameRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: EnumCaseElementSyntax) {
|
||||||
|
let emptyParams = node.associatedValue?.parameterList.isEmpty ?? true
|
||||||
|
if emptyParams, node.identifier.isNone {
|
||||||
|
violations.append(ReasonedRuleViolation(
|
||||||
|
position: node.positionAfterSkippingLeadingTrivia,
|
||||||
|
reason: reason(type: "`case`")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: VariableDeclSyntax) {
|
||||||
|
let type: String? = {
|
||||||
|
if node.modifiers.isClass {
|
||||||
|
return "`class` member"
|
||||||
|
} else if node.modifiers.isStatic {
|
||||||
|
return "`static` member"
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
guard let type else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for binding in node.bindings {
|
||||||
|
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self), pattern.identifier.isNone else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.append(ReasonedRuleViolation(
|
||||||
|
position: node.positionAfterSkippingLeadingTrivia,
|
||||||
|
reason: reason(type: type)
|
||||||
|
))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reason(type: String) -> String {
|
||||||
|
let reason = "Avoid naming \(type) `none` as the compiler can think you mean `Optional<T>.none`"
|
||||||
|
let recommendation = "consider using an Optional value instead"
|
||||||
|
return "\(reason); \(recommendation)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension TokenSyntax {
|
||||||
|
var isNone: Bool {
|
||||||
|
tokenKind == .identifier("none") || tokenKind == .identifier("`none`")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct DiscouragedObjectLiteralRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
|
||||||
|
var configuration = DiscouragedObjectLiteralConfiguration()
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "discouraged_object_literal",
|
||||||
|
name: "Discouraged Object Literal",
|
||||||
|
description: "Prefer initializers over object literals",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("let image = UIImage(named: aVariable)"),
|
||||||
|
Example("let image = UIImage(named: \"interpolated \\(variable)\")"),
|
||||||
|
Example("let color = UIColor(red: value, green: value, blue: value, alpha: 1)"),
|
||||||
|
Example("let image = NSImage(named: aVariable)"),
|
||||||
|
Example("let image = NSImage(named: \"interpolated \\(variable)\")"),
|
||||||
|
Example("let color = NSColor(red: value, green: value, blue: value, alpha: 1)")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("let image = ↓#imageLiteral(resourceName: \"image.jpg\")"),
|
||||||
|
Example("let color = ↓#colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(configuration: configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension DiscouragedObjectLiteralRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
private let configuration: ConfigurationType
|
||||||
|
|
||||||
|
init(configuration: ConfigurationType) {
|
||||||
|
self.configuration = configuration
|
||||||
|
super.init(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: MacroExpansionExprSyntax) {
|
||||||
|
guard
|
||||||
|
case let .identifier(identifierText) = node.macro.tokenKind,
|
||||||
|
["colorLiteral", "imageLiteral"].contains(identifierText)
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !configuration.imageLiteral && identifierText == "imageLiteral" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !configuration.colorLiteral && identifierText == "colorLiteral" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct DiscouragedOptionalBooleanRule: OptInRule, ConfigurationProviderRule, SwiftSyntaxRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "discouraged_optional_boolean",
|
||||||
|
name: "Discouraged Optional Boolean",
|
||||||
|
description: "Prefer non-optional booleans over optional booleans",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: DiscouragedOptionalBooleanRuleExamples.nonTriggeringExamples,
|
||||||
|
triggeringExamples: DiscouragedOptionalBooleanRuleExamples.triggeringExamples
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension DiscouragedOptionalBooleanRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: OptionalTypeSyntax) {
|
||||||
|
if node.wrappedType.as(SimpleTypeIdentifierSyntax.self)?.typeName == "Bool" {
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: OptionalChainingExprSyntax) {
|
||||||
|
if node.expression.as(IdentifierExprSyntax.self)?.identifier.text == "Bool" {
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: FunctionCallExprSyntax) {
|
||||||
|
guard
|
||||||
|
let calledExpression = node.calledExpression.as(MemberAccessExprSyntax.self),
|
||||||
|
let singleArgument = node.argumentList.onlyElement,
|
||||||
|
singleArgument.expression.is(BooleanLiteralExprSyntax.self),
|
||||||
|
let base = calledExpression.base?.as(IdentifierExprSyntax.self),
|
||||||
|
base.identifier.text == "Optional",
|
||||||
|
calledExpression.name.text == "some"
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -154,7 +154,10 @@ internal struct DiscouragedOptionalBooleanRuleExamples {
|
||||||
wrapExample("enum", "func foo(input: [↓Bool?]) {}"),
|
wrapExample("enum", "func foo(input: [↓Bool?]) {}"),
|
||||||
wrapExample("enum", "static func foo(input: ↓Bool?) {}"),
|
wrapExample("enum", "static func foo(input: ↓Bool?) {}"),
|
||||||
wrapExample("enum", "static func foo(input: [String: ↓Bool?]) {}"),
|
wrapExample("enum", "static func foo(input: [String: ↓Bool?]) {}"),
|
||||||
wrapExample("enum", "static func foo(input: [↓Bool?]) {}")
|
wrapExample("enum", "static func foo(input: [↓Bool?]) {}"),
|
||||||
|
|
||||||
|
// Optional chaining
|
||||||
|
Example("_ = ↓Bool?.values()")
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,25 @@
|
||||||
import SourceKittenFramework
|
import SourceKittenFramework
|
||||||
|
|
||||||
public struct DiscouragedOptionalCollectionRule: ASTRule, OptInRule, ConfigurationProviderRule, AutomaticTestableRule {
|
struct DiscouragedOptionalCollectionRule: ASTRule, OptInRule, ConfigurationProviderRule {
|
||||||
public var configuration = SeverityConfiguration(.warning)
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
public init() {}
|
static let description = RuleDescription(
|
||||||
|
|
||||||
public static let description = RuleDescription(
|
|
||||||
identifier: "discouraged_optional_collection",
|
identifier: "discouraged_optional_collection",
|
||||||
name: "Discouraged Optional Collection",
|
name: "Discouraged Optional Collection",
|
||||||
description: "Prefer empty collection over optional collection.",
|
description: "Prefer empty collection over optional collection",
|
||||||
kind: .idiomatic,
|
kind: .idiomatic,
|
||||||
nonTriggeringExamples: DiscouragedOptionalCollectionExamples.nonTriggeringExamples,
|
nonTriggeringExamples: DiscouragedOptionalCollectionExamples.nonTriggeringExamples,
|
||||||
triggeringExamples: DiscouragedOptionalCollectionExamples.triggeringExamples
|
triggeringExamples: DiscouragedOptionalCollectionExamples.triggeringExamples
|
||||||
)
|
)
|
||||||
|
|
||||||
public func validate(file: SwiftLintFile,
|
func validate(file: SwiftLintFile,
|
||||||
kind: SwiftDeclarationKind,
|
kind: SwiftDeclarationKind,
|
||||||
dictionary: SourceKittenDictionary) -> [StyleViolation] {
|
dictionary: SourceKittenDictionary) -> [StyleViolation] {
|
||||||
let offsets = variableViolations(file: file, kind: kind, dictionary: dictionary) +
|
let offsets = variableViolations(kind: kind, dictionary: dictionary) +
|
||||||
functionViolations(file: file, kind: kind, dictionary: dictionary)
|
functionViolations(file: file, kind: kind, dictionary: dictionary)
|
||||||
|
|
||||||
return offsets.map {
|
return offsets.map {
|
||||||
StyleViolation(ruleDescription: type(of: self).description,
|
StyleViolation(ruleDescription: Self.description,
|
||||||
severity: configuration.severity,
|
severity: configuration.severity,
|
||||||
location: Location(file: file, byteOffset: $0))
|
location: Location(file: file, byteOffset: $0))
|
||||||
}
|
}
|
||||||
|
@ -29,9 +27,7 @@ public struct DiscouragedOptionalCollectionRule: ASTRule, OptInRule, Configurati
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func variableViolations(file: SwiftLintFile,
|
private func variableViolations(kind: SwiftDeclarationKind, dictionary: SourceKittenDictionary) -> [ByteCount] {
|
||||||
kind: SwiftDeclarationKind,
|
|
||||||
dictionary: SourceKittenDictionary) -> [ByteCount] {
|
|
||||||
guard
|
guard
|
||||||
SwiftDeclarationKind.variableKinds.contains(kind),
|
SwiftDeclarationKind.variableKinds.contains(kind),
|
||||||
let offset = dictionary.offset,
|
let offset = dictionary.offset,
|
|
@ -0,0 +1,212 @@
|
||||||
|
import Foundation
|
||||||
|
import SourceKittenFramework
|
||||||
|
|
||||||
|
struct DuplicateImportsRule: ConfigurationProviderRule, CorrectableRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
// List of all possible import kinds
|
||||||
|
static let importKinds = [
|
||||||
|
"typealias", "struct", "class",
|
||||||
|
"enum", "protocol", "let",
|
||||||
|
"var", "func"
|
||||||
|
]
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "duplicate_imports",
|
||||||
|
name: "Duplicate Imports",
|
||||||
|
description: "Imports should be unique",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: DuplicateImportsRuleExamples.nonTriggeringExamples,
|
||||||
|
triggeringExamples: DuplicateImportsRuleExamples.triggeringExamples,
|
||||||
|
corrections: DuplicateImportsRuleExamples.corrections
|
||||||
|
)
|
||||||
|
|
||||||
|
private func rangesInConditionalCompilation(file: SwiftLintFile) -> [ByteRange] {
|
||||||
|
let contents = file.stringView
|
||||||
|
|
||||||
|
let ranges = file.syntaxMap.tokens
|
||||||
|
.filter { $0.kind == .buildconfigKeyword }
|
||||||
|
.map { $0.range }
|
||||||
|
.filter { range in
|
||||||
|
return ["#if", "#endif"].contains(contents.substringWithByteRange(range))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that each #if has corresponding #endif
|
||||||
|
guard ranges.count.isMultiple(of: 2) else { return [] }
|
||||||
|
|
||||||
|
return stride(from: 0, to: ranges.count, by: 2).reduce(into: []) { result, rangeIndex in
|
||||||
|
result.append(ranges[rangeIndex].union(with: ranges[rangeIndex + 1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildImportLineSlicesByImportSubpath(
|
||||||
|
importLines: [Line]
|
||||||
|
) -> [ImportSubpath: [ImportLineSlice]] {
|
||||||
|
var importLineSlices = [ImportSubpath: [ImportLineSlice]]()
|
||||||
|
|
||||||
|
importLines.forEach { importLine in
|
||||||
|
importLine.importSlices.forEach { slice in
|
||||||
|
importLineSlices[slice.subpath, default: []].append(
|
||||||
|
ImportLineSlice(
|
||||||
|
slice: slice,
|
||||||
|
line: importLine
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return importLineSlices
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findDuplicateImports(
|
||||||
|
file: SwiftLintFile,
|
||||||
|
importLineSlicesGroupedBySubpath: [[ImportLineSlice]]
|
||||||
|
) -> [DuplicateImport] {
|
||||||
|
typealias ImportLocation = Int
|
||||||
|
|
||||||
|
var duplicateImportsByLocation = [ImportLocation: DuplicateImport]()
|
||||||
|
|
||||||
|
importLineSlicesGroupedBySubpath.forEach { linesImportingSubpath in
|
||||||
|
guard linesImportingSubpath.count > 1 else { return }
|
||||||
|
guard let primaryImportIndex = linesImportingSubpath.firstIndex(where: {
|
||||||
|
$0.slice.type == .complete
|
||||||
|
}) else { return }
|
||||||
|
|
||||||
|
linesImportingSubpath.enumerated().forEach { index, importedLine in
|
||||||
|
guard index != primaryImportIndex else { return }
|
||||||
|
let location = Location(
|
||||||
|
file: file,
|
||||||
|
characterOffset: importedLine.line.range.location
|
||||||
|
)
|
||||||
|
duplicateImportsByLocation[importedLine.line.range.location] = DuplicateImport(
|
||||||
|
location: location,
|
||||||
|
range: importedLine.line.range
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array(duplicateImportsByLocation.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct DuplicateImport {
|
||||||
|
let location: Location
|
||||||
|
var range: NSRange
|
||||||
|
}
|
||||||
|
|
||||||
|
private func duplicateImports(file: SwiftLintFile) -> [DuplicateImport] {
|
||||||
|
let contents = file.stringView
|
||||||
|
|
||||||
|
let ignoredRanges = self.rangesInConditionalCompilation(file: file)
|
||||||
|
|
||||||
|
let importKinds = Self.importKinds.joined(separator: "|")
|
||||||
|
|
||||||
|
// Grammar of import declaration
|
||||||
|
// attributes(optional) import import-kind(optional) import-path
|
||||||
|
let regex = "^([a-zA-Z@_]+\\s)?import(\\s(\(importKinds)))?\\s+[a-zA-Z0-9._]+$"
|
||||||
|
let importRanges = file.match(pattern: regex)
|
||||||
|
.filter { $0.1.allSatisfy { [.keyword, .identifier, .attributeBuiltin].contains($0) } }
|
||||||
|
.compactMap { contents.NSRangeToByteRange(start: $0.0.location, length: $0.0.length) }
|
||||||
|
.filter { importRange -> Bool in
|
||||||
|
return !importRange.intersects(ignoredRanges)
|
||||||
|
}
|
||||||
|
|
||||||
|
let lines = file.lines
|
||||||
|
|
||||||
|
let importLines: [Line] = importRanges.compactMap { range in
|
||||||
|
guard let line = contents.lineAndCharacter(forByteOffset: range.location)?.line
|
||||||
|
else { return nil }
|
||||||
|
return lines[line - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
let importLineSlices = buildImportLineSlicesByImportSubpath(importLines: importLines)
|
||||||
|
|
||||||
|
let duplicateImports = findDuplicateImports(
|
||||||
|
file: file,
|
||||||
|
importLineSlicesGroupedBySubpath: Array(importLineSlices.values)
|
||||||
|
)
|
||||||
|
|
||||||
|
return duplicateImports.sorted(by: {
|
||||||
|
$0.range.lowerBound < $1.range.lowerBound
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
||||||
|
return duplicateImports(file: file).map { duplicateImport in
|
||||||
|
StyleViolation(
|
||||||
|
ruleDescription: Self.description,
|
||||||
|
severity: configuration.severity,
|
||||||
|
location: duplicateImport.location
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func correct(file: SwiftLintFile) -> [Correction] {
|
||||||
|
let duplicateImports = duplicateImports(file: file).reversed().filter {
|
||||||
|
file.ruleEnabled(violatingRange: $0.range, for: self) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let violatingRanges = duplicateImports.map(\.range)
|
||||||
|
let correctedFileContents = violatingRanges.reduce(file.stringView.nsString) { contents, range in
|
||||||
|
contents.replacingCharacters(
|
||||||
|
in: range,
|
||||||
|
with: ""
|
||||||
|
).bridge()
|
||||||
|
}
|
||||||
|
|
||||||
|
file.write(correctedFileContents.bridge())
|
||||||
|
|
||||||
|
return duplicateImports.map { duplicateImport in
|
||||||
|
Correction(
|
||||||
|
ruleDescription: Self.description,
|
||||||
|
location: duplicateImport.location
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private typealias ImportSubpath = ArraySlice<String>
|
||||||
|
|
||||||
|
private struct ImportSlice {
|
||||||
|
enum ImportSliceType {
|
||||||
|
/// For "import A.B.C" parent subpaths are ["A", "B"] and ["A"]
|
||||||
|
case parent
|
||||||
|
|
||||||
|
/// For "import A.B.C" complete subpath is ["A", "B", "C"]
|
||||||
|
case complete
|
||||||
|
}
|
||||||
|
|
||||||
|
let subpath: ImportSubpath
|
||||||
|
let type: ImportSliceType
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ImportLineSlice {
|
||||||
|
let slice: ImportSlice
|
||||||
|
let line: Line
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Line {
|
||||||
|
/// Returns name of the module being imported.
|
||||||
|
var importIdentifier: Substring? {
|
||||||
|
return self.content.split(separator: " ").last
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For "import A.B.C" returns slices [["A", "B", "C"], ["A", "B"], ["A"]]
|
||||||
|
var importSlices: [ImportSlice] {
|
||||||
|
guard let importIdentifier else { return [] }
|
||||||
|
|
||||||
|
let importedSubpathParts = importIdentifier.split(separator: ".").map { String($0) }
|
||||||
|
guard !importedSubpathParts.isEmpty else { return [] }
|
||||||
|
|
||||||
|
return [
|
||||||
|
ImportSlice(
|
||||||
|
subpath: importedSubpathParts[0..<importedSubpathParts.count],
|
||||||
|
type: .complete
|
||||||
|
)
|
||||||
|
] + (1..<importedSubpathParts.count).map {
|
||||||
|
ImportSlice(
|
||||||
|
subpath: importedSubpathParts[0..<importedSubpathParts.count - $0],
|
||||||
|
type: .parent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,219 @@
|
||||||
|
internal struct DuplicateImportsRuleExamples {
|
||||||
|
static let nonTriggeringExamples = [
|
||||||
|
Example("""
|
||||||
|
import A
|
||||||
|
import B
|
||||||
|
import C
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import A.B
|
||||||
|
import A.C
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@_implementationOnly import A
|
||||||
|
@_implementationOnly import B
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@testable import A
|
||||||
|
@testable import B
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
#if DEBUG
|
||||||
|
@testable import KsApi
|
||||||
|
#else
|
||||||
|
import KsApi
|
||||||
|
#endif
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import A // module
|
||||||
|
import B // module
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
#if TEST
|
||||||
|
func test() {
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
|
||||||
|
static let triggeringExamples = Array(corrections.keys.sorted())
|
||||||
|
|
||||||
|
static let corrections: [Example: Example] = {
|
||||||
|
var corrections = [
|
||||||
|
Example("""
|
||||||
|
import Foundation
|
||||||
|
import Dispatch
|
||||||
|
↓import Foundation
|
||||||
|
|
||||||
|
"""): Example(
|
||||||
|
"""
|
||||||
|
import Foundation
|
||||||
|
import Dispatch
|
||||||
|
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import Foundation
|
||||||
|
↓import Foundation.NSString
|
||||||
|
|
||||||
|
"""): Example("""
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
↓import Foundation.NSString
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
"""): Example("""
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@_implementationOnly import A
|
||||||
|
↓@_implementationOnly import A
|
||||||
|
|
||||||
|
"""): Example("""
|
||||||
|
@_implementationOnly import A
|
||||||
|
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@testable import A
|
||||||
|
↓@testable import A
|
||||||
|
|
||||||
|
"""): Example("""
|
||||||
|
@testable import A
|
||||||
|
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
↓import A.B.C
|
||||||
|
import A.B
|
||||||
|
|
||||||
|
"""): Example("""
|
||||||
|
import A.B
|
||||||
|
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import A.B
|
||||||
|
↓import A.B.C
|
||||||
|
|
||||||
|
"""): Example("""
|
||||||
|
import A.B
|
||||||
|
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import A
|
||||||
|
#if DEBUG
|
||||||
|
@testable import KsApi
|
||||||
|
#else
|
||||||
|
import KsApi
|
||||||
|
#endif
|
||||||
|
↓import A
|
||||||
|
|
||||||
|
"""): Example("""
|
||||||
|
import A
|
||||||
|
#if DEBUG
|
||||||
|
@testable import KsApi
|
||||||
|
#else
|
||||||
|
import KsApi
|
||||||
|
#endif
|
||||||
|
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import Foundation
|
||||||
|
↓import Foundation
|
||||||
|
↓import Foundation
|
||||||
|
|
||||||
|
"""): Example("""
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
↓import A.B.C
|
||||||
|
↓import A.B
|
||||||
|
import A
|
||||||
|
|
||||||
|
""", excludeFromDocumentation: true): Example("""
|
||||||
|
import A
|
||||||
|
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import A.B.C
|
||||||
|
↓import A.B.C.D
|
||||||
|
↓import A.B.C.E
|
||||||
|
|
||||||
|
""", excludeFromDocumentation: true): Example("""
|
||||||
|
import A.B.C
|
||||||
|
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
↓import A.B.C
|
||||||
|
import A
|
||||||
|
↓import A.B
|
||||||
|
|
||||||
|
""", excludeFromDocumentation: true): Example("""
|
||||||
|
import A
|
||||||
|
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
↓import A.B
|
||||||
|
import A
|
||||||
|
↓import A.B.C
|
||||||
|
|
||||||
|
""", excludeFromDocumentation: true): Example("""
|
||||||
|
import A
|
||||||
|
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import A
|
||||||
|
↓import A.B.C
|
||||||
|
↓import A.B
|
||||||
|
|
||||||
|
""", excludeFromDocumentation: true): Example("""
|
||||||
|
import A
|
||||||
|
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
|
||||||
|
DuplicateImportsRule.importKinds.map { importKind in
|
||||||
|
Example("""
|
||||||
|
import A
|
||||||
|
↓import \(importKind) A.Foo
|
||||||
|
|
||||||
|
""")
|
||||||
|
}.forEach {
|
||||||
|
corrections[$0] = Example(
|
||||||
|
"""
|
||||||
|
import A
|
||||||
|
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
DuplicateImportsRule.importKinds.map { importKind in
|
||||||
|
Example("""
|
||||||
|
import A
|
||||||
|
↓import \(importKind) A.B.Foo
|
||||||
|
|
||||||
|
""", excludeFromDocumentation: true)
|
||||||
|
}.forEach {
|
||||||
|
corrections[$0] = Example(
|
||||||
|
"""
|
||||||
|
import A
|
||||||
|
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
DuplicateImportsRule.importKinds.map { importKind in
|
||||||
|
Example("""
|
||||||
|
import A.B
|
||||||
|
↓import \(importKind) A.B.Foo
|
||||||
|
|
||||||
|
""", excludeFromDocumentation: true)
|
||||||
|
}.forEach {
|
||||||
|
corrections[$0] = Example(
|
||||||
|
"""
|
||||||
|
import A.B
|
||||||
|
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
return corrections
|
||||||
|
}()
|
||||||
|
}
|
|
@ -3,15 +3,13 @@ import SourceKittenFramework
|
||||||
|
|
||||||
private typealias SourceKittenElement = SourceKittenDictionary
|
private typealias SourceKittenElement = SourceKittenDictionary
|
||||||
|
|
||||||
public struct ExplicitACLRule: OptInRule, ConfigurationProviderRule, AutomaticTestableRule {
|
struct ExplicitACLRule: OptInRule, ConfigurationProviderRule {
|
||||||
public var configuration = SeverityConfiguration(.warning)
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
public init() {}
|
static let description = RuleDescription(
|
||||||
|
|
||||||
public static let description = RuleDescription(
|
|
||||||
identifier: "explicit_acl",
|
identifier: "explicit_acl",
|
||||||
name: "Explicit ACL",
|
name: "Explicit ACL",
|
||||||
description: "All declarations should specify Access Control Level keywords explicitly.",
|
description: "All declarations should specify Access Control Level keywords explicitly",
|
||||||
kind: .idiomatic,
|
kind: .idiomatic,
|
||||||
nonTriggeringExamples: [
|
nonTriggeringExamples: [
|
||||||
Example("internal enum A {}\n"),
|
Example("internal enum A {}\n"),
|
||||||
|
@ -41,15 +39,50 @@ public struct ExplicitACLRule: OptInRule, ConfigurationProviderRule, AutomaticTe
|
||||||
"""),
|
"""),
|
||||||
Example("internal class A { deinit {} }"),
|
Example("internal class A { deinit {} }"),
|
||||||
Example("extension A: Equatable {}"),
|
Example("extension A: Equatable {}"),
|
||||||
Example("extension A {}")
|
Example("extension A {}"),
|
||||||
|
Example("""
|
||||||
|
extension Foo {
|
||||||
|
internal func bar() {}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
internal enum Foo {
|
||||||
|
case bar
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
extension Foo {
|
||||||
|
public var isValid: Bool {
|
||||||
|
let result = true
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
extension Foo {
|
||||||
|
private var isValid: Bool {
|
||||||
|
get {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
set(newValue) {
|
||||||
|
print(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
],
|
],
|
||||||
triggeringExamples: [
|
triggeringExamples: [
|
||||||
Example("enum A {}\n"),
|
Example("↓enum A {}\n"),
|
||||||
Example("final class B {}\n"),
|
Example("final ↓class B {}\n"),
|
||||||
Example("internal struct C { let d = 5 }\n"),
|
Example("internal struct C { ↓let d = 5 }\n"),
|
||||||
Example("public struct C { let d = 5 }\n"),
|
Example("public struct C { ↓let d = 5 }\n"),
|
||||||
Example("func a() {}\n"),
|
Example("func a() {}\n"),
|
||||||
Example("internal let a = 0\nfunc b() {}\n")
|
Example("internal let a = 0\n↓func b() {}\n"),
|
||||||
|
Example("""
|
||||||
|
extension Foo {
|
||||||
|
↓func bar() {}
|
||||||
|
}
|
||||||
|
""")
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -62,16 +95,13 @@ public struct ExplicitACLRule: OptInRule, ConfigurationProviderRule, AutomaticTe
|
||||||
|
|
||||||
private func offsetOfElements(from elements: [SourceKittenElement], in file: SwiftLintFile,
|
private func offsetOfElements(from elements: [SourceKittenElement], in file: SwiftLintFile,
|
||||||
thatAreNotInRanges ranges: [ByteRange]) -> [ByteCount] {
|
thatAreNotInRanges ranges: [ByteRange]) -> [ByteCount] {
|
||||||
let extensionKinds: Set<SwiftDeclarationKind> = [.extension, .extensionClass, .extensionEnum,
|
|
||||||
.extensionProtocol, .extensionStruct]
|
|
||||||
|
|
||||||
return elements.compactMap { element in
|
return elements.compactMap { element in
|
||||||
guard let typeOffset = element.offset else {
|
guard let typeOffset = element.offset else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let kind = element.declarationKind,
|
guard let kind = element.declarationKind,
|
||||||
!extensionKinds.contains(kind) else {
|
!SwiftDeclarationKind.extensionKinds.contains(kind) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,10 +120,10 @@ public struct ExplicitACLRule: OptInRule, ConfigurationProviderRule, AutomaticTe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func validate(file: SwiftLintFile) -> [StyleViolation] {
|
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
||||||
let implicitAndExplicitInternalElements = internalTypeElements(in: file.structureDictionary )
|
let implicitAndExplicitInternalElements = internalTypeElements(in: file.structureDictionary)
|
||||||
|
|
||||||
guard !implicitAndExplicitInternalElements.isEmpty else {
|
guard implicitAndExplicitInternalElements.isNotEmpty else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +133,7 @@ public struct ExplicitACLRule: OptInRule, ConfigurationProviderRule, AutomaticTe
|
||||||
thatAreNotInRanges: explicitInternalRanges)
|
thatAreNotInRanges: explicitInternalRanges)
|
||||||
|
|
||||||
return violations.map {
|
return violations.map {
|
||||||
StyleViolation(ruleDescription: type(of: self).description,
|
StyleViolation(ruleDescription: Self.description,
|
||||||
severity: configuration.severity,
|
severity: configuration.severity,
|
||||||
location: Location(file: file, byteOffset: $0))
|
location: Location(file: file, byteOffset: $0))
|
||||||
}
|
}
|
||||||
|
@ -114,9 +144,10 @@ public struct ExplicitACLRule: OptInRule, ConfigurationProviderRule, AutomaticTe
|
||||||
return firstPartition.last
|
return firstPartition.last
|
||||||
}
|
}
|
||||||
|
|
||||||
private func internalTypeElements(in element: SourceKittenElement) -> [SourceKittenElement] {
|
private func internalTypeElements(in parent: SourceKittenElement) -> [SourceKittenElement] {
|
||||||
return element.substructure.flatMap { element -> [SourceKittenElement] in
|
return parent.substructure.flatMap { element -> [SourceKittenElement] in
|
||||||
guard let elementKind = element.declarationKind else {
|
guard let elementKind = element.declarationKind,
|
||||||
|
elementKind != .varLocal, elementKind != .varParameter else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +160,12 @@ public struct ExplicitACLRule: OptInRule, ConfigurationProviderRule, AutomaticTe
|
||||||
let internalTypeElementsInSubstructure = elementKind.childsAreExemptFromACL || isPrivate ? [] :
|
let internalTypeElementsInSubstructure = elementKind.childsAreExemptFromACL || isPrivate ? [] :
|
||||||
internalTypeElements(in: element)
|
internalTypeElements(in: element)
|
||||||
|
|
||||||
if element.accessibility == .internal {
|
var isInExtension = false
|
||||||
|
if let kind = parent.declarationKind {
|
||||||
|
isInExtension = SwiftDeclarationKind.extensionKinds.contains(kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
if element.accessibility == .internal || (element.accessibility == nil && isInExtension) {
|
||||||
return internalTypeElementsInSubstructure + [element]
|
return internalTypeElementsInSubstructure + [element]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +186,7 @@ private extension SwiftDeclarationKind {
|
||||||
.functionMethodInstance, .functionMethodStatic, .functionOperator, .functionOperatorInfix,
|
.functionMethodInstance, .functionMethodStatic, .functionOperator, .functionOperatorInfix,
|
||||||
.functionOperatorPostfix, .functionOperatorPrefix, .functionSubscript, .protocol, .opaqueType:
|
.functionOperatorPostfix, .functionOperatorPrefix, .functionSubscript, .protocol, .opaqueType:
|
||||||
return true
|
return true
|
||||||
case .class, .enum, .extension, .extensionClass, .extensionEnum,
|
case .actor, .class, .enum, .extension, .extensionClass, .extensionEnum,
|
||||||
.extensionProtocol, .extensionStruct, .struct:
|
.extensionProtocol, .extensionStruct, .struct:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct ExplicitEnumRawValueRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "explicit_enum_raw_value",
|
||||||
|
name: "Explicit Enum Raw Value",
|
||||||
|
description: "Enums should be explicitly assigned their raw values",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
enum Numbers {
|
||||||
|
case int(Int)
|
||||||
|
case short(Int16)
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum Numbers: Int {
|
||||||
|
case one = 1
|
||||||
|
case two = 2
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum Numbers: Double {
|
||||||
|
case one = 1.1
|
||||||
|
case two = 2.2
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum Numbers: String {
|
||||||
|
case one = "one"
|
||||||
|
case two = "two"
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
protocol Algebra {}
|
||||||
|
enum Numbers: Algebra {
|
||||||
|
case one
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
enum Numbers: Int {
|
||||||
|
case one = 10, ↓two, three = 30
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum Numbers: NSInteger {
|
||||||
|
case ↓one
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum Numbers: String {
|
||||||
|
case ↓one
|
||||||
|
case ↓two
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum Numbers: String {
|
||||||
|
case ↓one, two = "two"
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum Numbers: Decimal {
|
||||||
|
case ↓one, ↓two
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum Outer {
|
||||||
|
enum Numbers: Decimal {
|
||||||
|
case ↓one, ↓two
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ExplicitEnumRawValueRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] }
|
||||||
|
|
||||||
|
override func visitPost(_ node: EnumCaseElementSyntax) {
|
||||||
|
if node.rawValue == nil, node.enclosingEnum()?.supportsRawValues == true {
|
||||||
|
violations.append(node.identifier.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SyntaxProtocol {
|
||||||
|
func enclosingEnum() -> EnumDeclSyntax? {
|
||||||
|
if let node = self.as(EnumDeclSyntax.self) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent?.enclosingEnum()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,254 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
import SwiftSyntaxBuilder
|
||||||
|
|
||||||
|
struct ExplicitInitRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule, OptInRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "explicit_init",
|
||||||
|
name: "Explicit Init",
|
||||||
|
description: "Explicitly calling .init() should be avoided",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
import Foundation
|
||||||
|
class C: NSObject {
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""), // super
|
||||||
|
Example("""
|
||||||
|
struct S {
|
||||||
|
let n: Int
|
||||||
|
}
|
||||||
|
extension S {
|
||||||
|
init() {
|
||||||
|
self.init(n: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""), // self
|
||||||
|
Example("""
|
||||||
|
[1].flatMap(String.init)
|
||||||
|
"""), // pass init as closure
|
||||||
|
Example("""
|
||||||
|
[String.self].map { $0.init(1) }
|
||||||
|
"""), // initialize from a metatype value
|
||||||
|
Example("""
|
||||||
|
[String.self].map { type in type.init(1) }
|
||||||
|
"""), // initialize from a metatype value
|
||||||
|
Example("""
|
||||||
|
Observable.zip(obs1, obs2, resultSelector: MyType.init).asMaybe()
|
||||||
|
"""),
|
||||||
|
Example("_ = GleanMetrics.Tabs.someType.init()"),
|
||||||
|
Example("""
|
||||||
|
Observable.zip(
|
||||||
|
obs1,
|
||||||
|
obs2,
|
||||||
|
resultSelector: MyType.init
|
||||||
|
).asMaybe()
|
||||||
|
""")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
[1].flatMap{String↓.init($0)}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
[String.self].map { Type in Type↓.init(1) }
|
||||||
|
"""), // Starting with capital letter assumes a type
|
||||||
|
Example("""
|
||||||
|
func foo() -> [String] {
|
||||||
|
return [1].flatMap { String↓.init($0) }
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("_ = GleanMetrics.Tabs.GroupedTabExtra↓.init()"),
|
||||||
|
Example("_ = Set<KsApi.Category>↓.init()"),
|
||||||
|
Example("""
|
||||||
|
Observable.zip(
|
||||||
|
obs1,
|
||||||
|
obs2,
|
||||||
|
resultSelector: { MyType↓.init($0, $1) }
|
||||||
|
).asMaybe()
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
let int = In🤓t↓
|
||||||
|
.init(1.0)
|
||||||
|
""", excludeFromDocumentation: true),
|
||||||
|
Example("""
|
||||||
|
let int = Int↓
|
||||||
|
|
||||||
|
|
||||||
|
.init(1.0)
|
||||||
|
""", excludeFromDocumentation: true),
|
||||||
|
Example("""
|
||||||
|
let int = Int↓
|
||||||
|
|
||||||
|
|
||||||
|
.init(1.0)
|
||||||
|
""", excludeFromDocumentation: true)
|
||||||
|
],
|
||||||
|
corrections: [
|
||||||
|
Example("""
|
||||||
|
[1].flatMap{String↓.init($0)}
|
||||||
|
"""):
|
||||||
|
Example("""
|
||||||
|
[1].flatMap{String($0)}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
func foo() -> [String] {
|
||||||
|
return [1].flatMap { String↓.init($0) }
|
||||||
|
}
|
||||||
|
"""):
|
||||||
|
Example("""
|
||||||
|
func foo() -> [String] {
|
||||||
|
return [1].flatMap { String($0) }
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class C {
|
||||||
|
#if true
|
||||||
|
func f() {
|
||||||
|
[1].flatMap{String↓.init($0)}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
"""):
|
||||||
|
Example("""
|
||||||
|
class C {
|
||||||
|
#if true
|
||||||
|
func f() {
|
||||||
|
[1].flatMap{String($0)}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
let int = Int↓
|
||||||
|
.init(1.0)
|
||||||
|
"""):
|
||||||
|
Example("""
|
||||||
|
let int = Int(1.0)
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
let int = Int↓
|
||||||
|
|
||||||
|
|
||||||
|
.init(1.0)
|
||||||
|
"""):
|
||||||
|
Example("""
|
||||||
|
let int = Int(1.0)
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
let int = Int↓
|
||||||
|
|
||||||
|
|
||||||
|
.init(1.0)
|
||||||
|
"""):
|
||||||
|
Example("""
|
||||||
|
let int = Int(1.0)
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
let int = Int↓
|
||||||
|
|
||||||
|
|
||||||
|
.init(1.0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""):
|
||||||
|
Example("""
|
||||||
|
let int = Int(1.0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""),
|
||||||
|
Example("_ = GleanMetrics.Tabs.GroupedTabExtra↓.init()"):
|
||||||
|
Example("_ = GleanMetrics.Tabs.GroupedTabExtra()"),
|
||||||
|
Example("_ = Set<KsApi.Category>↓.init()"):
|
||||||
|
Example("_ = Set<KsApi.Category>()")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
|
||||||
|
Rewriter(
|
||||||
|
locationConverter: file.locationConverter,
|
||||||
|
disabledRegions: disabledRegions(file: file)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ExplicitInitRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: FunctionCallExprSyntax) {
|
||||||
|
guard
|
||||||
|
let calledExpression = node.calledExpression.as(MemberAccessExprSyntax.self),
|
||||||
|
let violationPosition = calledExpression.explicitInitPosition
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.append(violationPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
|
||||||
|
private(set) var correctionPositions: [AbsolutePosition] = []
|
||||||
|
let locationConverter: SourceLocationConverter
|
||||||
|
let disabledRegions: [SourceRange]
|
||||||
|
|
||||||
|
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
|
||||||
|
self.locationConverter = locationConverter
|
||||||
|
self.disabledRegions = disabledRegions
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
|
||||||
|
guard
|
||||||
|
let calledExpression = node.calledExpression.as(MemberAccessExprSyntax.self),
|
||||||
|
let violationPosition = calledExpression.explicitInitPosition,
|
||||||
|
let calledBase = calledExpression.base,
|
||||||
|
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
|
||||||
|
else {
|
||||||
|
return super.visit(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
correctionPositions.append(violationPosition)
|
||||||
|
let newNode = node.with(\.calledExpression, calledBase.trimmed)
|
||||||
|
return super.visit(newNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension MemberAccessExprSyntax {
|
||||||
|
var explicitInitPosition: AbsolutePosition? {
|
||||||
|
if let base, base.isTypeReferenceLike, name.text == "init" {
|
||||||
|
return base.endPositionBeforeTrailingTrivia
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ExprSyntax {
|
||||||
|
/// `String` or `Nested.Type`.
|
||||||
|
var isTypeReferenceLike: Bool {
|
||||||
|
if let expr = self.as(IdentifierExprSyntax.self), expr.identifier.text.startsWithUppercase {
|
||||||
|
return true
|
||||||
|
} else if let expr = self.as(MemberAccessExprSyntax.self),
|
||||||
|
expr.description.split(separator: ".").allSatisfy(\.startsWithUppercase) {
|
||||||
|
return true
|
||||||
|
} else if let expr = self.as(SpecializeExprSyntax.self)?.expression.as(IdentifierExprSyntax.self),
|
||||||
|
expr.identifier.text.startsWithUppercase {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension StringProtocol {
|
||||||
|
var startsWithUppercase: Bool { first?.isUppercase == true }
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct ExplicitTopLevelACLRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "explicit_top_level_acl",
|
||||||
|
name: "Explicit Top Level ACL",
|
||||||
|
description: "Top-level declarations should specify Access Control Level keywords explicitly",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("internal enum A {}\n"),
|
||||||
|
Example("public final class B {}\n"),
|
||||||
|
Example("private struct C {}\n"),
|
||||||
|
Example("internal enum A {\n enum B {}\n}"),
|
||||||
|
Example("internal final class Foo {}"),
|
||||||
|
Example("internal\nclass Foo {}"),
|
||||||
|
Example("internal func a() {}\n"),
|
||||||
|
Example("extension A: Equatable {}"),
|
||||||
|
Example("extension A {}")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("↓enum A {}\n"),
|
||||||
|
Example("final ↓class B {}\n"),
|
||||||
|
Example("↓struct C {}\n"),
|
||||||
|
Example("↓func a() {}\n"),
|
||||||
|
Example("internal let a = 0\n↓func b() {}\n")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ExplicitTopLevelACLRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .all }
|
||||||
|
|
||||||
|
override func visitPost(_ node: ClassDeclSyntax) {
|
||||||
|
if hasViolation(modifiers: node.modifiers) {
|
||||||
|
violations.append(node.classKeyword.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: StructDeclSyntax) {
|
||||||
|
if hasViolation(modifiers: node.modifiers) {
|
||||||
|
violations.append(node.structKeyword.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: EnumDeclSyntax) {
|
||||||
|
if hasViolation(modifiers: node.modifiers) {
|
||||||
|
violations.append(node.enumKeyword.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: ProtocolDeclSyntax) {
|
||||||
|
if hasViolation(modifiers: node.modifiers) {
|
||||||
|
violations.append(node.protocolKeyword.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: ActorDeclSyntax) {
|
||||||
|
if hasViolation(modifiers: node.modifiers) {
|
||||||
|
violations.append(node.actorKeyword.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: TypealiasDeclSyntax) {
|
||||||
|
if hasViolation(modifiers: node.modifiers) {
|
||||||
|
violations.append(node.typealiasKeyword.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: FunctionDeclSyntax) {
|
||||||
|
if hasViolation(modifiers: node.modifiers) {
|
||||||
|
violations.append(node.funcKeyword.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: VariableDeclSyntax) {
|
||||||
|
if hasViolation(modifiers: node.modifiers) {
|
||||||
|
violations.append(node.bindingKeyword.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
|
||||||
|
.skipChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
|
||||||
|
.skipChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hasViolation(modifiers: ModifierListSyntax?) -> Bool {
|
||||||
|
guard let modifiers else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return !modifiers.contains(where: \.isACLModifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension DeclModifierSyntax {
|
||||||
|
var isACLModifier: Bool {
|
||||||
|
let aclModifiers: Set<TokenKind> = [
|
||||||
|
.keyword(.private),
|
||||||
|
.keyword(.fileprivate),
|
||||||
|
.keyword(.internal),
|
||||||
|
.keyword(.public),
|
||||||
|
.keyword(.open)
|
||||||
|
]
|
||||||
|
|
||||||
|
return detail == nil && aclModifiers.contains(name.tokenKind)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct ExplicitTypeInterfaceRule: OptInRule, ConfigurationProviderRule, SwiftSyntaxRule {
|
||||||
|
var configuration = ExplicitTypeInterfaceConfiguration()
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "explicit_type_interface",
|
||||||
|
name: "Explicit Type Interface",
|
||||||
|
description: "Properties should have a type interface",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
class Foo {
|
||||||
|
var myVar: Int? = 0
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class Foo {
|
||||||
|
let myVar: Int? = 0, s: String = ""
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class Foo {
|
||||||
|
static var myVar: Int? = 0
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class Foo {
|
||||||
|
class var myVar: Int? = 0
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
func f() {
|
||||||
|
if case .failure(let error) = errorCompletion {}
|
||||||
|
}
|
||||||
|
""", excludeFromDocumentation: true)
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
class Foo {
|
||||||
|
var ↓myVar = 0
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class Foo {
|
||||||
|
let ↓mylet = 0
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class Foo {
|
||||||
|
static var ↓myStaticVar = 0
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class Foo {
|
||||||
|
class var ↓myClassVar = 0
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class Foo {
|
||||||
|
let ↓myVar = Int(0), ↓s = ""
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class Foo {
|
||||||
|
let ↓myVar = Set<Int>(0)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(configuration: configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
let configuration: ExplicitTypeInterfaceConfiguration
|
||||||
|
|
||||||
|
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] }
|
||||||
|
|
||||||
|
init(configuration: ExplicitTypeInterfaceConfiguration) {
|
||||||
|
self.configuration = configuration
|
||||||
|
super.init(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: VariableDeclSyntax) {
|
||||||
|
if node.modifiers.isClass {
|
||||||
|
if configuration.allowedKinds.contains(.class) {
|
||||||
|
checkViolation(node)
|
||||||
|
}
|
||||||
|
} else if node.modifiers.isStatic {
|
||||||
|
if configuration.allowedKinds.contains(.static) {
|
||||||
|
checkViolation(node)
|
||||||
|
}
|
||||||
|
} else if node.parent?.is(MemberDeclListItemSyntax.self) == true {
|
||||||
|
if configuration.allowedKinds.contains(.instance) {
|
||||||
|
checkViolation(node)
|
||||||
|
}
|
||||||
|
} else if node.parent?.is(CodeBlockItemSyntax.self) == true {
|
||||||
|
if configuration.allowedKinds.contains(.local) {
|
||||||
|
checkViolation(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkViolation(_ node: VariableDeclSyntax) {
|
||||||
|
for binding in node.bindings {
|
||||||
|
if configuration.allowRedundancy, let initializer = binding.initializer,
|
||||||
|
initializer.isTypeConstructor || initializer.isTypeReference {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if binding.typeAnnotation == nil {
|
||||||
|
violations.append(binding.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension InitializerClauseSyntax {
|
||||||
|
var isTypeConstructor: Bool {
|
||||||
|
if value.as(FunctionCallExprSyntax.self)?.callsPotentialType == true {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if let tryExpr = value.as(TryExprSyntax.self),
|
||||||
|
tryExpr.expression.as(FunctionCallExprSyntax.self)?.callsPotentialType == true {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var isTypeReference: Bool {
|
||||||
|
value.as(MemberAccessExprSyntax.self)?.name.tokenKind == .keyword(.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FunctionCallExprSyntax {
|
||||||
|
var callsPotentialType: Bool {
|
||||||
|
let name = calledExpression.debugDescription
|
||||||
|
return name.first?.isUppercase == true || (name.first == "[" && name.last == "]")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,10 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import SourceKittenFramework
|
import SourceKittenFramework
|
||||||
|
|
||||||
public struct ExtensionAccessModifierRule: ASTRule, ConfigurationProviderRule, OptInRule, AutomaticTestableRule {
|
struct ExtensionAccessModifierRule: ASTRule, ConfigurationProviderRule, OptInRule {
|
||||||
public var configuration = SeverityConfiguration(.warning)
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
public init() {}
|
static let description = RuleDescription(
|
||||||
|
|
||||||
public static let description = RuleDescription(
|
|
||||||
identifier: "extension_access_modifier",
|
identifier: "extension_access_modifier",
|
||||||
name: "Extension Access Modifier",
|
name: "Extension Access Modifier",
|
||||||
description: "Prefer to use extension access modifiers",
|
description: "Prefer to use extension access modifiers",
|
||||||
|
@ -52,6 +50,12 @@ public struct ExtensionAccessModifierRule: ASTRule, ConfigurationProviderRule, O
|
||||||
open bar: Int { return 1 }
|
open bar: Int { return 1 }
|
||||||
open baz: Int { return 1 }
|
open baz: Int { return 1 }
|
||||||
}
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
extension Foo {
|
||||||
|
func setup() {}
|
||||||
|
public func update() {}
|
||||||
|
}
|
||||||
""")
|
""")
|
||||||
],
|
],
|
||||||
triggeringExamples: [
|
triggeringExamples: [
|
||||||
|
@ -72,12 +76,22 @@ public struct ExtensionAccessModifierRule: ASTRule, ConfigurationProviderRule, O
|
||||||
public ↓func bar() {}
|
public ↓func bar() {}
|
||||||
public ↓func baz() {}
|
public ↓func baz() {}
|
||||||
}
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
↓extension Foo {
|
||||||
|
public var bar: Int {
|
||||||
|
let value = 1
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
public var baz: Int { return 1 }
|
||||||
|
}
|
||||||
""")
|
""")
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
public func validate(file: SwiftLintFile, kind: SwiftDeclarationKind,
|
func validate(file: SwiftLintFile, kind: SwiftDeclarationKind,
|
||||||
dictionary: SourceKittenDictionary) -> [StyleViolation] {
|
dictionary: SourceKittenDictionary) -> [StyleViolation] {
|
||||||
guard kind == .extension, let offset = dictionary.offset,
|
guard kind == .extension, let offset = dictionary.offset,
|
||||||
dictionary.inheritedTypes.isEmpty
|
dictionary.inheritedTypes.isEmpty
|
||||||
else {
|
else {
|
||||||
|
@ -86,14 +100,13 @@ public struct ExtensionAccessModifierRule: ASTRule, ConfigurationProviderRule, O
|
||||||
|
|
||||||
let declarations = dictionary.substructure
|
let declarations = dictionary.substructure
|
||||||
.compactMap { entry -> (acl: AccessControlLevel, offset: ByteCount)? in
|
.compactMap { entry -> (acl: AccessControlLevel, offset: ByteCount)? in
|
||||||
guard entry.declarationKind != nil,
|
guard let kind = entry.declarationKind,
|
||||||
let acl = entry.accessibility,
|
kind != .varLocal, kind != .varParameter,
|
||||||
let offset = entry.offset
|
let offset = entry.offset else {
|
||||||
else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return (acl: acl, offset: offset)
|
return (acl: entry.accessibility ?? .internal, offset: offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
let declarationsACLs = declarations.map { $0.acl }.unique
|
let declarationsACLs = declarations.map { $0.acl }.unique
|
||||||
|
@ -111,7 +124,7 @@ public struct ExtensionAccessModifierRule: ASTRule, ConfigurationProviderRule, O
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
StyleViolation(ruleDescription: type(of: self).description,
|
StyleViolation(ruleDescription: Self.description,
|
||||||
severity: configuration.severity,
|
severity: configuration.severity,
|
||||||
location: Location(file: file, byteOffset: offset))
|
location: Location(file: file, byteOffset: offset))
|
||||||
]
|
]
|
||||||
|
@ -134,7 +147,7 @@ public struct ExtensionAccessModifierRule: ASTRule, ConfigurationProviderRule, O
|
||||||
let violationOffsets = declarationOffsets.filter { typeOffset in
|
let violationOffsets = declarationOffsets.filter { typeOffset in
|
||||||
// find the last ACL token before the type
|
// find the last ACL token before the type
|
||||||
guard let previousInternalByteRange = lastACLByteRange(before: typeOffset, in: allACLRanges) else {
|
guard let previousInternalByteRange = lastACLByteRange(before: typeOffset, in: allACLRanges) else {
|
||||||
// didn't find a candidate token, so the ACL is implict (not a violation)
|
// didn't find a candidate token, so the ACL is implicit (not a violation)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,13 +155,11 @@ public struct ExtensionAccessModifierRule: ASTRule, ConfigurationProviderRule, O
|
||||||
// attributeBuiltin (`final` for example) tokens between them
|
// attributeBuiltin (`final` for example) tokens between them
|
||||||
let length = typeOffset - previousInternalByteRange.location
|
let length = typeOffset - previousInternalByteRange.location
|
||||||
let range = ByteRange(location: previousInternalByteRange.location, length: length)
|
let range = ByteRange(location: previousInternalByteRange.location, length: length)
|
||||||
let internalBelongsToType = Set(file.syntaxMap.kinds(inByteRange: range)) == [.attributeBuiltin]
|
return Set(file.syntaxMap.kinds(inByteRange: range)) == [.attributeBuiltin]
|
||||||
|
|
||||||
return internalBelongsToType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return violationOffsets.map {
|
return violationOffsets.map {
|
||||||
StyleViolation(ruleDescription: type(of: self).description,
|
StyleViolation(ruleDescription: Self.description,
|
||||||
severity: configuration.severity,
|
severity: configuration.severity,
|
||||||
location: Location(file: file, byteOffset: $0))
|
location: Location(file: file, byteOffset: $0))
|
||||||
}
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct FallthroughRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "fallthrough",
|
||||||
|
name: "Fallthrough",
|
||||||
|
description: "Fallthrough should be avoided",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
switch foo {
|
||||||
|
case .bar, .bar2, .bar3:
|
||||||
|
something()
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
switch foo {
|
||||||
|
case .bar:
|
||||||
|
↓fallthrough
|
||||||
|
case .bar2:
|
||||||
|
something()
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FallthroughRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: FallthroughStmtSyntax) {
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct FatalErrorMessageRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "fatal_error_message",
|
||||||
|
name: "Fatal Error Message",
|
||||||
|
description: "A fatalError call should have a message",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
func foo() {
|
||||||
|
fatalError("Foo")
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
func foo() {
|
||||||
|
fatalError(x)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
func foo() {
|
||||||
|
↓fatalError("")
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
func foo() {
|
||||||
|
↓fatalError()
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FatalErrorMessageRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: FunctionCallExprSyntax) {
|
||||||
|
guard let expression = node.calledExpression.as(IdentifierExprSyntax.self),
|
||||||
|
expression.identifier.text == "fatalError",
|
||||||
|
node.argumentList.isEmptyOrEmptyString else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension TupleExprElementListSyntax {
|
||||||
|
var isEmptyOrEmptyString: Bool {
|
||||||
|
if isEmpty {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return count == 1 && first?.expression.as(StringLiteralExprSyntax.self)?.isEmptyString == true
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,22 +1,17 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import SourceKittenFramework
|
import SourceKittenFramework
|
||||||
|
|
||||||
public struct FileNameNoSpaceRule: ConfigurationProviderRule, OptInRule {
|
struct FileNameNoSpaceRule: ConfigurationProviderRule, OptInRule, SourceKitFreeRule {
|
||||||
public var configuration = FileNameNoSpaceConfiguration(
|
var configuration = FileNameNoSpaceConfiguration()
|
||||||
severity: .warning,
|
|
||||||
excluded: []
|
|
||||||
)
|
|
||||||
|
|
||||||
public init() {}
|
static let description = RuleDescription(
|
||||||
|
|
||||||
public static let description = RuleDescription(
|
|
||||||
identifier: "file_name_no_space",
|
identifier: "file_name_no_space",
|
||||||
name: "File Name No Space",
|
name: "File Name no Space",
|
||||||
description: "File name should not contain any whitespace.",
|
description: "File name should not contain any whitespace",
|
||||||
kind: .idiomatic
|
kind: .idiomatic
|
||||||
)
|
)
|
||||||
|
|
||||||
public func validate(file: SwiftLintFile) -> [StyleViolation] {
|
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
||||||
guard let filePath = file.path,
|
guard let filePath = file.path,
|
||||||
case let fileName = filePath.bridge().lastPathComponent,
|
case let fileName = filePath.bridge().lastPathComponent,
|
||||||
!configuration.excluded.contains(fileName),
|
!configuration.excluded.contains(fileName),
|
||||||
|
@ -24,8 +19,8 @@ public struct FileNameNoSpaceRule: ConfigurationProviderRule, OptInRule {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return [StyleViolation(ruleDescription: type(of: self).description,
|
return [StyleViolation(ruleDescription: Self.description,
|
||||||
severity: configuration.severity.severity,
|
severity: configuration.severity,
|
||||||
location: Location(file: filePath, line: 1))]
|
location: Location(file: filePath, line: 1))]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct FileNameRule: ConfigurationProviderRule, OptInRule, SourceKitFreeRule {
|
||||||
|
var configuration = FileNameConfiguration()
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "file_name",
|
||||||
|
name: "File Name",
|
||||||
|
description: "File name should match a type or extension declared in the file (if any)",
|
||||||
|
kind: .idiomatic
|
||||||
|
)
|
||||||
|
|
||||||
|
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
||||||
|
guard let filePath = file.path,
|
||||||
|
case let fileName = filePath.bridge().lastPathComponent,
|
||||||
|
!configuration.excluded.contains(fileName) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefixRegex = regex("\\A(?:\(configuration.prefixPattern))")
|
||||||
|
let suffixRegex = regex("(?:\(configuration.suffixPattern))\\z")
|
||||||
|
|
||||||
|
var typeInFileName = fileName.bridge().deletingPathExtension
|
||||||
|
|
||||||
|
// Process prefix
|
||||||
|
if let match = prefixRegex.firstMatch(in: typeInFileName, options: [], range: typeInFileName.fullNSRange),
|
||||||
|
let range = typeInFileName.nsrangeToIndexRange(match.range) {
|
||||||
|
typeInFileName.removeSubrange(range)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process suffix
|
||||||
|
if let match = suffixRegex.firstMatch(in: typeInFileName, options: [], range: typeInFileName.fullNSRange),
|
||||||
|
let range = typeInFileName.nsrangeToIndexRange(match.range) {
|
||||||
|
typeInFileName.removeSubrange(range)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process nested type separator
|
||||||
|
let allDeclaredTypeNames = TypeNameCollectingVisitor(viewMode: .sourceAccurate)
|
||||||
|
.walk(tree: file.syntaxTree, handler: \.names)
|
||||||
|
.map {
|
||||||
|
$0.replacingOccurrences(of: ".", with: configuration.nestedTypeSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard allDeclaredTypeNames.isNotEmpty, !allDeclaredTypeNames.contains(typeInFileName) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [StyleViolation(ruleDescription: Self.description,
|
||||||
|
severity: configuration.severity,
|
||||||
|
location: Location(file: filePath, line: 1))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TypeNameCollectingVisitor: SyntaxVisitor {
|
||||||
|
private(set) var names: Set<String> = []
|
||||||
|
|
||||||
|
override func visitPost(_ node: ClassDeclSyntax) {
|
||||||
|
names.insert(node.identifier.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: ActorDeclSyntax) {
|
||||||
|
names.insert(node.identifier.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: StructDeclSyntax) {
|
||||||
|
names.insert(node.identifier.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: TypealiasDeclSyntax) {
|
||||||
|
names.insert(node.identifier.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: EnumDeclSyntax) {
|
||||||
|
names.insert(node.identifier.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: ProtocolDeclSyntax) {
|
||||||
|
names.insert(node.identifier.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: ExtensionDeclSyntax) {
|
||||||
|
names.insert(node.extendedType.trimmedDescription)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct ForWhereRule: SwiftSyntaxRule, ConfigurationProviderRule {
|
||||||
|
var configuration = ForWhereConfiguration()
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "for_where",
|
||||||
|
name: "Prefer For-Where",
|
||||||
|
description: "`where` clauses are preferred over a single `if` inside a `for`",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
for user in users where user.id == 1 { }
|
||||||
|
"""),
|
||||||
|
// if let
|
||||||
|
Example("""
|
||||||
|
for user in users {
|
||||||
|
if let id = user.id { }
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
// if var
|
||||||
|
Example("""
|
||||||
|
for user in users {
|
||||||
|
if var id = user.id { }
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
// if with else
|
||||||
|
Example("""
|
||||||
|
for user in users {
|
||||||
|
if user.id == 1 { } else { }
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
// if with else if
|
||||||
|
Example("""
|
||||||
|
for user in users {
|
||||||
|
if user.id == 1 {
|
||||||
|
} else if user.id == 2 { }
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
// if is not the only expression inside for
|
||||||
|
Example("""
|
||||||
|
for user in users {
|
||||||
|
if user.id == 1 { }
|
||||||
|
print(user)
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
// if a variable is used
|
||||||
|
Example("""
|
||||||
|
for user in users {
|
||||||
|
let id = user.id
|
||||||
|
if id == 1 { }
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
// if something is after if
|
||||||
|
Example("""
|
||||||
|
for user in users {
|
||||||
|
if user.id == 1 { }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
// condition with multiple clauses
|
||||||
|
Example("""
|
||||||
|
for user in users {
|
||||||
|
if user.id == 1 && user.age > 18 { }
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
for user in users {
|
||||||
|
if user.id == 1, user.age > 18 { }
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
// if case
|
||||||
|
Example("""
|
||||||
|
for (index, value) in array.enumerated() {
|
||||||
|
if case .valueB(_) = value {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
for user in users {
|
||||||
|
if user.id == 1 { return true }
|
||||||
|
}
|
||||||
|
""", configuration: ["allow_for_as_filter": true]),
|
||||||
|
Example("""
|
||||||
|
for user in users {
|
||||||
|
if user.id == 1 {
|
||||||
|
let derivedValue = calculateValue(from: user)
|
||||||
|
return derivedValue != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""", configuration: ["allow_for_as_filter": true])
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
for user in users {
|
||||||
|
↓if user.id == 1 { return true }
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
for subview in subviews {
|
||||||
|
↓if !(subview is UIStackView) {
|
||||||
|
subview.removeConstraints(subview.constraints)
|
||||||
|
subview.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
for subview in subviews {
|
||||||
|
↓if !(subview is UIStackView) {
|
||||||
|
subview.removeConstraints(subview.constraints)
|
||||||
|
subview.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""", configuration: ["allow_for_as_filter": true])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(allowForAsFilter: configuration.allowForAsFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ForWhereRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
private let allowForAsFilter: Bool
|
||||||
|
|
||||||
|
init(allowForAsFilter: Bool) {
|
||||||
|
self.allowForAsFilter = allowForAsFilter
|
||||||
|
super.init(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: ForInStmtSyntax) {
|
||||||
|
guard node.whereClause == nil,
|
||||||
|
let onlyExprStmt = node.body.statements.onlyElement?.item.as(ExpressionStmtSyntax.self),
|
||||||
|
let ifExpr = onlyExprStmt.expression.as(IfExprSyntax.self),
|
||||||
|
ifExpr.elseBody == nil,
|
||||||
|
!ifExpr.containsOptionalBinding,
|
||||||
|
!ifExpr.containsPatternCondition,
|
||||||
|
let condition = ifExpr.conditions.onlyElement,
|
||||||
|
!condition.containsMultipleConditions else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if allowForAsFilter, ifExpr.containsReturnStatement {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.append(ifExpr.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension IfExprSyntax {
|
||||||
|
var containsOptionalBinding: Bool {
|
||||||
|
conditions.contains { element in
|
||||||
|
element.condition.is(OptionalBindingConditionSyntax.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var containsPatternCondition: Bool {
|
||||||
|
conditions.contains { element in
|
||||||
|
element.condition.is(MatchingPatternConditionSyntax.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var containsReturnStatement: Bool {
|
||||||
|
body.statements.contains { element in
|
||||||
|
element.item.is(ReturnStmtSyntax.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ConditionElementSyntax {
|
||||||
|
var containsMultipleConditions: Bool {
|
||||||
|
guard let condition = condition.as(SequenceExprSyntax.self) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return condition.elements.contains { expr in
|
||||||
|
guard let binaryExpr = expr.as(BinaryOperatorExprSyntax.self) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let operators: Set = ["&&", "||"]
|
||||||
|
return operators.contains(binaryExpr.operatorToken.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct ForceCastRule: ConfigurationProviderRule, SwiftSyntaxRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.error)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "force_cast",
|
||||||
|
name: "Force Cast",
|
||||||
|
description: "Force casts should be avoided",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("NSNumber() as? Int\n")
|
||||||
|
],
|
||||||
|
triggeringExamples: [ Example("NSNumber() ↓as! Int\n") ]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
ForceCastRuleVisitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ForceCastRuleVisitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: AsExprSyntax) {
|
||||||
|
if node.questionOrExclamationMark?.tokenKind == .exclamationMark {
|
||||||
|
violations.append(node.asTok.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: UnresolvedAsExprSyntax) {
|
||||||
|
if node.questionOrExclamationMark?.tokenKind == .exclamationMark {
|
||||||
|
violations.append(node.asTok.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct ForceTryRule: ConfigurationProviderRule, SwiftSyntaxRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.error)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "force_try",
|
||||||
|
name: "Force Try",
|
||||||
|
description: "Force tries should be avoided",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
func a() throws {}
|
||||||
|
do {
|
||||||
|
try a()
|
||||||
|
} catch {}
|
||||||
|
""")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
func a() throws {}
|
||||||
|
↓try! a()
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ForceTryRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: TryExprSyntax) {
|
||||||
|
if node.questionOrExclamationMark?.tokenKind == .exclamationMark {
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct ForceUnwrappingRule: OptInRule, SwiftSyntaxRule, ConfigurationProviderRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "force_unwrapping",
|
||||||
|
name: "Force Unwrapping",
|
||||||
|
description: "Force unwrapping should be avoided",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("if let url = NSURL(string: query)"),
|
||||||
|
Example("navigationController?.pushViewController(viewController, animated: true)"),
|
||||||
|
Example("let s as! Test"),
|
||||||
|
Example("try! canThrowErrors()"),
|
||||||
|
Example("let object: Any!"),
|
||||||
|
Example("@IBOutlet var constraints: [NSLayoutConstraint]!"),
|
||||||
|
Example("setEditing(!editing, animated: true)"),
|
||||||
|
Example("navigationController.setNavigationBarHidden(!navigationController." +
|
||||||
|
"navigationBarHidden, animated: true)"),
|
||||||
|
Example("if addedToPlaylist && (!self.selectedFilters.isEmpty || " +
|
||||||
|
"self.searchBar?.text?.isEmpty == false) {}"),
|
||||||
|
Example("print(\"\\(xVar)!\")"),
|
||||||
|
Example("var test = (!bar)"),
|
||||||
|
Example("var a: [Int]!"),
|
||||||
|
Example("private var myProperty: (Void -> Void)!"),
|
||||||
|
Example("func foo(_ options: [AnyHashable: Any]!) {"),
|
||||||
|
Example("func foo() -> [Int]!"),
|
||||||
|
Example("func foo() -> [AnyHashable: Any]!"),
|
||||||
|
Example("func foo() -> [Int]! { return [] }"),
|
||||||
|
Example("return self")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("let url = NSURL(string: query)↓!"),
|
||||||
|
Example("navigationController↓!.pushViewController(viewController, animated: true)"),
|
||||||
|
Example("let unwrapped = optional↓!"),
|
||||||
|
Example("return cell↓!"),
|
||||||
|
Example("let url = NSURL(string: \"http://www.google.com\")↓!"),
|
||||||
|
Example("""
|
||||||
|
let dict = ["Boooo": "👻"]
|
||||||
|
func bla() -> String {
|
||||||
|
return dict["Boooo"]↓!
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
let dict = ["Boooo": "👻"]
|
||||||
|
func bla() -> String {
|
||||||
|
return dict["Boooo"]↓!.contains("B")
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("let a = dict[\"abc\"]↓!.contains(\"B\")"),
|
||||||
|
Example("dict[\"abc\"]↓!.bar(\"B\")"),
|
||||||
|
Example("if dict[\"a\"]↓!↓!↓!↓! {}"),
|
||||||
|
Example("var foo: [Bool]! = dict[\"abc\"]↓!"),
|
||||||
|
Example("realm.objects(SwiftUTF8Object.self).filter(\"%K == %@\", \"柱нǢкƱаم👍\", utf8TestString).first↓!"),
|
||||||
|
Example("""
|
||||||
|
context("abc") {
|
||||||
|
var foo: [Bool]! = dict["abc"]↓!
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("open var computed: String { return foo.bar↓! }"),
|
||||||
|
Example("return self↓!"),
|
||||||
|
Example("[1, 3, 5, 6].first { $0.isMultiple(of: 2) }↓!"),
|
||||||
|
Example("map[\"a\"]↓!↓!")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
ForceUnwrappingVisitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ForceUnwrappingVisitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: ForcedValueExprSyntax) {
|
||||||
|
violations.append(node.exclamationMark.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct FunctionDefaultParameterAtEndRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "function_default_parameter_at_end",
|
||||||
|
name: "Function Default Parameter at End",
|
||||||
|
description: "Prefer to locate parameters with defaults toward the end of the parameter list",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("func foo(baz: String, bar: Int = 0) {}"),
|
||||||
|
Example("func foo(x: String, y: Int = 0, z: CGFloat = 0) {}"),
|
||||||
|
Example("func foo(bar: String, baz: Int = 0, z: () -> Void) {}"),
|
||||||
|
Example("func foo(bar: String, z: () -> Void, baz: Int = 0) {}"),
|
||||||
|
Example("func foo(bar: Int = 0) {}"),
|
||||||
|
Example("func foo() {}"),
|
||||||
|
Example("""
|
||||||
|
class A: B {
|
||||||
|
override func foo(bar: Int = 0, baz: String) {}
|
||||||
|
"""),
|
||||||
|
Example("func foo(bar: Int = 0, completion: @escaping CompletionHandler) {}"),
|
||||||
|
Example("""
|
||||||
|
func foo(a: Int, b: CGFloat = 0) {
|
||||||
|
let block = { (error: Error?) in }
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
func foo(a: String, b: String? = nil,
|
||||||
|
c: String? = nil, d: @escaping AlertActionHandler = { _ in }) {}
|
||||||
|
"""),
|
||||||
|
Example("override init?(for date: Date = Date(), coordinate: CLLocationCoordinate2D) {}"),
|
||||||
|
Example("""
|
||||||
|
func handleNotification(_ userInfo: NSDictionary,
|
||||||
|
userInteraction: Bool = false,
|
||||||
|
completionHandler: ((UIBackgroundFetchResult) -> Void)?) {}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
func write(withoutNotifying tokens: [NotificationToken] = {}, _ block: (() throws -> Int)) {}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
func expect<T>(file: String = #file, _ expression: @autoclosure () -> (() throws -> T)) -> Expectation<T> {}
|
||||||
|
""", excludeFromDocumentation: true)
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("↓func foo(bar: Int = 0, baz: String) {}"),
|
||||||
|
Example("private ↓func foo(bar: Int = 0, baz: String) {}"),
|
||||||
|
Example("public ↓init?(for date: Date = Date(), coordinate: CLLocationCoordinate2D) {}")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FunctionDefaultParameterAtEndRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: FunctionDeclSyntax) {
|
||||||
|
guard !node.modifiers.containsOverride, node.signature.containsViolation else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.append(node.funcKeyword.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: InitializerDeclSyntax) {
|
||||||
|
guard !node.modifiers.containsOverride, node.signature.containsViolation else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.append(node.initKeyword.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FunctionSignatureSyntax {
|
||||||
|
var containsViolation: Bool {
|
||||||
|
let params = input.parameterList.filter { param in
|
||||||
|
!param.isClosure
|
||||||
|
}
|
||||||
|
|
||||||
|
guard params.isNotEmpty else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultParams = params.filter { param in
|
||||||
|
param.defaultArgument != nil
|
||||||
|
}
|
||||||
|
guard defaultParams.isNotEmpty else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastParameters = params.suffix(defaultParams.count)
|
||||||
|
let lastParametersWithDefaultValue = lastParameters.filter { param in
|
||||||
|
param.defaultArgument != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastParameters.count != lastParametersWithDefaultValue.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FunctionParameterSyntax {
|
||||||
|
var isClosure: Bool {
|
||||||
|
if isEscaping || type.is(FunctionTypeSyntax.self) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if let optionalType = type.as(OptionalTypeSyntax.self),
|
||||||
|
let tuple = optionalType.wrappedType.as(TupleTypeSyntax.self) {
|
||||||
|
return tuple.elements.onlyElement?.type.as(FunctionTypeSyntax.self) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let tuple = type.as(TupleTypeSyntax.self) {
|
||||||
|
return tuple.elements.onlyElement?.type.as(FunctionTypeSyntax.self) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let attrType = type.as(AttributedTypeSyntax.self) {
|
||||||
|
return attrType.baseType.is(FunctionTypeSyntax.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEscaping: Bool {
|
||||||
|
guard let attrType = type.as(AttributedTypeSyntax.self) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrType.attributes.contains(attributeNamed: "escaping")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
import Foundation
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct GenericTypeNameRule: SwiftSyntaxRule, ConfigurationProviderRule {
|
||||||
|
var configuration = NameConfiguration<Self>(minLengthWarning: 1,
|
||||||
|
minLengthError: 0,
|
||||||
|
maxLengthWarning: 20,
|
||||||
|
maxLengthError: 1000)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "generic_type_name",
|
||||||
|
name: "Generic Type Name",
|
||||||
|
description: "Generic type name should only contain alphanumeric characters, start with an " +
|
||||||
|
"uppercase character and span between 1 and 20 characters in length.",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("func foo<T>() {}\n"),
|
||||||
|
Example("func foo<T>() -> T {}\n"),
|
||||||
|
Example("func foo<T, U>(param: U) -> T {}\n"),
|
||||||
|
Example("func foo<T: Hashable, U: Rule>(param: U) -> T {}\n"),
|
||||||
|
Example("struct Foo<T> {}\n"),
|
||||||
|
Example("class Foo<T> {}\n"),
|
||||||
|
Example("enum Foo<T> {}\n"),
|
||||||
|
Example("func run(_ options: NoOptions<CommandantError<()>>) {}\n"),
|
||||||
|
Example("func foo(_ options: Set<type>) {}\n"),
|
||||||
|
Example("func < <T: Comparable>(lhs: T?, rhs: T?) -> Bool\n"),
|
||||||
|
Example("func configureWith(data: Either<MessageThread, (project: Project, backing: Backing)>)\n"),
|
||||||
|
Example("typealias StringDictionary<T> = Dictionary<String, T>\n"),
|
||||||
|
Example("typealias BackwardTriple<T1, T2, T3> = (T3, T2, T1)\n"),
|
||||||
|
Example("typealias DictionaryOfStrings<T : Hashable> = Dictionary<T, String>\n")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("func foo<↓T_Foo>() {}\n"),
|
||||||
|
Example("func foo<T, ↓U_Foo>(param: U_Foo) -> T {}\n"),
|
||||||
|
Example("func foo<↓\(String(repeating: "T", count: 21))>() {}\n"),
|
||||||
|
Example("func foo<↓type>() {}\n"),
|
||||||
|
Example("typealias StringDictionary<↓T_Foo> = Dictionary<String, T_Foo>\n"),
|
||||||
|
Example("typealias BackwardTriple<T1, ↓T2_Bar, T3> = (T3, T2_Bar, T1)\n"),
|
||||||
|
Example("typealias DictionaryOfStrings<↓T_Foo: Hashable> = Dictionary<T_Foo, String>\n")
|
||||||
|
] + ["class", "struct", "enum"].flatMap { type -> [Example] in
|
||||||
|
return [
|
||||||
|
Example("\(type) Foo<↓T_Foo> {}\n"),
|
||||||
|
Example("\(type) Foo<T, ↓U_Foo> {}\n"),
|
||||||
|
Example("\(type) Foo<↓T_Foo, ↓U_Foo> {}\n"),
|
||||||
|
Example("\(type) Foo<↓\(String(repeating: "T", count: 21))> {}\n"),
|
||||||
|
Example("\(type) Foo<↓type> {}\n")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(configuration: configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension GenericTypeNameRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
private let configuration: ConfigurationType
|
||||||
|
|
||||||
|
init(configuration: ConfigurationType) {
|
||||||
|
self.configuration = configuration
|
||||||
|
super.init(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: GenericParameterSyntax) {
|
||||||
|
let name = node.name.text
|
||||||
|
guard !name.isEmpty, !configuration.shouldExclude(name: name) else { return }
|
||||||
|
|
||||||
|
if !configuration.allowedSymbolsAndAlphanumerics.isSuperset(of: CharacterSet(charactersIn: name)) {
|
||||||
|
violations.append(
|
||||||
|
ReasonedRuleViolation(
|
||||||
|
position: node.positionAfterSkippingLeadingTrivia,
|
||||||
|
reason: """
|
||||||
|
Generic type name '\(name)' should only contain alphanumeric and other allowed characters
|
||||||
|
""",
|
||||||
|
severity: .error
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if let caseCheckSeverity = configuration.validatesStartWithLowercase.severity,
|
||||||
|
!String(name[name.startIndex]).isUppercase() {
|
||||||
|
violations.append(
|
||||||
|
ReasonedRuleViolation(
|
||||||
|
position: node.positionAfterSkippingLeadingTrivia,
|
||||||
|
reason: "Generic type name '\(name)' should start with an uppercase character",
|
||||||
|
severity: caseCheckSeverity
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if let severity = configuration.severity(forLength: name.count) {
|
||||||
|
let reason = "Generic type name '\(name)' should be between \(configuration.minLengthThreshold) and " +
|
||||||
|
"\(configuration.maxLengthThreshold) characters long"
|
||||||
|
violations.append(
|
||||||
|
ReasonedRuleViolation(
|
||||||
|
position: node.positionAfterSkippingLeadingTrivia,
|
||||||
|
reason: reason,
|
||||||
|
severity: severity
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct ImplicitlyUnwrappedOptionalRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
|
||||||
|
var configuration = ImplicitlyUnwrappedOptionalConfiguration()
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "implicitly_unwrapped_optional",
|
||||||
|
name: "Implicitly Unwrapped Optional",
|
||||||
|
description: "Implicitly unwrapped optionals should be avoided when possible",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("@IBOutlet private var label: UILabel!"),
|
||||||
|
Example("@IBOutlet var label: UILabel!"),
|
||||||
|
Example("@IBOutlet var label: [UILabel!]"),
|
||||||
|
Example("if !boolean {}"),
|
||||||
|
Example("let int: Int? = 42"),
|
||||||
|
Example("let int: Int? = nil"),
|
||||||
|
Example("""
|
||||||
|
class MyClass {
|
||||||
|
@IBOutlet
|
||||||
|
weak var bar: SomeObject!
|
||||||
|
}
|
||||||
|
""", configuration: ["mode": "all_except_iboutlets"], excludeFromDocumentation: true)
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("let label: ↓UILabel!"),
|
||||||
|
Example("let IBOutlet: ↓UILabel!"),
|
||||||
|
Example("let labels: [↓UILabel!]"),
|
||||||
|
Example("var ints: [↓Int!] = [42, nil, 42]"),
|
||||||
|
Example("let label: ↓IBOutlet!"),
|
||||||
|
Example("let int: ↓Int! = 42"),
|
||||||
|
Example("let int: ↓Int! = nil"),
|
||||||
|
Example("var int: ↓Int! = 42"),
|
||||||
|
Example("let collection: AnyCollection<↓Int!>"),
|
||||||
|
Example("func foo(int: ↓Int!) {}"),
|
||||||
|
Example("""
|
||||||
|
class MyClass {
|
||||||
|
weak var bar: ↓SomeObject!
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(mode: configuration.mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ImplicitlyUnwrappedOptionalRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
private let mode: ConfigurationType.ImplicitlyUnwrappedOptionalModeConfiguration
|
||||||
|
|
||||||
|
init(mode: ConfigurationType.ImplicitlyUnwrappedOptionalModeConfiguration) {
|
||||||
|
self.mode = mode
|
||||||
|
super.init(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: ImplicitlyUnwrappedOptionalTypeSyntax) {
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||||
|
switch mode {
|
||||||
|
case .all:
|
||||||
|
return .visitChildren
|
||||||
|
case .allExceptIBOutlets:
|
||||||
|
return node.isIBOutlet ? .skipChildren : .visitChildren
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct IsDisjointRule: SwiftSyntaxRule, ConfigurationProviderRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "is_disjoint",
|
||||||
|
name: "Is Disjoint",
|
||||||
|
description: "Prefer using `Set.isDisjoint(with:)` over `Set.intersection(_:).isEmpty`",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("_ = Set(syntaxKinds).isDisjoint(with: commentAndStringKindsSet)"),
|
||||||
|
Example("let isObjc = !objcAttributes.isDisjoint(with: dictionary.enclosedSwiftAttributes)"),
|
||||||
|
Example("_ = Set(syntaxKinds).intersection(commentAndStringKindsSet)"),
|
||||||
|
Example("_ = !objcAttributes.intersection(dictionary.enclosedSwiftAttributes)")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("_ = Set(syntaxKinds).↓intersection(commentAndStringKindsSet).isEmpty"),
|
||||||
|
Example("let isObjc = !objcAttributes.↓intersection(dictionary.enclosedSwiftAttributes).isEmpty")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension IsDisjointRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: MemberAccessExprSyntax) {
|
||||||
|
guard
|
||||||
|
node.name.text == "isEmpty",
|
||||||
|
let firstBase = node.base?.asFunctionCall,
|
||||||
|
let firstBaseCalledExpression = firstBase.calledExpression.as(MemberAccessExprSyntax.self),
|
||||||
|
firstBaseCalledExpression.name.text == "intersection"
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.append(firstBaseCalledExpression.name.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct JoinedDefaultParameterRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule, OptInRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "joined_default_parameter",
|
||||||
|
name: "Joined Default Parameter",
|
||||||
|
description: "Discouraged explicit usage of the default separator",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("let foo = bar.joined()"),
|
||||||
|
Example("let foo = bar.joined(separator: \",\")"),
|
||||||
|
Example("let foo = bar.joined(separator: toto)")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("let foo = bar.joined(↓separator: \"\")"),
|
||||||
|
Example("""
|
||||||
|
let foo = bar.filter(toto)
|
||||||
|
.joined(↓separator: ""),
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
func foo() -> String {
|
||||||
|
return ["1", "2"].joined(↓separator: "")
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
],
|
||||||
|
corrections: [
|
||||||
|
Example("let foo = bar.joined(↓separator: \"\")"): Example("let foo = bar.joined()"),
|
||||||
|
Example("let foo = bar.filter(toto)\n.joined(↓separator: \"\")"):
|
||||||
|
Example("let foo = bar.filter(toto)\n.joined()"),
|
||||||
|
Example("func foo() -> String {\n return [\"1\", \"2\"].joined(↓separator: \"\")\n}"):
|
||||||
|
Example("func foo() -> String {\n return [\"1\", \"2\"].joined()\n}"),
|
||||||
|
Example("class C {\n#if true\nlet foo = bar.joined(↓separator: \"\")\n#endif\n}"):
|
||||||
|
Example("class C {\n#if true\nlet foo = bar.joined()\n#endif\n}")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
|
||||||
|
Rewriter(
|
||||||
|
locationConverter: file.locationConverter,
|
||||||
|
disabledRegions: disabledRegions(file: file)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension JoinedDefaultParameterRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: FunctionCallExprSyntax) {
|
||||||
|
if let violationPosition = node.violationPosition {
|
||||||
|
violations.append(violationPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
|
||||||
|
private(set) var correctionPositions: [AbsolutePosition] = []
|
||||||
|
private let locationConverter: SourceLocationConverter
|
||||||
|
private let disabledRegions: [SourceRange]
|
||||||
|
|
||||||
|
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
|
||||||
|
self.locationConverter = locationConverter
|
||||||
|
self.disabledRegions = disabledRegions
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
|
||||||
|
guard let violationPosition = node.violationPosition,
|
||||||
|
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
|
||||||
|
return super.visit(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
correctionPositions.append(violationPosition)
|
||||||
|
let newNode = node.with(\.argumentList, [])
|
||||||
|
return super.visit(newNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FunctionCallExprSyntax {
|
||||||
|
var violationPosition: AbsolutePosition? {
|
||||||
|
guard let argument = argumentList.first,
|
||||||
|
let memberExp = calledExpression.as(MemberAccessExprSyntax.self),
|
||||||
|
memberExp.name.text == "joined",
|
||||||
|
argument.label?.text == "separator",
|
||||||
|
let strLiteral = argument.expression.as(StringLiteralExprSyntax.self),
|
||||||
|
strLiteral.isEmptyString else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return argument.positionAfterSkippingLeadingTrivia
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,7 @@
|
||||||
import Foundation
|
struct LegacyCGGeometryFunctionsRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule {
|
||||||
import SourceKittenFramework
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
public struct LegacyCGGeometryFunctionsRule: CorrectableRule, ConfigurationProviderRule, AutomaticTestableRule {
|
static let description = RuleDescription(
|
||||||
public var configuration = SeverityConfiguration(.warning)
|
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
public static let description = RuleDescription(
|
|
||||||
identifier: "legacy_cggeometry_functions",
|
identifier: "legacy_cggeometry_functions",
|
||||||
name: "Legacy CGGeometry Functions",
|
name: "Legacy CGGeometry Functions",
|
||||||
description: "Struct extension properties and methods are preferred over legacy functions",
|
description: "Struct extension properties and methods are preferred over legacy functions",
|
||||||
|
@ -73,7 +68,7 @@ public struct LegacyCGGeometryFunctionsRule: CorrectableRule, ConfigurationProvi
|
||||||
Example("↓CGRectInset(rect, 5.0, -7.0)\n"): Example("rect.insetBy(dx: 5.0, dy: -7.0)\n"),
|
Example("↓CGRectInset(rect, 5.0, -7.0)\n"): Example("rect.insetBy(dx: 5.0, dy: -7.0)\n"),
|
||||||
Example("↓CGRectOffset(rect, -2, 8.3)\n"): Example("rect.offsetBy(dx: -2, dy: 8.3)\n"),
|
Example("↓CGRectOffset(rect, -2, 8.3)\n"): Example("rect.offsetBy(dx: -2, dy: 8.3)\n"),
|
||||||
Example("↓CGRectUnion(rect1, rect2)\n"): Example("rect1.union(rect2)\n"),
|
Example("↓CGRectUnion(rect1, rect2)\n"): Example("rect1.union(rect2)\n"),
|
||||||
Example("↓CGRectIntersection( rect1 ,rect2)\n"): Example("rect1.intersect(rect2)\n"),
|
Example("↓CGRectIntersection( rect1 ,rect2)\n"): Example("rect1.intersection(rect2)\n"),
|
||||||
Example("↓CGRectContainsRect( rect1,rect2 )\n"): Example("rect1.contains(rect2)\n"),
|
Example("↓CGRectContainsRect( rect1,rect2 )\n"): Example("rect1.contains(rect2)\n"),
|
||||||
Example("↓CGRectContainsPoint(rect ,point)\n"): Example("rect.contains(point)\n"),
|
Example("↓CGRectContainsPoint(rect ,point)\n"): Example("rect.contains(point)\n"),
|
||||||
Example("↓CGRectIntersectsRect( rect1,rect2 )\n"): Example("rect1.intersects(rect2)\n"),
|
Example("↓CGRectIntersectsRect( rect1,rect2 )\n"): Example("rect1.intersects(rect2)\n"),
|
||||||
|
@ -82,49 +77,38 @@ public struct LegacyCGGeometryFunctionsRule: CorrectableRule, ConfigurationProvi
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
public func validate(file: SwiftLintFile) -> [StyleViolation] {
|
private static let legacyFunctions: [String: LegacyFunctionRuleHelper.RewriteStrategy] = [
|
||||||
let functions = ["CGRectGetWidth", "CGRectGetHeight", "CGRectGetMinX", "CGRectGetMidX",
|
"CGRectGetWidth": .property(name: "width"),
|
||||||
"CGRectGetMaxX", "CGRectGetMinY", "CGRectGetMidY", "CGRectGetMaxY",
|
"CGRectGetHeight": .property(name: "height"),
|
||||||
"CGRectIsNull", "CGRectIsEmpty", "CGRectIsInfinite", "CGRectStandardize",
|
"CGRectGetMinX": .property(name: "minX"),
|
||||||
"CGRectIntegral", "CGRectInset", "CGRectOffset", "CGRectUnion",
|
"CGRectGetMidX": .property(name: "midX"),
|
||||||
"CGRectIntersection", "CGRectContainsRect", "CGRectContainsPoint",
|
"CGRectGetMaxX": .property(name: "maxX"),
|
||||||
"CGRectIntersectsRect"]
|
"CGRectGetMinY": .property(name: "minY"),
|
||||||
|
"CGRectGetMidY": .property(name: "midY"),
|
||||||
|
"CGRectGetMaxY": .property(name: "maxY"),
|
||||||
|
"CGRectIsNull": .property(name: "isNull"),
|
||||||
|
"CGRectIsEmpty": .property(name: "isEmpty"),
|
||||||
|
"CGRectIsInfinite": .property(name: "isInfinite"),
|
||||||
|
"CGRectStandardize": .property(name: "standardized"),
|
||||||
|
"CGRectIntegral": .property(name: "integral"),
|
||||||
|
"CGRectInset": .function(name: "insetBy", argumentLabels: ["dx", "dy"]),
|
||||||
|
"CGRectOffset": .function(name: "offsetBy", argumentLabels: ["dx", "dy"]),
|
||||||
|
"CGRectUnion": .function(name: "union", argumentLabels: [""]),
|
||||||
|
"CGRectContainsRect": .function(name: "contains", argumentLabels: [""]),
|
||||||
|
"CGRectContainsPoint": .function(name: "contains", argumentLabels: [""]),
|
||||||
|
"CGRectIntersectsRect": .function(name: "intersects", argumentLabels: [""]),
|
||||||
|
"CGRectIntersection": .function(name: "intersection", argumentLabels: [""])
|
||||||
|
]
|
||||||
|
|
||||||
let pattern = "\\b(" + functions.joined(separator: "|") + ")\\b"
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
LegacyFunctionRuleHelper.Visitor(legacyFunctions: Self.legacyFunctions)
|
||||||
return file.match(pattern: pattern, with: [.identifier]).map {
|
|
||||||
StyleViolation(ruleDescription: type(of: self).description,
|
|
||||||
severity: configuration.severity,
|
|
||||||
location: Location(file: file, characterOffset: $0.location))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func correct(file: SwiftLintFile) -> [Correction] {
|
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
|
||||||
let varName = RegexHelpers.varNameGroup
|
LegacyFunctionRuleHelper.Rewriter(
|
||||||
let twoVars = RegexHelpers.twoVars
|
legacyFunctions: Self.legacyFunctions,
|
||||||
let twoVariableOrNumber = RegexHelpers.twoVariableOrNumber
|
locationConverter: file.locationConverter,
|
||||||
let patterns: [String: String] = [
|
disabledRegions: disabledRegions(file: file)
|
||||||
"CGRectGetWidth\\(\(varName)\\)": "$1.width",
|
)
|
||||||
"CGRectGetHeight\\(\(varName)\\)": "$1.height",
|
|
||||||
"CGRectGetMinX\\(\(varName)\\)": "$1.minX",
|
|
||||||
"CGRectGetMidX\\(\(varName)\\)": "$1.midX",
|
|
||||||
"CGRectGetMaxX\\(\(varName)\\)": "$1.maxX",
|
|
||||||
"CGRectGetMinY\\(\(varName)\\)": "$1.minY",
|
|
||||||
"CGRectGetMidY\\(\(varName)\\)": "$1.midY",
|
|
||||||
"CGRectGetMaxY\\(\(varName)\\)": "$1.maxY",
|
|
||||||
"CGRectIsNull\\(\(varName)\\)": "$1.isNull",
|
|
||||||
"CGRectIsEmpty\\(\(varName)\\)": "$1.isEmpty",
|
|
||||||
"CGRectIsInfinite\\(\(varName)\\)": "$1.isInfinite",
|
|
||||||
"CGRectStandardize\\(\(varName)\\)": "$1.standardized",
|
|
||||||
"CGRectIntegral\\(\(varName)\\)": "$1.integral",
|
|
||||||
"CGRectInset\\(\(varName),\(twoVariableOrNumber)\\)": "$1.insetBy(dx: $2, dy: $3)",
|
|
||||||
"CGRectOffset\\(\(varName),\(twoVariableOrNumber)\\)": "$1.offsetBy(dx: $2, dy: $3)",
|
|
||||||
"CGRectUnion\\(\(twoVars)\\)": "$1.union($2)",
|
|
||||||
"CGRectIntersection\\(\(twoVars)\\)": "$1.intersect($2)",
|
|
||||||
"CGRectContainsRect\\(\(twoVars)\\)": "$1.contains($2)",
|
|
||||||
"CGRectContainsPoint\\(\(twoVars)\\)": "$1.contains($2)",
|
|
||||||
"CGRectIntersectsRect\\(\(twoVars)\\)": "$1.intersects($2)"
|
|
||||||
]
|
|
||||||
return file.correct(legacyRule: self, patterns: patterns)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
import SwiftSyntaxBuilder
|
||||||
|
|
||||||
|
struct LegacyConstantRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "legacy_constant",
|
||||||
|
name: "Legacy Constant",
|
||||||
|
description: "Struct-scoped constants are preferred over legacy global constants",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: LegacyConstantRuleExamples.nonTriggeringExamples,
|
||||||
|
triggeringExamples: LegacyConstantRuleExamples.triggeringExamples,
|
||||||
|
corrections: LegacyConstantRuleExamples.corrections
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
|
||||||
|
Rewriter(
|
||||||
|
locationConverter: file.locationConverter,
|
||||||
|
disabledRegions: disabledRegions(file: file)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension LegacyConstantRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: IdentifierExprSyntax) {
|
||||||
|
if LegacyConstantRuleExamples.patterns.keys.contains(node.identifier.text) {
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: FunctionCallExprSyntax) {
|
||||||
|
if node.isLegacyPiExpression {
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
|
||||||
|
private(set) var correctionPositions: [AbsolutePosition] = []
|
||||||
|
let locationConverter: SourceLocationConverter
|
||||||
|
let disabledRegions: [SourceRange]
|
||||||
|
|
||||||
|
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
|
||||||
|
self.locationConverter = locationConverter
|
||||||
|
self.disabledRegions = disabledRegions
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: IdentifierExprSyntax) -> ExprSyntax {
|
||||||
|
guard
|
||||||
|
let correction = LegacyConstantRuleExamples.patterns[node.identifier.text],
|
||||||
|
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
|
||||||
|
else {
|
||||||
|
return super.visit(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
return ("\(raw: correction)" as ExprSyntax)
|
||||||
|
.with(\.leadingTrivia, node.leadingTrivia)
|
||||||
|
.with(\.trailingTrivia, node.trailingTrivia)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
|
||||||
|
guard
|
||||||
|
node.isLegacyPiExpression,
|
||||||
|
let calledExpression = node.calledExpression.as(IdentifierExprSyntax.self),
|
||||||
|
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
|
||||||
|
else {
|
||||||
|
return super.visit(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
return ("\(raw: calledExpression.identifier.text).pi" as ExprSyntax)
|
||||||
|
.with(\.leadingTrivia, node.leadingTrivia)
|
||||||
|
.with(\.trailingTrivia, node.trailingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FunctionCallExprSyntax {
|
||||||
|
var isLegacyPiExpression: Bool {
|
||||||
|
guard
|
||||||
|
let calledExpression = calledExpression.as(IdentifierExprSyntax.self),
|
||||||
|
calledExpression.identifier.text == "CGFloat" || calledExpression.identifier.text == "Float",
|
||||||
|
let argument = argumentList.onlyElement?.expression.as(IdentifierExprSyntax.self),
|
||||||
|
argument.identifier.text == "M_PI"
|
||||||
|
else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,8 +48,6 @@ internal struct LegacyConstantRuleExamples {
|
||||||
"NSZeroPoint": "NSPoint.zero",
|
"NSZeroPoint": "NSPoint.zero",
|
||||||
"NSZeroRect": "NSRect.zero",
|
"NSZeroRect": "NSRect.zero",
|
||||||
"NSZeroSize": "NSSize.zero",
|
"NSZeroSize": "NSSize.zero",
|
||||||
"CGRectNull": "CGRect.null",
|
"CGRectNull": "CGRect.null"
|
||||||
"CGFloat\\(M_PI\\)": "CGFloat.pi",
|
|
||||||
"Float\\(M_PI\\)": "Float.pi"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1,15 +1,12 @@
|
||||||
import Foundation
|
import SwiftSyntax
|
||||||
import SourceKittenFramework
|
|
||||||
|
|
||||||
public struct LegacyConstructorRule: ASTRule, CorrectableRule, ConfigurationProviderRule, AutomaticTestableRule {
|
struct LegacyConstructorRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule {
|
||||||
public var configuration = SeverityConfiguration(.warning)
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
public init() {}
|
static let description = RuleDescription(
|
||||||
|
|
||||||
public static let description = RuleDescription(
|
|
||||||
identifier: "legacy_constructor",
|
identifier: "legacy_constructor",
|
||||||
name: "Legacy Constructor",
|
name: "Legacy Constructor",
|
||||||
description: "Swift constructors are preferred over legacy convenience functions.",
|
description: "Swift constructors are preferred over legacy convenience functions",
|
||||||
kind: .idiomatic,
|
kind: .idiomatic,
|
||||||
nonTriggeringExamples: [
|
nonTriggeringExamples: [
|
||||||
Example("CGPoint(x: 10, y: 10)"),
|
Example("CGPoint(x: 10, y: 10)"),
|
||||||
|
@ -62,22 +59,22 @@ public struct LegacyConstructorRule: ASTRule, CorrectableRule, ConfigurationProv
|
||||||
Example("↓UIOffsetMake(horizontal, vertical)")
|
Example("↓UIOffsetMake(horizontal, vertical)")
|
||||||
],
|
],
|
||||||
corrections: [
|
corrections: [
|
||||||
Example("↓CGPointMake(10, 10 )\n"): Example("CGPoint(x: 10, y: 10)\n"),
|
Example("↓CGPointMake(10, 10)\n"): Example("CGPoint(x: 10, y: 10)\n"),
|
||||||
Example("↓CGPointMake(xPos, yPos )\n"): Example("CGPoint(x: xPos, y: yPos)\n"),
|
Example("↓CGPointMake(xPos, yPos)\n"): Example("CGPoint(x: xPos, y: yPos)\n"),
|
||||||
Example("↓CGSizeMake(10, 10)\n"): Example("CGSize(width: 10, height: 10)\n"),
|
Example("↓CGSizeMake(10, 10)\n"): Example("CGSize(width: 10, height: 10)\n"),
|
||||||
Example("↓CGSizeMake( aWidth, aHeight )\n"): Example("CGSize(width: aWidth, height: aHeight)\n"),
|
Example("↓CGSizeMake( aWidth, aHeight )\n"): Example("CGSize( width: aWidth, height: aHeight )\n"),
|
||||||
Example("↓CGRectMake(0, 0, 10, 10)\n"): Example("CGRect(x: 0, y: 0, width: 10, height: 10)\n"),
|
Example("↓CGRectMake(0, 0, 10, 10)\n"): Example("CGRect(x: 0, y: 0, width: 10, height: 10)\n"),
|
||||||
Example("↓CGRectMake(xPos, yPos , width, height)\n"):
|
Example("↓CGRectMake(xPos, yPos , width, height)\n"):
|
||||||
Example("CGRect(x: xPos, y: yPos, width: width, height: height)\n"),
|
Example("CGRect(x: xPos, y: yPos , width: width, height: height)\n"),
|
||||||
Example("↓CGVectorMake(10, 10)\n"): Example("CGVector(dx: 10, dy: 10)\n"),
|
Example("↓CGVectorMake(10, 10)\n"): Example("CGVector(dx: 10, dy: 10)\n"),
|
||||||
Example("↓CGVectorMake(deltaX, deltaY)\n"): Example("CGVector(dx: deltaX, dy: deltaY)\n"),
|
Example("↓CGVectorMake(deltaX, deltaY)\n"): Example("CGVector(dx: deltaX, dy: deltaY)\n"),
|
||||||
Example("↓NSMakePoint(10, 10 )\n"): Example("NSPoint(x: 10, y: 10)\n"),
|
Example("↓NSMakePoint(10, 10 )\n"): Example("NSPoint(x: 10, y: 10 )\n"),
|
||||||
Example("↓NSMakePoint(xPos, yPos )\n"): Example("NSPoint(x: xPos, y: yPos)\n"),
|
Example("↓NSMakePoint(xPos, yPos )\n"): Example("NSPoint(x: xPos, y: yPos )\n"),
|
||||||
Example("↓NSMakeSize(10, 10)\n"): Example("NSSize(width: 10, height: 10)\n"),
|
Example("↓NSMakeSize(10, 10)\n"): Example("NSSize(width: 10, height: 10)\n"),
|
||||||
Example("↓NSMakeSize( aWidth, aHeight )\n"): Example("NSSize(width: aWidth, height: aHeight)\n"),
|
Example("↓NSMakeSize( aWidth, aHeight )\n"): Example("NSSize( width: aWidth, height: aHeight )\n"),
|
||||||
Example("↓NSMakeRect(0, 0, 10, 10)\n"): Example("NSRect(x: 0, y: 0, width: 10, height: 10)\n"),
|
Example("↓NSMakeRect(0, 0, 10, 10)\n"): Example("NSRect(x: 0, y: 0, width: 10, height: 10)\n"),
|
||||||
Example("↓NSMakeRect(xPos, yPos , width, height)\n"):
|
Example("↓NSMakeRect(xPos, yPos , width, height)\n"):
|
||||||
Example("NSRect(x: xPos, y: yPos, width: width, height: height)\n"),
|
Example("NSRect(x: xPos, y: yPos , width: width, height: height)\n"),
|
||||||
Example("↓NSMakeRange(10, 1)\n"): Example("NSRange(location: 10, length: 1)\n"),
|
Example("↓NSMakeRange(10, 1)\n"): Example("NSRange(location: 10, length: 1)\n"),
|
||||||
Example("↓NSMakeRange(loc, len)\n"): Example("NSRange(location: loc, length: len)\n"),
|
Example("↓NSMakeRange(loc, len)\n"): Example("NSRange(location: loc, length: len)\n"),
|
||||||
Example("↓CGVectorMake(10, 10)\n↓NSMakeRange(10, 1)\n"):
|
Example("↓CGVectorMake(10, 10)\n↓NSMakeRange(10, 1)\n"):
|
||||||
|
@ -125,99 +122,66 @@ public struct LegacyConstructorRule: ASTRule, CorrectableRule, ConfigurationProv
|
||||||
"NSEdgeInsetsMake": "NSEdgeInsets",
|
"NSEdgeInsetsMake": "NSEdgeInsets",
|
||||||
"UIOffsetMake": "UIOffset"]
|
"UIOffsetMake": "UIOffset"]
|
||||||
|
|
||||||
public func validate(file: SwiftLintFile, kind: SwiftExpressionKind,
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
dictionary: SourceKittenDictionary) -> [StyleViolation] {
|
Visitor(viewMode: .sourceAccurate)
|
||||||
guard containsViolation(kind: kind, dictionary: dictionary),
|
|
||||||
let offset = dictionary.offset else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
StyleViolation(ruleDescription: type(of: self).description,
|
|
||||||
severity: configuration.severity,
|
|
||||||
location: Location(file: file, byteOffset: offset))
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func violations(in file: SwiftLintFile, kind: SwiftExpressionKind,
|
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
|
||||||
dictionary: SourceKittenDictionary) -> [SourceKittenDictionary] {
|
Rewriter(
|
||||||
guard containsViolation(kind: kind, dictionary: dictionary) else {
|
locationConverter: file.locationConverter,
|
||||||
return []
|
disabledRegions: disabledRegions(file: file)
|
||||||
}
|
)
|
||||||
|
|
||||||
return [dictionary]
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func containsViolation(kind: SwiftExpressionKind,
|
private extension LegacyConstructorRule {
|
||||||
dictionary: SourceKittenDictionary) -> Bool {
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
guard kind == .call,
|
override func visitPost(_ node: FunctionCallExprSyntax) {
|
||||||
let name = dictionary.name,
|
if let identifierExpr = node.calledExpression.as(IdentifierExprSyntax.self),
|
||||||
dictionary.offset != nil,
|
constructorsToCorrectedNames[identifierExpr.identifier.text] != nil {
|
||||||
let expectedArguments = type(of: self).constructorsToArguments[name],
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
dictionary.enclosedArguments.count == expectedArguments.count else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func violations(in file: SwiftLintFile,
|
|
||||||
dictionary: SourceKittenDictionary) -> [SourceKittenDictionary] {
|
|
||||||
return dictionary.traverseDepthFirst { subDict in
|
|
||||||
guard let kind = subDict.expressionKind else { return nil }
|
|
||||||
return violations(in: file, kind: kind, dictionary: subDict)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func violations(in file: SwiftLintFile) -> [SourceKittenDictionary] {
|
|
||||||
return violations(in: file, dictionary: file.structureDictionary).sorted { lhs, rhs in
|
|
||||||
(lhs.offset ?? 0) < (rhs.offset ?? 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func correct(file: SwiftLintFile) -> [Correction] {
|
|
||||||
let violatingDictionaries = violations(in: file)
|
|
||||||
var correctedContents = file.contents
|
|
||||||
var adjustedLocations = [Int]()
|
|
||||||
|
|
||||||
for dictionary in violatingDictionaries.reversed() {
|
|
||||||
guard let byteRange = dictionary.byteRange,
|
|
||||||
let range = file.stringView.byteRangeToNSRange(byteRange),
|
|
||||||
let name = dictionary.name,
|
|
||||||
let correctedName = type(of: self).constructorsToCorrectedNames[name],
|
|
||||||
file.ruleEnabled(violatingRanges: [range], for: self) == [range],
|
|
||||||
case let arguments = argumentsContents(file: file, arguments: dictionary.enclosedArguments),
|
|
||||||
let expectedArguments = type(of: self).constructorsToArguments[name],
|
|
||||||
arguments.count == expectedArguments.count else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if let indexRange = correctedContents.nsrangeToIndexRange(range) {
|
|
||||||
let joinedArguments = zip(expectedArguments, arguments).map { "\($0): \($1)" }.joined(separator: ", ")
|
|
||||||
let replacement = correctedName + "(" + joinedArguments + ")"
|
|
||||||
correctedContents = correctedContents.replacingCharacters(in: indexRange, with: replacement)
|
|
||||||
adjustedLocations.insert(range.location, at: 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let corrections = adjustedLocations.map {
|
|
||||||
Correction(ruleDescription: type(of: self).description,
|
|
||||||
location: Location(file: file, characterOffset: $0))
|
|
||||||
}
|
|
||||||
|
|
||||||
file.write(correctedContents)
|
|
||||||
|
|
||||||
return corrections
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func argumentsContents(file: SwiftLintFile, arguments: [SourceKittenDictionary]) -> [String] {
|
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
|
||||||
let contents = file.stringView
|
private(set) var correctionPositions: [AbsolutePosition] = []
|
||||||
return arguments.compactMap { argument -> String? in
|
let locationConverter: SourceLocationConverter
|
||||||
guard argument.name == nil, let byteRange = argument.byteRange else {
|
let disabledRegions: [SourceRange]
|
||||||
return nil
|
|
||||||
|
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
|
||||||
|
self.locationConverter = locationConverter
|
||||||
|
self.disabledRegions = disabledRegions
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
|
||||||
|
guard let identifierExpr = node.calledExpression.as(IdentifierExprSyntax.self),
|
||||||
|
case let identifier = identifierExpr.identifier.text,
|
||||||
|
let correctedName = constructorsToCorrectedNames[identifier],
|
||||||
|
let args = constructorsToArguments[identifier],
|
||||||
|
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
|
||||||
|
return super.visit(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
return contents.substringWithByteRange(byteRange)
|
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
|
||||||
|
let arguments = TupleExprElementListSyntax(node.argumentList.enumerated().map { index, elem in
|
||||||
|
elem
|
||||||
|
.with(\.label, .identifier(args[index]))
|
||||||
|
.with(\.colon, .colonToken(trailingTrivia: .space))
|
||||||
|
})
|
||||||
|
let newExpression = identifierExpr.with(
|
||||||
|
\.identifier,
|
||||||
|
.identifier(
|
||||||
|
correctedName,
|
||||||
|
leadingTrivia: identifierExpr.identifier.leadingTrivia,
|
||||||
|
trailingTrivia: identifierExpr.identifier.trailingTrivia
|
||||||
|
)
|
||||||
|
)
|
||||||
|
let newNode = node
|
||||||
|
.with(\.calledExpression, ExprSyntax(newExpression))
|
||||||
|
.with(\.argumentList, arguments)
|
||||||
|
return super.visit(newNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,16 +1,13 @@
|
||||||
import SourceKittenFramework
|
import SwiftSyntax
|
||||||
|
|
||||||
public struct LegacyHashingRule: ASTRule, ConfigurationProviderRule, AutomaticTestableRule {
|
struct LegacyHashingRule: SwiftSyntaxRule, ConfigurationProviderRule {
|
||||||
public var configuration = SeverityConfiguration(.warning)
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
public init() {}
|
static let description = RuleDescription(
|
||||||
|
|
||||||
public static let description = RuleDescription(
|
|
||||||
identifier: "legacy_hashing",
|
identifier: "legacy_hashing",
|
||||||
name: "Legacy Hashing",
|
name: "Legacy Hashing",
|
||||||
description: "Prefer using the `hash(into:)` function instead of overriding `hashValue`",
|
description: "Prefer using the `hash(into:)` function instead of overriding `hashValue`",
|
||||||
kind: .idiomatic,
|
kind: .idiomatic,
|
||||||
minSwiftVersion: .fourDotTwo,
|
|
||||||
nonTriggeringExamples: [
|
nonTriggeringExamples: [
|
||||||
Example("""
|
Example("""
|
||||||
struct Foo: Hashable {
|
struct Foo: Hashable {
|
||||||
|
@ -76,21 +73,27 @@ public struct LegacyHashingRule: ASTRule, ConfigurationProviderRule, AutomaticTe
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
// MARK: - ASTRule
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
public func validate(file: SwiftLintFile,
|
}
|
||||||
kind: SwiftDeclarationKind,
|
}
|
||||||
dictionary: SourceKittenDictionary) -> [StyleViolation] {
|
|
||||||
guard kind == .varInstance,
|
extension LegacyHashingRule {
|
||||||
dictionary.setterAccessibility == nil,
|
private final class Visitor: ViolationsSyntaxVisitor {
|
||||||
dictionary.typeName == "Int",
|
override func visitPost(_ node: VariableDeclSyntax) {
|
||||||
dictionary.name == "hashValue",
|
guard
|
||||||
let offset = dictionary.offset else {
|
node.parent?.is(MemberDeclListItemSyntax.self) == true,
|
||||||
return []
|
node.bindingKeyword.tokenKind == .keyword(.var),
|
||||||
}
|
let binding = node.bindings.onlyElement,
|
||||||
|
let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
|
||||||
return [StyleViolation(ruleDescription: type(of: self).description,
|
identifier.identifier.text == "hashValue",
|
||||||
severity: configuration.severity,
|
let returnType = binding.typeAnnotation?.type.as(SimpleTypeIdentifierSyntax.self),
|
||||||
location: Location(file: file, byteOffset: offset))]
|
returnType.name.text == "Int"
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.append(node.bindingKeyword.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct LegacyMultipleRule: OptInRule, ConfigurationProviderRule, SwiftSyntaxRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "legacy_multiple",
|
||||||
|
name: "Legacy Multiple",
|
||||||
|
description: "Prefer using the `isMultiple(of:)` function instead of using the remainder operator (`%`)",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("cell.contentView.backgroundColor = indexPath.row.isMultiple(of: 2) ? .gray : .white"),
|
||||||
|
Example("guard count.isMultiple(of: 2) else { throw DecodingError.dataCorrupted(...) }"),
|
||||||
|
Example("sanityCheck(bytes > 0 && bytes.isMultiple(of: 4), \"capacity must be multiple of 4 bytes\")"),
|
||||||
|
Example("guard let i = reversedNumbers.firstIndex(where: { $0.isMultiple(of: 2) }) else { return }"),
|
||||||
|
Example("""
|
||||||
|
let constant = 56
|
||||||
|
let isMultiple = value.isMultiple(of: constant)
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
let constant = 56
|
||||||
|
let secret = value % constant == 5
|
||||||
|
"""),
|
||||||
|
Example("let secretValue = (value % 3) + 2")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("cell.contentView.backgroundColor = indexPath.row ↓% 2 == 0 ? .gray : .white"),
|
||||||
|
Example("cell.contentView.backgroundColor = 0 == indexPath.row ↓% 2 ? .gray : .white"),
|
||||||
|
Example("cell.contentView.backgroundColor = indexPath.row ↓% 2 != 0 ? .gray : .white"),
|
||||||
|
Example("guard count ↓% 2 == 0 else { throw DecodingError.dataCorrupted(...) }"),
|
||||||
|
Example("sanityCheck(bytes > 0 && bytes ↓% 4 == 0, \"capacity must be multiple of 4 bytes\")"),
|
||||||
|
Example("guard let i = reversedNumbers.firstIndex(where: { $0 ↓% 2 == 0 }) else { return }"),
|
||||||
|
Example("""
|
||||||
|
let constant = 56
|
||||||
|
let isMultiple = value ↓% constant == 0
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func preprocess(file: SwiftLintFile) -> SourceFileSyntax? {
|
||||||
|
file.foldedSyntaxTree
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension LegacyMultipleRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: InfixOperatorExprSyntax) {
|
||||||
|
guard let operatorNode = node.operatorOperand.as(BinaryOperatorExprSyntax.self),
|
||||||
|
operatorNode.operatorToken.tokenKind == .binaryOperator("%"),
|
||||||
|
let parent = node.parent?.as(InfixOperatorExprSyntax.self),
|
||||||
|
let parentOperatorNode = parent.operatorOperand.as(BinaryOperatorExprSyntax.self),
|
||||||
|
parentOperatorNode.isEqualityOrInequalityOperator else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let isExprEqualTo0 = {
|
||||||
|
parent.leftOperand.as(InfixOperatorExprSyntax.self) == node &&
|
||||||
|
parent.rightOperand.as(IntegerLiteralExprSyntax.self)?.isZero == true
|
||||||
|
}
|
||||||
|
|
||||||
|
let is0EqualToExpr = {
|
||||||
|
parent.leftOperand.as(IntegerLiteralExprSyntax.self)?.isZero == true &&
|
||||||
|
parent.rightOperand.as(InfixOperatorExprSyntax.self) == node
|
||||||
|
}
|
||||||
|
|
||||||
|
guard isExprEqualTo0() || is0EqualToExpr() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.append(node.operatorOperand.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension BinaryOperatorExprSyntax {
|
||||||
|
var isEqualityOrInequalityOperator: Bool {
|
||||||
|
operatorToken.tokenKind == .binaryOperator("==") ||
|
||||||
|
operatorToken.tokenKind == .binaryOperator("!=")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,7 @@
|
||||||
import Foundation
|
struct LegacyNSGeometryFunctionsRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule {
|
||||||
import SourceKittenFramework
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
public struct LegacyNSGeometryFunctionsRule: CorrectableRule, ConfigurationProviderRule, AutomaticTestableRule {
|
static let description = RuleDescription(
|
||||||
public var configuration = SeverityConfiguration(.warning)
|
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
public static let description = RuleDescription(
|
|
||||||
identifier: "legacy_nsgeometry_functions",
|
identifier: "legacy_nsgeometry_functions",
|
||||||
name: "Legacy NSGeometry Functions",
|
name: "Legacy NSGeometry Functions",
|
||||||
description: "Struct extension properties and methods are preferred over legacy functions",
|
description: "Struct extension properties and methods are preferred over legacy functions",
|
||||||
|
@ -25,7 +20,7 @@ public struct LegacyNSGeometryFunctionsRule: CorrectableRule, ConfigurationProvi
|
||||||
Example("rect.insetBy(dx: 5.0, dy: -7.0)"),
|
Example("rect.insetBy(dx: 5.0, dy: -7.0)"),
|
||||||
Example("rect.offsetBy(dx: 5.0, dy: -7.0)"),
|
Example("rect.offsetBy(dx: 5.0, dy: -7.0)"),
|
||||||
Example("rect1.union(rect2)"),
|
Example("rect1.union(rect2)"),
|
||||||
Example("rect1.intersect(rect2)"),
|
Example("rect1.intersection(rect2)"),
|
||||||
// "rect.divide(atDistance: 10.2, fromEdge: edge)", No correction available for divide
|
// "rect.divide(atDistance: 10.2, fromEdge: edge)", No correction available for divide
|
||||||
Example("rect1.contains(rect2)"),
|
Example("rect1.contains(rect2)"),
|
||||||
Example("rect.contains(point)"),
|
Example("rect.contains(point)"),
|
||||||
|
@ -72,59 +67,48 @@ public struct LegacyNSGeometryFunctionsRule: CorrectableRule, ConfigurationProvi
|
||||||
Example("↓NSInsetRect(rect, 5.0, -7.0)\n"): Example("rect.insetBy(dx: 5.0, dy: -7.0)\n"),
|
Example("↓NSInsetRect(rect, 5.0, -7.0)\n"): Example("rect.insetBy(dx: 5.0, dy: -7.0)\n"),
|
||||||
Example("↓NSOffsetRect(rect, -2, 8.3)\n"): Example("rect.offsetBy(dx: -2, dy: 8.3)\n"),
|
Example("↓NSOffsetRect(rect, -2, 8.3)\n"): Example("rect.offsetBy(dx: -2, dy: 8.3)\n"),
|
||||||
Example("↓NSUnionRect(rect1, rect2)\n"): Example("rect1.union(rect2)\n"),
|
Example("↓NSUnionRect(rect1, rect2)\n"): Example("rect1.union(rect2)\n"),
|
||||||
Example("↓NSIntersectionRect( rect1 ,rect2)\n"): Example("rect1.intersect(rect2)\n"),
|
|
||||||
Example("↓NSContainsRect( rect1,rect2 )\n"): Example("rect1.contains(rect2)\n"),
|
Example("↓NSContainsRect( rect1,rect2 )\n"): Example("rect1.contains(rect2)\n"),
|
||||||
Example("↓NSPointInRect(point ,rect)\n"): Example("rect.contains(point)\n"), // note order of arguments
|
Example("↓NSPointInRect(point ,rect)\n"): Example("rect.contains(point)\n"), // note order of arguments
|
||||||
Example("↓NSIntersectsRect( rect1,rect2 )\n"): Example("rect1.intersects(rect2)\n"),
|
Example("↓NSIntersectsRect( rect1,rect2 )\n"): Example("rect1.intersects(rect2)\n"),
|
||||||
Example("↓NSIntersectsRect(rect1, rect2 )\n↓NSWidth(rect )\n"):
|
Example("↓NSIntersectsRect(rect1, rect2 )\n↓NSWidth(rect )\n"):
|
||||||
Example("rect1.intersects(rect2)\nrect.width\n")
|
Example("rect1.intersects(rect2)\nrect.width\n"),
|
||||||
|
Example("↓NSIntersectionRect(rect1, rect2)"): Example("rect1.intersection(rect2)")
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
public func validate(file: SwiftLintFile) -> [StyleViolation] {
|
private static let legacyFunctions: [String: LegacyFunctionRuleHelper.RewriteStrategy] = [
|
||||||
let functions = ["NSWidth", "NSHeight", "NSMinX", "NSMidX",
|
"NSHeight": .property(name: "height"),
|
||||||
"NSMaxX", "NSMinY", "NSMidY", "NSMaxY",
|
"NSIntegralRect": .property(name: "integral"),
|
||||||
"NSEqualRects", "NSEqualSizes", "NSEqualPoints", "NSEdgeInsetsEqual",
|
"NSIsEmptyRect": .property(name: "isEmpty"),
|
||||||
"NSIsEmptyRect", "NSIntegralRect", "NSInsetRect",
|
"NSMaxX": .property(name: "maxX"),
|
||||||
"NSOffsetRect", "NSUnionRect", "NSIntersectionRect",
|
"NSMaxY": .property(name: "maxY"),
|
||||||
"NSContainsRect", "NSPointInRect", "NSIntersectsRect"]
|
"NSMidX": .property(name: "midX"),
|
||||||
|
"NSMidY": .property(name: "midY"),
|
||||||
|
"NSMinX": .property(name: "minX"),
|
||||||
|
"NSMinY": .property(name: "minY"),
|
||||||
|
"NSWidth": .property(name: "width"),
|
||||||
|
"NSEqualPoints": .equal,
|
||||||
|
"NSEqualSizes": .equal,
|
||||||
|
"NSEqualRects": .equal,
|
||||||
|
"NSEdgeInsetsEqual": .equal,
|
||||||
|
"NSInsetRect": .function(name: "insetBy", argumentLabels: ["dx", "dy"]),
|
||||||
|
"NSOffsetRect": .function(name: "offsetBy", argumentLabels: ["dx", "dy"]),
|
||||||
|
"NSUnionRect": .function(name: "union", argumentLabels: [""]),
|
||||||
|
"NSContainsRect": .function(name: "contains", argumentLabels: [""]),
|
||||||
|
"NSIntersectsRect": .function(name: "intersects", argumentLabels: [""]),
|
||||||
|
"NSIntersectionRect": .function(name: "intersection", argumentLabels: [""]),
|
||||||
|
"NSPointInRect": .function(name: "contains", argumentLabels: [""], reversed: true)
|
||||||
|
]
|
||||||
|
|
||||||
let pattern = "\\b(" + functions.joined(separator: "|") + ")\\b"
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
LegacyFunctionRuleHelper.Visitor(legacyFunctions: Self.legacyFunctions)
|
||||||
return file.match(pattern: pattern, with: [.identifier]).map {
|
|
||||||
StyleViolation(ruleDescription: type(of: self).description,
|
|
||||||
severity: configuration.severity,
|
|
||||||
location: Location(file: file, characterOffset: $0.location))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func correct(file: SwiftLintFile) -> [Correction] {
|
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
|
||||||
let varName = RegexHelpers.varNameGroup
|
LegacyFunctionRuleHelper.Rewriter(
|
||||||
let twoVars = RegexHelpers.twoVars
|
legacyFunctions: Self.legacyFunctions,
|
||||||
let twoVariableOrNumber = RegexHelpers.twoVariableOrNumber
|
locationConverter: file.locationConverter,
|
||||||
let patterns: [String: String] = [
|
disabledRegions: disabledRegions(file: file)
|
||||||
"NSWidth\\(\(varName)\\)": "$1.width",
|
)
|
||||||
"NSHeight\\(\(varName)\\)": "$1.height",
|
|
||||||
"NSMinX\\(\(varName)\\)": "$1.minX",
|
|
||||||
"NSMidX\\(\(varName)\\)": "$1.midX",
|
|
||||||
"NSMaxX\\(\(varName)\\)": "$1.maxX",
|
|
||||||
"NSMinY\\(\(varName)\\)": "$1.minY",
|
|
||||||
"NSMidY\\(\(varName)\\)": "$1.midY",
|
|
||||||
"NSMaxY\\(\(varName)\\)": "$1.maxY",
|
|
||||||
"NSEqualRects\\(\(twoVars)\\)": "$1 == $2",
|
|
||||||
"NSEqualSizes\\(\(twoVars)\\)": "$1 == $2",
|
|
||||||
"NSEqualPoints\\(\(twoVars)\\)": "$1 == $2",
|
|
||||||
"NSEdgeInsetsEqual\\(\(twoVars)\\)": "$1 == $2",
|
|
||||||
"NSIsEmptyRect\\(\(varName)\\)": "$1.isEmpty",
|
|
||||||
"NSIntegralRect\\(\(varName)\\)": "$1.integral",
|
|
||||||
"NSInsetRect\\(\(varName),\(twoVariableOrNumber)\\)": "$1.insetBy(dx: $2, dy: $3)",
|
|
||||||
"NSOffsetRect\\(\(varName),\(twoVariableOrNumber)\\)": "$1.offsetBy(dx: $2, dy: $3)",
|
|
||||||
"NSUnionRect\\(\(twoVars)\\)": "$1.union($2)",
|
|
||||||
"NSIntersectionRect\\(\(twoVars)\\)": "$1.intersect($2)",
|
|
||||||
"NSContainsRect\\(\(twoVars)\\)": "$1.contains($2)",
|
|
||||||
"NSPointInRect\\(\(twoVars)\\)": "$2.contains($1)", // note order of arguments
|
|
||||||
"NSIntersectsRect\\(\(twoVars)\\)": "$1.intersects($2)"
|
|
||||||
]
|
|
||||||
return file.correct(legacyRule: self, patterns: patterns)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
private let legacyObjcTypes = [
|
||||||
|
"NSAffineTransform",
|
||||||
|
"NSArray",
|
||||||
|
"NSCalendar",
|
||||||
|
"NSCharacterSet",
|
||||||
|
"NSData",
|
||||||
|
"NSDateComponents",
|
||||||
|
"NSDateInterval",
|
||||||
|
"NSDate",
|
||||||
|
"NSDecimalNumber",
|
||||||
|
"NSDictionary",
|
||||||
|
"NSIndexPath",
|
||||||
|
"NSIndexSet",
|
||||||
|
"NSLocale",
|
||||||
|
"NSMeasurement",
|
||||||
|
"NSNotification",
|
||||||
|
"NSNumber",
|
||||||
|
"NSPersonNameComponents",
|
||||||
|
"NSSet",
|
||||||
|
"NSString",
|
||||||
|
"NSTimeZone",
|
||||||
|
"NSURL",
|
||||||
|
"NSURLComponents",
|
||||||
|
"NSURLQueryItem",
|
||||||
|
"NSURLRequest",
|
||||||
|
"NSUUID"
|
||||||
|
]
|
||||||
|
|
||||||
|
struct LegacyObjcTypeRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "legacy_objc_type",
|
||||||
|
name: "Legacy Objective-C Reference Type",
|
||||||
|
description: "Prefer Swift value types to bridged Objective-C reference types",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("var array = Array<Int>()\n"),
|
||||||
|
Example("var calendar: Calendar? = nil"),
|
||||||
|
Example("var formatter: NSDataDetector"),
|
||||||
|
Example("var className: String = NSStringFromClass(MyClass.self)"),
|
||||||
|
Example("_ = URLRequest.CachePolicy.reloadIgnoringLocalCacheData"),
|
||||||
|
Example(#"_ = Notification.Name("com.apple.Music.playerInfo")"#)
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("var array = ↓NSArray()"),
|
||||||
|
Example("var calendar: ↓NSCalendar? = nil"),
|
||||||
|
Example("_ = ↓NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData"),
|
||||||
|
Example(#"_ = ↓NSNotification.Name("com.apple.Music.playerInfo")"#),
|
||||||
|
Example(#"""
|
||||||
|
let keyValuePair: (Int) -> (↓NSString, ↓NSString) = {
|
||||||
|
let n = "\($0)" as ↓NSString; return (n, n)
|
||||||
|
}
|
||||||
|
dictionary = [↓NSString: ↓NSString](uniqueKeysWithValues:
|
||||||
|
(1...10_000).lazy.map(keyValuePair))
|
||||||
|
"""#),
|
||||||
|
Example("""
|
||||||
|
extension Foundation.Notification.Name {
|
||||||
|
static var reachabilityChanged: Foundation.↓NSNotification.Name {
|
||||||
|
return Foundation.Notification.Name("org.wordpress.reachability.changed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension LegacyObjcTypeRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: SimpleTypeIdentifierSyntax) {
|
||||||
|
if let typeName = node.typeName, legacyObjcTypes.contains(typeName) {
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: IdentifierExprSyntax) {
|
||||||
|
if legacyObjcTypes.contains(node.identifier.text) {
|
||||||
|
violations.append(node.identifier.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: MemberTypeIdentifierSyntax) {
|
||||||
|
guard node.baseType.as(SimpleTypeIdentifierSyntax.self)?.typeName == "Foundation",
|
||||||
|
legacyObjcTypes.contains(node.name.text)
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.append(node.name.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct LegacyRandomRule: SwiftSyntaxRule, ConfigurationProviderRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static var description = RuleDescription(
|
||||||
|
identifier: "legacy_random",
|
||||||
|
name: "Legacy Random",
|
||||||
|
description: "Prefer using `type.random(in:)` over legacy functions",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("Int.random(in: 0..<10)\n"),
|
||||||
|
Example("Double.random(in: 8.6...111.34)\n"),
|
||||||
|
Example("Float.random(in: 0 ..< 1)\n")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("↓arc4random()\n"),
|
||||||
|
Example("↓arc4random_uniform(83)\n"),
|
||||||
|
Example("↓drand48()\n")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension LegacyRandomRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
private static let legacyRandomFunctions: Set<String> = [
|
||||||
|
"arc4random",
|
||||||
|
"arc4random_uniform",
|
||||||
|
"drand48"
|
||||||
|
]
|
||||||
|
|
||||||
|
override func visitPost(_ node: FunctionCallExprSyntax) {
|
||||||
|
if let function = node.calledExpression.as(IdentifierExprSyntax.self)?.identifier.text,
|
||||||
|
Self.legacyRandomFunctions.contains(function) {
|
||||||
|
violations.append(node.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,13 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import SourceKittenFramework
|
import SourceKittenFramework
|
||||||
|
|
||||||
public struct NimbleOperatorRule: ConfigurationProviderRule, OptInRule, CorrectableRule, AutomaticTestableRule {
|
struct NimbleOperatorRule: ConfigurationProviderRule, OptInRule, CorrectableRule {
|
||||||
public var configuration = SeverityConfiguration(.warning)
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
public init() {}
|
static let description = RuleDescription(
|
||||||
|
|
||||||
public static let description = RuleDescription(
|
|
||||||
identifier: "nimble_operator",
|
identifier: "nimble_operator",
|
||||||
name: "Nimble Operator",
|
name: "Nimble Operator",
|
||||||
description: "Prefer Nimble operator overloads over free matcher functions.",
|
description: "Prefer Nimble operator overloads over free matcher functions",
|
||||||
kind: .idiomatic,
|
kind: .idiomatic,
|
||||||
nonTriggeringExamples: [
|
nonTriggeringExamples: [
|
||||||
Example("expect(seagull.squawk) != \"Hi!\"\n"),
|
Example("expect(seagull.squawk) != \"Hi!\"\n"),
|
||||||
|
@ -21,6 +19,8 @@ public struct NimbleOperatorRule: ConfigurationProviderRule, OptInRule, Correcta
|
||||||
Example("expect(x) === x"),
|
Example("expect(x) === x"),
|
||||||
Example("expect(10) == 10"),
|
Example("expect(10) == 10"),
|
||||||
Example("expect(success) == true"),
|
Example("expect(success) == true"),
|
||||||
|
Example("expect(value) == nil"),
|
||||||
|
Example("expect(value) != nil"),
|
||||||
Example("expect(object.asyncFunction()).toEventually(equal(1))\n"),
|
Example("expect(object.asyncFunction()).toEventually(equal(1))\n"),
|
||||||
Example("expect(actual).to(haveCount(expected))\n"),
|
Example("expect(actual).to(haveCount(expected))\n"),
|
||||||
Example("""
|
Example("""
|
||||||
|
@ -42,6 +42,8 @@ public struct NimbleOperatorRule: ConfigurationProviderRule, OptInRule, Correcta
|
||||||
Example("↓expect(x).to(beIdenticalTo(x))\n"),
|
Example("↓expect(x).to(beIdenticalTo(x))\n"),
|
||||||
Example("↓expect(success).to(beTrue())\n"),
|
Example("↓expect(success).to(beTrue())\n"),
|
||||||
Example("↓expect(success).to(beFalse())\n"),
|
Example("↓expect(success).to(beFalse())\n"),
|
||||||
|
Example("↓expect(value).to(beNil())\n"),
|
||||||
|
Example("↓expect(value).toNot(beNil())\n"),
|
||||||
Example("expect(10) > 2\n ↓expect(10).to(beGreaterThan(2))\n")
|
Example("expect(10) > 2\n ↓expect(10).to(beGreaterThan(2))\n")
|
||||||
],
|
],
|
||||||
corrections: [
|
corrections: [
|
||||||
|
@ -60,6 +62,8 @@ public struct NimbleOperatorRule: ConfigurationProviderRule, OptInRule, Correcta
|
||||||
Example("↓expect(success).to(beFalse())\n"): Example("expect(success) == false\n"),
|
Example("↓expect(success).to(beFalse())\n"): Example("expect(success) == false\n"),
|
||||||
Example("↓expect(success).toNot(beFalse())\n"): Example("expect(success) != false\n"),
|
Example("↓expect(success).toNot(beFalse())\n"): Example("expect(success) != false\n"),
|
||||||
Example("↓expect(success).toNot(beTrue())\n"): Example("expect(success) != true\n"),
|
Example("↓expect(success).toNot(beTrue())\n"): Example("expect(success) != true\n"),
|
||||||
|
Example("↓expect(value).to(beNil())\n"): Example("expect(value) == nil\n"),
|
||||||
|
Example("↓expect(value).toNot(beNil())\n"): Example("expect(value) != nil\n"),
|
||||||
Example("expect(10) > 2\n ↓expect(10).to(beGreaterThan(2))\n"): Example("expect(10) > 2\n expect(10) > 2\n")
|
Example("expect(10) > 2\n ↓expect(10).to(beGreaterThan(2))\n"): Example("expect(10) > 2\n expect(10) > 2\n")
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -88,19 +92,34 @@ public struct NimbleOperatorRule: ConfigurationProviderRule, OptInRule, Correcta
|
||||||
"beLessThan": (to: "<", toNot: nil, .withArguments),
|
"beLessThan": (to: "<", toNot: nil, .withArguments),
|
||||||
"beLessThanOrEqualTo": (to: "<=", toNot: nil, .withArguments),
|
"beLessThanOrEqualTo": (to: "<=", toNot: nil, .withArguments),
|
||||||
"beTrue": (to: "==", toNot: "!=", .nullary(analogueValue: "true")),
|
"beTrue": (to: "==", toNot: "!=", .nullary(analogueValue: "true")),
|
||||||
"beFalse": (to: "==", toNot: "!=", .nullary(analogueValue: "false"))
|
"beFalse": (to: "==", toNot: "!=", .nullary(analogueValue: "false")),
|
||||||
|
"beNil": (to: "==", toNot: "!=", .nullary(analogueValue: "nil"))
|
||||||
]
|
]
|
||||||
|
|
||||||
public func validate(file: SwiftLintFile) -> [StyleViolation] {
|
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
||||||
let matches = violationMatchesRanges(in: file)
|
let matches = violationMatchesRanges(in: file)
|
||||||
return matches.map {
|
return matches.map {
|
||||||
StyleViolation(ruleDescription: type(of: self).description,
|
StyleViolation(ruleDescription: Self.description,
|
||||||
severity: configuration.severity,
|
severity: configuration.severity,
|
||||||
location: Location(file: file, characterOffset: $0.location))
|
location: Location(file: file, characterOffset: $0.location))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func violationMatchesRanges(in file: SwiftLintFile) -> [NSRange] {
|
private func violationMatchesRanges(in file: SwiftLintFile) -> [NSRange] {
|
||||||
|
let contents = file.stringView
|
||||||
|
return rawRegexResults(in: file).filter { range in
|
||||||
|
guard let byteRange = contents.NSRangeToByteRange(start: range.location, length: range.length) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.structureDictionary.structures(forByteOffset: byteRange.upperBound - 1)
|
||||||
|
.contains(where: { dict -> Bool in
|
||||||
|
return dict.expressionKind == .call && (dict.name ?? "").starts(with: "expect")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func rawRegexResults(in file: SwiftLintFile) -> [NSRange] {
|
||||||
let operandPattern = "(.(?!expect\\())+?"
|
let operandPattern = "(.(?!expect\\())+?"
|
||||||
|
|
||||||
let operatorsPattern = "(" + predicatesMapping.map { name, predicateDescription in
|
let operatorsPattern = "(" + predicatesMapping.map { name, predicateDescription in
|
||||||
|
@ -112,34 +131,21 @@ public struct NimbleOperatorRule: ConfigurationProviderRule, OptInRule, Correcta
|
||||||
}.joined(separator: "|") + ")"
|
}.joined(separator: "|") + ")"
|
||||||
|
|
||||||
let pattern = "expect\\(\(operandPattern)\\)\\.to(Not)?\\(\(operatorsPattern)\\)"
|
let pattern = "expect\\(\(operandPattern)\\)\\.to(Not)?\\(\(operatorsPattern)\\)"
|
||||||
|
|
||||||
let excludingKinds = SyntaxKind.commentKinds
|
let excludingKinds = SyntaxKind.commentKinds
|
||||||
|
|
||||||
return file.match(pattern: pattern)
|
return file.match(pattern: pattern)
|
||||||
.filter { _, kinds in
|
.filter { _, kinds in
|
||||||
excludingKinds.isDisjoint(with: kinds) && kinds.first == .identifier
|
excludingKinds.isDisjoint(with: kinds) && kinds.first == .identifier
|
||||||
}.map { $0.0 }
|
|
||||||
.filter { range in
|
|
||||||
let contents = file.stringView
|
|
||||||
guard let byteRange = contents.NSRangeToByteRange(start: range.location, length: range.length) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let containsCall = file.structureDictionary.structures(forByteOffset: byteRange.upperBound - 1)
|
|
||||||
.contains(where: { dict -> Bool in
|
|
||||||
return dict.expressionKind == .call && (dict.name ?? "").starts(with: "expect")
|
|
||||||
})
|
|
||||||
|
|
||||||
return containsCall
|
|
||||||
}
|
}
|
||||||
|
.map { $0.0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
public func correct(file: SwiftLintFile) -> [Correction] {
|
func correct(file: SwiftLintFile) -> [Correction] {
|
||||||
let matches = violationMatchesRanges(in: file)
|
let matches = violationMatchesRanges(in: file)
|
||||||
.filter { !file.ruleEnabled(violatingRanges: [$0], for: self).isEmpty }
|
.filter { file.ruleEnabled(violatingRanges: [$0], for: self).isNotEmpty }
|
||||||
guard !matches.isEmpty else { return [] }
|
guard matches.isNotEmpty else { return [] }
|
||||||
|
|
||||||
let description = type(of: self).description
|
let description = Self.description
|
||||||
var corrections: [Correction] = []
|
var corrections: [Correction] = []
|
||||||
var contents = file.contents
|
var contents = file.contents
|
||||||
|
|
||||||
|
@ -183,7 +189,7 @@ private extension String {
|
||||||
|
|
||||||
for case let (pattern, operatorString?) in [toPattern, toNotPattern] {
|
for case let (pattern, operatorString?) in [toPattern, toNotPattern] {
|
||||||
let expression = regex(pattern)
|
let expression = regex(pattern)
|
||||||
guard !expression.matches(in: self, options: [], range: range).isEmpty else {
|
guard expression.matches(in: self, options: [], range: range).isNotEmpty else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct NoExtensionAccessModifierRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.error)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "no_extension_access_modifier",
|
||||||
|
name: "No Extension Access Modifier",
|
||||||
|
description: "Prefer not to use extension access modifiers",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("extension String {}"),
|
||||||
|
Example("\n\n extension String {}")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("↓private extension String {}"),
|
||||||
|
Example("↓public \n extension String {}"),
|
||||||
|
Example("↓open extension String {}"),
|
||||||
|
Example("↓internal extension String {}"),
|
||||||
|
Example("↓fileprivate extension String {}")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NoExtensionAccessModifierRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .all }
|
||||||
|
|
||||||
|
override func visitPost(_ node: ExtensionDeclSyntax) {
|
||||||
|
if let modifiers = node.modifiers, modifiers.isNotEmpty {
|
||||||
|
violations.append(modifiers.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct NoFallthroughOnlyRule: SwiftSyntaxRule, ConfigurationProviderRule {
|
||||||
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "no_fallthrough_only",
|
||||||
|
name: "No Fallthrough only",
|
||||||
|
description: "Fallthroughs can only be used if the `case` contains at least one other statement",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: NoFallthroughOnlyRuleExamples.nonTriggeringExamples,
|
||||||
|
triggeringExamples: NoFallthroughOnlyRuleExamples.triggeringExamples
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NoFallthroughOnlyRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
override func visitPost(_ node: SwitchCaseListSyntax) {
|
||||||
|
let cases = node.compactMap { $0.as(SwitchCaseSyntax.self) }
|
||||||
|
|
||||||
|
let localViolations = cases.enumerated()
|
||||||
|
.compactMap { index, element -> AbsolutePosition? in
|
||||||
|
if let fallthroughStmt = element.statements.onlyElement?.item.as(FallthroughStmtSyntax.self) {
|
||||||
|
if case let nextCaseIndex = cases.index(after: index),
|
||||||
|
nextCaseIndex < cases.endIndex,
|
||||||
|
case let nextCase = cases[nextCaseIndex],
|
||||||
|
nextCase.unknownAttr != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fallthroughStmt.positionAfterSkippingLeadingTrivia
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.append(contentsOf: localViolations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
internal struct NoFallthroughOnlyRuleExamples {
|
internal struct NoFallthroughOnlyRuleExamples {
|
||||||
static let nonTriggeringExamples: [Example] = {
|
static let nonTriggeringExamples: [Example] = [
|
||||||
let commonExamples = [
|
Example("""
|
||||||
Example("""
|
|
||||||
switch myvar {
|
switch myvar {
|
||||||
case 1:
|
case 1:
|
||||||
var a = 1
|
var a = 1
|
||||||
|
@ -10,7 +9,7 @@ internal struct NoFallthroughOnlyRuleExamples {
|
||||||
var a = 2
|
var a = 2
|
||||||
}
|
}
|
||||||
"""),
|
"""),
|
||||||
Example("""
|
Example("""
|
||||||
switch myvar {
|
switch myvar {
|
||||||
case "a":
|
case "a":
|
||||||
var one = 1
|
var one = 1
|
||||||
|
@ -20,7 +19,7 @@ internal struct NoFallthroughOnlyRuleExamples {
|
||||||
var three = 3
|
var three = 3
|
||||||
}
|
}
|
||||||
"""),
|
"""),
|
||||||
Example("""
|
Example("""
|
||||||
switch myvar {
|
switch myvar {
|
||||||
case 1:
|
case 1:
|
||||||
let one = 1
|
let one = 1
|
||||||
|
@ -29,7 +28,7 @@ internal struct NoFallthroughOnlyRuleExamples {
|
||||||
var two = 2
|
var two = 2
|
||||||
}
|
}
|
||||||
"""),
|
"""),
|
||||||
Example("""
|
Example("""
|
||||||
switch myvar {
|
switch myvar {
|
||||||
case MyFunc(x: [1, 2, YourFunc(a: 23)], y: 2):
|
case MyFunc(x: [1, 2, YourFunc(a: 23)], y: 2):
|
||||||
var three = 3
|
var three = 3
|
||||||
|
@ -38,7 +37,7 @@ internal struct NoFallthroughOnlyRuleExamples {
|
||||||
var three = 4
|
var three = 4
|
||||||
}
|
}
|
||||||
"""),
|
"""),
|
||||||
Example("""
|
Example("""
|
||||||
switch myvar {
|
switch myvar {
|
||||||
case .alpha:
|
case .alpha:
|
||||||
var one = 1
|
var one = 1
|
||||||
|
@ -49,7 +48,7 @@ internal struct NoFallthroughOnlyRuleExamples {
|
||||||
var four = 4
|
var four = 4
|
||||||
}
|
}
|
||||||
"""),
|
"""),
|
||||||
Example("""
|
Example("""
|
||||||
let aPoint = (1, -1)
|
let aPoint = (1, -1)
|
||||||
switch aPoint {
|
switch aPoint {
|
||||||
case let (x, y) where x == y:
|
case let (x, y) where x == y:
|
||||||
|
@ -61,7 +60,7 @@ internal struct NoFallthroughOnlyRuleExamples {
|
||||||
let C = "C"
|
let C = "C"
|
||||||
}
|
}
|
||||||
"""),
|
"""),
|
||||||
Example("""
|
Example("""
|
||||||
switch myvar {
|
switch myvar {
|
||||||
case MyFun(with: { $1 }):
|
case MyFun(with: { $1 }):
|
||||||
let one = 1
|
let one = 1
|
||||||
|
@ -69,15 +68,8 @@ internal struct NoFallthroughOnlyRuleExamples {
|
||||||
case "abc":
|
case "abc":
|
||||||
let two = 2
|
let two = 2
|
||||||
}
|
}
|
||||||
""")
|
"""),
|
||||||
]
|
Example("""
|
||||||
|
|
||||||
guard SwiftVersion.current >= .five else {
|
|
||||||
return commonExamples
|
|
||||||
}
|
|
||||||
|
|
||||||
return commonExamples + [
|
|
||||||
Example("""
|
|
||||||
switch enumInstance {
|
switch enumInstance {
|
||||||
case .caseA:
|
case .caseA:
|
||||||
print("it's a")
|
print("it's a")
|
||||||
|
@ -87,8 +79,7 @@ internal struct NoFallthroughOnlyRuleExamples {
|
||||||
print("it's not a")
|
print("it's not a")
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
]
|
]
|
||||||
}()
|
|
||||||
|
|
||||||
static let triggeringExamples = [
|
static let triggeringExamples = [
|
||||||
Example("""
|
Example("""
|
|
@ -1,14 +1,12 @@
|
||||||
import SourceKittenFramework
|
import SourceKittenFramework
|
||||||
|
|
||||||
public struct NoGroupingExtensionRule: OptInRule, ConfigurationProviderRule, AutomaticTestableRule {
|
struct NoGroupingExtensionRule: OptInRule, ConfigurationProviderRule {
|
||||||
public var configuration = SeverityConfiguration(.warning)
|
var configuration = SeverityConfiguration<Self>(.warning)
|
||||||
|
|
||||||
public init() {}
|
static let description = RuleDescription(
|
||||||
|
|
||||||
public static let description = RuleDescription(
|
|
||||||
identifier: "no_grouping_extension",
|
identifier: "no_grouping_extension",
|
||||||
name: "No Grouping Extension",
|
name: "No Grouping Extension",
|
||||||
description: "Extensions shouldn't be used to group code within the same source file.",
|
description: "Extensions shouldn't be used to group code within the same source file",
|
||||||
kind: .idiomatic,
|
kind: .idiomatic,
|
||||||
nonTriggeringExamples: [
|
nonTriggeringExamples: [
|
||||||
Example("protocol Food {}\nextension Food {}\n"),
|
Example("protocol Food {}\nextension Food {}\n"),
|
||||||
|
@ -23,7 +21,7 @@ public struct NoGroupingExtensionRule: OptInRule, ConfigurationProviderRule, Aut
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
public func validate(file: SwiftLintFile) -> [StyleViolation] {
|
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
||||||
let collector = NamespaceCollector(dictionary: file.structureDictionary)
|
let collector = NamespaceCollector(dictionary: file.structureDictionary)
|
||||||
let elements = collector.findAllElements(of: [.class, .enum, .struct, .extension])
|
let elements = collector.findAllElements(of: [.class, .enum, .struct, .extension])
|
||||||
|
|
||||||
|
@ -38,7 +36,7 @@ public struct NoGroupingExtensionRule: OptInRule, ConfigurationProviderRule, Aut
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return StyleViolation(ruleDescription: type(of: self).description,
|
return StyleViolation(ruleDescription: Self.description,
|
||||||
severity: configuration.severity,
|
severity: configuration.severity,
|
||||||
location: Location(file: file, byteOffset: element.offset))
|
location: Location(file: file, byteOffset: element.offset))
|
||||||
}
|
}
|
||||||
|
@ -56,6 +54,6 @@ public struct NoGroupingExtensionRule: OptInRule, ConfigurationProviderRule, Aut
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return !file.match(pattern: "\\bwhere\\b", with: [.keyword], range: range).isEmpty
|
return file.match(pattern: "\\bwhere\\b", with: [.keyword], range: range).isNotEmpty
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct NoMagicNumbersRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
|
||||||
|
var configuration = NoMagicNumbersConfiguration()
|
||||||
|
|
||||||
|
static let description = RuleDescription(
|
||||||
|
identifier: "no_magic_numbers",
|
||||||
|
name: "No Magic Numbers",
|
||||||
|
description: "Magic numbers should be replaced by named constants",
|
||||||
|
kind: .idiomatic,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("var foo = 123"),
|
||||||
|
Example("static let bar: Double = 0.123"),
|
||||||
|
Example("let a = b + 1.0"),
|
||||||
|
Example("array[0] + array[1] "),
|
||||||
|
Example("let foo = 1_000.000_01"),
|
||||||
|
Example("// array[1337]"),
|
||||||
|
Example("baz(\"9999\")"),
|
||||||
|
Example("""
|
||||||
|
func foo() {
|
||||||
|
let x: Int = 2
|
||||||
|
let y = 3
|
||||||
|
let vector = [x, y, -1]
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class A {
|
||||||
|
var foo: Double = 132
|
||||||
|
static let bar: Double = 0.98
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@available(iOS 13, *)
|
||||||
|
func version() {
|
||||||
|
if #available(iOS 13, OSX 10.10, *) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
enum Example: Int {
|
||||||
|
case positive = 2
|
||||||
|
case negative = -2
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class FooTests: XCTestCase {
|
||||||
|
let array: [Int] = []
|
||||||
|
let bar = array[42]
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class FooTests: XCTestCase {
|
||||||
|
class Bar {
|
||||||
|
let array: [Int] = []
|
||||||
|
let bar = array[42]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("foo(↓321)"),
|
||||||
|
Example("bar(↓1_000.005_01)"),
|
||||||
|
Example("array[↓42]"),
|
||||||
|
Example("let box = array[↓12 + ↓14]"),
|
||||||
|
Example("let a = b + ↓2.0"),
|
||||||
|
Example("Color.primary.opacity(isAnimate ? ↓0.1 : ↓1.5)")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
Visitor(viewMode: .sourceAccurate, testParentClasses: configuration.testParentClasses)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NoMagicNumbersRule {
|
||||||
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
|
private let testParentClasses: Set<String>
|
||||||
|
|
||||||
|
init(viewMode: SyntaxTreeViewMode, testParentClasses: Set<String>) {
|
||||||
|
self.testParentClasses = testParentClasses
|
||||||
|
super.init(viewMode: viewMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: FloatLiteralExprSyntax) {
|
||||||
|
if node.isMemberOfATestClass(testParentClasses) == false, node.floatingDigits.isMagicNumber {
|
||||||
|
violations.append(node.floatingDigits.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: IntegerLiteralExprSyntax) {
|
||||||
|
if node.isMemberOfATestClass(testParentClasses) == false, node.digits.isMagicNumber {
|
||||||
|
violations.append(node.digits.positionAfterSkippingLeadingTrivia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension TokenSyntax {
|
||||||
|
var isMagicNumber: Bool {
|
||||||
|
guard let number = Double(text.replacingOccurrences(of: "_", with: "")) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if [0, 1].contains(number) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard let grandparent = parent?.parent else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !grandparent.is(InitializerClauseSyntax.self)
|
||||||
|
&& grandparent.as(PrefixOperatorExprSyntax.self)?.parent?.is(InitializerClauseSyntax.self) != true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ExprSyntaxProtocol {
|
||||||
|
func isMemberOfATestClass(_ testParentClasses: Set<String>) -> Bool {
|
||||||
|
var parent = parent
|
||||||
|
while parent != nil {
|
||||||
|
if
|
||||||
|
let classDecl = parent?.as(ClassDeclSyntax.self),
|
||||||
|
classDecl.isXCTestCase(testParentClasses)
|
||||||
|
{
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
parent = parent?.parent
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue