mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
413 Commits
0.0.3
...
078ddeaf07
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
078ddeaf07 | ||
|
|
c79a8c6502 | ||
|
|
cfbd8f9e76 | ||
|
|
e329f0fc78 | ||
|
|
bc2164499b | ||
|
|
f5ff6193e8 | ||
|
|
36b2b07a1d | ||
|
|
73bf19d766 | ||
|
|
093b6767c4 | ||
|
|
e4671e50bb | ||
|
|
ab48098e06 | ||
|
|
e99a95ef7c | ||
|
|
bcb34055ac | ||
|
|
fe935b5775 | ||
|
|
7dcd523bfe | ||
|
|
30bc1d3330 | ||
|
|
a5a4ad05a0 | ||
|
|
0b77895ca5 | ||
|
|
54994755b1 | ||
|
|
aee63cb9f2 | ||
|
|
4bdd3ccc6c | ||
|
|
6aa72e45e8 | ||
|
|
76e8d47e03 | ||
|
|
6304b8ab9c | ||
|
|
98b50d0e8e | ||
|
|
5aea869c2a | ||
|
|
425c8f4022 | ||
|
|
490398f773 | ||
|
|
5854f36756 | ||
|
|
ec6c72e6a3 | ||
|
|
41bc64be4a | ||
|
|
7df0e77e4d | ||
|
|
914e8e17cd | ||
|
|
40f106d0b0 | ||
|
|
566dd4a9a7 | ||
|
|
9beb439323 | ||
|
|
029257a915 | ||
|
|
d330fbbb63 | ||
|
|
236867f547 | ||
|
|
b41e9b4929 | ||
|
|
ff06b8896f | ||
|
|
0fe9c89fa0 | ||
|
|
8646c9de5e | ||
|
|
a33c42a163 | ||
|
|
55cea48cbd | ||
|
|
e67eda3515 | ||
|
|
4412c20e97 | ||
|
|
9eb84c6649 | ||
|
|
2ef37ab6d9 | ||
|
|
38a73772fc | ||
|
|
aed53eb090 | ||
|
|
21b601da66 | ||
|
|
a4726fcefd | ||
|
|
ab24ca8604 | ||
|
|
3533bff344 | ||
|
|
1b096b679e | ||
|
|
cb61b31e9d | ||
|
|
d8f183c404 | ||
|
|
c95b6c32d5 | ||
|
|
d2e390c691 | ||
|
|
66ef221586 | ||
|
|
2d3bb30125 | ||
|
|
5d72692aa5 | ||
|
|
3be17db784 | ||
|
|
4aef8ce8fb | ||
|
|
8c1cff3bb7 | ||
|
|
669d8bfe20 | ||
|
|
4dce7bddb4 | ||
|
|
a621e89e89 | ||
|
|
5ea11e3a23 | ||
|
|
7cb61182d2 | ||
|
|
99c59431c4 | ||
|
|
f376081489 | ||
|
|
00a1e12b5c | ||
|
|
81f8b17451 | ||
|
|
aa8315b68d | ||
|
|
e52781c25a | ||
|
|
01f29a5375 | ||
|
|
013cb8f66b | ||
|
|
9c715f458e | ||
|
|
90d93a57ee | ||
|
|
8da4a61eb7 | ||
|
|
f718370642 | ||
|
|
83c7af72bf | ||
|
|
eff84fd7ae | ||
|
|
f66fa97b87 | ||
|
|
9f309b5d4a | ||
|
|
456099591a | ||
|
|
bf7f607f9b | ||
|
|
a4041ab019 | ||
|
|
a0fde872ec | ||
|
|
f0c040c7b9 | ||
|
|
a09818d452 | ||
|
|
1c331df4b1 | ||
|
|
dc20fe9730 | ||
|
|
31ae0271b9 | ||
|
|
6ed6d2ced9 | ||
|
|
01a4846159 | ||
|
|
02dc7de127 | ||
|
|
a1ff1a1539 | ||
|
|
a02951f755 | ||
|
|
7cb25254e8 | ||
|
|
3d9ad16117 | ||
|
|
d0ad3bc45d | ||
|
|
6541ce568d | ||
|
|
32d3b66185 | ||
|
|
48f157a51e | ||
|
|
b1995fa4f7 | ||
|
|
74bc973f77 | ||
|
|
3420c3d039 | ||
|
|
b10577fec5 | ||
|
|
af96d0d31d | ||
|
|
bd29ad31cc | ||
|
|
15150cb3ed | ||
|
|
aac9c968eb | ||
|
|
85a9f157ad | ||
|
|
d24a79361d | ||
|
|
5785720f31 | ||
|
|
3f6f0b9f1b | ||
|
|
128bb5be8b | ||
|
|
36b3814f4e | ||
|
|
c4a975d5f1 | ||
|
|
79d86d39c1 | ||
|
|
c476700168 | ||
|
|
5e97ebe7f0 | ||
|
|
64cbdaaeab | ||
|
|
ae1f03914c | ||
|
|
ff25dccf8a | ||
|
|
6e0d881682 | ||
|
|
89fd42888a | ||
|
|
eeac82a6e7 | ||
|
|
c641c6fbe2 | ||
|
|
5ec732fe9a | ||
|
|
6d87411dbf | ||
|
|
ed3054c855 | ||
|
|
5d00de4dfe | ||
|
|
016ec8b186 | ||
|
|
9141092919 | ||
|
|
1fe97b0140 | ||
|
|
6ad5989c25 | ||
|
|
7e1db916fc | ||
|
|
1c69d5c80d | ||
|
|
ab87225f1f | ||
|
|
6d33c5cdad | ||
|
|
e4c899c6c2 | ||
|
|
35b3ad0d63 | ||
|
|
4e70557b47 | ||
|
|
0a8d58255a | ||
|
|
d3fbc9c643 | ||
|
|
1cbf8776be | ||
|
|
16e33f7b8f | ||
|
|
5c848056c5 | ||
|
|
864efd3179 | ||
|
|
7f206a0c77 | ||
|
|
22c15f8ec6 | ||
|
|
59373eadc2 | ||
|
|
ed3e4f471e | ||
|
|
41cb8647b5 | ||
|
|
c7015181e1 | ||
|
|
86742755e8 | ||
|
|
33f95d941d | ||
|
|
1328592cb5 | ||
|
|
0711b863ea | ||
|
|
a2f5cd54be | ||
|
|
7836ec610f | ||
|
|
2e489927eb | ||
|
|
02e8d19e48 | ||
|
|
eb7107fb0a | ||
|
|
a396009b62 | ||
|
|
1d9c7e942c | ||
|
|
0f3abb9db4 | ||
|
|
896482821c | ||
|
|
aa3094ee54 | ||
|
|
712580e3d7 | ||
|
|
c08102f85f | ||
|
|
5e684c8b36 | ||
|
|
300ae70564 | ||
|
|
76f0c77f1e | ||
|
|
0f7cea4ed1 | ||
|
|
32ee0b2bd6 | ||
|
|
4ff1e1d3e1 | ||
|
|
8e96d2701d | ||
|
|
8e307df231 | ||
|
|
ff38f4916a | ||
|
|
7cbbb220b4 | ||
|
|
ae2d4299f0 | ||
|
|
21bc69d116 | ||
|
|
05a70175cc | ||
|
|
33ec2eb3a0 | ||
|
|
f6ef6cd4c0 | ||
|
|
a9ef693dc1 | ||
|
|
98bbd666dc | ||
|
|
4e7ed830f8 | ||
|
|
ef87ff76fc | ||
|
|
2feeb21270 | ||
|
|
9990387cfa | ||
|
|
bc1bdca7c6 | ||
|
|
2a992d37df | ||
|
|
15c87aecbb | ||
|
|
10a46451ac | ||
|
|
e4c6a4174b | ||
|
|
4c65f7bbee | ||
|
|
5f21de0df5 | ||
|
|
9b01b67d98 | ||
|
|
4508f5e211 | ||
|
|
f0cbc46df4 | ||
|
|
6c96e9e173 | ||
|
|
51cca36d2a | ||
|
|
84672c92f6 | ||
|
|
b1d01898b6 | ||
|
|
441a47a1a8 | ||
|
|
8abd7219a1 | ||
|
|
df73a0bfe8 | ||
|
|
55d12dc721 | ||
|
|
a6ee44c1bb | ||
|
|
76816e22f1 | ||
|
|
daf25e59d6 | ||
|
|
f2b4e53615 | ||
|
|
2d519ab190 | ||
|
|
2d479c9cb6 | ||
|
|
2bb7e13e51 | ||
|
|
6e1dfdcdd4 | ||
|
|
5ba647e5c1 | ||
|
|
853492695f | ||
|
|
d5d72c7c50 | ||
|
|
d676b5832e | ||
|
|
28097afc1e | ||
|
|
fda96586f3 | ||
|
|
fc5af8dbbc | ||
|
|
4835e64388 | ||
|
|
0999c33f93 | ||
|
|
595805255a | ||
|
|
65eaa912cf | ||
|
|
038f48b78e | ||
|
|
d7460244b7 | ||
|
|
02766868fc | ||
|
|
8d7d25a144 | ||
|
|
17ded54e24 | ||
|
|
54a4c32ddf | ||
|
|
6d46e82145 | ||
|
|
fd4a2a18fe | ||
|
|
bfe99d620e | ||
|
|
c5a111207f | ||
|
|
544945c0e6 | ||
|
|
c616cdd750 | ||
|
|
d3c396956d | ||
|
|
d0cbbc6d9a | ||
|
|
49c7905150 | ||
|
|
f5a992a16e | ||
|
|
bade0a0048 | ||
|
|
7d3d79b861 | ||
|
|
58df63a7ad | ||
|
|
b938eef013 | ||
|
|
94f63631db | ||
|
|
90d1b11430 | ||
|
|
550e54b86d | ||
|
|
90a01e729b | ||
|
|
ac01c2aecb | ||
|
|
4acffe925c | ||
|
|
18f53eeeef | ||
|
|
03d6942540 | ||
|
|
9be811a89a | ||
|
|
f9f5a4696b | ||
|
|
d6da687170 | ||
|
|
eba66d0878 | ||
|
|
8c682766bd | ||
|
|
39d626c8d8 | ||
|
|
a338ac8ce2 | ||
|
|
11637127cb | ||
|
|
4e12aefafb | ||
|
|
144d3592fb | ||
|
|
6f82c2f0f9 | ||
|
|
b8c60717d5 | ||
|
|
fec6850c39 | ||
|
|
6a378ad946 | ||
|
|
11579f11b1 | ||
|
|
60a3b26fd1 | ||
|
|
3abdfb1acf | ||
|
|
9557d386e2 | ||
|
|
d0d024c427 | ||
|
|
f765af6061 | ||
|
|
7f2202e869 | ||
|
|
14ad9d5738 | ||
|
|
b120138de3 | ||
|
|
8df1d607c1 | ||
|
|
c06f2810b9 | ||
|
|
d52a205f13 | ||
|
|
0ec12e57c1 | ||
|
|
c322b7029c | ||
|
|
6a38c04c11 | ||
|
|
5e53107def | ||
|
|
36cea937de | ||
|
|
438d6b98ac | ||
|
|
8e1488c395 | ||
|
|
65d321b476 | ||
|
|
c6d2359d6b | ||
|
|
0d32876bad | ||
|
|
c063251d89 | ||
|
|
3831cfc7c0 | ||
|
|
b17341b56c | ||
|
|
5bda964fb5 | ||
|
|
432430489a | ||
|
|
9a20101f30 | ||
|
|
b491818779 | ||
|
|
69c24c8dfc | ||
|
|
004f906148 | ||
|
|
ac83233dc2 | ||
|
|
082910c968 | ||
|
|
11e3e0f85d | ||
|
|
42f4d7d5a7 | ||
|
|
bed22b6500 | ||
|
|
17449e0794 | ||
|
|
4732166f5f | ||
|
|
f5e37b96fc | ||
|
|
4cef596fe8 | ||
|
|
19b87717c1 | ||
|
|
7e4c6b20ff | ||
|
|
fb2071ed2b | ||
|
|
7d2f934310 | ||
|
|
95a00b0952 | ||
|
|
cb3fee65f3 | ||
|
|
65628b145a | ||
|
|
802bbfccc6 | ||
|
|
6e7742a4f3 | ||
|
|
f6a1a40471 | ||
|
|
33ca4da260 | ||
|
|
cbb72b16ae | ||
|
|
c58629e999 | ||
|
|
387fb72718 | ||
|
|
e04f0da318 | ||
|
|
d25873ee10 | ||
|
|
a28223fc8b | ||
|
|
1dab27de55 | ||
|
|
698629b153 | ||
|
|
65b66b0d27 | ||
|
|
7d3ba612c4 | ||
|
|
8c3b8d1f49 | ||
|
|
fdd39855ad | ||
|
|
671532efce | ||
|
|
5b124345b0 | ||
|
|
b812bd1423 | ||
|
|
c854f5fb8d | ||
|
|
f38bd32510 | ||
|
|
765fa5503e | ||
|
|
57f168723b | ||
|
|
79e1a2e3d7 | ||
|
|
f4f6d04857 | ||
|
|
015ede0d15 | ||
|
|
4fd7f7c3ca | ||
|
|
896dd49eb4 | ||
|
|
4365ad457a | ||
|
|
fb3617980e | ||
|
|
7690aae456 | ||
|
|
076678a08c | ||
|
|
104279d6e9 | ||
|
|
515d51a91d | ||
|
|
4fdf543190 | ||
|
|
4e1ab096c9 | ||
|
|
8aa6911cca | ||
|
|
f0362019ed | ||
|
|
82895f2e42 | ||
|
|
4cf622abe5 | ||
|
|
d4e22a78d6 | ||
|
|
3883c831e9 | ||
|
|
63441688fe | ||
|
|
e48839b938 | ||
|
|
ed87373dc3 | ||
|
|
6ce52c70f7 | ||
|
|
d2b0b16121 | ||
|
|
d67a9fe762 | ||
|
|
ce2a3153e6 | ||
|
|
d4b54231fb | ||
|
|
70bfe0bf91 | ||
|
|
9690c380d3 | ||
|
|
85caa275ae | ||
|
|
32026e59c0 | ||
|
|
486ccb9685 | ||
|
|
7b766f70f3 | ||
|
|
f73e96488f | ||
|
|
af63fa5a1f | ||
|
|
e8f53c9463 | ||
|
|
9564cd5d30 | ||
|
|
ed458c3980 | ||
|
|
25538f99db | ||
|
|
36436e7a4b | ||
|
|
a6070332c9 | ||
|
|
25cbfdb4b8 | ||
|
|
d1b5107c2c | ||
|
|
03873d63cd | ||
|
|
89aba39964 | ||
|
|
ab57a103d1 | ||
|
|
d0b2ebc061 | ||
|
|
857257ca73 | ||
|
|
3587155c7e | ||
|
|
ae05e0db96 | ||
|
|
41c0493e66 | ||
|
|
43a304bb26 | ||
|
|
cd3892bf83 | ||
|
|
3f7c02342d | ||
|
|
c65cdf465e | ||
|
|
b5d67ecf24 | ||
|
|
a94b2296e1 | ||
|
|
fa05e4df3f | ||
|
|
b70b25076e | ||
|
|
0662f341e6 | ||
|
|
80bf477f3b | ||
|
|
e4a502d9d6 | ||
|
|
13b15b98ed | ||
|
|
80465e0e51 | ||
|
|
9a1ce7e7e5 | ||
|
|
b45da64664 | ||
|
|
df01dc055e | ||
|
|
31dd24d189 |
BIN
.assets/help-screen.png
Normal file
BIN
.assets/help-screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
63
.gitattributes
vendored
63
.gitattributes
vendored
@@ -1,63 +0,0 @@
|
|||||||
###############################################################################
|
|
||||||
# Set default behavior to automatically normalize line endings.
|
|
||||||
###############################################################################
|
|
||||||
* text=auto
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Set default behavior for command prompt diff.
|
|
||||||
#
|
|
||||||
# This is need for earlier builds of msysgit that does not have it on by
|
|
||||||
# default for csharp files.
|
|
||||||
# Note: This is only used by command line
|
|
||||||
###############################################################################
|
|
||||||
#*.cs diff=csharp
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Set the merge driver for project and solution files
|
|
||||||
#
|
|
||||||
# Merging from the command prompt will add diff markers to the files if there
|
|
||||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
|
||||||
# the diff markers are never inserted). Diff markers may cause the following
|
|
||||||
# file extensions to fail to load in VS. An alternative would be to treat
|
|
||||||
# these files as binary and thus will always conflict and require user
|
|
||||||
# intervention with every merge. To do so, just uncomment the entries below
|
|
||||||
###############################################################################
|
|
||||||
#*.sln merge=binary
|
|
||||||
#*.csproj merge=binary
|
|
||||||
#*.vbproj merge=binary
|
|
||||||
#*.vcxproj merge=binary
|
|
||||||
#*.vcproj merge=binary
|
|
||||||
#*.dbproj merge=binary
|
|
||||||
#*.fsproj merge=binary
|
|
||||||
#*.lsproj merge=binary
|
|
||||||
#*.wixproj merge=binary
|
|
||||||
#*.modelproj merge=binary
|
|
||||||
#*.sqlproj merge=binary
|
|
||||||
#*.wwaproj merge=binary
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# behavior for image files
|
|
||||||
#
|
|
||||||
# image files are treated as binary by default.
|
|
||||||
###############################################################################
|
|
||||||
#*.jpg binary
|
|
||||||
#*.png binary
|
|
||||||
#*.gif binary
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# diff behavior for common document formats
|
|
||||||
#
|
|
||||||
# Convert binary document formats to text before diffing them. This feature
|
|
||||||
# is only available from the command line. Turn it on by uncommenting the
|
|
||||||
# entries below.
|
|
||||||
###############################################################################
|
|
||||||
#*.doc diff=astextplain
|
|
||||||
#*.DOC diff=astextplain
|
|
||||||
#*.docx diff=astextplain
|
|
||||||
#*.DOCX diff=astextplain
|
|
||||||
#*.dot diff=astextplain
|
|
||||||
#*.DOT diff=astextplain
|
|
||||||
#*.pdf diff=astextplain
|
|
||||||
#*.PDF diff=astextplain
|
|
||||||
#*.rtf diff=astextplain
|
|
||||||
#*.RTF diff=astextplain
|
|
||||||
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,4 +0,0 @@
|
|||||||
github: Tyrrrz
|
|
||||||
patreon: Tyrrrz
|
|
||||||
open_collective: Tyrrrz
|
|
||||||
custom: ['buymeacoffee.com/Tyrrrz']
|
|
||||||
77
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
77
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
name: 🐛 Bug report
|
||||||
|
description: Report broken functionality.
|
||||||
|
labels: [bug]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
- Avoid generic or vague titles such as "Something's not working" or "A couple of problems" — be as descriptive as possible.
|
||||||
|
- Keep your issue focused on one single problem. If you have multiple bug reports, please create a separate issue for each of them.
|
||||||
|
- Issues should represent **complete and actionable** work items. If you are unsure about something or have a question, please start a [discussion](https://github.com/Tyrrrz/CliFx/discussions/new) instead.
|
||||||
|
- Remember that **CliFx** is an open-source project funded by the community. If you find it useful, **please consider [donating](https://tyrrrz.me/donate) to support its development**.
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: Which version of the package does this bug affect? Make sure you're not using an outdated version.
|
||||||
|
placeholder: v1.0.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Platform
|
||||||
|
description: Which platform do you experience this bug on?
|
||||||
|
placeholder: .NET 7.0 / Windows 11
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: >
|
||||||
|
Minimum steps required to reproduce the bug, including prerequisites, code snippets, or other relevant items.
|
||||||
|
The information provided in this field must be readily actionable, meaning that anyone should be able to reproduce the bug by following these steps.
|
||||||
|
If the reproduction steps are too complex to fit in this field, please provide a link to a repository instead.
|
||||||
|
placeholder: |
|
||||||
|
- Step 1
|
||||||
|
- Step 2
|
||||||
|
- Step 3
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Details
|
||||||
|
description: Clear and thorough explanation of the bug, including any additional information you may find relevant.
|
||||||
|
placeholder: |
|
||||||
|
- Expected behavior: ...
|
||||||
|
- Actual behavior: ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Checklist
|
||||||
|
description: Quick list of checks to ensure that everything is in order.
|
||||||
|
options:
|
||||||
|
- label: I have looked through existing issues to make sure that this bug has not been reported before
|
||||||
|
required: true
|
||||||
|
- label: I have provided a descriptive title for this issue
|
||||||
|
required: true
|
||||||
|
- label: I have made sure that this bug is reproducible on the latest version of the package
|
||||||
|
required: true
|
||||||
|
- label: I have provided all the information needed to reproduce this bug as efficiently as possible
|
||||||
|
required: true
|
||||||
|
- label: I have sponsored this project
|
||||||
|
required: false
|
||||||
|
- label: I have not read any of the above and just checked all the boxes to submit the issue
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
If you are struggling to provide actionable reproduction steps, or if something else is preventing you from creating a complete bug report, please start a [discussion](https://github.com/Tyrrrz/CliFx/discussions/new) instead.
|
||||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: ⚠ Feature request
|
||||||
|
url: https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md
|
||||||
|
about: Sorry, but this project is in maintenance mode and no longer accepts new feature requests.
|
||||||
|
- name: 🗨 Discussions
|
||||||
|
url: https://github.com/Tyrrrz/CliFx/discussions/new
|
||||||
|
about: Ask and answer questions.
|
||||||
|
- name: 💬 Discord server
|
||||||
|
url: https://discord.gg/2SUWKFnHSm
|
||||||
|
about: Chat with the project community.
|
||||||
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
- package-ecosystem: nuget
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
groups:
|
||||||
|
nuget:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
34
.github/workflows/main.yml
vendored
Normal file
34
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: main
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
package-version:
|
||||||
|
type: string
|
||||||
|
description: Package version
|
||||||
|
required: false
|
||||||
|
deploy:
|
||||||
|
type: boolean
|
||||||
|
description: Deploy package
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
uses: Tyrrrz/.github/.github/workflows/nuget.yml@master
|
||||||
|
with:
|
||||||
|
deploy: ${{ inputs.deploy || github.ref_type == 'tag' }}
|
||||||
|
package-version: ${{ inputs.package-version || (github.ref_type == 'tag' && github.ref_name) || format('0.0.0-ci-{0}', github.sha) }}
|
||||||
|
dotnet-version: 9.0.x
|
||||||
|
secrets:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
|
||||||
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
340
.gitignore
vendored
340
.gitignore
vendored
@@ -1,340 +1,12 @@
|
|||||||
## Ignore Visual Studio temporary files, build results, and
|
|
||||||
## files generated by popular Visual Studio add-ons.
|
|
||||||
##
|
|
||||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
|
||||||
|
|
||||||
# User-specific files
|
# User-specific files
|
||||||
*.rsuser
|
.vs/
|
||||||
|
.idea/
|
||||||
*.suo
|
*.suo
|
||||||
*.user
|
*.user
|
||||||
*.userosscache
|
|
||||||
*.sln.docstates
|
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
|
||||||
*.userprefs
|
|
||||||
|
|
||||||
# Build results
|
# Build results
|
||||||
[Dd]ebug/
|
bin/
|
||||||
[Dd]ebugPublic/
|
obj/
|
||||||
[Rr]elease/
|
|
||||||
[Rr]eleases/
|
|
||||||
x64/
|
|
||||||
x86/
|
|
||||||
[Aa][Rr][Mm]/
|
|
||||||
[Aa][Rr][Mm]64/
|
|
||||||
bld/
|
|
||||||
[Bb]in/
|
|
||||||
[Oo]bj/
|
|
||||||
[Ll]og/
|
|
||||||
|
|
||||||
# Visual Studio 2015/2017 cache/options directory
|
# Test results
|
||||||
.vs/
|
TestResults/
|
||||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
|
||||||
#wwwroot/
|
|
||||||
|
|
||||||
# Visual Studio 2017 auto generated files
|
|
||||||
Generated\ Files/
|
|
||||||
|
|
||||||
# MSTest test Results
|
|
||||||
[Tt]est[Rr]esult*/
|
|
||||||
[Bb]uild[Ll]og.*
|
|
||||||
|
|
||||||
# NUNIT
|
|
||||||
*.VisualState.xml
|
|
||||||
TestResult.xml
|
|
||||||
|
|
||||||
# Build Results of an ATL Project
|
|
||||||
[Dd]ebugPS/
|
|
||||||
[Rr]eleasePS/
|
|
||||||
dlldata.c
|
|
||||||
|
|
||||||
# Benchmark Results
|
|
||||||
BenchmarkDotNet.Artifacts/
|
|
||||||
|
|
||||||
# .NET Core
|
|
||||||
project.lock.json
|
|
||||||
project.fragment.lock.json
|
|
||||||
artifacts/
|
|
||||||
|
|
||||||
# StyleCop
|
|
||||||
StyleCopReport.xml
|
|
||||||
|
|
||||||
# Files built by Visual Studio
|
|
||||||
*_i.c
|
|
||||||
*_p.c
|
|
||||||
*_h.h
|
|
||||||
*.ilk
|
|
||||||
*.meta
|
|
||||||
*.obj
|
|
||||||
*.iobj
|
|
||||||
*.pch
|
|
||||||
*.pdb
|
|
||||||
*.ipdb
|
|
||||||
*.pgc
|
|
||||||
*.pgd
|
|
||||||
*.rsp
|
|
||||||
*.sbr
|
|
||||||
*.tlb
|
|
||||||
*.tli
|
|
||||||
*.tlh
|
|
||||||
*.tmp
|
|
||||||
*.tmp_proj
|
|
||||||
*_wpftmp.csproj
|
|
||||||
*.log
|
|
||||||
*.vspscc
|
|
||||||
*.vssscc
|
|
||||||
.builds
|
|
||||||
*.pidb
|
|
||||||
*.svclog
|
|
||||||
*.scc
|
|
||||||
|
|
||||||
# Chutzpah Test files
|
|
||||||
_Chutzpah*
|
|
||||||
|
|
||||||
# Visual C++ cache files
|
|
||||||
ipch/
|
|
||||||
*.aps
|
|
||||||
*.ncb
|
|
||||||
*.opendb
|
|
||||||
*.opensdf
|
|
||||||
*.sdf
|
|
||||||
*.cachefile
|
|
||||||
*.VC.db
|
|
||||||
*.VC.VC.opendb
|
|
||||||
|
|
||||||
# Visual Studio profiler
|
|
||||||
*.psess
|
|
||||||
*.vsp
|
|
||||||
*.vspx
|
|
||||||
*.sap
|
|
||||||
|
|
||||||
# Visual Studio Trace Files
|
|
||||||
*.e2e
|
|
||||||
|
|
||||||
# TFS 2012 Local Workspace
|
|
||||||
$tf/
|
|
||||||
|
|
||||||
# Guidance Automation Toolkit
|
|
||||||
*.gpState
|
|
||||||
|
|
||||||
# ReSharper is a .NET coding add-in
|
|
||||||
_ReSharper*/
|
|
||||||
*.[Rr]e[Ss]harper
|
|
||||||
*.DotSettings.user
|
|
||||||
|
|
||||||
# JustCode is a .NET coding add-in
|
|
||||||
.JustCode
|
|
||||||
|
|
||||||
# TeamCity is a build add-in
|
|
||||||
_TeamCity*
|
|
||||||
|
|
||||||
# DotCover is a Code Coverage Tool
|
|
||||||
*.dotCover
|
|
||||||
|
|
||||||
# AxoCover is a Code Coverage Tool
|
|
||||||
.axoCover/*
|
|
||||||
!.axoCover/settings.json
|
|
||||||
|
|
||||||
# Visual Studio code coverage results
|
|
||||||
*.coverage
|
|
||||||
*.coveragexml
|
|
||||||
|
|
||||||
# NCrunch
|
|
||||||
_NCrunch_*
|
|
||||||
.*crunch*.local.xml
|
|
||||||
nCrunchTemp_*
|
|
||||||
|
|
||||||
# MightyMoose
|
|
||||||
*.mm.*
|
|
||||||
AutoTest.Net/
|
|
||||||
|
|
||||||
# Web workbench (sass)
|
|
||||||
.sass-cache/
|
|
||||||
|
|
||||||
# Installshield output folder
|
|
||||||
[Ee]xpress/
|
|
||||||
|
|
||||||
# DocProject is a documentation generator add-in
|
|
||||||
DocProject/buildhelp/
|
|
||||||
DocProject/Help/*.HxT
|
|
||||||
DocProject/Help/*.HxC
|
|
||||||
DocProject/Help/*.hhc
|
|
||||||
DocProject/Help/*.hhk
|
|
||||||
DocProject/Help/*.hhp
|
|
||||||
DocProject/Help/Html2
|
|
||||||
DocProject/Help/html
|
|
||||||
|
|
||||||
# Click-Once directory
|
|
||||||
publish/
|
|
||||||
|
|
||||||
# Publish Web Output
|
|
||||||
*.[Pp]ublish.xml
|
|
||||||
*.azurePubxml
|
|
||||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
|
||||||
# but database connection strings (with potential passwords) will be unencrypted
|
|
||||||
*.pubxml
|
|
||||||
*.publishproj
|
|
||||||
|
|
||||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
|
||||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
|
||||||
# in these scripts will be unencrypted
|
|
||||||
PublishScripts/
|
|
||||||
|
|
||||||
# NuGet Packages
|
|
||||||
*.nupkg
|
|
||||||
# The packages folder can be ignored because of Package Restore
|
|
||||||
**/[Pp]ackages/*
|
|
||||||
# except build/, which is used as an MSBuild target.
|
|
||||||
!**/[Pp]ackages/build/
|
|
||||||
# Uncomment if necessary however generally it will be regenerated when needed
|
|
||||||
#!**/[Pp]ackages/repositories.config
|
|
||||||
# NuGet v3's project.json files produces more ignorable files
|
|
||||||
*.nuget.props
|
|
||||||
*.nuget.targets
|
|
||||||
|
|
||||||
# Microsoft Azure Build Output
|
|
||||||
csx/
|
|
||||||
*.build.csdef
|
|
||||||
|
|
||||||
# Microsoft Azure Emulator
|
|
||||||
ecf/
|
|
||||||
rcf/
|
|
||||||
|
|
||||||
# Windows Store app package directories and files
|
|
||||||
AppPackages/
|
|
||||||
BundleArtifacts/
|
|
||||||
Package.StoreAssociation.xml
|
|
||||||
_pkginfo.txt
|
|
||||||
*.appx
|
|
||||||
|
|
||||||
# Visual Studio cache files
|
|
||||||
# files ending in .cache can be ignored
|
|
||||||
*.[Cc]ache
|
|
||||||
# but keep track of directories ending in .cache
|
|
||||||
!?*.[Cc]ache/
|
|
||||||
|
|
||||||
# Others
|
|
||||||
ClientBin/
|
|
||||||
~$*
|
|
||||||
*~
|
|
||||||
*.dbmdl
|
|
||||||
*.dbproj.schemaview
|
|
||||||
*.jfm
|
|
||||||
*.pfx
|
|
||||||
*.publishsettings
|
|
||||||
orleans.codegen.cs
|
|
||||||
|
|
||||||
# Including strong name files can present a security risk
|
|
||||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
|
||||||
#*.snk
|
|
||||||
|
|
||||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
|
||||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
|
||||||
#bower_components/
|
|
||||||
|
|
||||||
# RIA/Silverlight projects
|
|
||||||
Generated_Code/
|
|
||||||
|
|
||||||
# Backup & report files from converting an old project file
|
|
||||||
# to a newer Visual Studio version. Backup files are not needed,
|
|
||||||
# because we have git ;-)
|
|
||||||
_UpgradeReport_Files/
|
|
||||||
Backup*/
|
|
||||||
UpgradeLog*.XML
|
|
||||||
UpgradeLog*.htm
|
|
||||||
ServiceFabricBackup/
|
|
||||||
*.rptproj.bak
|
|
||||||
|
|
||||||
# SQL Server files
|
|
||||||
*.mdf
|
|
||||||
*.ldf
|
|
||||||
*.ndf
|
|
||||||
|
|
||||||
# Business Intelligence projects
|
|
||||||
*.rdl.data
|
|
||||||
*.bim.layout
|
|
||||||
*.bim_*.settings
|
|
||||||
*.rptproj.rsuser
|
|
||||||
*- Backup*.rdl
|
|
||||||
|
|
||||||
# Microsoft Fakes
|
|
||||||
FakesAssemblies/
|
|
||||||
|
|
||||||
# GhostDoc plugin setting file
|
|
||||||
*.GhostDoc.xml
|
|
||||||
|
|
||||||
# Node.js Tools for Visual Studio
|
|
||||||
.ntvs_analysis.dat
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Visual Studio 6 build log
|
|
||||||
*.plg
|
|
||||||
|
|
||||||
# Visual Studio 6 workspace options file
|
|
||||||
*.opt
|
|
||||||
|
|
||||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
|
||||||
*.vbw
|
|
||||||
|
|
||||||
# Visual Studio LightSwitch build output
|
|
||||||
**/*.HTMLClient/GeneratedArtifacts
|
|
||||||
**/*.DesktopClient/GeneratedArtifacts
|
|
||||||
**/*.DesktopClient/ModelManifest.xml
|
|
||||||
**/*.Server/GeneratedArtifacts
|
|
||||||
**/*.Server/ModelManifest.xml
|
|
||||||
_Pvt_Extensions
|
|
||||||
|
|
||||||
# Paket dependency manager
|
|
||||||
.paket/paket.exe
|
|
||||||
paket-files/
|
|
||||||
|
|
||||||
# FAKE - F# Make
|
|
||||||
.fake/
|
|
||||||
|
|
||||||
# JetBrains Rider
|
|
||||||
.idea/
|
|
||||||
*.sln.iml
|
|
||||||
|
|
||||||
# CodeRush personal settings
|
|
||||||
.cr/personal
|
|
||||||
|
|
||||||
# Python Tools for Visual Studio (PTVS)
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
|
|
||||||
# Cake - Uncomment if you are using it
|
|
||||||
# tools/**
|
|
||||||
# !tools/packages.config
|
|
||||||
|
|
||||||
# Tabs Studio
|
|
||||||
*.tss
|
|
||||||
|
|
||||||
# Telerik's JustMock configuration file
|
|
||||||
*.jmconfig
|
|
||||||
|
|
||||||
# BizTalk build output
|
|
||||||
*.btp.cs
|
|
||||||
*.btm.cs
|
|
||||||
*.odx.cs
|
|
||||||
*.xsd.cs
|
|
||||||
|
|
||||||
# OpenCover UI analysis results
|
|
||||||
OpenCover/
|
|
||||||
|
|
||||||
# Azure Stream Analytics local run output
|
|
||||||
ASALocalRun/
|
|
||||||
|
|
||||||
# MSBuild Binary and Structured Log
|
|
||||||
*.binlog
|
|
||||||
|
|
||||||
# NVidia Nsight GPU debugger configuration file
|
|
||||||
*.nvuser
|
|
||||||
|
|
||||||
# MFractors (Xamarin productivity tool) working folder
|
|
||||||
.mfractor/
|
|
||||||
|
|
||||||
# Local History for Visual Studio
|
|
||||||
.localhistory/
|
|
||||||
|
|
||||||
# BeatPulse healthcheck temp database
|
|
||||||
healthchecksdb
|
|
||||||
28
CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj
Normal file
28
CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Basic.Reference.Assemblies.Net80" Version="1.8.0" />
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="GitHubActionsTestLogger" Version="2.4.1" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" />
|
||||||
|
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
75
CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs
Normal file
75
CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class CommandMustBeAnnotatedAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } = new CommandMustBeAnnotatedAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_command_is_not_annotated_with_the_command_attribute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_command_is_annotated_with_the_command_attribute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public abstract class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_command_is_implemented_as_an_abstract_class()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public abstract class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_class_that_is_not_a_command()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class Foo
|
||||||
|
{
|
||||||
|
public int Bar { get; init; } = 5;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class CommandMustImplementInterfaceAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new CommandMustImplementInterfaceAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_command_does_not_implement_ICommand_interface()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand
|
||||||
|
{
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_command_implements_ICommand_interface()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_class_that_is_not_a_command()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class Foo
|
||||||
|
{
|
||||||
|
public int Bar { get; init; } = 5;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
CliFx.Analyzers.Tests/GeneralSpecs.cs
Normal file
29
CliFx.Analyzers.Tests/GeneralSpecs.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class GeneralSpecs
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void All_analyzers_have_unique_diagnostic_IDs()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var analyzers = typeof(AnalyzerBase)
|
||||||
|
.Assembly.GetTypes()
|
||||||
|
.Where(t => !t.IsAbstract && t.IsAssignableTo(typeof(DiagnosticAnalyzer)))
|
||||||
|
.Select(t => (DiagnosticAnalyzer)Activator.CreateInstance(t)!)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var diagnosticIds = analyzers
|
||||||
|
.SelectMany(a => a.SupportedDiagnostics.Select(d => d.Id))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
diagnosticIds.Should().OnlyHaveUniqueItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class OptionMustBeInsideCommandAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustBeInsideCommandAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_an_option_is_inside_a_class_that_is_not_a_command()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyClass
|
||||||
|
{
|
||||||
|
[CommandOption("foo")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_is_inside_a_command()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_is_inside_an_abstract_class()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public abstract class MyCommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class OptionMustBeRequiredIfPropertyRequiredAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new OptionMustBeRequiredIfPropertyRequiredAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_non_required_option_is_bound_to_a_required_property()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption('f', IsRequired = false)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_required_option_is_bound_to_a_required_property()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption('f')]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_non_required_option_is_bound_to_a_non_required_property()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption('f', IsRequired = false)]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_required_option_is_bound_to_a_non_required_property()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption('f')]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class OptionMustHaveNameOrShortNameAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new OptionMustHaveNameOrShortNameAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_an_option_does_not_have_a_name_or_short_name()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption(null)]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_has_a_name()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_has_a_short_name()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption('f')]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class OptionMustHaveUniqueNameAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveUniqueNameAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_an_option_has_the_same_name_as_another_option()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandOption("foo")]
|
||||||
|
public string? Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_has_a_unique_name()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandOption("bar")]
|
||||||
|
public string? Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_name()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption('f')]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class OptionMustHaveUniqueShortNameAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new OptionMustHaveUniqueShortNameAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_an_option_has_the_same_short_name_as_another_option()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption('f')]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandOption('f')]
|
||||||
|
public string? Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_has_a_unique_short_name()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption('f')]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandOption('b')]
|
||||||
|
public string? Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_has_a_short_name_which_is_unique_only_in_casing()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption('f')]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandOption('F')]
|
||||||
|
public string? Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_short_name()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class OptionMustHaveValidConverterAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new OptionMustHaveValidConverterAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_an_option_has_a_converter_that_does_not_derive_from_BindingConverter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyConverter
|
||||||
|
{
|
||||||
|
public string Convert(string? rawValue) => rawValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo", Converter = typeof(MyConverter))]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_an_option_has_a_converter_that_does_not_derive_from_a_compatible_BindingConverter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyConverter : BindingConverter<int>
|
||||||
|
{
|
||||||
|
public override int Convert(string? rawValue) => 42;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo", Converter = typeof(MyConverter))]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_has_a_converter_that_derives_from_a_compatible_BindingConverter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyConverter : BindingConverter<string>
|
||||||
|
{
|
||||||
|
public override string Convert(string? rawValue) => rawValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo", Converter = typeof(MyConverter))]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_nullable_option_has_a_converter_that_derives_from_a_compatible_BindingConverter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyConverter : BindingConverter<int>
|
||||||
|
{
|
||||||
|
public override int Convert(string? rawValue) => 42;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo", Converter = typeof(MyConverter))]
|
||||||
|
public int? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_non_scalar_option_has_a_converter_that_derives_from_a_compatible_BindingConverter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyConverter : BindingConverter<string>
|
||||||
|
{
|
||||||
|
public override string Convert(string? rawValue) => rawValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo", Converter = typeof(MyConverter))]
|
||||||
|
public IReadOnlyList<string>? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_converter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs
Normal file
109
CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class OptionMustHaveValidNameAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidNameAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_an_option_has_a_name_which_is_too_short()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("f")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_an_option_has_a_name_that_starts_with_a_non_letter_character()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("1foo")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_has_a_valid_name()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_name()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption('f')]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class OptionMustHaveValidShortNameAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new OptionMustHaveValidShortNameAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_an_option_has_a_short_name_which_is_not_a_letter_character()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption('1')]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_has_a_valid_short_name()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption('f')]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_short_name()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class OptionMustHaveValidValidatorsAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new OptionMustHaveValidValidatorsAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_an_option_has_a_validator_that_does_not_derive_from_BindingValidator()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyValidator
|
||||||
|
{
|
||||||
|
public void Validate(string value) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo", Validators = new[] { typeof(MyValidator) })]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_an_option_has_a_validator_that_does_not_derive_from_a_compatible_BindingValidator()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyValidator : BindingValidator<int>
|
||||||
|
{
|
||||||
|
public override BindingValidationError Validate(int value) => Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo", Validators = new[] { typeof(MyValidator) })]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_has_validators_that_all_derive_from_compatible_BindingValidators()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyValidator : BindingValidator<string>
|
||||||
|
{
|
||||||
|
public override BindingValidationError Validate(string value) => Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo", Validators = new[] { typeof(MyValidator) })]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_validators()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class ParameterMustBeInsideCommandAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new ParameterMustBeInsideCommandAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_parameter_is_inside_a_class_that_is_not_a_command()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyClass
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_parameter_is_inside_a_command()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_parameter_is_inside_an_abstract_class()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public abstract class MyCommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class ParameterMustBeLastIfNonRequiredAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new ParameterMustBeLastIfNonRequiredAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_non_required_parameter_is_not_the_last_in_order()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0, IsRequired = false)]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1)]
|
||||||
|
public required string Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_non_required_parameter_is_the_last_in_order()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1, IsRequired = false)]
|
||||||
|
public string? Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_no_non_required_parameters_are_defined()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1)]
|
||||||
|
public required string Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class ParameterMustBeLastIfNonScalarAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new ParameterMustBeLastIfNonScalarAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_non_scalar_parameter_is_not_the_last_in_order()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string[] Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1)]
|
||||||
|
public required string Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_is_the_last_in_order()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1)]
|
||||||
|
public required string[] Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_no_non_scalar_parameters_are_defined()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1)]
|
||||||
|
public required string Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class ParameterMustBeRequiredIfPropertyRequiredAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new ParameterMustBeRequiredIfPropertyRequiredAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_non_required_parameter_is_bound_to_a_required_property()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0, IsRequired = false)]
|
||||||
|
public required string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_required_parameter_is_bound_to_a_required_property()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_non_required_parameter_is_bound_to_a_non_required_property()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0, IsRequired = false)]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_required_parameter_is_bound_to_a_non_required_property()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class ParameterMustBeSingleIfNonRequiredAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new ParameterMustBeSingleIfNonRequiredAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_more_than_one_non_required_parameters_are_defined()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0, IsRequired = false)]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1, IsRequired = false)]
|
||||||
|
public string? Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_only_one_non_required_parameter_is_defined()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1, IsRequired = false)]
|
||||||
|
public string? Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_no_non_required_parameters_are_defined()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1)]
|
||||||
|
public required string Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class ParameterMustBeSingleIfNonScalarAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new ParameterMustBeSingleIfNonScalarAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_more_than_one_non_scalar_parameters_are_defined()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string[] Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1)]
|
||||||
|
public required string[] Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_only_one_non_scalar_parameter_is_defined()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1)]
|
||||||
|
public required string[] Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_no_non_scalar_parameters_are_defined()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1)]
|
||||||
|
public required string Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class ParameterMustHaveUniqueNameAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveUniqueNameAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_parameter_has_the_same_name_as_another_parameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0, Name = "foo")]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1, Name = "foo")]
|
||||||
|
public required string Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_parameter_has_unique_name()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0, Name = "foo")]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1, Name = "bar")]
|
||||||
|
public required string Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class ParameterMustHaveUniqueOrderAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new ParameterMustHaveUniqueOrderAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_parameter_has_the_same_order_as_another_parameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_parameter_has_unique_order()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
[CommandParameter(1)]
|
||||||
|
public required string Bar { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class ParameterMustHaveValidConverterAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new ParameterMustHaveValidConverterAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_parameter_has_a_converter_that_does_not_derive_from_BindingConverter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyConverter
|
||||||
|
{
|
||||||
|
public string Convert(string? rawValue) => rawValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0, Converter = typeof(MyConverter))]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_parameter_has_a_converter_that_does_not_derive_from_a_compatible_BindingConverter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyConverter : BindingConverter<int>
|
||||||
|
{
|
||||||
|
public override int Convert(string? rawValue) => 42;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0, Converter = typeof(MyConverter))]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyConverter : BindingConverter<string>
|
||||||
|
{
|
||||||
|
public override string Convert(string? rawValue) => rawValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0, Converter = typeof(MyConverter))]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_nullable_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyConverter : BindingConverter<int>
|
||||||
|
{
|
||||||
|
public override int Convert(string? rawValue) => 42;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("foo", Converter = typeof(MyConverter))]
|
||||||
|
public int? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyConverter : BindingConverter<string>
|
||||||
|
{
|
||||||
|
public override string Convert(string? rawValue) => rawValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0, Converter = typeof(MyConverter))]
|
||||||
|
public required IReadOnlyList<string> Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_parameter_does_not_have_a_converter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class ParameterMustHaveValidValidatorsAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new ParameterMustHaveValidValidatorsAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_a_parameter_has_a_validator_that_does_not_derive_from_BindingValidator()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyValidator
|
||||||
|
{
|
||||||
|
public void Validate(string value) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0, Validators = new[] { typeof(MyValidator) })]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_parameter_has_a_validator_that_does_not_derive_from_a_compatible_BindingValidator()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyValidator : BindingValidator<int>
|
||||||
|
{
|
||||||
|
public override BindingValidationError Validate(int value) => Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0, Validators = new[] { typeof(MyValidator) })]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_parameter_has_validators_that_all_derive_from_compatible_BindingValidators()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
public class MyValidator : BindingValidator<string>
|
||||||
|
{
|
||||||
|
public override BindingValidationError Validate(string value) => Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0, Validators = new[] { typeof(MyValidator) })]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_parameter_does_not_have_validators()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandParameter(0)]
|
||||||
|
public required string Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
using CliFx.Analyzers.Tests.Utils;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests;
|
||||||
|
|
||||||
|
public class SystemConsoleShouldBeAvoidedAnalyzerSpecs
|
||||||
|
{
|
||||||
|
private static DiagnosticAnalyzer Analyzer { get; } =
|
||||||
|
new SystemConsoleShouldBeAvoidedAnalyzer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_command_calls_a_method_on_SystemConsole()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Hello world");
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_command_accesses_a_property_on_SystemConsole()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = ConsoleColor.Black;
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_reports_an_error_if_a_command_calls_a_method_on_a_property_of_SystemConsole()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Hello world");
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().ProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_command_interacts_with_the_console_through_IConsole()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
|
{
|
||||||
|
console.WriteLine("Hello world");
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_IConsole_is_not_available_in_the_current_method()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public void SomeOtherMethod() => Console.WriteLine("Test");
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Analyzer_does_not_report_an_error_if_a_command_does_not_access_SystemConsole()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// lang=csharp
|
||||||
|
const string code = """
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Analyzer.Should().NotProduceDiagnostics(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
177
CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs
Normal file
177
CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Basic.Reference.Assemblies;
|
||||||
|
using FluentAssertions.Execution;
|
||||||
|
using FluentAssertions.Primitives;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Microsoft.CodeAnalysis.Text;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Tests.Utils;
|
||||||
|
|
||||||
|
internal class AnalyzerAssertions(DiagnosticAnalyzer analyzer, AssertionChain assertionChain)
|
||||||
|
: ReferenceTypeAssertions<DiagnosticAnalyzer, AnalyzerAssertions>(analyzer, assertionChain)
|
||||||
|
{
|
||||||
|
private readonly AssertionChain _assertionChain = assertionChain;
|
||||||
|
|
||||||
|
protected override string Identifier => "analyzer";
|
||||||
|
|
||||||
|
private Compilation Compile(string sourceCode)
|
||||||
|
{
|
||||||
|
// Get default system namespaces
|
||||||
|
var defaultSystemNamespaces = new[]
|
||||||
|
{
|
||||||
|
"System",
|
||||||
|
"System.Collections.Generic",
|
||||||
|
"System.Threading.Tasks",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get default CliFx namespaces
|
||||||
|
var defaultCliFxNamespaces = typeof(ICommand)
|
||||||
|
.Assembly.GetTypes()
|
||||||
|
.Where(t => t.IsPublic)
|
||||||
|
.Select(t => t.Namespace)
|
||||||
|
.Distinct()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// Append default imports to the source code
|
||||||
|
var sourceCodeWithUsings =
|
||||||
|
string.Join(Environment.NewLine, defaultSystemNamespaces.Select(n => $"using {n};"))
|
||||||
|
+ string.Join(Environment.NewLine, defaultCliFxNamespaces.Select(n => $"using {n};"))
|
||||||
|
+ Environment.NewLine
|
||||||
|
+ sourceCode;
|
||||||
|
|
||||||
|
// Parse the source code
|
||||||
|
var ast = SyntaxFactory.ParseSyntaxTree(
|
||||||
|
SourceText.From(sourceCodeWithUsings),
|
||||||
|
CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Compile the code to IL
|
||||||
|
var compilation = CSharpCompilation.Create(
|
||||||
|
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
|
||||||
|
[ast],
|
||||||
|
Net80.References.All.Append(
|
||||||
|
MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location)
|
||||||
|
),
|
||||||
|
// DLL to avoid having to define the Main() method
|
||||||
|
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
||||||
|
);
|
||||||
|
|
||||||
|
var compilationErrors = compilation
|
||||||
|
.GetDiagnostics()
|
||||||
|
.Where(d => d.Severity >= DiagnosticSeverity.Error)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (compilationErrors.Any())
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"""
|
||||||
|
Failed to compile code.
|
||||||
|
{string.Join(Environment.NewLine, compilationErrors.Select(e => e.ToString()))}
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return compilation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<Diagnostic> GetProducedDiagnostics(string sourceCode)
|
||||||
|
{
|
||||||
|
var analyzers = ImmutableArray.Create(Subject);
|
||||||
|
var compilation = Compile(sourceCode);
|
||||||
|
|
||||||
|
return compilation
|
||||||
|
.WithAnalyzers(analyzers)
|
||||||
|
.GetAnalyzerDiagnosticsAsync(analyzers, default)
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProduceDiagnostics(string sourceCode)
|
||||||
|
{
|
||||||
|
var expectedDiagnostics = Subject.SupportedDiagnostics;
|
||||||
|
var producedDiagnostics = GetProducedDiagnostics(sourceCode);
|
||||||
|
|
||||||
|
var expectedDiagnosticIds = expectedDiagnostics.Select(d => d.Id).Distinct().ToArray();
|
||||||
|
var producedDiagnosticIds = producedDiagnostics.Select(d => d.Id).Distinct().ToArray();
|
||||||
|
|
||||||
|
var isSuccessfulAssertion =
|
||||||
|
expectedDiagnosticIds.Intersect(producedDiagnosticIds).Count()
|
||||||
|
== expectedDiagnosticIds.Length;
|
||||||
|
|
||||||
|
_assertionChain
|
||||||
|
.ForCondition(isSuccessfulAssertion)
|
||||||
|
.FailWith(() =>
|
||||||
|
{
|
||||||
|
var buffer = new StringBuilder();
|
||||||
|
|
||||||
|
buffer.AppendLine("Expected and produced diagnostics do not match.");
|
||||||
|
buffer.AppendLine();
|
||||||
|
|
||||||
|
buffer.AppendLine("Expected diagnostics:");
|
||||||
|
|
||||||
|
foreach (var expectedDiagnostic in expectedDiagnostics)
|
||||||
|
{
|
||||||
|
buffer.Append(" - ");
|
||||||
|
buffer.Append(expectedDiagnostic.Id);
|
||||||
|
buffer.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.AppendLine();
|
||||||
|
|
||||||
|
buffer.AppendLine("Produced diagnostics:");
|
||||||
|
|
||||||
|
if (producedDiagnostics.Any())
|
||||||
|
{
|
||||||
|
foreach (var producedDiagnostic in producedDiagnostics)
|
||||||
|
{
|
||||||
|
buffer.Append(" - ");
|
||||||
|
buffer.Append(producedDiagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
buffer.AppendLine(" < none >");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FailReason(buffer.ToString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotProduceDiagnostics(string sourceCode)
|
||||||
|
{
|
||||||
|
var producedDiagnostics = GetProducedDiagnostics(sourceCode);
|
||||||
|
var isSuccessfulAssertion = !producedDiagnostics.Any();
|
||||||
|
|
||||||
|
_assertionChain
|
||||||
|
.ForCondition(isSuccessfulAssertion)
|
||||||
|
.FailWith(() =>
|
||||||
|
{
|
||||||
|
var buffer = new StringBuilder();
|
||||||
|
|
||||||
|
buffer.AppendLine("Expected no produced diagnostics.");
|
||||||
|
buffer.AppendLine();
|
||||||
|
|
||||||
|
buffer.AppendLine("Produced diagnostics:");
|
||||||
|
|
||||||
|
foreach (var producedDiagnostic in producedDiagnostics)
|
||||||
|
{
|
||||||
|
buffer.Append(" - ");
|
||||||
|
buffer.Append(producedDiagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FailReason(buffer.ToString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class AnalyzerAssertionsExtensions
|
||||||
|
{
|
||||||
|
public static AnalyzerAssertions Should(this DiagnosticAnalyzer analyzer) =>
|
||||||
|
new(analyzer, AssertionChain.GetOrCreate());
|
||||||
|
}
|
||||||
5
CliFx.Analyzers.Tests/xunit.runner.json
Normal file
5
CliFx.Analyzers.Tests/xunit.runner.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||||
|
"methodDisplayOptions": "all",
|
||||||
|
"methodDisplay": "method"
|
||||||
|
}
|
||||||
40
CliFx.Analyzers/AnalyzerBase.cs
Normal file
40
CliFx.Analyzers/AnalyzerBase.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
public abstract class AnalyzerBase : DiagnosticAnalyzer
|
||||||
|
{
|
||||||
|
public DiagnosticDescriptor SupportedDiagnostic { get; }
|
||||||
|
|
||||||
|
public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
|
||||||
|
|
||||||
|
protected AnalyzerBase(
|
||||||
|
string diagnosticTitle,
|
||||||
|
string diagnosticMessage,
|
||||||
|
DiagnosticSeverity diagnosticSeverity = DiagnosticSeverity.Error
|
||||||
|
)
|
||||||
|
{
|
||||||
|
SupportedDiagnostic = new DiagnosticDescriptor(
|
||||||
|
"CliFx_" + GetType().Name.TrimEnd("Analyzer"),
|
||||||
|
diagnosticTitle,
|
||||||
|
diagnosticMessage,
|
||||||
|
"CliFx",
|
||||||
|
diagnosticSeverity,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
SupportedDiagnostics = ImmutableArray.Create(SupportedDiagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Diagnostic CreateDiagnostic(Location location, params object?[]? messageArgs) =>
|
||||||
|
Diagnostic.Create(SupportedDiagnostic, location, messageArgs);
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
context.EnableConcurrentExecution();
|
||||||
|
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
CliFx.Analyzers/CliFx.Analyzers.csproj
Normal file
27
CliFx.Analyzers/CliFx.Analyzers.csproj
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
|
<GenerateDependencyFile>true</GenerateDependencyFile>
|
||||||
|
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||||
|
<NoWarn>$(NoWarn);RS1025;RS1026</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<!--
|
||||||
|
Because this project only has a single target framework, the condition in
|
||||||
|
Directory.Build.props does not appear to work. This is a workaround for that.
|
||||||
|
-->
|
||||||
|
<Nullable>annotations</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
||||||
|
<!-- Make sure to target the lowest possible version of the compiler for wider support -->
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.0.0" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.0.0" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="PolyShim" Version="1.14.0" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
50
CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs
Normal file
50
CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class CommandMustBeAnnotatedAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
$"Commands must be annotated with `{SymbolNames.CliFxCommandAttribute}`",
|
||||||
|
$"This type must be annotated with `{SymbolNames.CliFxCommandAttribute}` in order to be a valid command."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
ClassDeclarationSyntax classDeclaration,
|
||||||
|
ITypeSymbol type
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Ignore abstract classes, because they may be used to define
|
||||||
|
// base implementations for commands, in which case the command
|
||||||
|
// attribute doesn't make sense.
|
||||||
|
if (type.IsAbstract)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var implementsCommandInterface = type.AllInterfaces.Any(i =>
|
||||||
|
i.DisplayNameMatches(SymbolNames.CliFxCommandInterface)
|
||||||
|
);
|
||||||
|
|
||||||
|
var hasCommandAttribute = type.GetAttributes()
|
||||||
|
.Select(a => a.AttributeClass)
|
||||||
|
.Any(c => c.DisplayNameMatches(SymbolNames.CliFxCommandAttribute));
|
||||||
|
|
||||||
|
// If the interface is implemented, but the attribute is missing,
|
||||||
|
// then it's very likely a user error.
|
||||||
|
if (implementsCommandInterface && !hasCommandAttribute)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(CreateDiagnostic(classDeclaration.Identifier.GetLocation()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandleClassDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs
Normal file
44
CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class CommandMustImplementInterfaceAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
$"Commands must implement `{SymbolNames.CliFxCommandInterface}` interface",
|
||||||
|
$"This type must implement `{SymbolNames.CliFxCommandInterface}` interface in order to be a valid command."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
ClassDeclarationSyntax classDeclaration,
|
||||||
|
ITypeSymbol type
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var hasCommandAttribute = type.GetAttributes()
|
||||||
|
.Select(a => a.AttributeClass)
|
||||||
|
.Any(c => c.DisplayNameMatches(SymbolNames.CliFxCommandAttribute));
|
||||||
|
|
||||||
|
var implementsCommandInterface = type.AllInterfaces.Any(i =>
|
||||||
|
i.DisplayNameMatches(SymbolNames.CliFxCommandInterface)
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the attribute is present, but the interface is not implemented,
|
||||||
|
// it's very likely a user error.
|
||||||
|
if (hasCommandAttribute && !implementsCommandInterface)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(CreateDiagnostic(classDeclaration.Identifier.GetLocation()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandleClassDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs
Normal file
89
CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.ObjectModel;
|
||||||
|
|
||||||
|
internal partial class CommandOptionSymbol(
|
||||||
|
IPropertySymbol property,
|
||||||
|
string? name,
|
||||||
|
char? shortName,
|
||||||
|
bool? isRequired,
|
||||||
|
ITypeSymbol? converterType,
|
||||||
|
IReadOnlyList<ITypeSymbol> validatorTypes
|
||||||
|
) : ICommandMemberSymbol
|
||||||
|
{
|
||||||
|
public IPropertySymbol Property { get; } = property;
|
||||||
|
|
||||||
|
public string? Name { get; } = name;
|
||||||
|
|
||||||
|
public char? ShortName { get; } = shortName;
|
||||||
|
|
||||||
|
public bool? IsRequired { get; } = isRequired;
|
||||||
|
|
||||||
|
public ITypeSymbol? ConverterType { get; } = converterType;
|
||||||
|
|
||||||
|
public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; } = validatorTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal partial class CommandOptionSymbol
|
||||||
|
{
|
||||||
|
private static AttributeData? TryGetOptionAttribute(IPropertySymbol property) =>
|
||||||
|
property
|
||||||
|
.GetAttributes()
|
||||||
|
.FirstOrDefault(a =>
|
||||||
|
a.AttributeClass?.DisplayNameMatches(SymbolNames.CliFxCommandOptionAttribute)
|
||||||
|
== true
|
||||||
|
);
|
||||||
|
|
||||||
|
public static CommandOptionSymbol? TryResolve(IPropertySymbol property)
|
||||||
|
{
|
||||||
|
var attribute = TryGetOptionAttribute(property);
|
||||||
|
if (attribute is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var name =
|
||||||
|
attribute
|
||||||
|
.ConstructorArguments.Where(a => a.Type?.SpecialType == SpecialType.System_String)
|
||||||
|
.Select(a => a.Value)
|
||||||
|
.FirstOrDefault() as string;
|
||||||
|
|
||||||
|
var shortName =
|
||||||
|
attribute
|
||||||
|
.ConstructorArguments.Where(a => a.Type?.SpecialType == SpecialType.System_Char)
|
||||||
|
.Select(a => a.Value)
|
||||||
|
.FirstOrDefault() as char?;
|
||||||
|
|
||||||
|
var isRequired =
|
||||||
|
attribute
|
||||||
|
.NamedArguments.Where(a => a.Key == "IsRequired")
|
||||||
|
.Select(a => a.Value.Value)
|
||||||
|
.FirstOrDefault() as bool?;
|
||||||
|
|
||||||
|
var converter = attribute
|
||||||
|
.NamedArguments.Where(a => a.Key == "Converter")
|
||||||
|
.Select(a => a.Value.Value)
|
||||||
|
.Cast<ITypeSymbol?>()
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
var validators = attribute
|
||||||
|
.NamedArguments.Where(a => a.Key == "Validators")
|
||||||
|
.SelectMany(a => a.Value.Values)
|
||||||
|
.Select(c => c.Value)
|
||||||
|
.Cast<ITypeSymbol>()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return new CommandOptionSymbol(
|
||||||
|
property,
|
||||||
|
name,
|
||||||
|
shortName,
|
||||||
|
isRequired,
|
||||||
|
converter,
|
||||||
|
validators
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsOptionProperty(IPropertySymbol property) =>
|
||||||
|
TryGetOptionAttribute(property) is not null;
|
||||||
|
}
|
||||||
78
CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs
Normal file
78
CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.ObjectModel;
|
||||||
|
|
||||||
|
internal partial class CommandParameterSymbol(
|
||||||
|
IPropertySymbol property,
|
||||||
|
int order,
|
||||||
|
string? name,
|
||||||
|
bool? isRequired,
|
||||||
|
ITypeSymbol? converterType,
|
||||||
|
IReadOnlyList<ITypeSymbol> validatorTypes
|
||||||
|
) : ICommandMemberSymbol
|
||||||
|
{
|
||||||
|
public IPropertySymbol Property { get; } = property;
|
||||||
|
|
||||||
|
public int Order { get; } = order;
|
||||||
|
|
||||||
|
public string? Name { get; } = name;
|
||||||
|
|
||||||
|
public bool? IsRequired { get; } = isRequired;
|
||||||
|
|
||||||
|
public ITypeSymbol? ConverterType { get; } = converterType;
|
||||||
|
|
||||||
|
public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; } = validatorTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal partial class CommandParameterSymbol
|
||||||
|
{
|
||||||
|
private static AttributeData? TryGetParameterAttribute(IPropertySymbol property) =>
|
||||||
|
property
|
||||||
|
.GetAttributes()
|
||||||
|
.FirstOrDefault(a =>
|
||||||
|
a.AttributeClass?.DisplayNameMatches(SymbolNames.CliFxCommandParameterAttribute)
|
||||||
|
== true
|
||||||
|
);
|
||||||
|
|
||||||
|
public static CommandParameterSymbol? TryResolve(IPropertySymbol property)
|
||||||
|
{
|
||||||
|
var attribute = TryGetParameterAttribute(property);
|
||||||
|
if (attribute is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var order = (int)attribute.ConstructorArguments.Select(a => a.Value).First()!;
|
||||||
|
|
||||||
|
var name =
|
||||||
|
attribute
|
||||||
|
.NamedArguments.Where(a => a.Key == "Name")
|
||||||
|
.Select(a => a.Value.Value)
|
||||||
|
.FirstOrDefault() as string;
|
||||||
|
|
||||||
|
var isRequired =
|
||||||
|
attribute
|
||||||
|
.NamedArguments.Where(a => a.Key == "IsRequired")
|
||||||
|
.Select(a => a.Value.Value)
|
||||||
|
.FirstOrDefault() as bool?;
|
||||||
|
|
||||||
|
var converter = attribute
|
||||||
|
.NamedArguments.Where(a => a.Key == "Converter")
|
||||||
|
.Select(a => a.Value.Value)
|
||||||
|
.Cast<ITypeSymbol?>()
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
var validators = attribute
|
||||||
|
.NamedArguments.Where(a => a.Key == "Validators")
|
||||||
|
.SelectMany(a => a.Value.Values)
|
||||||
|
.Select(c => c.Value)
|
||||||
|
.Cast<ITypeSymbol>()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return new CommandParameterSymbol(property, order, name, isRequired, converter, validators);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsParameterProperty(IPropertySymbol property) =>
|
||||||
|
TryGetParameterAttribute(property) is not null;
|
||||||
|
}
|
||||||
21
CliFx.Analyzers/ObjectModel/ICommandMemberSymbol.cs
Normal file
21
CliFx.Analyzers/ObjectModel/ICommandMemberSymbol.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.ObjectModel;
|
||||||
|
|
||||||
|
internal interface ICommandMemberSymbol
|
||||||
|
{
|
||||||
|
IPropertySymbol Property { get; }
|
||||||
|
|
||||||
|
ITypeSymbol? ConverterType { get; }
|
||||||
|
|
||||||
|
IReadOnlyList<ITypeSymbol> ValidatorTypes { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class CommandMemberSymbolExtensions
|
||||||
|
{
|
||||||
|
public static bool IsScalar(this ICommandMemberSymbol member) =>
|
||||||
|
member.Property.Type.SpecialType == SpecialType.System_String
|
||||||
|
|| member.Property.Type.TryGetEnumerableUnderlyingType() is null;
|
||||||
|
}
|
||||||
13
CliFx.Analyzers/ObjectModel/SymbolNames.cs
Normal file
13
CliFx.Analyzers/ObjectModel/SymbolNames.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace CliFx.Analyzers.ObjectModel;
|
||||||
|
|
||||||
|
internal static class SymbolNames
|
||||||
|
{
|
||||||
|
public const string CliFxCommandInterface = "CliFx.ICommand";
|
||||||
|
public const string CliFxCommandAttribute = "CliFx.Attributes.CommandAttribute";
|
||||||
|
public const string CliFxCommandParameterAttribute =
|
||||||
|
"CliFx.Attributes.CommandParameterAttribute";
|
||||||
|
public const string CliFxCommandOptionAttribute = "CliFx.Attributes.CommandOptionAttribute";
|
||||||
|
public const string CliFxConsoleInterface = "CliFx.Infrastructure.IConsole";
|
||||||
|
public const string CliFxBindingConverterClass = "CliFx.Extensibility.BindingConverter<T>";
|
||||||
|
public const string CliFxBindingValidatorClass = "CliFx.Extensibility.BindingValidator<T>";
|
||||||
|
}
|
||||||
49
CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.cs
Normal file
49
CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class OptionMustBeInsideCommandAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Options must be defined inside commands",
|
||||||
|
$"This option must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (property.ContainingType.IsAbstract)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!CommandOptionSymbol.IsOptionProperty(property))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var isInsideCommand = property.ContainingType.AllInterfaces.Any(i =>
|
||||||
|
i.DisplayNameMatches(SymbolNames.CliFxCommandInterface)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isInsideCommand)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class OptionMustBeRequiredIfPropertyRequiredAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Options bound to required properties cannot be marked as non-required",
|
||||||
|
"This option cannot be marked as non-required because it's bound to a required property."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!property.IsRequired())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var option = CommandOptionSymbol.TryResolve(property);
|
||||||
|
if (option is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (option.IsRequired != false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.Identifier.GetLocation()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
CliFx.Analyzers/OptionMustHaveNameOrShortNameAnalyzer.cs
Normal file
39
CliFx.Analyzers/OptionMustHaveNameOrShortNameAnalyzer.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class OptionMustHaveNameOrShortNameAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Options must have either a name or short name specified",
|
||||||
|
"This option must have either a name or short name specified."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var option = CommandOptionSymbol.TryResolve(property);
|
||||||
|
if (option is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(option.Name) && option.ShortName is null)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs
Normal file
69
CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class OptionMustHaveUniqueNameAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Options must have unique names",
|
||||||
|
"This option's name must be unique within the command (comparison IS NOT case sensitive). "
|
||||||
|
+ "Specified name: `{0}`. "
|
||||||
|
+ "Property bound to another option with the same name: `{1}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var option = CommandOptionSymbol.TryResolve(property);
|
||||||
|
if (option is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(option.Name))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var otherProperties = property
|
||||||
|
.ContainingType.GetMembers()
|
||||||
|
.OfType<IPropertySymbol>()
|
||||||
|
.Where(m => !m.Equals(property))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var otherProperty in otherProperties)
|
||||||
|
{
|
||||||
|
var otherOption = CommandOptionSymbol.TryResolve(otherProperty);
|
||||||
|
if (otherOption is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(otherOption.Name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (string.Equals(option.Name, otherOption.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(
|
||||||
|
propertyDeclaration.Identifier.GetLocation(),
|
||||||
|
option.Name,
|
||||||
|
otherProperty.Name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.cs
Normal file
68
CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class OptionMustHaveUniqueShortNameAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Options must have unique short names",
|
||||||
|
"This option's short name must be unique within the command (comparison IS case sensitive). "
|
||||||
|
+ "Specified short name: `{0}` "
|
||||||
|
+ "Property bound to another option with the same short name: `{1}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var option = CommandOptionSymbol.TryResolve(property);
|
||||||
|
if (option is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (option.ShortName is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var otherProperties = property
|
||||||
|
.ContainingType.GetMembers()
|
||||||
|
.OfType<IPropertySymbol>()
|
||||||
|
.Where(m => !m.Equals(property))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var otherProperty in otherProperties)
|
||||||
|
{
|
||||||
|
var otherOption = CommandOptionSymbol.TryResolve(otherProperty);
|
||||||
|
if (otherOption is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (otherOption.ShortName is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (option.ShortName == otherOption.ShortName)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(
|
||||||
|
propertyDeclaration.Identifier.GetLocation(),
|
||||||
|
option.ShortName,
|
||||||
|
otherProperty.Name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs
Normal file
65
CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class OptionMustHaveValidConverterAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
$"Option converters must derive from `{SymbolNames.CliFxBindingConverterClass}`",
|
||||||
|
$"Converter specified for this option must derive from a compatible `{SymbolNames.CliFxBindingConverterClass}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var option = CommandOptionSymbol.TryResolve(property);
|
||||||
|
if (option is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (option.ConverterType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var converterValueType = option
|
||||||
|
.ConverterType.GetBaseTypes()
|
||||||
|
.FirstOrDefault(t =>
|
||||||
|
t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingConverterClass)
|
||||||
|
)
|
||||||
|
?.TypeArguments.FirstOrDefault();
|
||||||
|
|
||||||
|
// Value returned by the converter must be assignable to the property type
|
||||||
|
var isCompatible =
|
||||||
|
converterValueType is not null
|
||||||
|
&& (
|
||||||
|
option.IsScalar()
|
||||||
|
// Scalar
|
||||||
|
? context.Compilation.IsAssignable(converterValueType, property.Type)
|
||||||
|
// Non-scalar (assume we can handle all IEnumerable types for simplicity)
|
||||||
|
: property.Type.TryGetEnumerableUnderlyingType() is { } enumerableUnderlyingType
|
||||||
|
&& context.Compilation.IsAssignable(
|
||||||
|
converterValueType,
|
||||||
|
enumerableUnderlyingType
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isCompatible)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs
Normal file
43
CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class OptionMustHaveValidNameAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Options must have valid names",
|
||||||
|
"This option's name must be at least 2 characters long and must start with a letter. "
|
||||||
|
+ "Specified name: `{0}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var option = CommandOptionSymbol.TryResolve(property);
|
||||||
|
if (option is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(option.Name))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (option.Name.Length < 2 || !char.IsLetter(option.Name[0]))
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation(), option.Name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs
Normal file
43
CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class OptionMustHaveValidShortNameAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Option short names must be letter characters",
|
||||||
|
"This option's short name must be a single letter character. "
|
||||||
|
+ "Specified short name: `{0}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var option = CommandOptionSymbol.TryResolve(property);
|
||||||
|
if (option is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (option.ShortName is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!char.IsLetter(option.ShortName.Value))
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation(), option.ShortName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs
Normal file
58
CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class OptionMustHaveValidValidatorsAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
$"Option validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`",
|
||||||
|
$"Each validator specified for this option must derive from a compatible `{SymbolNames.CliFxBindingValidatorClass}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var option = CommandOptionSymbol.TryResolve(property);
|
||||||
|
if (option is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var validatorType in option.ValidatorTypes)
|
||||||
|
{
|
||||||
|
var validatorValueType = validatorType
|
||||||
|
.GetBaseTypes()
|
||||||
|
.FirstOrDefault(t =>
|
||||||
|
t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingValidatorClass)
|
||||||
|
)
|
||||||
|
?.TypeArguments.FirstOrDefault();
|
||||||
|
|
||||||
|
// Value passed to the validator must be assignable from the property type
|
||||||
|
var isCompatible =
|
||||||
|
validatorValueType is not null
|
||||||
|
&& context.Compilation.IsAssignable(property.Type, validatorValueType);
|
||||||
|
|
||||||
|
if (!isCompatible)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
||||||
|
);
|
||||||
|
|
||||||
|
// No need to report multiple identical diagnostics on the same node
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.cs
Normal file
49
CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class ParameterMustBeInsideCommandAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Parameters must be defined inside commands",
|
||||||
|
$"This parameter must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (property.ContainingType.IsAbstract)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!CommandParameterSymbol.IsParameterProperty(property))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var isInsideCommand = property.ContainingType.AllInterfaces.Any(i =>
|
||||||
|
i.DisplayNameMatches(SymbolNames.CliFxCommandInterface)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isInsideCommand)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
CliFx.Analyzers/ParameterMustBeLastIfNonRequiredAnalyzer.cs
Normal file
63
CliFx.Analyzers/ParameterMustBeLastIfNonRequiredAnalyzer.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class ParameterMustBeLastIfNonRequiredAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Parameters marked as non-required must be the last in order",
|
||||||
|
"This parameter is non-required so it must be the last in order (its order must be highest within the command). "
|
||||||
|
+ "Property bound to another non-required parameter: `{0}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var parameter = CommandParameterSymbol.TryResolve(property);
|
||||||
|
if (parameter is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (parameter.IsRequired != false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var otherProperties = property
|
||||||
|
.ContainingType.GetMembers()
|
||||||
|
.OfType<IPropertySymbol>()
|
||||||
|
.Where(m => !m.Equals(property))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var otherProperty in otherProperties)
|
||||||
|
{
|
||||||
|
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
|
||||||
|
if (otherParameter is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (otherParameter.Order > parameter.Order)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(
|
||||||
|
propertyDeclaration.Identifier.GetLocation(),
|
||||||
|
otherProperty.Name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs
Normal file
63
CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class ParameterMustBeLastIfNonScalarAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Parameters of non-scalar types must be the last in order",
|
||||||
|
"This parameter has a non-scalar type so it must be the last in order (its order must be highest within the command). "
|
||||||
|
+ "Property bound to another non-scalar parameter: `{0}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var parameter = CommandParameterSymbol.TryResolve(property);
|
||||||
|
if (parameter is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (parameter.IsScalar())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var otherProperties = property
|
||||||
|
.ContainingType.GetMembers()
|
||||||
|
.OfType<IPropertySymbol>()
|
||||||
|
.Where(m => !m.Equals(property))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var otherProperty in otherProperties)
|
||||||
|
{
|
||||||
|
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
|
||||||
|
if (otherParameter is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (otherParameter.Order > parameter.Order)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(
|
||||||
|
propertyDeclaration.Identifier.GetLocation(),
|
||||||
|
otherProperty.Name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class ParameterMustBeRequiredIfPropertyRequiredAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Parameters bound to required properties cannot be marked as non-required",
|
||||||
|
"This parameter cannot be marked as non-required because it's bound to a required property."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!property.IsRequired())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var parameter = CommandParameterSymbol.TryResolve(property);
|
||||||
|
if (parameter is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (parameter.IsRequired != false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.Identifier.GetLocation()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class ParameterMustBeSingleIfNonRequiredAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Parameters marked as non-required are limited to one per command",
|
||||||
|
"This parameter is non-required so it must be the only such parameter in the command. "
|
||||||
|
+ "Property bound to another non-required parameter: `{0}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var parameter = CommandParameterSymbol.TryResolve(property);
|
||||||
|
if (parameter is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (parameter.IsRequired != false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var otherProperties = property
|
||||||
|
.ContainingType.GetMembers()
|
||||||
|
.OfType<IPropertySymbol>()
|
||||||
|
.Where(m => !m.Equals(property))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var otherProperty in otherProperties)
|
||||||
|
{
|
||||||
|
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
|
||||||
|
if (otherParameter is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (otherParameter.IsRequired == false)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(
|
||||||
|
propertyDeclaration.Identifier.GetLocation(),
|
||||||
|
otherProperty.Name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs
Normal file
63
CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class ParameterMustBeSingleIfNonScalarAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Parameters of non-scalar types are limited to one per command",
|
||||||
|
"This parameter has a non-scalar type so it must be the only such parameter in the command. "
|
||||||
|
+ "Property bound to another non-scalar parameter: `{0}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var parameter = CommandParameterSymbol.TryResolve(property);
|
||||||
|
if (parameter is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (parameter.IsScalar())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var otherProperties = property
|
||||||
|
.ContainingType.GetMembers()
|
||||||
|
.OfType<IPropertySymbol>()
|
||||||
|
.Where(m => !m.Equals(property))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var otherProperty in otherProperties)
|
||||||
|
{
|
||||||
|
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
|
||||||
|
if (otherParameter is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!otherParameter.IsScalar())
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(
|
||||||
|
propertyDeclaration.Identifier.GetLocation(),
|
||||||
|
otherProperty.Name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs
Normal file
75
CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class ParameterMustHaveUniqueNameAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Parameters must have unique names",
|
||||||
|
"This parameter's name must be unique within the command (comparison IS NOT case sensitive). "
|
||||||
|
+ "Specified name: `{0}`. "
|
||||||
|
+ "Property bound to another parameter with the same name: `{1}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var parameter = CommandParameterSymbol.TryResolve(property);
|
||||||
|
if (parameter is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(parameter.Name))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var otherProperties = property
|
||||||
|
.ContainingType.GetMembers()
|
||||||
|
.OfType<IPropertySymbol>()
|
||||||
|
.Where(m => !m.Equals(property))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var otherProperty in otherProperties)
|
||||||
|
{
|
||||||
|
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
|
||||||
|
if (otherParameter is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(otherParameter.Name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (
|
||||||
|
string.Equals(
|
||||||
|
parameter.Name,
|
||||||
|
otherParameter.Name,
|
||||||
|
StringComparison.OrdinalIgnoreCase
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(
|
||||||
|
propertyDeclaration.Identifier.GetLocation(),
|
||||||
|
parameter.Name,
|
||||||
|
otherProperty.Name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs
Normal file
62
CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class ParameterMustHaveUniqueOrderAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
"Parameters must have unique order",
|
||||||
|
"This parameter's order must be unique within the command. "
|
||||||
|
+ "Specified order: {0}. "
|
||||||
|
+ "Property bound to another parameter with the same order: `{1}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var parameter = CommandParameterSymbol.TryResolve(property);
|
||||||
|
if (parameter is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var otherProperties = property
|
||||||
|
.ContainingType.GetMembers()
|
||||||
|
.OfType<IPropertySymbol>()
|
||||||
|
.Where(m => !m.Equals(property))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var otherProperty in otherProperties)
|
||||||
|
{
|
||||||
|
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
|
||||||
|
if (otherParameter is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (parameter.Order == otherParameter.Order)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(
|
||||||
|
propertyDeclaration.Identifier.GetLocation(),
|
||||||
|
parameter.Order,
|
||||||
|
otherProperty.Name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs
Normal file
65
CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class ParameterMustHaveValidConverterAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
$"Parameter converters must derive from `{SymbolNames.CliFxBindingConverterClass}`",
|
||||||
|
$"Converter specified for this parameter must derive from a compatible `{SymbolNames.CliFxBindingConverterClass}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var parameter = CommandParameterSymbol.TryResolve(property);
|
||||||
|
if (parameter is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (parameter.ConverterType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var converterValueType = parameter
|
||||||
|
.ConverterType.GetBaseTypes()
|
||||||
|
.FirstOrDefault(t =>
|
||||||
|
t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingConverterClass)
|
||||||
|
)
|
||||||
|
?.TypeArguments.FirstOrDefault();
|
||||||
|
|
||||||
|
// Value returned by the converter must be assignable to the property type
|
||||||
|
var isCompatible =
|
||||||
|
converterValueType is not null
|
||||||
|
&& (
|
||||||
|
parameter.IsScalar()
|
||||||
|
// Scalar
|
||||||
|
? context.Compilation.IsAssignable(converterValueType, property.Type)
|
||||||
|
// Non-scalar (assume we can handle all IEnumerable types for simplicity)
|
||||||
|
: property.Type.TryGetEnumerableUnderlyingType() is { } enumerableUnderlyingType
|
||||||
|
&& context.Compilation.IsAssignable(
|
||||||
|
converterValueType,
|
||||||
|
enumerableUnderlyingType
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isCompatible)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs
Normal file
58
CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class ParameterMustHaveValidValidatorsAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
$"Parameter validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`",
|
||||||
|
$"Each validator specified for this parameter must derive from a compatible `{SymbolNames.CliFxBindingValidatorClass}`."
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private void Analyze(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
|
IPropertySymbol property
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var parameter = CommandParameterSymbol.TryResolve(property);
|
||||||
|
if (parameter is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var validatorType in parameter.ValidatorTypes)
|
||||||
|
{
|
||||||
|
var validatorValueType = validatorType
|
||||||
|
.GetBaseTypes()
|
||||||
|
.FirstOrDefault(t =>
|
||||||
|
t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingValidatorClass)
|
||||||
|
)
|
||||||
|
?.TypeArguments.FirstOrDefault();
|
||||||
|
|
||||||
|
// Value passed to the validator must be assignable from the property type
|
||||||
|
var isCompatible =
|
||||||
|
validatorValueType is not null
|
||||||
|
&& context.Compilation.IsAssignable(property.Type, validatorValueType);
|
||||||
|
|
||||||
|
if (!isCompatible)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(
|
||||||
|
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
||||||
|
);
|
||||||
|
|
||||||
|
// No need to report multiple identical diagnostics on the same node
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.HandlePropertyDeclaration(Analyze);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs
Normal file
74
CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class SystemConsoleShouldBeAvoidedAnalyzer()
|
||||||
|
: AnalyzerBase(
|
||||||
|
$"Avoid calling `System.Console` where `{SymbolNames.CliFxConsoleInterface}` is available",
|
||||||
|
$"Use the provided `{SymbolNames.CliFxConsoleInterface}` abstraction instead of `System.Console` to ensure that the command can be tested in isolation.",
|
||||||
|
DiagnosticSeverity.Warning
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private MemberAccessExpressionSyntax? TryGetSystemConsoleMemberAccess(
|
||||||
|
SyntaxNodeAnalysisContext context,
|
||||||
|
SyntaxNode node
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var currentNode = node;
|
||||||
|
|
||||||
|
while (currentNode is MemberAccessExpressionSyntax memberAccess)
|
||||||
|
{
|
||||||
|
var member = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
|
||||||
|
|
||||||
|
if (member?.ContainingType?.DisplayNameMatches("System.Console") == true)
|
||||||
|
{
|
||||||
|
return memberAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get inner expression, which may be another member access expression.
|
||||||
|
// Example: System.Console.Error
|
||||||
|
// ~~~~~~~~~~~~~~ <- inner member access expression
|
||||||
|
// -------------------- <- outer member access expression
|
||||||
|
currentNode = memberAccess.Expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Analyze(SyntaxNodeAnalysisContext context)
|
||||||
|
{
|
||||||
|
// Try to get a member access on System.Console in the current expression,
|
||||||
|
// or in any of its inner expressions.
|
||||||
|
var systemConsoleMemberAccess = TryGetSystemConsoleMemberAccess(context, context.Node);
|
||||||
|
if (systemConsoleMemberAccess is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Check if IConsole is available in scope as an alternative to System.Console
|
||||||
|
var isConsoleInterfaceAvailable = context
|
||||||
|
.Node.Ancestors()
|
||||||
|
.OfType<MethodDeclarationSyntax>()
|
||||||
|
.SelectMany(m => m.ParameterList.Parameters)
|
||||||
|
.Select(p => p.Type)
|
||||||
|
.Select(t => context.SemanticModel.GetSymbolInfo(t).Symbol)
|
||||||
|
.Where(s => s is not null)
|
||||||
|
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxConsoleInterface));
|
||||||
|
|
||||||
|
if (isConsoleInterfaceAvailable)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(CreateDiagnostic(systemConsoleMemberAccess.GetLocation()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
base.Initialize(context);
|
||||||
|
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.SimpleMemberAccessExpression);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs
Normal file
98
CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Utils.Extensions;
|
||||||
|
|
||||||
|
internal static class RoslynExtensions
|
||||||
|
{
|
||||||
|
public static bool DisplayNameMatches(this ISymbol symbol, string name) =>
|
||||||
|
string.Equals(
|
||||||
|
// Fully qualified name, without `global::`
|
||||||
|
symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
|
||||||
|
name,
|
||||||
|
StringComparison.Ordinal
|
||||||
|
);
|
||||||
|
|
||||||
|
public static IEnumerable<INamedTypeSymbol> GetBaseTypes(this ITypeSymbol type)
|
||||||
|
{
|
||||||
|
var current = type.BaseType;
|
||||||
|
|
||||||
|
while (current is not null)
|
||||||
|
{
|
||||||
|
yield return current;
|
||||||
|
current = current.BaseType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ITypeSymbol? TryGetEnumerableUnderlyingType(this ITypeSymbol type) =>
|
||||||
|
type
|
||||||
|
.AllInterfaces.FirstOrDefault(i =>
|
||||||
|
i.ConstructedFrom.SpecialType
|
||||||
|
== SpecialType.System_Collections_Generic_IEnumerable_T
|
||||||
|
)
|
||||||
|
?.TypeArguments[0];
|
||||||
|
|
||||||
|
// Detect if the property is required through roundabout means so as to not have to take dependency
|
||||||
|
// on higher versions of the C# compiler.
|
||||||
|
public static bool IsRequired(this IPropertySymbol property) =>
|
||||||
|
property
|
||||||
|
// Can't rely on the RequiredMemberAttribute because it's generated by the compiler, not added by the user,
|
||||||
|
// so we have to check for the presence of the `required` modifier in the syntax tree instead.
|
||||||
|
.DeclaringSyntaxReferences.Select(r => r.GetSyntax())
|
||||||
|
.OfType<PropertyDeclarationSyntax>()
|
||||||
|
.SelectMany(p => p.Modifiers)
|
||||||
|
.Any(m => m.IsKind((SyntaxKind)8447));
|
||||||
|
|
||||||
|
public static bool IsAssignable(
|
||||||
|
this Compilation compilation,
|
||||||
|
ITypeSymbol source,
|
||||||
|
ITypeSymbol destination
|
||||||
|
) => compilation.ClassifyConversion(source, destination).Exists;
|
||||||
|
|
||||||
|
public static void HandleClassDeclaration(
|
||||||
|
this AnalysisContext analysisContext,
|
||||||
|
Action<SyntaxNodeAnalysisContext, ClassDeclarationSyntax, ITypeSymbol> analyze
|
||||||
|
)
|
||||||
|
{
|
||||||
|
analysisContext.RegisterSyntaxNodeAction(
|
||||||
|
ctx =>
|
||||||
|
{
|
||||||
|
if (ctx.Node is not ClassDeclarationSyntax classDeclaration)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var type = ctx.SemanticModel.GetDeclaredSymbol(classDeclaration);
|
||||||
|
if (type is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
analyze(ctx, classDeclaration, type);
|
||||||
|
},
|
||||||
|
SyntaxKind.ClassDeclaration
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void HandlePropertyDeclaration(
|
||||||
|
this AnalysisContext analysisContext,
|
||||||
|
Action<SyntaxNodeAnalysisContext, PropertyDeclarationSyntax, IPropertySymbol> analyze
|
||||||
|
)
|
||||||
|
{
|
||||||
|
analysisContext.RegisterSyntaxNodeAction(
|
||||||
|
ctx =>
|
||||||
|
{
|
||||||
|
if (ctx.Node is not PropertyDeclarationSyntax propertyDeclaration)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var property = ctx.SemanticModel.GetDeclaredSymbol(propertyDeclaration);
|
||||||
|
if (property is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
analyze(ctx, propertyDeclaration, property);
|
||||||
|
},
|
||||||
|
SyntaxKind.PropertyDeclaration
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
CliFx.Analyzers/Utils/Extensions/StringExtensions.cs
Normal file
18
CliFx.Analyzers/Utils/Extensions/StringExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Utils.Extensions;
|
||||||
|
|
||||||
|
internal static class StringExtensions
|
||||||
|
{
|
||||||
|
public static string TrimEnd(
|
||||||
|
this string str,
|
||||||
|
string sub,
|
||||||
|
StringComparison comparison = StringComparison.Ordinal
|
||||||
|
)
|
||||||
|
{
|
||||||
|
while (str.EndsWith(sub, comparison))
|
||||||
|
str = str[..^sub.Length];
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using BenchmarkDotNet.Attributes;
|
|
||||||
using CliFx.Benchmarks.Commands;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks
|
|
||||||
{
|
|
||||||
[CoreJob]
|
|
||||||
[RankColumn]
|
|
||||||
public class Benchmark
|
|
||||||
{
|
|
||||||
private static readonly string[] Arguments = { "--str", "hello world", "-i", "13", "-b" };
|
|
||||||
|
|
||||||
[Benchmark(Description = "CliFx", Baseline = true)]
|
|
||||||
public Task<int> ExecuteWithCliFx() => new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments);
|
|
||||||
|
|
||||||
[Benchmark(Description = "System.CommandLine")]
|
|
||||||
public Task<int> ExecuteWithSystemCommandLine() => new SystemCommandLineCommand().ExecuteAsync(Arguments);
|
|
||||||
|
|
||||||
[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
|
|
||||||
public int ExecuteWithMcMaster() => McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute<McMasterCommand>(Arguments);
|
|
||||||
|
|
||||||
// Skipped because this benchmark freezes after a couple of iterations
|
|
||||||
// Probably wasn't designed to run multiple times in single process execution
|
|
||||||
//[Benchmark(Description = "CommandLineParser")]
|
|
||||||
public void ExecuteWithCommandLineParser()
|
|
||||||
{
|
|
||||||
var parsed = CommandLine.Parser.Default.ParseArguments(Arguments, typeof(CommandLineParserCommand));
|
|
||||||
CommandLine.ParserResultExtensions.WithParsed<CommandLineParserCommand>(parsed, c => c.Execute());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Benchmark(Description = "PowerArgs")]
|
|
||||||
public void ExecuteWithPowerArgs() => PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
32
CliFx.Benchmarks/Benchmarks.CliFx.cs
Normal file
32
CliFx.Benchmarks/Benchmarks.CliFx.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks;
|
||||||
|
|
||||||
|
public partial class Benchmarks
|
||||||
|
{
|
||||||
|
[Command]
|
||||||
|
public class CliFxCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("str", 's')]
|
||||||
|
public string? StrOption { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("int", 'i')]
|
||||||
|
public int IntOption { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("bool", 'b')]
|
||||||
|
public bool BoolOption { get; set; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Description = "CliFx", Baseline = true)]
|
||||||
|
public async ValueTask<int> ExecuteWithCliFx() =>
|
||||||
|
await new CliApplicationBuilder()
|
||||||
|
.AddCommand<CliFxCommand>()
|
||||||
|
.Build()
|
||||||
|
.RunAsync(Arguments, new Dictionary<string, string>());
|
||||||
|
}
|
||||||
24
CliFx.Benchmarks/Benchmarks.Clipr.cs
Normal file
24
CliFx.Benchmarks/Benchmarks.Clipr.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using clipr;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks;
|
||||||
|
|
||||||
|
public partial class Benchmarks
|
||||||
|
{
|
||||||
|
public class CliprCommand
|
||||||
|
{
|
||||||
|
[NamedArgument('s', "str")]
|
||||||
|
public string? StrOption { get; set; }
|
||||||
|
|
||||||
|
[NamedArgument('i', "int")]
|
||||||
|
public int IntOption { get; set; }
|
||||||
|
|
||||||
|
[NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)]
|
||||||
|
public bool BoolOption { get; set; }
|
||||||
|
|
||||||
|
public void Execute() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Description = "Clipr")]
|
||||||
|
public void ExecuteWithClipr() => CliParser.Parse<CliprCommand>(Arguments).Execute();
|
||||||
|
}
|
||||||
19
CliFx.Benchmarks/Benchmarks.Cocona.cs
Normal file
19
CliFx.Benchmarks/Benchmarks.Cocona.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using Cocona;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks;
|
||||||
|
|
||||||
|
public partial class Benchmarks
|
||||||
|
{
|
||||||
|
public class CoconaCommand
|
||||||
|
{
|
||||||
|
public void Execute(
|
||||||
|
[Option("str", ['s'])] string? strOption,
|
||||||
|
[Option("int", ['i'])] int intOption,
|
||||||
|
[Option("bool", ['b'])] bool boolOption
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Description = "Cocona")]
|
||||||
|
public void ExecuteWithCocona() => CoconaApp.Run<CoconaCommand>(Arguments);
|
||||||
|
}
|
||||||
27
CliFx.Benchmarks/Benchmarks.CommandLineParser.cs
Normal file
27
CliFx.Benchmarks/Benchmarks.CommandLineParser.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using CommandLine;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks;
|
||||||
|
|
||||||
|
public partial class Benchmarks
|
||||||
|
{
|
||||||
|
public class CommandLineParserCommand
|
||||||
|
{
|
||||||
|
[Option('s', "str")]
|
||||||
|
public string? StrOption { get; set; }
|
||||||
|
|
||||||
|
[Option('i', "int")]
|
||||||
|
public int IntOption { get; set; }
|
||||||
|
|
||||||
|
[Option('b', "bool")]
|
||||||
|
public bool BoolOption { get; set; }
|
||||||
|
|
||||||
|
public void Execute() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Description = "CommandLineParser")]
|
||||||
|
public void ExecuteWithCommandLineParser() =>
|
||||||
|
new Parser()
|
||||||
|
.ParseArguments(Arguments, typeof(CommandLineParserCommand))
|
||||||
|
.WithParsed<CommandLineParserCommand>(c => c.Execute());
|
||||||
|
}
|
||||||
24
CliFx.Benchmarks/Benchmarks.McMaster.cs
Normal file
24
CliFx.Benchmarks/Benchmarks.McMaster.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using McMaster.Extensions.CommandLineUtils;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks;
|
||||||
|
|
||||||
|
public partial class Benchmarks
|
||||||
|
{
|
||||||
|
public class McMasterCommand
|
||||||
|
{
|
||||||
|
[Option("--str|-s")]
|
||||||
|
public string? StrOption { get; set; }
|
||||||
|
|
||||||
|
[Option("--int|-i")]
|
||||||
|
public int IntOption { get; set; }
|
||||||
|
|
||||||
|
[Option("--bool|-b")]
|
||||||
|
public bool BoolOption { get; set; }
|
||||||
|
|
||||||
|
public int OnExecute() => 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
|
||||||
|
public int ExecuteWithMcMaster() => CommandLineApplication.Execute<McMasterCommand>(Arguments);
|
||||||
|
}
|
||||||
24
CliFx.Benchmarks/Benchmarks.PowerArgs.cs
Normal file
24
CliFx.Benchmarks/Benchmarks.PowerArgs.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using PowerArgs;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks;
|
||||||
|
|
||||||
|
public partial class Benchmarks
|
||||||
|
{
|
||||||
|
public class PowerArgsCommand
|
||||||
|
{
|
||||||
|
[ArgShortcut("--str"), ArgShortcut("-s")]
|
||||||
|
public string? StrOption { get; set; }
|
||||||
|
|
||||||
|
[ArgShortcut("--int"), ArgShortcut("-i")]
|
||||||
|
public int IntOption { get; set; }
|
||||||
|
|
||||||
|
[ArgShortcut("--bool"), ArgShortcut("-b")]
|
||||||
|
public bool BoolOption { get; set; }
|
||||||
|
|
||||||
|
public void Main() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Description = "PowerArgs")]
|
||||||
|
public void ExecuteWithPowerArgs() => Args.InvokeMain<PowerArgsCommand>(Arguments);
|
||||||
|
}
|
||||||
33
CliFx.Benchmarks/Benchmarks.SystemCommandLine.cs
Normal file
33
CliFx.Benchmarks/Benchmarks.SystemCommandLine.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks;
|
||||||
|
|
||||||
|
public partial class Benchmarks
|
||||||
|
{
|
||||||
|
public class SystemCommandLineCommand
|
||||||
|
{
|
||||||
|
public static void ExecuteHandler(string s, int i, bool b) { }
|
||||||
|
|
||||||
|
public Task<int> ExecuteAsync(string[] args)
|
||||||
|
{
|
||||||
|
var stringOption = new Option<string>(["--str", "-s"]);
|
||||||
|
var intOption = new Option<int>(["--int", "-i"]);
|
||||||
|
var boolOption = new Option<bool>(["--bool", "-b"]);
|
||||||
|
|
||||||
|
var command = new RootCommand();
|
||||||
|
command.AddOption(stringOption);
|
||||||
|
command.AddOption(intOption);
|
||||||
|
command.AddOption(boolOption);
|
||||||
|
|
||||||
|
command.SetHandler(ExecuteHandler, stringOption, intOption, boolOption);
|
||||||
|
|
||||||
|
return command.InvokeAsync(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Description = "System.CommandLine")]
|
||||||
|
public async Task<int> ExecuteWithSystemCommandLine() =>
|
||||||
|
await new SystemCommandLineCommand().ExecuteAsync(Arguments);
|
||||||
|
}
|
||||||
18
CliFx.Benchmarks/Benchmarks.cs
Normal file
18
CliFx.Benchmarks/Benchmarks.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using BenchmarkDotNet.Configs;
|
||||||
|
using BenchmarkDotNet.Order;
|
||||||
|
using BenchmarkDotNet.Running;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks;
|
||||||
|
|
||||||
|
[RankColumn]
|
||||||
|
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
||||||
|
public partial class Benchmarks
|
||||||
|
{
|
||||||
|
private static readonly string[] Arguments = ["--str", "hello world", "-i", "13", "-b"];
|
||||||
|
|
||||||
|
public static void Main() =>
|
||||||
|
BenchmarkRunner.Run<Benchmarks>(
|
||||||
|
DefaultConfig.Instance.WithOptions(ConfigOptions.DisableOptimizationsValidator)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,16 +2,19 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
<NuGetAudit>false</NuGetAudit>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.11.5" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||||
<PackageReference Include="CommandLineParser" Version="2.6.0" />
|
<PackageReference Include="clipr" Version="1.6.1" />
|
||||||
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.3.4" />
|
<PackageReference Include="Cocona" Version="2.2.0" />
|
||||||
<PackageReference Include="PowerArgs" Version="3.6.0" />
|
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||||
<PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" />
|
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
|
||||||
|
<PackageReference Include="PowerArgs" Version="4.0.3" />
|
||||||
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
[Command]
|
|
||||||
public class CliFxCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("str", 's')]
|
|
||||||
public string StrOption { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("int", 'i')]
|
|
||||||
public int IntOption { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("bool", 'b')]
|
|
||||||
public bool BoolOption { get; set; }
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using CommandLine;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
public class CommandLineParserCommand
|
|
||||||
{
|
|
||||||
[Option('s', "str")]
|
|
||||||
public string StrOption { get; set; }
|
|
||||||
|
|
||||||
[Option('i', "int")]
|
|
||||||
public int IntOption { get; set; }
|
|
||||||
|
|
||||||
[Option('b', "bool")]
|
|
||||||
public bool BoolOption { get; set; }
|
|
||||||
|
|
||||||
public void Execute()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using McMaster.Extensions.CommandLineUtils;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
public class McMasterCommand
|
|
||||||
{
|
|
||||||
[Option("--str|-s")]
|
|
||||||
public string StrOption { get; set; }
|
|
||||||
|
|
||||||
[Option("--int|-i")]
|
|
||||||
public int IntOption { get; set; }
|
|
||||||
|
|
||||||
[Option("--bool|-b")]
|
|
||||||
public bool BoolOption { get; set; }
|
|
||||||
|
|
||||||
public int OnExecute() => 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using PowerArgs;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
public class PowerArgsCommand
|
|
||||||
{
|
|
||||||
[ArgShortcut("--str"), ArgShortcut("-s")]
|
|
||||||
public string StrOption { get; set; }
|
|
||||||
|
|
||||||
[ArgShortcut("--int"), ArgShortcut("-i")]
|
|
||||||
public int IntOption { get; set; }
|
|
||||||
|
|
||||||
[ArgShortcut("--bool"), ArgShortcut("-b")]
|
|
||||||
public bool BoolOption { get; set; }
|
|
||||||
|
|
||||||
public void Main()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using System.CommandLine;
|
|
||||||
using System.CommandLine.Invocation;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
public class SystemCommandLineCommand
|
|
||||||
{
|
|
||||||
public static int ExecuteHandler(string s, int i, bool b) => 0;
|
|
||||||
|
|
||||||
public Task<int> ExecuteAsync(string[] args)
|
|
||||||
{
|
|
||||||
var command = new RootCommand
|
|
||||||
{
|
|
||||||
new Option(new[] {"--str", "-s"})
|
|
||||||
{
|
|
||||||
Argument = new Argument<string>()
|
|
||||||
},
|
|
||||||
new Option(new[] {"--int", "-i"})
|
|
||||||
{
|
|
||||||
Argument = new Argument<int>()
|
|
||||||
},
|
|
||||||
new Option(new[] {"--bool", "-b"})
|
|
||||||
{
|
|
||||||
Argument = new Argument<bool>()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
command.Handler = CommandHandler.Create(typeof(SystemCommandLineCommand).GetMethod(nameof(ExecuteHandler)));
|
|
||||||
|
|
||||||
return command.InvokeAsync(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using BenchmarkDotNet.Configs;
|
|
||||||
using BenchmarkDotNet.Running;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks
|
|
||||||
{
|
|
||||||
public static class Program
|
|
||||||
{
|
|
||||||
public static void Main() =>
|
|
||||||
BenchmarkRunner.Run(typeof(Program).Assembly, DefaultConfig.Instance
|
|
||||||
.With(ConfigOptions.DisableOptimizationsValidator));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
22
CliFx.Benchmarks/Readme.md
Normal file
22
CliFx.Benchmarks/Readme.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
## CliFx.Benchmarks
|
||||||
|
|
||||||
|
All benchmarks below were ran with the following configuration:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
BenchmarkDotNet=v0.12.0, OS=Windows 10.0.14393.3443 (1607/AnniversaryUpdate/Redstone1)
|
||||||
|
Intel Core i5-4460 CPU 3.20GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
|
||||||
|
Frequency=3124994 Hz, Resolution=320.0006 ns, Timer=TSC
|
||||||
|
.NET Core SDK=3.1.100
|
||||||
|
[Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT
|
||||||
|
DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT
|
||||||
|
```
|
||||||
|
|
||||||
|
| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank |
|
||||||
|
| ------------------------------------ | ----------: | --------: | ---------: | ----: | ------: | ---: |
|
||||||
|
| CommandLineParser | 24.79 us | 0.166 us | 0.155 us | 0.49 | 0.00 | 1 |
|
||||||
|
| CliFx | 50.27 us | 0.248 us | 0.232 us | 1.00 | 0.00 | 2 |
|
||||||
|
| Clipr | 160.22 us | 0.817 us | 0.764 us | 3.19 | 0.02 | 3 |
|
||||||
|
| McMaster.Extensions.CommandLineUtils | 166.45 us | 1.111 us | 1.039 us | 3.31 | 0.03 | 4 |
|
||||||
|
| System.CommandLine | 170.27 us | 0.599 us | 0.560 us | 3.39 | 0.02 | 5 |
|
||||||
|
| PowerArgs | 306.12 us | 1.495 us | 1.398 us | 6.09 | 0.03 | 6 |
|
||||||
|
| Cocona | 1,856.07 us | 48.727 us | 141.367 us | 37.88 | 2.60 | 7 |
|
||||||
@@ -2,17 +2,18 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
|
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||||
|
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,75 +1,55 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Demo.Internal;
|
using CliFx.Demo.Domain;
|
||||||
using CliFx.Demo.Models;
|
using CliFx.Demo.Utils;
|
||||||
using CliFx.Demo.Services;
|
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Services;
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace CliFx.Demo.Commands
|
namespace CliFx.Demo.Commands;
|
||||||
|
|
||||||
|
[Command("book add", Description = "Adds a book to the library.")]
|
||||||
|
public class BookAddCommand(LibraryProvider libraryProvider) : ICommand
|
||||||
{
|
{
|
||||||
[Command("book add", Description = "Add a book to the library.")]
|
[CommandParameter(0, Name = "title", Description = "Book title.")]
|
||||||
public partial class BookAddCommand : ICommand
|
public required string Title { get; init; }
|
||||||
|
|
||||||
|
[CommandOption("author", 'a', Description = "Book author.")]
|
||||||
|
public required string Author { get; init; }
|
||||||
|
|
||||||
|
[CommandOption("published", 'p', Description = "Book publish date.")]
|
||||||
|
public DateTimeOffset Published { get; init; } =
|
||||||
|
new(
|
||||||
|
Random.Shared.Next(1800, 2020),
|
||||||
|
Random.Shared.Next(1, 12),
|
||||||
|
Random.Shared.Next(1, 28),
|
||||||
|
Random.Shared.Next(1, 23),
|
||||||
|
Random.Shared.Next(1, 59),
|
||||||
|
Random.Shared.Next(1, 59),
|
||||||
|
TimeSpan.Zero
|
||||||
|
);
|
||||||
|
|
||||||
|
[CommandOption("isbn", 'n', Description = "Book ISBN.")]
|
||||||
|
public Isbn Isbn { get; init; } =
|
||||||
|
new(
|
||||||
|
Random.Shared.Next(0, 999),
|
||||||
|
Random.Shared.Next(0, 99),
|
||||||
|
Random.Shared.Next(0, 99999),
|
||||||
|
Random.Shared.Next(0, 99),
|
||||||
|
Random.Shared.Next(0, 9)
|
||||||
|
);
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
private readonly LibraryService _libraryService;
|
if (libraryProvider.TryGetBook(Title) is not null)
|
||||||
|
throw new CommandException($"Book '{Title}' already exists.", 10);
|
||||||
|
|
||||||
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
|
var book = new Book(Title, Author, Published, Isbn);
|
||||||
public string Title { get; set; }
|
libraryProvider.AddBook(book);
|
||||||
|
|
||||||
[CommandOption("author", 'a', IsRequired = true, Description = "Book author.")]
|
console.WriteLine($"Book '{Title}' added.");
|
||||||
public string Author { get; set; }
|
console.WriteBook(book);
|
||||||
|
|
||||||
[CommandOption("published", 'p', Description = "Book publish date.")]
|
return default;
|
||||||
public DateTimeOffset Published { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("isbn", 'n', Description = "Book ISBN.")]
|
|
||||||
public Isbn Isbn { get; set; }
|
|
||||||
|
|
||||||
public BookAddCommand(LibraryService libraryService)
|
|
||||||
{
|
|
||||||
_libraryService = libraryService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
// To make the demo simpler, we will just generate random publish date and ISBN if they were not set
|
|
||||||
if (Published == default)
|
|
||||||
Published = CreateRandomDate();
|
|
||||||
if (Isbn == default)
|
|
||||||
Isbn = CreateRandomIsbn();
|
|
||||||
|
|
||||||
if (_libraryService.GetBook(Title) != null)
|
|
||||||
throw new CommandException("Book already exists.", 1);
|
|
||||||
|
|
||||||
var book = new Book(Title, Author, Published, Isbn);
|
|
||||||
_libraryService.AddBook(book);
|
|
||||||
|
|
||||||
console.Output.WriteLine("Book added.");
|
|
||||||
console.RenderBook(book);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public partial class BookAddCommand
|
|
||||||
{
|
|
||||||
private static readonly Random Random = new Random();
|
|
||||||
|
|
||||||
private static DateTimeOffset CreateRandomDate() => new DateTimeOffset(
|
|
||||||
Random.Next(1800, 2020),
|
|
||||||
Random.Next(1, 12),
|
|
||||||
Random.Next(1, 28),
|
|
||||||
Random.Next(1, 23),
|
|
||||||
Random.Next(1, 59),
|
|
||||||
Random.Next(1, 59),
|
|
||||||
TimeSpan.Zero);
|
|
||||||
|
|
||||||
public static Isbn CreateRandomIsbn() => new Isbn(
|
|
||||||
Random.Next(0, 999),
|
|
||||||
Random.Next(0, 99),
|
|
||||||
Random.Next(0, 99999),
|
|
||||||
Random.Next(0, 99),
|
|
||||||
Random.Next(0, 9));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,35 +1,27 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Demo.Internal;
|
using CliFx.Demo.Domain;
|
||||||
using CliFx.Demo.Services;
|
using CliFx.Demo.Utils;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Services;
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace CliFx.Demo.Commands
|
namespace CliFx.Demo.Commands;
|
||||||
|
|
||||||
|
[Command("book", Description = "Retrieves a book from the library.")]
|
||||||
|
public class BookCommand(LibraryProvider libraryProvider) : ICommand
|
||||||
{
|
{
|
||||||
[Command("book", Description = "View, list, add or remove books.")]
|
[CommandParameter(0, Name = "title", Description = "Title of the book to retrieve.")]
|
||||||
public class BookCommand : ICommand
|
public required string Title { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
private readonly LibraryService _libraryService;
|
var book = libraryProvider.TryGetBook(Title);
|
||||||
|
|
||||||
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
|
if (book is null)
|
||||||
public string Title { get; set; }
|
throw new CommandException($"Book '{Title}' not found.", 10);
|
||||||
|
|
||||||
public BookCommand(LibraryService libraryService)
|
console.WriteBook(book);
|
||||||
{
|
|
||||||
_libraryService = libraryService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console)
|
return default;
|
||||||
{
|
|
||||||
var book = _libraryService.GetBook(Title);
|
|
||||||
|
|
||||||
if (book == null)
|
|
||||||
throw new CommandException("Book not found.", 1);
|
|
||||||
|
|
||||||
console.RenderBook(book);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,29 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Demo.Internal;
|
using CliFx.Demo.Domain;
|
||||||
using CliFx.Demo.Services;
|
using CliFx.Demo.Utils;
|
||||||
using CliFx.Services;
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace CliFx.Demo.Commands
|
namespace CliFx.Demo.Commands;
|
||||||
|
|
||||||
|
[Command("book list", Description = "Lists all books in the library.")]
|
||||||
|
public class BookListCommand(LibraryProvider libraryProvider) : ICommand
|
||||||
{
|
{
|
||||||
[Command("book list", Description = "List all books in the library.")]
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
public class BookListCommand : ICommand
|
|
||||||
{
|
{
|
||||||
private readonly LibraryService _libraryService;
|
var library = libraryProvider.GetLibrary();
|
||||||
|
|
||||||
public BookListCommand(LibraryService libraryService)
|
for (var i = 0; i < library.Books.Count; i++)
|
||||||
{
|
{
|
||||||
_libraryService = libraryService;
|
// Add margin
|
||||||
|
if (i != 0)
|
||||||
|
console.WriteLine();
|
||||||
|
|
||||||
|
// Render book
|
||||||
|
var book = library.Books[i];
|
||||||
|
console.WriteBook(book);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console)
|
return default;
|
||||||
{
|
|
||||||
var library = _libraryService.GetLibrary();
|
|
||||||
|
|
||||||
var isFirst = true;
|
|
||||||
foreach (var book in library.Books)
|
|
||||||
{
|
|
||||||
// Margin
|
|
||||||
if (!isFirst)
|
|
||||||
console.Output.WriteLine();
|
|
||||||
isFirst = false;
|
|
||||||
|
|
||||||
// Render book
|
|
||||||
console.RenderBook(book);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,28 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Demo.Services;
|
using CliFx.Demo.Domain;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Services;
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace CliFx.Demo.Commands
|
namespace CliFx.Demo.Commands;
|
||||||
|
|
||||||
|
[Command("book remove", Description = "Removes a book from the library.")]
|
||||||
|
public class BookRemoveCommand(LibraryProvider libraryProvider) : ICommand
|
||||||
{
|
{
|
||||||
[Command("book remove", Description = "Remove a book from the library.")]
|
[CommandParameter(0, Name = "title", Description = "Title of the book to remove.")]
|
||||||
public class BookRemoveCommand : ICommand
|
public required string Title { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
private readonly LibraryService _libraryService;
|
var book = libraryProvider.TryGetBook(Title);
|
||||||
|
|
||||||
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
|
if (book is null)
|
||||||
public string Title { get; set; }
|
throw new CommandException($"Book '{Title}' not found.", 10);
|
||||||
|
|
||||||
public BookRemoveCommand(LibraryService libraryService)
|
libraryProvider.RemoveBook(book);
|
||||||
{
|
|
||||||
_libraryService = libraryService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console)
|
console.WriteLine($"Book '{Title}' removed.");
|
||||||
{
|
|
||||||
var book = _libraryService.GetBook(Title);
|
|
||||||
|
|
||||||
if (book == null)
|
return default;
|
||||||
throw new CommandException("Book not found.", 1);
|
|
||||||
|
|
||||||
_libraryService.RemoveBook(book);
|
|
||||||
|
|
||||||
console.Output.WriteLine($"Book {Title} removed.");
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
CliFx.Demo/Domain/Book.cs
Normal file
5
CliFx.Demo/Domain/Book.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Domain;
|
||||||
|
|
||||||
|
public record Book(string Title, string Author, DateTimeOffset Published, Isbn Isbn);
|
||||||
31
CliFx.Demo/Domain/Isbn.cs
Normal file
31
CliFx.Demo/Domain/Isbn.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Domain;
|
||||||
|
|
||||||
|
public partial record Isbn(
|
||||||
|
int EanPrefix,
|
||||||
|
int RegistrationGroup,
|
||||||
|
int Registrant,
|
||||||
|
int Publication,
|
||||||
|
int CheckDigit
|
||||||
|
)
|
||||||
|
{
|
||||||
|
public override string ToString() =>
|
||||||
|
$"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial record Isbn
|
||||||
|
{
|
||||||
|
public static Isbn Parse(string value, IFormatProvider formatProvider)
|
||||||
|
{
|
||||||
|
var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
return new Isbn(
|
||||||
|
int.Parse(components[0], formatProvider),
|
||||||
|
int.Parse(components[1], formatProvider),
|
||||||
|
int.Parse(components[2], formatProvider),
|
||||||
|
int.Parse(components[3], formatProvider),
|
||||||
|
int.Parse(components[4], formatProvider)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
CliFx.Demo/Domain/Library.cs
Normal file
28
CliFx.Demo/Domain/Library.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Domain;
|
||||||
|
|
||||||
|
public partial record Library(IReadOnlyList<Book> Books)
|
||||||
|
{
|
||||||
|
public Library WithBook(Book book)
|
||||||
|
{
|
||||||
|
var books = Books.ToList();
|
||||||
|
books.Add(book);
|
||||||
|
|
||||||
|
return new Library(books);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Library WithoutBook(Book book)
|
||||||
|
{
|
||||||
|
var books = Books.Where(b => b != book).ToArray();
|
||||||
|
|
||||||
|
return new Library(books);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial record Library
|
||||||
|
{
|
||||||
|
public static Library Empty { get; } = new(Array.Empty<Book>());
|
||||||
|
}
|
||||||
42
CliFx.Demo/Domain/LibraryProvider.cs
Normal file
42
CliFx.Demo/Domain/LibraryProvider.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Domain;
|
||||||
|
|
||||||
|
public class LibraryProvider
|
||||||
|
{
|
||||||
|
private static string StorageFilePath { get; } =
|
||||||
|
Path.Combine(Directory.GetCurrentDirectory(), "Library.json");
|
||||||
|
|
||||||
|
private void StoreLibrary(Library library)
|
||||||
|
{
|
||||||
|
var data = JsonSerializer.Serialize(library);
|
||||||
|
File.WriteAllText(StorageFilePath, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Library GetLibrary()
|
||||||
|
{
|
||||||
|
if (!File.Exists(StorageFilePath))
|
||||||
|
return Library.Empty;
|
||||||
|
|
||||||
|
var data = File.ReadAllText(StorageFilePath);
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<Library>(data) ?? Library.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Book? TryGetBook(string title) =>
|
||||||
|
GetLibrary().Books.FirstOrDefault(b => b.Title == title);
|
||||||
|
|
||||||
|
public void AddBook(Book book)
|
||||||
|
{
|
||||||
|
var updatedLibrary = GetLibrary().WithBook(book);
|
||||||
|
StoreLibrary(updatedLibrary);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveBook(Book book)
|
||||||
|
{
|
||||||
|
var updatedLibrary = GetLibrary().WithoutBook(book);
|
||||||
|
StoreLibrary(updatedLibrary);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System;
|
|
||||||
using CliFx.Demo.Models;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Demo.Internal
|
|
||||||
{
|
|
||||||
internal static class Extensions
|
|
||||||
{
|
|
||||||
public static void RenderBook(this IConsole console, Book book)
|
|
||||||
{
|
|
||||||
// Title
|
|
||||||
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Title));
|
|
||||||
|
|
||||||
// Author
|
|
||||||
console.Output.Write(" ");
|
|
||||||
console.Output.Write("Author: ");
|
|
||||||
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Author));
|
|
||||||
|
|
||||||
// Published
|
|
||||||
console.Output.Write(" ");
|
|
||||||
console.Output.Write("Published: ");
|
|
||||||
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine($"{book.Published:d}"));
|
|
||||||
|
|
||||||
// ISBN
|
|
||||||
console.Output.Write(" ");
|
|
||||||
console.Output.Write("ISBN: ");
|
|
||||||
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Isbn));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CliFx.Demo.Models
|
|
||||||
{
|
|
||||||
public class Book
|
|
||||||
{
|
|
||||||
public string Title { get; }
|
|
||||||
|
|
||||||
public string Author { get; }
|
|
||||||
|
|
||||||
public DateTimeOffset Published { get; }
|
|
||||||
|
|
||||||
public Isbn Isbn { get; }
|
|
||||||
|
|
||||||
public Book(string title, string author, DateTimeOffset published, Isbn isbn)
|
|
||||||
{
|
|
||||||
Title = title;
|
|
||||||
Author = author;
|
|
||||||
Published = published;
|
|
||||||
Isbn = isbn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace CliFx.Demo.Models
|
|
||||||
{
|
|
||||||
public static class Extensions
|
|
||||||
{
|
|
||||||
public static Library WithBook(this Library library, Book book)
|
|
||||||
{
|
|
||||||
var books = library.Books.ToList();
|
|
||||||
books.Add(book);
|
|
||||||
|
|
||||||
return new Library(books);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Library WithoutBook(this Library library, Book book)
|
|
||||||
{
|
|
||||||
var books = library.Books.Where(b => b != book).ToArray();
|
|
||||||
|
|
||||||
return new Library(books);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace CliFx.Demo.Models
|
|
||||||
{
|
|
||||||
public partial class Isbn
|
|
||||||
{
|
|
||||||
public int EanPrefix { get; }
|
|
||||||
|
|
||||||
public int RegistrationGroup { get; }
|
|
||||||
|
|
||||||
public int Registrant { get; }
|
|
||||||
|
|
||||||
public int Publication { get; }
|
|
||||||
|
|
||||||
public int CheckDigit { get; }
|
|
||||||
|
|
||||||
public Isbn(int eanPrefix, int registrationGroup, int registrant, int publication, int checkDigit)
|
|
||||||
{
|
|
||||||
EanPrefix = eanPrefix;
|
|
||||||
RegistrationGroup = registrationGroup;
|
|
||||||
Registrant = registrant;
|
|
||||||
Publication = publication;
|
|
||||||
CheckDigit = checkDigit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => $"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class Isbn
|
|
||||||
{
|
|
||||||
public static Isbn Parse(string value)
|
|
||||||
{
|
|
||||||
var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
return new Isbn(
|
|
||||||
int.Parse(components[0], CultureInfo.InvariantCulture),
|
|
||||||
int.Parse(components[1], CultureInfo.InvariantCulture),
|
|
||||||
int.Parse(components[2], CultureInfo.InvariantCulture),
|
|
||||||
int.Parse(components[3], CultureInfo.InvariantCulture),
|
|
||||||
int.Parse(components[4], CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CliFx.Demo.Models
|
|
||||||
{
|
|
||||||
public partial class Library
|
|
||||||
{
|
|
||||||
public IReadOnlyList<Book> Books { get; }
|
|
||||||
|
|
||||||
public Library(IReadOnlyList<Book> books)
|
|
||||||
{
|
|
||||||
Books = books;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class Library
|
|
||||||
{
|
|
||||||
public static Library Empty { get; } = new Library(Array.Empty<Book>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,21 @@
|
|||||||
using System.Threading.Tasks;
|
using CliFx;
|
||||||
using CliFx.Demo.Commands;
|
using CliFx.Demo.Domain;
|
||||||
using CliFx.Demo.Services;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace CliFx.Demo
|
return await new CliApplicationBuilder()
|
||||||
{
|
.SetDescription("Demo application showcasing CliFx features.")
|
||||||
public static class Program
|
.AddCommandsFromThisAssembly()
|
||||||
|
.UseTypeActivator(commandTypes =>
|
||||||
{
|
{
|
||||||
public static Task<int> Main(string[] args)
|
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
|
||||||
{
|
var services = new ServiceCollection();
|
||||||
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
|
services.AddSingleton<LibraryProvider>();
|
||||||
var services = new ServiceCollection();
|
|
||||||
|
|
||||||
// Register services
|
// Register all commands as transient services
|
||||||
services.AddSingleton<LibraryService>();
|
foreach (var commandType in commandTypes)
|
||||||
|
services.AddTransient(commandType);
|
||||||
|
|
||||||
// Register commands
|
return services.BuildServiceProvider();
|
||||||
services.AddTransient<BookCommand>();
|
})
|
||||||
services.AddTransient<BookAddCommand>();
|
.Build()
|
||||||
services.AddTransient<BookRemoveCommand>();
|
.RunAsync();
|
||||||
services.AddTransient<BookListCommand>();
|
|
||||||
|
|
||||||
var serviceProvider = services.BuildServiceProvider();
|
|
||||||
|
|
||||||
return new CliApplicationBuilder()
|
|
||||||
.AddCommandsFromThisAssembly()
|
|
||||||
.UseCommandFactory(schema => (ICommand) serviceProvider.GetRequiredService(schema.Type))
|
|
||||||
.Build()
|
|
||||||
.RunAsync(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
# CliFx Demo Project
|
# CliFx Demo Project
|
||||||
|
|
||||||
Sample command line interface for managing a library of books.
|
Sample command-line interface for managing a library of books.
|
||||||
|
|
||||||
This demo project shows basic CliFx functionality such as command routing, option parsing, autogenerated help text, and some other things.
|
This demo project showcases basic CliFx functionality such as command routing, argument parsing, and autogenerated help text.
|
||||||
|
|
||||||
You can get a list of available commands by running `CliFx.Demo --help`.
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using CliFx.Demo.Models;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CliFx.Demo.Services
|
|
||||||
{
|
|
||||||
public class LibraryService
|
|
||||||
{
|
|
||||||
private string StorageFilePath => Path.Combine(Directory.GetCurrentDirectory(), "Data.json");
|
|
||||||
|
|
||||||
private void StoreLibrary(Library library)
|
|
||||||
{
|
|
||||||
var data = JsonConvert.SerializeObject(library);
|
|
||||||
File.WriteAllText(StorageFilePath, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Library GetLibrary()
|
|
||||||
{
|
|
||||||
if (!File.Exists(StorageFilePath))
|
|
||||||
return Library.Empty;
|
|
||||||
|
|
||||||
var data = File.ReadAllText(StorageFilePath);
|
|
||||||
|
|
||||||
return JsonConvert.DeserializeObject<Library>(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Book GetBook(string title) => GetLibrary().Books.FirstOrDefault(b => b.Title == title);
|
|
||||||
|
|
||||||
public void AddBook(Book book)
|
|
||||||
{
|
|
||||||
var updatedLibrary = GetLibrary().WithBook(book);
|
|
||||||
StoreLibrary(updatedLibrary);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveBook(Book book)
|
|
||||||
{
|
|
||||||
var updatedLibrary = GetLibrary().WithoutBook(book);
|
|
||||||
StoreLibrary(updatedLibrary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
39
CliFx.Demo/Utils/ConsoleExtensions.cs
Normal file
39
CliFx.Demo/Utils/ConsoleExtensions.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using CliFx.Demo.Domain;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Utils;
|
||||||
|
|
||||||
|
internal static class ConsoleExtensions
|
||||||
|
{
|
||||||
|
public static void WriteBook(this ConsoleWriter writer, Book book)
|
||||||
|
{
|
||||||
|
// Title
|
||||||
|
using (writer.Console.WithForegroundColor(ConsoleColor.White))
|
||||||
|
writer.WriteLine(book.Title);
|
||||||
|
|
||||||
|
// Author
|
||||||
|
writer.Write(" ");
|
||||||
|
writer.Write("Author: ");
|
||||||
|
|
||||||
|
using (writer.Console.WithForegroundColor(ConsoleColor.White))
|
||||||
|
writer.WriteLine(book.Author);
|
||||||
|
|
||||||
|
// Published
|
||||||
|
writer.Write(" ");
|
||||||
|
writer.Write("Published: ");
|
||||||
|
|
||||||
|
using (writer.Console.WithForegroundColor(ConsoleColor.White))
|
||||||
|
writer.WriteLine($"{book.Published:d}");
|
||||||
|
|
||||||
|
// ISBN
|
||||||
|
writer.Write(" ");
|
||||||
|
writer.Write("ISBN: ");
|
||||||
|
|
||||||
|
using (writer.Console.WithForegroundColor(ConsoleColor.White))
|
||||||
|
writer.WriteLine(book.Isbn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WriteBook(this IConsole console, Book book) =>
|
||||||
|
console.Output.WriteBook(book);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user