mirror of
				https://github.com/Tyrrrz/CliFx.git
				synced 2025-10-25 15:19:17 +00:00 
			
		
		
		
	Compare commits
	
		
			84 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 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 | 
							
								
								
									
										3
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +0,0 @@ | ||||
| github: Tyrrrz | ||||
| patreon: Tyrrrz | ||||
| custom: ['buymeacoffee.com/Tyrrrz', 'tyrrrz.me/donate'] | ||||
							
								
								
									
										42
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| name: 🐞 Bug report | ||||
| description: Report broken functionality. | ||||
| labels: [bug] | ||||
|  | ||||
| body: | ||||
| - type: markdown | ||||
|   attributes: | ||||
|     value: | | ||||
|       🧐 **Guidelines:** | ||||
|  | ||||
|       - Search through [existing issues](https://github.com/Tyrrrz/CliFx/issues?q=is%3Aissue) first to ensure that this bug has not been reported before. | ||||
|       - Write a descriptive title for your issue. Avoid generic or vague titles such as "Something's not working" or "A couple of problems". | ||||
|       - Keep your issue focused on one single problem. If you have multiple bug reports, please create separate issues for each of them. | ||||
|       - Provide as much context as possible in the details section. Include screenshots, screen recordings, links, references, or anything else you may consider relevant. | ||||
|       - If you want to ask a question instead of reporting a bug, please use [discussions](https://github.com/Tyrrrz/CliFx/discussions/new) instead. | ||||
|  | ||||
| - type: input | ||||
|   attributes: | ||||
|     label: Version | ||||
|     description: Which version of CliFx does this bug affect? | ||||
|     placeholder: ver X.Y.Z | ||||
|   validations: | ||||
|     required: true | ||||
|  | ||||
| - type: textarea | ||||
|   attributes: | ||||
|     label: Details | ||||
|     description: Clear and thorough explanation of the bug. | ||||
|     placeholder: I was doing X expecting Y to happen, but Z happened instead. | ||||
|   validations: | ||||
|     required: true | ||||
|  | ||||
| - type: textarea | ||||
|   attributes: | ||||
|     label: Steps to reproduce | ||||
|     description: Minimum steps required to reproduce the bug. | ||||
|     placeholder: | | ||||
|       - Step 1 | ||||
|       - Step 2 | ||||
|       - Step 3 | ||||
|   validations: | ||||
|     required: true | ||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| blank_issues_enabled: false | ||||
| contact_links: | ||||
|   - name: 💬 Discord server | ||||
|     url: https://discord.gg/2SUWKFnHSm | ||||
|     about: Chat with the project community. | ||||
|   - name: 🗨 Discussions | ||||
|     url: https://github.com/Tyrrrz/CliFx/discussions/new | ||||
|     about: Ask and answer questions. | ||||
							
								
								
									
										22
									
								
								.github/ISSUE_TEMPLATE/feature-request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.github/ISSUE_TEMPLATE/feature-request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| name: ✨ Feature request | ||||
| description: Request a new feature. | ||||
| labels: [enhancement] | ||||
|  | ||||
| body: | ||||
| - type: markdown | ||||
|   attributes: | ||||
|     value: | | ||||
|       🧐 **Guidelines:** | ||||
|  | ||||
|       - Search through [existing issues](https://github.com/Tyrrrz/CliFx/issues?q=is%3Aissue) first to ensure that this feature has not been requested before. | ||||
|       - Write a descriptive title for your issue. Avoid generic or vague titles such as "Some suggestions" or "Ideas for improvement". | ||||
|       - Keep your issue focused on one single problem. If you have multiple feature requests, please create separate issues for each of them. | ||||
|       - Provide as much context as possible in the details section. Include screenshots, screen recordings, links, references, or anything else you may consider relevant. | ||||
|       - If you want to ask a question instead of requesting a feature, please use [discussions](https://github.com/Tyrrrz/CliFx/discussions/new) instead. | ||||
|  | ||||
| - type: textarea | ||||
|   attributes: | ||||
|     label: Details | ||||
|     description: Clear and thorough explanation of the feature you have in mind. | ||||
|   validations: | ||||
|     required: true | ||||
							
								
								
									
										25
									
								
								.github/workflows/CD.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								.github/workflows/CD.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,25 +0,0 @@ | ||||
| name: CD | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     tags: | ||||
|       - "*" | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v2.3.3 | ||||
|  | ||||
|       - name: Install .NET | ||||
|         uses: actions/setup-dotnet@v1.7.2 | ||||
|         with: | ||||
|           dotnet-version: 5.0.100 | ||||
|  | ||||
|       - name: Pack | ||||
|         run: dotnet pack CliFx --configuration Release | ||||
|  | ||||
|       - name: Deploy | ||||
|         run: dotnet nuget push CliFx/bin/Release/*.nupkg -s nuget.org -k ${{ secrets.NUGET_TOKEN }} | ||||
							
								
								
									
										28
									
								
								.github/workflows/CI.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/CI.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,28 +0,0 @@ | ||||
| name: CI | ||||
|  | ||||
| on: [push, pull_request] | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: ${{ matrix.os }} | ||||
|  | ||||
|     strategy: | ||||
|       matrix: | ||||
|         os: [ubuntu-latest, windows-latest, macos-latest] | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v2.3.3 | ||||
|  | ||||
|       - name: Install .NET | ||||
|         uses: actions/setup-dotnet@v1.7.2 | ||||
|         with: | ||||
|           dotnet-version: 5.0.100 | ||||
|  | ||||
|       - name: Build & test | ||||
|         run: dotnet test --configuration Release --logger GitHubActions | ||||
|  | ||||
|       - name: Upload coverage | ||||
|         uses: codecov/codecov-action@v1.0.5 | ||||
|         with: | ||||
|           token: ${{ secrets.CODECOV_TOKEN }} | ||||
							
								
								
									
										11
									
								
								.github/workflows/main.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/main.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| name: main | ||||
|  | ||||
| on: [push, pull_request] | ||||
|  | ||||
| jobs: | ||||
|   main: | ||||
|     uses: Tyrrrz/.github/.github/workflows/NuGet.yml@master | ||||
|     secrets: | ||||
|       CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | ||||
|       NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} | ||||
|       DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 14 KiB | 
							
								
								
									
										81
									
								
								Changelog.md
									
									
									
									
									
								
							
							
						
						
									
										81
									
								
								Changelog.md
									
									
									
									
									
								
							| @@ -1,3 +1,82 @@ | ||||
| ### v2.2.1 (16-Jan-2022) | ||||
|  | ||||
| - Fixed an issue which caused help text to not show default values for optional parameters. (Thanks [@AliReZa Sabouri](https://github.com/alirezanet)) | ||||
|  | ||||
| ### v2.2 (11-Jan-2022) | ||||
|  | ||||
| - Added support for optional parameters. A parameter can be marked as optional by setting `IsRequired = false` on the attribute. Only one parameter is allowed to be optional and such parameter must be the last in order. (Thanks [@AliReZa Sabouri](https://github.com/alirezanet)) | ||||
| - Fixed an issue where parameters and options bound to properties implemented as default interface members were not working correctly. (Thanks [@AliReZa Sabouri](https://github.com/alirezanet)) | ||||
|  | ||||
| ### v2.1 (04-Jan-2022) | ||||
|  | ||||
| - Added `IConsole.Clear()` with corresponding implementations in `SystemConsole`, `FakeConsole`, and `FakeInMemoryConsole`. (Thanks [@Alex Rosenfeld](https://github.com/alexrosenfeld10)) | ||||
| - Added `IConsole.ReadKey()` with corresponding implementations in `SystemConsole`, `FakeConsole`, and `FakeInMemoryConsole`. (Thanks [@Alex Rosenfeld](https://github.com/alexrosenfeld10)) | ||||
| - Fixed an issue that caused parameters to appear out of order in the usage format section of the help text. (Thanks [@David Fallah](https://github.com/TAGC)) | ||||
|  | ||||
| ### v2.0.6 (17-Jul-2021) | ||||
|  | ||||
| - Fixed an issue where an exception thrown via reflection during parameter or option binding resulted in `Exception has been thrown by the target of an invocation` error instead of a more useful message. Such exceptions will now be unwrapped to provide better user experience. | ||||
|  | ||||
| ### v2.0.5 (09-Jul-2021) | ||||
|  | ||||
| - Fixed an issue where calling `IConsole.Output.Encoding.EncodingName` and some other members threw an exception. | ||||
| - Added readme file to the package. | ||||
|  | ||||
| ### v2.0.4 (24-Apr-2021) | ||||
|  | ||||
| - Fixed an issue where output and error streams in `SystemConsole` defaulted to UTF8 encoding with BOM when the application was running with UTF8 codepage. `ConsoleWriter` will now discard preamble from the specified encoding. This fix brings the behavior of `SystemConsole` in line with .NET's own `System.Console` which also discards preamble for output and error streams. | ||||
| - Fixed an issue where help text tried to show default values for parameters and options whose type does not override `ToString()` method. | ||||
| - Fixed an issue where help text didn't show default values for parameters and options whose type is an enumerable of nullable enums. (Thanks [@Robert Dailey](https://github.com/rcdailey)) | ||||
| - Fixed an issue where specific parts of the help text weren't legible in some terminals due to low color resolution. Removed the usage of `ConsoleColor.DarkGray` in help text. | ||||
|  | ||||
| ### v2.0.3 (09-Apr-2021) | ||||
|  | ||||
| - Improved help text by showing valid values for non-scalar enum parameters and options. (Thanks [@Robert Dailey](https://github.com/rcdailey)) | ||||
|  | ||||
| ### v2.0.2 (31-Mar-2021) | ||||
|  | ||||
| - Fixed an issue where having a transitive reference to CliFx sometimes resulted in `SystemConsoleShouldBeAvoidedAnalyzer` throwing `NullReferenceException` during build. | ||||
| - Fixed some documentation typos and inconsistencies. | ||||
|  | ||||
| ### v2.0.1 (24-Mar-2021) | ||||
|  | ||||
| - Fixed an issue where some exceptions with async stack traces generated on .NET 3.1 or earlier were not parsed and formatted correctly. | ||||
| - Fixed an issue where help text applied slightly incorrect formatting when displaying choices for enum-based parameters and properties. | ||||
|  | ||||
| ### v2.0 (21-Mar-2021) | ||||
|  | ||||
| > Note: this major release includes many breaking changes. | ||||
| Please refer to the readme to find updated instructions and usage examples. | ||||
|  | ||||
| - Renamed property `EnvironmentVariableName` to `EnvironmentVariable` on `CommandOption` attribute. | ||||
| - Removed most of schema validation checks that used to take place during application startup. Going forward, CliFx will be relying solely on its built-in set of Roslyn analyzers to catch common errors in command configuration. | ||||
| - Removed `ProgressTicker` utility. The recommended migration path is to use the [Spectre.Console](https://github.com/spectresystems/spectre.console) library which provides a wide array of console widgets and components. See [this wiki page](https://github.com/Tyrrrz/CliFx/wiki/Integrating-with-Spectre.Console) to learn how to integrate Spectre.Console with CliFx. | ||||
| - Removed `MemoryStreamWriter` utility as it's no longer used within CliFx. | ||||
| - Improved wording in all error messages. | ||||
| - Renamed some methods on `CliApplicationBuilder`: | ||||
|   - `UseTitle()` renamed to `SetTitle()` | ||||
|   - `UseExecutableName()` renamed to `SetExecutableName()` | ||||
|   - `UseVersionText()` renamed to `SetVersion()` | ||||
|   - `UseDescription()` renamed to `SetDescription()` | ||||
| - Changed the behavior of autogenerated help text: | ||||
|   - Changed the color scheme to a more neutral set of tones | ||||
|   - Assigned separate colors to parameters and options to make them visually stand out | ||||
|   - Usage section no longer lists usage formats of all descendant commands | ||||
|   - Command section now also lists available subcommands for each of the current command's subcommands | ||||
| - Changed the behavior of `[preview]` directive. Running the application with this directive will now also print all resolved environment variables, in addition to parsed command line arguments. | ||||
| - Reworked `IArgumentValueConverter`/`ArgumentValueConverter` into `BindingConverter`. Method `ConvertFrom(...)` has been renamed to `Convert(...)`. | ||||
| - Reworked `ArgumentValueValidator` into `BindingValidator`. This class exposes an abstract `Validate(...)` method that returns a nullable `BindingValidationError`. This class also provides utility methods `Ok()` and `Error(...)` to help create corresponding validation results. | ||||
| - Changed the type of `IConsole.Output` and `IConsole.Error` from `StreamWriter` to `ConsoleWriter`. This type derives from `StreamWriter` and additionally exposes a `Console` property that refers to the console instance that owns the stream. This change enables you to author extension methods scoped specifically to console output and error streams. | ||||
| - Changed the type of `IConsole.Input` from `StreamReader` to `ConsoleReader`. This type derives from `StreamReader` and additionally exposes a `Console` property that refers to the console instance that owns the stream. This change enables you to author extension methods scoped specifically to the console input stream. | ||||
| - Changed methods `IConsole.WithForegroundColor(...)`, `IConsole.WithBackgroundColor(...)`, and `IConsole.WithColors(...)` to return `IDisposable`, replacing the delegate parameter they previously had. You can wrap the returned `IDisposable` in a using statement to ensure that the console colors get reset back to their original values once the execution reaches the end of the block. | ||||
| - Renamed `IConsole.GetCancellationToken()` to `IConsole.RegisterCancellationHandler()`. | ||||
| - Reworked `VirtualConsole` into `FakeConsole`. This class no longer takes `CancellationToken` as a constructor parameter, but instead encapsulates its own instance of `CancellationTokenSource` that can be triggered using the provided `RequestCancellation()` method. | ||||
| - Removed `VirtualConsole.CreateBuffered()` and replaced it with the `FakeInMemoryConsole` class. This class derives from `FakeConsole` and uses in-memory standard input, output, and error streams. It also provides methods to easily read the data written to the streams. | ||||
| - Moved some types to different namespaces: | ||||
|   - `IConsole`/`FakeConsole`/`FakeInMemoryConsole` moved from `CliFx` to `CliFx.Infrastructure` | ||||
|   - `ITypeActivator`/`DefaultTypeActivator`/`DelegateTypeActivator` moved from `CliFx` to `CliFx.Infrastructure` | ||||
|   - `BindingValidator`/`BindingConverter` moved from `CliFx` to `CliFx.Extensibility` | ||||
|  | ||||
| ### v1.6 (06-Dec-2020) | ||||
|  | ||||
| - Added support for custom value validators. You can now create a type that inherits from `CliFx.ArgumentValueValidator<T>` to implement reusable validation logic for command arguments. To use a validator, include it in the `Validators` property on the `CommandOption` or `CommandParameter` attribute. (Thanks [@Oleksandr Shustov](https://github.com/AlexandrShustov)) | ||||
| @@ -17,7 +96,7 @@ | ||||
|  | ||||
| ### v1.3.2 (31-Jul-2020) | ||||
|  | ||||
| - Fixed an issue where a command was incorrectly allowed to execute when the user did not specify any value for a non-scalar parameter. Since they are always required, a parameter needs to be bound to (at least) one value. (Thanks [@Daniel Hix](https://github.com/ADustyOldMuffin)) | ||||
| - Fixed an issue where a command was incorrectly allowed to execute when the user did not specify any value for a non-scalar parameter. Since they are always required, a parameter needs to be bound to at least one value. (Thanks [@Daniel Hix](https://github.com/ADustyOldMuffin)) | ||||
| - Fixed an issue where `CliApplication.RunAsync(...)` threw `ArgumentException` if there were two environment variables, whose names differed only in case. Environment variable names are now treated case-sensitively. (Thanks [@Ron Myers](https://github.com/ron-myers)) | ||||
|  | ||||
| ### v1.3.1 (19-Jul-2020) | ||||
|   | ||||
| @@ -1,43 +0,0 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace CliFx.Analyzers.Tests | ||||
| { | ||||
|     public class AnalyzerTestCase | ||||
|     { | ||||
|         public string Name { get; } | ||||
|  | ||||
|         public IReadOnlyList<DiagnosticDescriptor> TestedDiagnostics { get; } | ||||
|  | ||||
|         public IReadOnlyList<string> SourceCodes { get; } | ||||
|  | ||||
|         public AnalyzerTestCase( | ||||
|             string name, | ||||
|             IReadOnlyList<DiagnosticDescriptor> testedDiagnostics, | ||||
|             IReadOnlyList<string> sourceCodes) | ||||
|         { | ||||
|             Name = name; | ||||
|             TestedDiagnostics = testedDiagnostics; | ||||
|             SourceCodes = sourceCodes; | ||||
|         } | ||||
|  | ||||
|         public AnalyzerTestCase( | ||||
|             string name, | ||||
|             IReadOnlyList<DiagnosticDescriptor> testedDiagnostics, | ||||
|             string sourceCode) | ||||
|             : this(name, testedDiagnostics, new[] {sourceCode}) | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         public AnalyzerTestCase( | ||||
|             string name, | ||||
|             DiagnosticDescriptor testedDiagnostic, | ||||
|             string sourceCode) | ||||
|             : this(name, new[] {testedDiagnostic}, sourceCode) | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         public override string ToString() => $"{Name} [{string.Join(", ", TestedDiagnostics.Select(d => d.Id))}]"; | ||||
|     } | ||||
| } | ||||
| @@ -1,23 +1,24 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|   <Import Project="../CliFx.props" /> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>net5.0</TargetFramework> | ||||
|     <IsPackable>false</IsPackable> | ||||
|     <IsTestProject>true</IsTestProject> | ||||
|     <TargetFramework>net6.0</TargetFramework> | ||||
|     <CollectCoverage>true</CollectCoverage> | ||||
|     <CoverletOutputFormat>opencover</CoverletOutputFormat> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Gu.Roslyn.Asserts" Version="3.3.1" /> | ||||
|     <PackageReference Include="GitHubActionsTestLogger" Version="1.1.2" /> | ||||
|     <PackageReference Include="FluentAssertions" Version="5.10.3" /> | ||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> | ||||
|     <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.4.0" /> | ||||
|     <PackageReference Include="xunit" Version="2.4.0" /> | ||||
|     <Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Basic.Reference.Assemblies" Version="1.2.4" /> | ||||
|     <PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" /> | ||||
|     <PackageReference Include="FluentAssertions" Version="6.3.0" /> | ||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> | ||||
|     <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" /> | ||||
|     <PackageReference Include="xunit" Version="2.4.1" /> | ||||
|     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" /> | ||||
|     <PackageReference Include="coverlet.msbuild" Version="2.9.0" PrivateAssets="all" /> | ||||
|     <PackageReference Include="coverlet.msbuild" Version="3.1.0" PrivateAssets="all" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|   | ||||
							
								
								
									
										71
									
								
								CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         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 | ||||
|         // language=cs | ||||
|         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 | ||||
|         // language=cs | ||||
|         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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| public class Foo | ||||
| { | ||||
|     public int Bar { get; set; } = 5; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,57 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         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 | ||||
|         // language=cs | ||||
|         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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| public class Foo | ||||
| { | ||||
|     public int Bar { get; set; } = 5; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
| @@ -1,719 +0,0 @@ | ||||
| using System.Collections.Generic; | ||||
| using CliFx.Analyzers.Tests.Internal; | ||||
| using Microsoft.CodeAnalysis.Diagnostics; | ||||
| using Xunit; | ||||
|  | ||||
| namespace CliFx.Analyzers.Tests | ||||
| { | ||||
|     public class CommandSchemaAnalyzerTests | ||||
|     { | ||||
|         private static DiagnosticAnalyzer Analyzer { get; } = new CommandSchemaAnalyzer(); | ||||
|  | ||||
|         public static IEnumerable<object[]> GetValidCases() | ||||
|         { | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Non-command type", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| public class Foo | ||||
| { | ||||
|     public int Bar { get; set; } = 5; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Command implements interface and has attribute", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Command doesn't have an attribute but is an abstract type", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| public abstract class MyCommand : ICommand | ||||
| { | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Parameters with unique order", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(13)] | ||||
|     public string ParamA { get; set; } | ||||
|      | ||||
|     [CommandParameter(15)] | ||||
|     public string ParamB { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Parameters with unique names", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(13, Name = ""foo"")] | ||||
|     public string ParamA { get; set; } | ||||
|      | ||||
|     [CommandParameter(15, Name = ""bar"")] | ||||
|     public string ParamB { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Single non-scalar parameter", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(1)] | ||||
|     public string ParamA { get; set; } | ||||
|      | ||||
|     [CommandParameter(2)] | ||||
|     public HashSet<string> ParamB { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Non-scalar parameter is last in order", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(1)] | ||||
|     public string ParamA { get; set; } | ||||
|      | ||||
|     [CommandParameter(2)] | ||||
|     public IReadOnlyList<string> ParamB { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Parameter with valid converter", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| public class MyConverter : ArgumentValueConverter<string> | ||||
| { | ||||
|     public string ConvertFrom(string value) => value; | ||||
| } | ||||
|  | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0, Converter = typeof(MyConverter))] | ||||
|     public string Param { get; set; } | ||||
|      | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Parameter with valid validator", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| public class MyValidator : ArgumentValueValidator<string> | ||||
| { | ||||
|     public ValidationResult Validate(string value) => ValidationResult.Ok(); | ||||
| } | ||||
|  | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0, Validators = new[] {typeof(MyValidator)})] | ||||
|     public string Param { get; set; } | ||||
|      | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Option with a proper name", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""foo"")] | ||||
|     public string Option { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Option with a proper name and short name", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""foo"", 'f')] | ||||
|     public string Option { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Options with unique names", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""foo"")] | ||||
|     public string OptionA { get; set; } | ||||
|      | ||||
|     [CommandOption(""bar"")] | ||||
|     public string OptionB { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Options with unique short names", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('f')] | ||||
|     public string OptionA { get; set; } | ||||
|      | ||||
|     [CommandOption('x')] | ||||
|     public string OptionB { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Options with unique environment variable names", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('a', EnvironmentVariableName = ""env_var_a"")] | ||||
|     public string OptionA { get; set; } | ||||
|      | ||||
|     [CommandOption('b', EnvironmentVariableName = ""env_var_b"")] | ||||
|     public string OptionB { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Option with valid converter", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| public class MyConverter : ArgumentValueConverter<string> | ||||
| { | ||||
|     public string ConvertFrom(string value) => value; | ||||
| } | ||||
|  | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('o', Converter = typeof(MyConverter))] | ||||
|     public string Option { get; set; } | ||||
|      | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Option with valid validator", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| public class MyValidator : ArgumentValueValidator<string> | ||||
| { | ||||
|     public ValidationResult Validate(string value) => ValidationResult.Ok(); | ||||
| } | ||||
|  | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('o', Validators = new[] {typeof(MyValidator)})] | ||||
|     public string Option { get; set; } | ||||
|      | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         public static IEnumerable<object[]> GetInvalidCases() | ||||
|         { | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Command is missing the attribute", | ||||
|                     DiagnosticDescriptors.CliFx0002, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Command doesn't implement the interface", | ||||
|                     DiagnosticDescriptors.CliFx0001, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand | ||||
| { | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Parameters with duplicate order", | ||||
|                     DiagnosticDescriptors.CliFx0021, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(13)] | ||||
|     public string ParamA { get; set; } | ||||
|      | ||||
|     [CommandParameter(13)] | ||||
|     public string ParamB { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Parameters with duplicate names", | ||||
|                     DiagnosticDescriptors.CliFx0022, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(13, Name = ""foo"")] | ||||
|     public string ParamA { get; set; } | ||||
|      | ||||
|     [CommandParameter(15, Name = ""foo"")] | ||||
|     public string ParamB { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Multiple non-scalar parameters", | ||||
|                     DiagnosticDescriptors.CliFx0023, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(1)] | ||||
|     public IReadOnlyList<string> ParamA { get; set; } | ||||
|      | ||||
|     [CommandParameter(2)] | ||||
|     public HashSet<string> ParamB { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Non-last non-scalar parameter", | ||||
|                     DiagnosticDescriptors.CliFx0024, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(1)] | ||||
|     public IReadOnlyList<string> ParamA { get; set; } | ||||
|      | ||||
|     [CommandParameter(2)] | ||||
|     public string ParamB { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Parameter with invalid converter", | ||||
|                     DiagnosticDescriptors.CliFx0025, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| public class MyConverter | ||||
| { | ||||
|     public object ConvertFrom(string value) => value; | ||||
| } | ||||
|  | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0, Converter = typeof(MyConverter))] | ||||
|     public string Param { get; set; } | ||||
|      | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Parameter with invalid validator", | ||||
|                     DiagnosticDescriptors.CliFx0026, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| public class MyValidator | ||||
| { | ||||
|     public ValidationResult Validate(string value) => ValidationResult.Ok(); | ||||
| } | ||||
|  | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0, Validators = new[] {typeof(MyValidator)})] | ||||
|     public string Param { get; set; } | ||||
|      | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Option with an empty name", | ||||
|                     DiagnosticDescriptors.CliFx0041, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption("""")] | ||||
|     public string Option { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Option with a name which is too short", | ||||
|                     DiagnosticDescriptors.CliFx0042, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""a"")] | ||||
|     public string Option { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Options with duplicate names", | ||||
|                     DiagnosticDescriptors.CliFx0043, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""foo"")] | ||||
|     public string OptionA { get; set; } | ||||
|      | ||||
|     [CommandOption(""foo"")] | ||||
|     public string OptionB { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Options with duplicate short names", | ||||
|                     DiagnosticDescriptors.CliFx0044, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('f')] | ||||
|     public string OptionA { get; set; } | ||||
|      | ||||
|     [CommandOption('f')] | ||||
|     public string OptionB { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Options with duplicate environment variable names", | ||||
|                     DiagnosticDescriptors.CliFx0045, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('a', EnvironmentVariableName = ""env_var"")] | ||||
|     public string OptionA { get; set; } | ||||
|      | ||||
|     [CommandOption('b', EnvironmentVariableName = ""env_var"")] | ||||
|     public string OptionB { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Option with invalid converter", | ||||
|                     DiagnosticDescriptors.CliFx0046, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| public class MyConverter | ||||
| { | ||||
|     public object ConvertFrom(string value) => value; | ||||
| } | ||||
|  | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('o', Converter = typeof(MyConverter))] | ||||
|     public string Option { get; set; } | ||||
|      | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Option with invalid validator", | ||||
|                     DiagnosticDescriptors.CliFx0047, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| public class MyValidator | ||||
| { | ||||
|     public ValidationResult Validate(string value) => ValidationResult.Ok(); | ||||
| } | ||||
|  | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('o', Validators = new[] {typeof(MyValidator)})] | ||||
|     public string Option { get; set; } | ||||
|      | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Option with a name that doesn't start with a letter character", | ||||
|                     DiagnosticDescriptors.CliFx0048, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""0foo"")] | ||||
|     public string Option { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Option with a short name that isn't a letter character", | ||||
|                     DiagnosticDescriptors.CliFx0049, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('0')] | ||||
|     public string Option { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         [Theory] | ||||
|         [MemberData(nameof(GetValidCases))] | ||||
|         public void Valid(AnalyzerTestCase testCase) => | ||||
|             Analyzer.Should().NotProduceDiagnostics(testCase); | ||||
|  | ||||
|         [Theory] | ||||
|         [MemberData(nameof(GetInvalidCases))] | ||||
|         public void Invalid(AnalyzerTestCase testCase) => | ||||
|             Analyzer.Should().ProduceDiagnostics(testCase); | ||||
|     } | ||||
| } | ||||
| @@ -1,144 +0,0 @@ | ||||
| using System.Collections.Generic; | ||||
| using CliFx.Analyzers.Tests.Internal; | ||||
| using Microsoft.CodeAnalysis.Diagnostics; | ||||
| using Xunit; | ||||
|  | ||||
| namespace CliFx.Analyzers.Tests | ||||
| { | ||||
|     public class ConsoleUsageAnalyzerTests | ||||
|     { | ||||
|         private static DiagnosticAnalyzer Analyzer { get; } = new ConsoleUsageAnalyzer(); | ||||
|  | ||||
|         public static IEnumerable<object[]> GetValidCases() | ||||
|         { | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Using console abstraction", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|         console.Output.WriteLine(""Hello world""); | ||||
|         return default; | ||||
|     } | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Console abstraction is not available in scope", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public void SomeOtherMethod() => Console.WriteLine(""Test""); | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         public static IEnumerable<object[]> GetInvalidCases() | ||||
|         { | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Not using available console abstraction in the ExecuteAsync method", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|         Console.WriteLine(""Hello world""); | ||||
|         return default; | ||||
|     } | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Not using available console abstraction in the ExecuteAsync method when writing stderr", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|         Console.Error.WriteLine(""Hello world""); | ||||
|         return default; | ||||
|     } | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Not using available console abstraction while referencing System.Console by full name", | ||||
|                     Analyzer.SupportedDiagnostics, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|         System.Console.Error.WriteLine(""Hello world""); | ||||
|         return default; | ||||
|     } | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             yield return new object[] | ||||
|             { | ||||
|                 new AnalyzerTestCase( | ||||
|                     "Not using available console abstraction in another method", | ||||
|                     DiagnosticDescriptors.CliFx0100, | ||||
|  | ||||
|                     // language=cs | ||||
|                     @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public void SomeOtherMethod(IConsole console) => Console.WriteLine(""Test""); | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }" | ||||
|                 ) | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         [Theory] | ||||
|         [MemberData(nameof(GetValidCases))] | ||||
|         public void Valid(AnalyzerTestCase testCase) => | ||||
|             Analyzer.Should().NotProduceDiagnostics(testCase); | ||||
|  | ||||
|         [Theory] | ||||
|         [MemberData(nameof(GetInvalidCases))] | ||||
|         public void Invalid(AnalyzerTestCase testCase) => | ||||
|             Analyzer.Should().ProduceDiagnostics(testCase); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										30
									
								
								CliFx.Analyzers.Tests/GeneralSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								CliFx.Analyzers.Tests/GeneralSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| 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(); | ||||
|     } | ||||
| } | ||||
| @@ -1,107 +0,0 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using FluentAssertions.Execution; | ||||
| using FluentAssertions.Primitives; | ||||
| using Gu.Roslyn.Asserts; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp; | ||||
| using Microsoft.CodeAnalysis.Diagnostics; | ||||
|  | ||||
| namespace CliFx.Analyzers.Tests.Internal | ||||
| { | ||||
|     internal partial class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer, AnalyzerAssertions> | ||||
|     { | ||||
|         protected override string Identifier { get; } = "analyzer"; | ||||
|  | ||||
|         public AnalyzerAssertions(DiagnosticAnalyzer analyzer) | ||||
|             : base(analyzer) | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         public void ProduceDiagnostics( | ||||
|             IReadOnlyList<DiagnosticDescriptor> diagnostics, | ||||
|             IReadOnlyList<string> sourceCodes) | ||||
|         { | ||||
|             var producedDiagnostics = GetProducedDiagnostics(Subject, sourceCodes); | ||||
|  | ||||
|             var expectedIds = diagnostics.Select(d => d.Id).Distinct().OrderBy(d => d).ToArray(); | ||||
|             var producedIds = producedDiagnostics.Select(d => d.Id).Distinct().OrderBy(d => d).ToArray(); | ||||
|  | ||||
|             var result = expectedIds.Intersect(producedIds).Count() == expectedIds.Length; | ||||
|  | ||||
|             Execute.Assertion.ForCondition(result).FailWith($@" | ||||
| Expected and produced diagnostics do not match. | ||||
|  | ||||
| Expected: {string.Join(", ", expectedIds)} | ||||
| Produced: {(producedIds.Any() ? string.Join(", ", producedIds) : "<none>")} | ||||
| ".Trim()); | ||||
|         } | ||||
|  | ||||
|         public void ProduceDiagnostics(AnalyzerTestCase testCase) => | ||||
|             ProduceDiagnostics(testCase.TestedDiagnostics, testCase.SourceCodes); | ||||
|  | ||||
|         public void NotProduceDiagnostics( | ||||
|             IReadOnlyList<DiagnosticDescriptor> diagnostics, | ||||
|             IReadOnlyList<string> sourceCodes) | ||||
|         { | ||||
|             var producedDiagnostics = GetProducedDiagnostics(Subject, sourceCodes); | ||||
|  | ||||
|             var expectedIds = diagnostics.Select(d => d.Id).Distinct().OrderBy(d => d).ToArray(); | ||||
|             var producedIds = producedDiagnostics.Select(d => d.Id).Distinct().OrderBy(d => d).ToArray(); | ||||
|  | ||||
|             var result = !expectedIds.Intersect(producedIds).Any(); | ||||
|  | ||||
|             Execute.Assertion.ForCondition(result).FailWith($@" | ||||
| Expected no produced diagnostics. | ||||
|  | ||||
| Produced: {string.Join(", ", producedIds)} | ||||
| ".Trim()); | ||||
|         } | ||||
|  | ||||
|         public void NotProduceDiagnostics(AnalyzerTestCase testCase) => | ||||
|             NotProduceDiagnostics(testCase.TestedDiagnostics, testCase.SourceCodes); | ||||
|     } | ||||
|  | ||||
|     internal partial class AnalyzerAssertions | ||||
|     { | ||||
|         private static IReadOnlyList<MetadataReference> DefaultMetadataReferences { get; } = | ||||
|             MetadataReferences.Transitive(typeof(CliApplication).Assembly).ToArray(); | ||||
|  | ||||
|         private static string WrapCodeWithUsingDirectives(string code) | ||||
|         { | ||||
|             var usingDirectives = new[] | ||||
|             { | ||||
|                 "using System;", | ||||
|                 "using System.Collections.Generic;", | ||||
|                 "using System.Threading.Tasks;", | ||||
|                 "using CliFx;", | ||||
|                 "using CliFx.Attributes;", | ||||
|                 "using CliFx.Exceptions;", | ||||
|                 "using CliFx.Utilities;" | ||||
|             }; | ||||
|  | ||||
|             return | ||||
|                 string.Join(Environment.NewLine, usingDirectives) + | ||||
|                 Environment.NewLine + | ||||
|                 code; | ||||
|         } | ||||
|  | ||||
|         private static IReadOnlyList<Diagnostic> GetProducedDiagnostics( | ||||
|             DiagnosticAnalyzer analyzer, | ||||
|             IReadOnlyList<string> sourceCodes) | ||||
|         { | ||||
|             var compilationOptions = new CSharpCompilationOptions(OutputKind.ConsoleApplication); | ||||
|             var wrappedSourceCodes = sourceCodes.Select(WrapCodeWithUsingDirectives).ToArray(); | ||||
|  | ||||
|             return Analyze.GetDiagnostics(analyzer, wrappedSourceCodes, compilationOptions, DefaultMetadataReferences) | ||||
|                 .SelectMany(d => d) | ||||
|                 .ToArray(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     internal static class AnalyzerAssertionsExtensions | ||||
|     { | ||||
|         public static AnalyzerAssertions Should(this DiagnosticAnalyzer analyzer) => new AnalyzerAssertions(analyzer); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,79 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| public class MyClass | ||||
| { | ||||
|     [CommandOption(""foo"")] | ||||
|     public string Foo { get; set; } | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().ProduceDiagnostics(code); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public void Analyzer_does_not_report_an_error_if_an_option_is_inside_a_command() | ||||
|     { | ||||
|         // Arrange | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""foo"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| public abstract class MyCommand | ||||
| { | ||||
|     [CommandOption(""foo"")] | ||||
|     public string Foo { get; set; } | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() | ||||
|     { | ||||
|         // Arrange | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,85 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(null)] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""foo"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('f')] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,91 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""foo"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandOption(""foo"")] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""foo"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandOption(""bar"")] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('f')] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,113 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('f')] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandOption('f')] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('f')] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandOption('b')] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('f')] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandOption('F')] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""foo"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 OptionMustHaveValidConverterAnalyzerSpecs | ||||
| { | ||||
|     private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidConverterAnalyzer(); | ||||
|  | ||||
|     [Fact] | ||||
|     public void Analyzer_reports_an_error_if_the_specified_option_converter_does_not_derive_from_BindingConverter() | ||||
|     { | ||||
|         // Arrange | ||||
|         // language=cs | ||||
|         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; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().ProduceDiagnostics(code); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public void Analyzer_does_not_report_an_error_if_the_specified_option_converter_derives_from_BindingConverter() | ||||
|     { | ||||
|         // Arrange | ||||
|         // language=cs | ||||
|         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; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""foo"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										104
									
								
								CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""f"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""1foo"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""foo"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('f')] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,85 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('1')] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption('f')] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""foo"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 OptionMustHaveValidValidatorsAnalyzerSpecs | ||||
| { | ||||
|     private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidValidatorsAnalyzer(); | ||||
|  | ||||
|     [Fact] | ||||
|     public void Analyzer_reports_an_error_if_one_of_the_specified_option_validators_does_not_derive_from_BindingValidator() | ||||
|     { | ||||
|         // Arrange | ||||
|         // language=cs | ||||
|         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; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().ProduceDiagnostics(code); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public void Analyzer_does_not_report_an_error_if_all_specified_option_validators_derive_from_BindingValidator() | ||||
|     { | ||||
|         // Arrange | ||||
|         // language=cs | ||||
|         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; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandOption(""foo"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,79 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| public class MyClass | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string Foo { get; set; } | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().ProduceDiagnostics(code); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public void Analyzer_does_not_report_an_error_if_a_parameter_is_inside_a_command() | ||||
|     { | ||||
|         // Arrange | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| public abstract class MyCommand | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string Foo { get; set; } | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() | ||||
|     { | ||||
|         // Arrange | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,94 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0, Name = ""foo"", IsRequired = false)] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1, Name = ""bar"")] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0, Name = ""foo"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1, Name = ""bar"", IsRequired = false)] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0, Name = ""foo"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1, Name = ""bar"", IsRequired = true)] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,94 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string[] Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1)] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1)] | ||||
|     public string[] Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1)] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,94 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0, IsRequired = false)] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1, IsRequired = false)] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1, IsRequired = false)] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1, IsRequired = true)] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,94 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string[] Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1)] | ||||
|     public string[] Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1)] | ||||
|     public string[] Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1)] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,72 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0, Name = ""foo"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1, Name = ""foo"")] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0, Name = ""foo"")] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1, Name = ""bar"")] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,72 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(0)] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     [CommandParameter(1)] | ||||
|     public string Bar { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 ParameterMustHaveValidConverterAnalyzerSpecs | ||||
| { | ||||
|     private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveValidConverterAnalyzer(); | ||||
|  | ||||
|     [Fact] | ||||
|     public void Analyzer_reports_an_error_if_the_specified_parameter_converter_does_not_derive_from_BindingConverter() | ||||
|     { | ||||
|         // Arrange | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| public class MyConverter | ||||
| { | ||||
|     public string Convert(string rawValue) => rawValue; | ||||
| } | ||||
|  | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0, Converter = typeof(MyConverter))] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().ProduceDiagnostics(code); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public void Analyzer_does_not_report_an_error_if_the_specified_parameter_converter_derives_from_BindingConverter() | ||||
|     { | ||||
|         // Arrange | ||||
|         // language=cs | ||||
|         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 string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 ParameterMustHaveValidValidatorsAnalyzerSpecs | ||||
| { | ||||
|     private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveValidValidatorsAnalyzer(); | ||||
|  | ||||
|     [Fact] | ||||
|     public void Analyzer_reports_an_error_if_one_of_the_specified_parameter_validators_does_not_derive_from_BindingValidator() | ||||
|     { | ||||
|         // Arrange | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| public class MyValidator | ||||
| { | ||||
|     public void Validate(string value) {} | ||||
| } | ||||
|  | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0, Validators = new[] {typeof(MyValidator)})] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().ProduceDiagnostics(code); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public void Analyzer_does_not_report_an_error_if_all_specified_parameter_validators_derive_from_BindingValidator() | ||||
|     { | ||||
|         // Arrange | ||||
|         // language=cs | ||||
|         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 string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     [CommandParameter(0)] | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public string Foo { get; set; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,126 @@ | ||||
| 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 | ||||
|         // language=cs | ||||
|         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 | ||||
|         // language=cs | ||||
|         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 | ||||
|         // language=cs | ||||
|         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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|         console.Output.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 | ||||
|         // language=cs | ||||
|         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 | ||||
|         // language=cs | ||||
|         const string code = @" | ||||
| [Command] | ||||
| public class MyCommand : ICommand | ||||
| { | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|         return default; | ||||
|     } | ||||
| }"; | ||||
|  | ||||
|         // Act & assert | ||||
|         Analyzer.Should().NotProduceDiagnostics(code); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										167
									
								
								CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | ||||
| 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 : ReferenceTypeAssertions<DiagnosticAnalyzer, AnalyzerAssertions> | ||||
| { | ||||
|     protected override string Identifier { get; } = "analyzer"; | ||||
|  | ||||
|     public AnalyzerAssertions(DiagnosticAnalyzer analyzer) | ||||
|         : base(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 | ||||
|         ); | ||||
|  | ||||
|         // Compile the code to IL | ||||
|         var compilation = CSharpCompilation.Create( | ||||
|             "CliFxTests_DynamicAssembly_" + Guid.NewGuid(), | ||||
|             new[] {ast}, | ||||
|             ReferenceAssemblies.Net50 | ||||
|                 .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." + | ||||
|                 Environment.NewLine + | ||||
|                 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 result = | ||||
|             expectedDiagnosticIds.Intersect(producedDiagnosticIds).Count() == | ||||
|             expectedDiagnosticIds.Length; | ||||
|  | ||||
|         Execute.Assertion.ForCondition(result).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:"); | ||||
|  | ||||
|             foreach (var producedDiagnostic in producedDiagnostics) | ||||
|             { | ||||
|                 buffer.Append("  - "); | ||||
|                 buffer.Append(producedDiagnostic); | ||||
|             } | ||||
|  | ||||
|             return new FailReason(buffer.ToString()); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public void NotProduceDiagnostics(string sourceCode) | ||||
|     { | ||||
|         var producedDiagnostics = GetProducedDiagnostics(sourceCode); | ||||
|  | ||||
|         var result = !producedDiagnostics.Any(); | ||||
|  | ||||
|         Execute.Assertion.ForCondition(result).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); | ||||
| } | ||||
							
								
								
									
										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" | ||||
| } | ||||
							
								
								
									
										39
									
								
								CliFx.Analyzers/AnalyzerBase.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								CliFx.Analyzers/AnalyzerBase.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| 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) => | ||||
|         Diagnostic.Create(SupportedDiagnostic, location); | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         context.EnableConcurrentExecution(); | ||||
|         context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||||
|     } | ||||
| } | ||||
| @@ -1,13 +1,13 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|   <Import Project="../CliFx.props" /> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>netstandard2.0</TargetFramework> | ||||
|     <Nullable>annotations</Nullable> | ||||
|     <NoWarn>$(NoWarn);RS1025;RS1026</NoWarn> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.4.0" PrivateAssets="all" /> | ||||
|     <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
							
								
								
									
										53
									
								
								CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| 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 | ||||
| { | ||||
|     public CommandMustBeAnnotatedAnalyzer() | ||||
|         : base( | ||||
|             $"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(s => s.DisplayNameMatches(SymbolNames.CliFxCommandInterface)); | ||||
|  | ||||
|         var hasCommandAttribute = type | ||||
|             .GetAttributes() | ||||
|             .Select(a => a.AttributeClass) | ||||
|             .Any(s => s.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.GetLocation())); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandleClassDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										47
									
								
								CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| 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 | ||||
| { | ||||
|     public CommandMustImplementInterfaceAnalyzer() | ||||
|         : base( | ||||
|             $"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(s => s.DisplayNameMatches(SymbolNames.CliFxCommandAttribute)); | ||||
|  | ||||
|         var implementsCommandInterface = type | ||||
|             .AllInterfaces | ||||
|             .Any(s => s.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.GetLocation())); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandleClassDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
| @@ -1,423 +0,0 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | ||||
| using System.Linq; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.Diagnostics; | ||||
|  | ||||
| namespace CliFx.Analyzers | ||||
| { | ||||
|     // TODO: split into multiple analyzers | ||||
|     [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||||
|     public class CommandSchemaAnalyzer : DiagnosticAnalyzer | ||||
|     { | ||||
|         public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create( | ||||
|             DiagnosticDescriptors.CliFx0001, | ||||
|             DiagnosticDescriptors.CliFx0002, | ||||
|             DiagnosticDescriptors.CliFx0021, | ||||
|             DiagnosticDescriptors.CliFx0022, | ||||
|             DiagnosticDescriptors.CliFx0023, | ||||
|             DiagnosticDescriptors.CliFx0024, | ||||
|             DiagnosticDescriptors.CliFx0025, | ||||
|             DiagnosticDescriptors.CliFx0026, | ||||
|             DiagnosticDescriptors.CliFx0041, | ||||
|             DiagnosticDescriptors.CliFx0042, | ||||
|             DiagnosticDescriptors.CliFx0043, | ||||
|             DiagnosticDescriptors.CliFx0044, | ||||
|             DiagnosticDescriptors.CliFx0045, | ||||
|             DiagnosticDescriptors.CliFx0046, | ||||
|             DiagnosticDescriptors.CliFx0047, | ||||
|             DiagnosticDescriptors.CliFx0048, | ||||
|             DiagnosticDescriptors.CliFx0049 | ||||
|         ); | ||||
|  | ||||
|         private static bool IsScalarType(ITypeSymbol typeSymbol) => | ||||
|             KnownSymbols.IsSystemString(typeSymbol) || | ||||
|             !typeSymbol.AllInterfaces.Select(i => i.ConstructedFrom) | ||||
|                 .Any(KnownSymbols.IsSystemCollectionsGenericIEnumerable); | ||||
|  | ||||
|         private static void CheckCommandParameterProperties( | ||||
|             SymbolAnalysisContext context, | ||||
|             IReadOnlyList<IPropertySymbol> properties) | ||||
|         { | ||||
|             var parameters = properties | ||||
|                 .Select(p => | ||||
|                 { | ||||
|                     var attribute = p | ||||
|                         .GetAttributes() | ||||
|                         .First(a => KnownSymbols.IsCommandParameterAttribute(a.AttributeClass)); | ||||
|  | ||||
|                     var order = attribute | ||||
|                         .ConstructorArguments | ||||
|                         .Select(a => a.Value) | ||||
|                         .FirstOrDefault() as int?; | ||||
|  | ||||
|                     var name = attribute | ||||
|                         .NamedArguments | ||||
|                         .Where(a => a.Key == "Name") | ||||
|                         .Select(a => a.Value.Value) | ||||
|                         .FirstOrDefault() as string; | ||||
|  | ||||
|                     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 | ||||
|                     { | ||||
|                         Property = p, | ||||
|                         Order = order, | ||||
|                         Name = name, | ||||
|                         Converter = converter, | ||||
|                         Validators = validators | ||||
|                     }; | ||||
|                 }) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             // Duplicate order | ||||
|             var duplicateOrderParameters = parameters | ||||
|                 .Where(p => p.Order != null) | ||||
|                 .GroupBy(p => p.Order) | ||||
|                 .Where(g => g.Count() > 1) | ||||
|                 .SelectMany(g => g.AsEnumerable()) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             foreach (var parameter in duplicateOrderParameters) | ||||
|             { | ||||
|                 context.ReportDiagnostic(Diagnostic.Create( | ||||
|                     DiagnosticDescriptors.CliFx0021, parameter.Property.Locations.First() | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Duplicate name | ||||
|             var duplicateNameParameters = parameters | ||||
|                 .Where(p => !string.IsNullOrWhiteSpace(p.Name)) | ||||
|                 .GroupBy(p => p.Name, StringComparer.OrdinalIgnoreCase) | ||||
|                 .Where(g => g.Count() > 1) | ||||
|                 .SelectMany(g => g.AsEnumerable()) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             foreach (var parameter in duplicateNameParameters) | ||||
|             { | ||||
|                 context.ReportDiagnostic(Diagnostic.Create( | ||||
|                     DiagnosticDescriptors.CliFx0022, parameter.Property.Locations.First() | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Multiple non-scalar | ||||
|             var nonScalarParameters = parameters | ||||
|                 .Where(p => !IsScalarType(p.Property.Type)) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             if (nonScalarParameters.Length > 1) | ||||
|             { | ||||
|                 foreach (var parameter in nonScalarParameters) | ||||
|                 { | ||||
|                     context.ReportDiagnostic(Diagnostic.Create( | ||||
|                         DiagnosticDescriptors.CliFx0023, parameter.Property.Locations.First() | ||||
|                     )); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Non-last non-scalar | ||||
|             var nonLastNonScalarParameter = parameters | ||||
|                 .OrderByDescending(a => a.Order) | ||||
|                 .Skip(1) | ||||
|                 .LastOrDefault(p => !IsScalarType(p.Property.Type)); | ||||
|  | ||||
|             if (nonLastNonScalarParameter != null) | ||||
|             { | ||||
|                 context.ReportDiagnostic(Diagnostic.Create( | ||||
|                     DiagnosticDescriptors.CliFx0024, nonLastNonScalarParameter.Property.Locations.First() | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Invalid converter | ||||
|             var invalidConverterParameters = parameters | ||||
|                 .Where(p => | ||||
|                     p.Converter != null && | ||||
|                     !p.Converter.AllInterfaces.Any(KnownSymbols.IsArgumentValueConverterInterface)) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             foreach (var parameter in invalidConverterParameters) | ||||
|             { | ||||
|                 context.ReportDiagnostic(Diagnostic.Create( | ||||
|                     DiagnosticDescriptors.CliFx0025, parameter.Property.Locations.First() | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Invalid validators | ||||
|             var invalidValidatorsParameters = parameters | ||||
|                 .Where(p => !p.Validators.All(v => v.AllInterfaces.Any(KnownSymbols.IsArgumentValueValidatorInterface))) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             foreach (var parameter in invalidValidatorsParameters) | ||||
|             { | ||||
|                 context.ReportDiagnostic(Diagnostic.Create( | ||||
|                     DiagnosticDescriptors.CliFx0026, parameter.Property.Locations.First() | ||||
|                 )); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private static void CheckCommandOptionProperties( | ||||
|             SymbolAnalysisContext context, | ||||
|             IReadOnlyList<IPropertySymbol> properties) | ||||
|         { | ||||
|             var options = properties | ||||
|                 .Select(p => | ||||
|                 { | ||||
|                     var attribute = p | ||||
|                         .GetAttributes() | ||||
|                         .First(a => KnownSymbols.IsCommandOptionAttribute(a.AttributeClass)); | ||||
|  | ||||
|                     var name = attribute | ||||
|                         .ConstructorArguments | ||||
|                         .Where(a => KnownSymbols.IsSystemString(a.Type)) | ||||
|                         .Select(a => a.Value) | ||||
|                         .FirstOrDefault() as string; | ||||
|  | ||||
|                     var shortName = attribute | ||||
|                         .ConstructorArguments | ||||
|                         .Where(a => KnownSymbols.IsSystemChar(a.Type)) | ||||
|                         .Select(a => a.Value) | ||||
|                         .FirstOrDefault() as char?; | ||||
|  | ||||
|                     var envVarName = attribute | ||||
|                         .NamedArguments | ||||
|                         .Where(a => a.Key == "EnvironmentVariableName") | ||||
|                         .Select(a => a.Value.Value) | ||||
|                         .FirstOrDefault() as string; | ||||
|  | ||||
|                     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 | ||||
|                     { | ||||
|                         Property = p, | ||||
|                         Name = name, | ||||
|                         ShortName = shortName, | ||||
|                         EnvironmentVariableName = envVarName, | ||||
|                         Converter = converter, | ||||
|                         Validators = validators | ||||
|                     }; | ||||
|                 }) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             // No name | ||||
|             var noNameOptions = options | ||||
|                 .Where(o => string.IsNullOrWhiteSpace(o.Name) && o.ShortName == null) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             foreach (var option in noNameOptions) | ||||
|             { | ||||
|                 context.ReportDiagnostic(Diagnostic.Create( | ||||
|                     DiagnosticDescriptors.CliFx0041, option.Property.Locations.First() | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Too short name | ||||
|             var invalidNameLengthOptions = options | ||||
|                 .Where(o => !string.IsNullOrWhiteSpace(o.Name) && o.Name.Length <= 1) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             foreach (var option in invalidNameLengthOptions) | ||||
|             { | ||||
|                 context.ReportDiagnostic(Diagnostic.Create( | ||||
|                     DiagnosticDescriptors.CliFx0042, option.Property.Locations.First() | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Duplicate name | ||||
|             var duplicateNameOptions = options | ||||
|                 .Where(p => !string.IsNullOrWhiteSpace(p.Name)) | ||||
|                 .GroupBy(p => p.Name, StringComparer.OrdinalIgnoreCase) | ||||
|                 .Where(g => g.Count() > 1) | ||||
|                 .SelectMany(g => g.AsEnumerable()) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             foreach (var option in duplicateNameOptions) | ||||
|             { | ||||
|                 context.ReportDiagnostic(Diagnostic.Create( | ||||
|                     DiagnosticDescriptors.CliFx0043, option.Property.Locations.First() | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Duplicate name | ||||
|             var duplicateShortNameOptions = options | ||||
|                 .Where(p => p.ShortName != null) | ||||
|                 .GroupBy(p => p.ShortName) | ||||
|                 .Where(g => g.Count() > 1) | ||||
|                 .SelectMany(g => g.AsEnumerable()) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             foreach (var option in duplicateShortNameOptions) | ||||
|             { | ||||
|                 context.ReportDiagnostic(Diagnostic.Create( | ||||
|                     DiagnosticDescriptors.CliFx0044, option.Property.Locations.First() | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Duplicate environment variable name | ||||
|             var duplicateEnvironmentVariableNameOptions = options | ||||
|                 .Where(p => !string.IsNullOrWhiteSpace(p.EnvironmentVariableName)) | ||||
|                 .GroupBy(p => p.EnvironmentVariableName, StringComparer.Ordinal) | ||||
|                 .Where(g => g.Count() > 1) | ||||
|                 .SelectMany(g => g.AsEnumerable()) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             foreach (var option in duplicateEnvironmentVariableNameOptions) | ||||
|             { | ||||
|                 context.ReportDiagnostic(Diagnostic.Create( | ||||
|                     DiagnosticDescriptors.CliFx0045, option.Property.Locations.First() | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Invalid converter | ||||
|             var invalidConverterOptions = options | ||||
|                 .Where(o => | ||||
|                     o.Converter != null && | ||||
|                     !o.Converter.AllInterfaces.Any(KnownSymbols.IsArgumentValueConverterInterface)) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             foreach (var option in invalidConverterOptions) | ||||
|             { | ||||
|                 context.ReportDiagnostic(Diagnostic.Create( | ||||
|                     DiagnosticDescriptors.CliFx0046, option.Property.Locations.First() | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Invalid validators | ||||
|             var invalidValidatorsOptions = options | ||||
|                 .Where(o => !o.Validators.All(v => v.AllInterfaces.Any(KnownSymbols.IsArgumentValueValidatorInterface))) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             foreach (var option in invalidValidatorsOptions) | ||||
|             { | ||||
|                 context.ReportDiagnostic(Diagnostic.Create( | ||||
|                     DiagnosticDescriptors.CliFx0047, option.Property.Locations.First() | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Non-letter first character in name | ||||
|             var nonLetterFirstCharacterInNameOptions = options | ||||
|                 .Where(o => !string.IsNullOrWhiteSpace(o.Name) && !char.IsLetter(o.Name[0])) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             foreach (var option in nonLetterFirstCharacterInNameOptions) | ||||
|             { | ||||
|                 context.ReportDiagnostic(Diagnostic.Create( | ||||
|                     DiagnosticDescriptors.CliFx0048, option.Property.Locations.First() | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Non-letter short name | ||||
|             var nonLetterShortNameOptions = options | ||||
|                 .Where(o => o.ShortName != null && !char.IsLetter(o.ShortName.Value)) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             foreach (var option in nonLetterShortNameOptions) | ||||
|             { | ||||
|                 context.ReportDiagnostic(Diagnostic.Create( | ||||
|                     DiagnosticDescriptors.CliFx0049, option.Property.Locations.First() | ||||
|                 )); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private static void CheckCommandType(SymbolAnalysisContext context) | ||||
|         { | ||||
|             // Named type: MyCommand | ||||
|             if (!(context.Symbol is INamedTypeSymbol namedTypeSymbol) || | ||||
|                 namedTypeSymbol.TypeKind != TypeKind.Class) | ||||
|                 return; | ||||
|  | ||||
|             // Implements ICommand? | ||||
|             var implementsCommandInterface = namedTypeSymbol | ||||
|                 .AllInterfaces | ||||
|                 .Any(KnownSymbols.IsCommandInterface); | ||||
|  | ||||
|             // Has CommandAttribute? | ||||
|             var hasCommandAttribute = namedTypeSymbol | ||||
|                 .GetAttributes() | ||||
|                 .Select(a => a.AttributeClass) | ||||
|                 .Any(KnownSymbols.IsCommandAttribute); | ||||
|  | ||||
|             var isValidCommandType = | ||||
|                 // implements interface | ||||
|                 implementsCommandInterface && ( | ||||
|                     // and either abstract class or has attribute | ||||
|                     namedTypeSymbol.IsAbstract || hasCommandAttribute | ||||
|                 ); | ||||
|  | ||||
|             if (!isValidCommandType) | ||||
|             { | ||||
|                 // See if this was meant to be a command type (either interface or attribute present) | ||||
|                 var isAlmostValidCommandType = implementsCommandInterface ^ hasCommandAttribute; | ||||
|  | ||||
|                 if (isAlmostValidCommandType && !implementsCommandInterface) | ||||
|                     context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.CliFx0001, | ||||
|                         namedTypeSymbol.Locations.First())); | ||||
|  | ||||
|                 if (isAlmostValidCommandType && !hasCommandAttribute) | ||||
|                     context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.CliFx0002, | ||||
|                         namedTypeSymbol.Locations.First())); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             var properties = namedTypeSymbol | ||||
|                 .GetMembers() | ||||
|                 .Where(m => m.Kind == SymbolKind.Property) | ||||
|                 .OfType<IPropertySymbol>().ToArray(); | ||||
|  | ||||
|             // Check parameters | ||||
|             var parameterProperties = properties | ||||
|                 .Where(p => p | ||||
|                     .GetAttributes() | ||||
|                     .Select(a => a.AttributeClass) | ||||
|                     .Any(KnownSymbols.IsCommandParameterAttribute)) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             CheckCommandParameterProperties(context, parameterProperties); | ||||
|  | ||||
|             // Check options | ||||
|             var optionsProperties = properties | ||||
|                 .Where(p => p | ||||
|                     .GetAttributes() | ||||
|                     .Select(a => a.AttributeClass) | ||||
|                     .Any(KnownSymbols.IsCommandOptionAttribute)) | ||||
|                 .ToArray(); | ||||
|  | ||||
|             CheckCommandOptionProperties(context, optionsProperties); | ||||
|         } | ||||
|  | ||||
|         public override void Initialize(AnalysisContext context) | ||||
|         { | ||||
|             context.EnableConcurrentExecution(); | ||||
|             context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||||
|  | ||||
|             context.RegisterSymbolAction(CheckCommandType, SymbolKind.NamedType); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,74 +0,0 @@ | ||||
| using System.Collections.Immutable; | ||||
| using System.Linq; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
| using Microsoft.CodeAnalysis.Diagnostics; | ||||
|  | ||||
| namespace CliFx.Analyzers | ||||
| { | ||||
|     [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||||
|     public class ConsoleUsageAnalyzer : DiagnosticAnalyzer | ||||
|     { | ||||
|         public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create( | ||||
|             DiagnosticDescriptors.CliFx0100 | ||||
|         ); | ||||
|  | ||||
|         private static bool IsSystemConsoleInvocation( | ||||
|             SyntaxNodeAnalysisContext context, | ||||
|             InvocationExpressionSyntax invocationSyntax) | ||||
|         { | ||||
|             if (invocationSyntax.Expression is MemberAccessExpressionSyntax memberAccessSyntax && | ||||
|                 context.SemanticModel.GetSymbolInfo(memberAccessSyntax).Symbol is IMethodSymbol methodSymbol) | ||||
|             { | ||||
|                 // Direct call to System.Console (e.g. System.Console.WriteLine()) | ||||
|                 if (KnownSymbols.IsSystemConsole(methodSymbol.ContainingType)) | ||||
|                 { | ||||
|                     return true; | ||||
|                 } | ||||
|  | ||||
|                 // Indirect call to System.Console (e.g. System.Console.Error.WriteLine()) | ||||
|                 if (memberAccessSyntax.Expression is MemberAccessExpressionSyntax parentMemberAccessSyntax && | ||||
|                     context.SemanticModel.GetSymbolInfo(parentMemberAccessSyntax).Symbol is IPropertySymbol propertySymbol) | ||||
|                 { | ||||
|                     return KnownSymbols.IsSystemConsole(propertySymbol.ContainingType); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         private static void CheckSystemConsoleUsage(SyntaxNodeAnalysisContext context) | ||||
|         { | ||||
|             if (context.Node is InvocationExpressionSyntax invocationSyntax && | ||||
|                 IsSystemConsoleInvocation(context, invocationSyntax)) | ||||
|             { | ||||
|                 // Check if IConsole is available in scope as alternative to System.Console | ||||
|                 var isConsoleInterfaceAvailable = invocationSyntax | ||||
|                     .Ancestors() | ||||
|                     .OfType<MethodDeclarationSyntax>() | ||||
|                     .SelectMany(m => m.ParameterList.Parameters) | ||||
|                     .Select(p => p.Type) | ||||
|                     .Select(t => context.SemanticModel.GetSymbolInfo(t).Symbol) | ||||
|                     .Where(s => s != null) | ||||
|                     .Any(KnownSymbols.IsConsoleInterface!); | ||||
|  | ||||
|                 if (isConsoleInterfaceAvailable) | ||||
|                 { | ||||
|                     context.ReportDiagnostic(Diagnostic.Create( | ||||
|                         DiagnosticDescriptors.CliFx0100, | ||||
|                         invocationSyntax.GetLocation() | ||||
|                     )); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public override void Initialize(AnalysisContext context) | ||||
|         { | ||||
|             context.EnableConcurrentExecution(); | ||||
|             context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||||
|  | ||||
|             context.RegisterSyntaxNodeAction(CheckSystemConsoleUsage, SyntaxKind.InvocationExpression); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,133 +0,0 @@ | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace CliFx.Analyzers | ||||
| { | ||||
|     public static class DiagnosticDescriptors | ||||
|     { | ||||
|         public static readonly DiagnosticDescriptor CliFx0001 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0001), | ||||
|                 "Type must implement the 'CliFx.ICommand' interface in order to be a valid command", | ||||
|                 "Type must implement the 'CliFx.ICommand' interface in order to be a valid command", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0002 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0002), | ||||
|                 "Type must be annotated with the 'CliFx.Attributes.CommandAttribute' in order to be a valid command", | ||||
|                 "Type must be annotated with the 'CliFx.Attributes.CommandAttribute' in order to be a valid command", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0021 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0021), | ||||
|                 "Parameter order must be unique within its command", | ||||
|                 "Parameter order must be unique within its command", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0022 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0022), | ||||
|                 "Parameter order must have unique name within its command", | ||||
|                 "Parameter order must have unique name within its command", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0023 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0023), | ||||
|                 "Only one non-scalar parameter per command is allowed", | ||||
|                 "Only one non-scalar parameter per command is allowed", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0024 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0024), | ||||
|                 "Non-scalar parameter must be last in order", | ||||
|                 "Non-scalar parameter must be last in order", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0025 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0025), | ||||
|                 "Parameter converter must implement 'CliFx.IArgumentValueConverter'", | ||||
|                 "Parameter converter must implement 'CliFx.IArgumentValueConverter'", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0026 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0026), | ||||
|                 "Parameter validator must implement 'CliFx.ArgumentValueValidator<T>'", | ||||
|                 "Parameter validator must implement 'CliFx.ArgumentValueValidator<T>'", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0041 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0041), | ||||
|                 "Option must have a name or short name specified", | ||||
|                 "Option must have a name or short name specified", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0042 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0042), | ||||
|                 "Option name must be at least 2 characters long", | ||||
|                 "Option name must be at least 2 characters long", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0043 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0043), | ||||
|                 "Option name must be unique within its command", | ||||
|                 "Option name must be unique within its command", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0044 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0044), | ||||
|                 "Option short name must be unique within its command", | ||||
|                 "Option short name must be unique within its command", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0045 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0045), | ||||
|                 "Option environment variable name must be unique within its command", | ||||
|                 "Option environment variable name must be unique within its command", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0046 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0046), | ||||
|                 "Option converter must implement 'CliFx.IArgumentValueConverter'", | ||||
|                 "Option converter must implement 'CliFx.IArgumentValueConverter'", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0047 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0047), | ||||
|                 "Option validator must implement 'CliFx.ArgumentValueValidator<T>'", | ||||
|                 "Option validator must implement 'CliFx.ArgumentValueValidator<T>'", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0048 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0048), | ||||
|                 "Option name must begin with a letter character.", | ||||
|                 "Option name must begin with a letter character.", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0049 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0049), | ||||
|                 "Option short name must be a letter character.", | ||||
|                 "Option short name must be a letter character.", | ||||
|                 "Usage", DiagnosticSeverity.Error, true | ||||
|             ); | ||||
|  | ||||
|         public static readonly DiagnosticDescriptor CliFx0100 = | ||||
|             new DiagnosticDescriptor(nameof(CliFx0100), | ||||
|                 "Use the provided IConsole abstraction instead of System.Console to ensure that the command can be tested in isolation", | ||||
|                 "Use the provided IConsole abstraction instead of System.Console to ensure that the command can be tested in isolation", | ||||
|                 "Usage", DiagnosticSeverity.Warning, true | ||||
|             ); | ||||
|     } | ||||
| } | ||||
| @@ -1,11 +0,0 @@ | ||||
| using System; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace CliFx.Analyzers.Internal | ||||
| { | ||||
|     internal static class RoslynExtensions | ||||
|     { | ||||
|         public static bool DisplayNameMatches(this ISymbol symbol, string name) => | ||||
|             string.Equals(symbol.ToDisplayString(), name, StringComparison.Ordinal); | ||||
|     } | ||||
| } | ||||
| @@ -1,43 +0,0 @@ | ||||
| using CliFx.Analyzers.Internal; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace CliFx.Analyzers | ||||
| { | ||||
|     internal static class KnownSymbols | ||||
|     { | ||||
|         public static bool IsSystemString(ISymbol symbol) => | ||||
|             symbol.DisplayNameMatches("string") || | ||||
|             symbol.DisplayNameMatches("System.String"); | ||||
|  | ||||
|         public static bool IsSystemChar(ISymbol symbol) => | ||||
|             symbol.DisplayNameMatches("char") || | ||||
|             symbol.DisplayNameMatches("System.Char"); | ||||
|  | ||||
|         public static bool IsSystemCollectionsGenericIEnumerable(ISymbol symbol) => | ||||
|             symbol.DisplayNameMatches("System.Collections.Generic.IEnumerable<T>"); | ||||
|  | ||||
|         public static bool IsSystemConsole(ISymbol symbol) => | ||||
|             symbol.DisplayNameMatches("System.Console"); | ||||
|  | ||||
|         public static bool IsConsoleInterface(ISymbol symbol) => | ||||
|             symbol.DisplayNameMatches("CliFx.IConsole"); | ||||
|  | ||||
|         public static bool IsCommandInterface(ISymbol symbol) => | ||||
|             symbol.DisplayNameMatches("CliFx.ICommand"); | ||||
|  | ||||
|         public static bool IsArgumentValueConverterInterface(ISymbol symbol) => | ||||
|             symbol.DisplayNameMatches("CliFx.IArgumentValueConverter"); | ||||
|  | ||||
|         public static bool IsArgumentValueValidatorInterface(ISymbol symbol) => | ||||
|             symbol.DisplayNameMatches("CliFx.IArgumentValueValidator"); | ||||
|  | ||||
|         public static bool IsCommandAttribute(ISymbol symbol) => | ||||
|             symbol.DisplayNameMatches("CliFx.Attributes.CommandAttribute"); | ||||
|  | ||||
|         public static bool IsCommandParameterAttribute(ISymbol symbol) => | ||||
|             symbol.DisplayNameMatches("CliFx.Attributes.CommandParameterAttribute"); | ||||
|  | ||||
|         public static bool IsCommandOptionAttribute(ISymbol symbol) => | ||||
|             symbol.DisplayNameMatches("CliFx.Attributes.CommandOptionAttribute"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										81
									
								
								CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| using System.Collections.Generic; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using System.Linq; | ||||
| using CliFx.Analyzers.Utils.Extensions; | ||||
|  | ||||
| namespace CliFx.Analyzers.ObjectModel; | ||||
|  | ||||
| internal partial class CommandOptionSymbol | ||||
| { | ||||
|     public string? Name { get; } | ||||
|  | ||||
|     public char? ShortName { get; } | ||||
|  | ||||
|     public ITypeSymbol? ConverterType { get; } | ||||
|  | ||||
|     public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; } | ||||
|  | ||||
|     public CommandOptionSymbol( | ||||
|         string? name, | ||||
|         char? shortName, | ||||
|         ITypeSymbol? converterType, | ||||
|         IReadOnlyList<ITypeSymbol> validatorTypes) | ||||
|     { | ||||
|         Name = name; | ||||
|         ShortName = shortName; | ||||
|         ConverterType = converterType; | ||||
|         ValidatorTypes = validatorTypes; | ||||
|     } | ||||
| } | ||||
|  | ||||
| internal partial class CommandOptionSymbol | ||||
| { | ||||
|     private static AttributeData? TryGetOptionAttribute(IPropertySymbol property) => | ||||
|         property | ||||
|             .GetAttributes() | ||||
|             .FirstOrDefault(a => a.AttributeClass.DisplayNameMatches(SymbolNames.CliFxCommandOptionAttribute)); | ||||
|  | ||||
|     private static CommandOptionSymbol FromAttribute(AttributeData attribute) | ||||
|     { | ||||
|         var name = attribute | ||||
|             .ConstructorArguments | ||||
|             .Where(a => a.Type.DisplayNameMatches("string") || a.Type.DisplayNameMatches("System.String")) | ||||
|             .Select(a => a.Value) | ||||
|             .FirstOrDefault() as string; | ||||
|  | ||||
|         var shortName = attribute | ||||
|             .ConstructorArguments | ||||
|             .Where(a => a.Type.DisplayNameMatches("char") || a.Type.DisplayNameMatches("System.Char")) | ||||
|             .Select(a => a.Value) | ||||
|             .FirstOrDefault() as char?; | ||||
|  | ||||
|         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(name, shortName, converter, validators); | ||||
|     } | ||||
|  | ||||
|     public static CommandOptionSymbol? TryResolve(IPropertySymbol property) | ||||
|     { | ||||
|         var attribute = TryGetOptionAttribute(property); | ||||
|  | ||||
|         return attribute is not null | ||||
|             ? FromAttribute(attribute) | ||||
|             : null; | ||||
|     } | ||||
|  | ||||
|     public static bool IsOptionProperty(IPropertySymbol property) => | ||||
|         TryGetOptionAttribute(property) is not null; | ||||
| } | ||||
							
								
								
									
										90
									
								
								CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using CliFx.Analyzers.Utils.Extensions; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace CliFx.Analyzers.ObjectModel; | ||||
|  | ||||
| internal partial class CommandParameterSymbol | ||||
| { | ||||
|     public int Order { get; } | ||||
|  | ||||
|     public string? Name { get; } | ||||
|  | ||||
|     public bool? IsRequired { get; } | ||||
|  | ||||
|     public ITypeSymbol? ConverterType { get; } | ||||
|  | ||||
|     public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; } | ||||
|  | ||||
|     public CommandParameterSymbol( | ||||
|         int order, | ||||
|         string? name, | ||||
|         bool? isRequired, | ||||
|         ITypeSymbol? converterType, | ||||
|         IReadOnlyList<ITypeSymbol> validatorTypes) | ||||
|     { | ||||
|         Order = order; | ||||
|         Name = name; | ||||
|         IsRequired = isRequired; | ||||
|         ConverterType = converterType; | ||||
|         ValidatorTypes = validatorTypes; | ||||
|     } | ||||
| } | ||||
|  | ||||
| internal partial class CommandParameterSymbol | ||||
| { | ||||
|     private static AttributeData? TryGetParameterAttribute(IPropertySymbol property) => | ||||
|         property | ||||
|             .GetAttributes() | ||||
|             .FirstOrDefault(a => a.AttributeClass.DisplayNameMatches(SymbolNames.CliFxCommandParameterAttribute)); | ||||
|  | ||||
|     private static CommandParameterSymbol FromAttribute(AttributeData attribute) | ||||
|     { | ||||
|         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(order, name, isRequired, converter, validators); | ||||
|     } | ||||
|  | ||||
|     public static CommandParameterSymbol? TryResolve(IPropertySymbol property) | ||||
|     { | ||||
|         var attribute = TryGetParameterAttribute(property); | ||||
|  | ||||
|         return attribute is not null | ||||
|             ? FromAttribute(attribute) | ||||
|             : null; | ||||
|     } | ||||
|  | ||||
|     public static bool IsParameterProperty(IPropertySymbol property) => | ||||
|         TryGetParameterAttribute(property) is not null; | ||||
| } | ||||
							
								
								
									
										14
									
								
								CliFx.Analyzers/ObjectModel/SymbolNames.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								CliFx.Analyzers/ObjectModel/SymbolNames.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| 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 CliFxBindingConverterInterface = "CliFx.Extensibility.IBindingConverter"; | ||||
|     public const string CliFxBindingConverterClass = "CliFx.Extensibility.BindingConverter<T>"; | ||||
|     public const string CliFxBindingValidatorInterface = "CliFx.Extensibility.IBindingValidator"; | ||||
|     public const string CliFxBindingValidatorClass = "CliFx.Extensibility.BindingValidator<T>"; | ||||
| } | ||||
							
								
								
									
										50
									
								
								CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.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 OptionMustBeInsideCommandAnalyzer : AnalyzerBase | ||||
| { | ||||
|     public OptionMustBeInsideCommandAnalyzer() | ||||
|         : base( | ||||
|             "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(s => s.DisplayNameMatches(SymbolNames.CliFxCommandInterface)); | ||||
|  | ||||
|         if (!isInsideCommand) | ||||
|         { | ||||
|             context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.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 | ||||
| { | ||||
|     public OptionMustHaveNameOrShortNameAnalyzer() | ||||
|         : base( | ||||
|             "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.GetLocation())); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandlePropertyDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										64
									
								
								CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| 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 | ||||
| { | ||||
|     public OptionMustHaveUniqueNameAnalyzer() | ||||
|         : base( | ||||
|             "Options must have unique names", | ||||
|             "This option's name must be unique within the command (comparison IS NOT case sensitive).") | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     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, SymbolEqualityComparer.Default)) | ||||
|             .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.GetLocation())); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandlePropertyDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										63
									
								
								CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.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 OptionMustHaveUniqueShortNameAnalyzer : AnalyzerBase | ||||
| { | ||||
|     public OptionMustHaveUniqueShortNameAnalyzer() | ||||
|         : base( | ||||
|             "Options must have unique short names", | ||||
|             "This option's short name must be unique within the command (comparison IS case sensitive).") | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     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, SymbolEqualityComparer.Default)) | ||||
|             .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.GetLocation())); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandlePropertyDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										49
									
								
								CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.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 OptionMustHaveValidConverterAnalyzer : AnalyzerBase | ||||
| { | ||||
|     public OptionMustHaveValidConverterAnalyzer() | ||||
|         : base( | ||||
|             $"Option converters must derive from `{SymbolNames.CliFxBindingConverterClass}`", | ||||
|             $"Converter specified for this option must derive from `{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; | ||||
|  | ||||
|         // We check against an internal interface because checking against a generic class is a pain | ||||
|         var converterImplementsInterface = option | ||||
|             .ConverterType | ||||
|             .AllInterfaces | ||||
|             .Any(s => s.DisplayNameMatches(SymbolNames.CliFxBindingConverterInterface)); | ||||
|  | ||||
|         if (!converterImplementsInterface) | ||||
|         { | ||||
|             context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation())); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandlePropertyDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										42
									
								
								CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| 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 | ||||
| { | ||||
|     public OptionMustHaveValidNameAnalyzer() | ||||
|         : base( | ||||
|             "Options must have valid names", | ||||
|             "This option's name must be at least 2 characters long and must start with a letter.") | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     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.GetLocation())); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandlePropertyDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										42
									
								
								CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| 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 | ||||
| { | ||||
|     public OptionMustHaveValidShortNameAnalyzer() | ||||
|         : base( | ||||
|             "Option short names must be letter characters", | ||||
|             "This option's short name must be a single letter character.") | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     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.GetLocation())); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandlePropertyDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										51
									
								
								CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| 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 | ||||
| { | ||||
|     public OptionMustHaveValidValidatorsAnalyzer() | ||||
|         : base( | ||||
|             $"Option validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`", | ||||
|             $"All validators specified for this option must derive from `{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) | ||||
|         { | ||||
|             // We check against an internal interface because checking against a generic class is a pain | ||||
|             var validatorImplementsInterface = validatorType | ||||
|                 .AllInterfaces | ||||
|                 .Any(s => s.DisplayNameMatches(SymbolNames.CliFxBindingValidatorInterface)); | ||||
|  | ||||
|             if (!validatorImplementsInterface) | ||||
|             { | ||||
|                 context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										50
									
								
								CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.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 ParameterMustBeInsideCommandAnalyzer : AnalyzerBase | ||||
| { | ||||
|     public ParameterMustBeInsideCommandAnalyzer() | ||||
|         : base( | ||||
|             "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(s => s.DisplayNameMatches(SymbolNames.CliFxCommandInterface)); | ||||
|  | ||||
|         if (!isInsideCommand) | ||||
|         { | ||||
|             context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation())); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandlePropertyDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										60
									
								
								CliFx.Analyzers/ParameterMustBeLastIfNonRequiredAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								CliFx.Analyzers/ParameterMustBeLastIfNonRequiredAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| 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 | ||||
| { | ||||
|     public ParameterMustBeLastIfNonRequiredAnalyzer() | ||||
|         : base( | ||||
|             "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).") | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     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, SymbolEqualityComparer.Default)) | ||||
|             .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.GetLocation())); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandlePropertyDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										67
									
								
								CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| 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 | ||||
| { | ||||
|     public ParameterMustBeLastIfNonScalarAnalyzer() | ||||
|         : base( | ||||
|             "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).") | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     private static bool IsScalar(ITypeSymbol type) => | ||||
|         type.DisplayNameMatches("string") || | ||||
|         type.DisplayNameMatches("System.String") || | ||||
|         !type.AllInterfaces | ||||
|             .Select(i => i.ConstructedFrom) | ||||
|             .Any(s => s.DisplayNameMatches("System.Collections.Generic.IEnumerable<T>")); | ||||
|  | ||||
|     private void Analyze( | ||||
|         SyntaxNodeAnalysisContext context, | ||||
|         PropertyDeclarationSyntax propertyDeclaration, | ||||
|         IPropertySymbol property) | ||||
|     { | ||||
|         if (property.ContainingType is null) | ||||
|             return; | ||||
|  | ||||
|         if (IsScalar(property.Type)) | ||||
|             return; | ||||
|  | ||||
|         var parameter = CommandParameterSymbol.TryResolve(property); | ||||
|         if (parameter is null) | ||||
|             return; | ||||
|  | ||||
|         var otherProperties = property | ||||
|             .ContainingType | ||||
|             .GetMembers() | ||||
|             .OfType<IPropertySymbol>() | ||||
|             .Where(m => !m.Equals(property, SymbolEqualityComparer.Default)) | ||||
|             .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.GetLocation())); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandlePropertyDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,60 @@ | ||||
| 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 | ||||
| { | ||||
|     public ParameterMustBeSingleIfNonRequiredAnalyzer() | ||||
|         : base( | ||||
|             "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.") | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     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, SymbolEqualityComparer.Default)) | ||||
|             .ToArray(); | ||||
|  | ||||
|         foreach (var otherProperty in otherProperties) | ||||
|         { | ||||
|             var otherParameter = CommandParameterSymbol.TryResolve(otherProperty); | ||||
|             if (otherParameter is null) | ||||
|                 continue; | ||||
|  | ||||
|             if (otherParameter.IsRequired == false) | ||||
|             { | ||||
|                 context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation())); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandlePropertyDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										65
									
								
								CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.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 ParameterMustBeSingleIfNonScalarAnalyzer : AnalyzerBase | ||||
| { | ||||
|     public ParameterMustBeSingleIfNonScalarAnalyzer() | ||||
|         : base( | ||||
|             "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.") | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     private static bool IsScalar(ITypeSymbol type) => | ||||
|         type.DisplayNameMatches("string") || | ||||
|         type.DisplayNameMatches("System.String") || | ||||
|         !type.AllInterfaces | ||||
|             .Select(i => i.ConstructedFrom) | ||||
|             .Any(s => s.DisplayNameMatches("System.Collections.Generic.IEnumerable<T>")); | ||||
|  | ||||
|     private void Analyze( | ||||
|         SyntaxNodeAnalysisContext context, | ||||
|         PropertyDeclarationSyntax propertyDeclaration, | ||||
|         IPropertySymbol property) | ||||
|     { | ||||
|         if (property.ContainingType is null) | ||||
|             return; | ||||
|  | ||||
|         if (!CommandParameterSymbol.IsParameterProperty(property)) | ||||
|             return; | ||||
|  | ||||
|         if (IsScalar(property.Type)) | ||||
|             return; | ||||
|  | ||||
|         var otherProperties = property | ||||
|             .ContainingType | ||||
|             .GetMembers() | ||||
|             .OfType<IPropertySymbol>() | ||||
|             .Where(m => !m.Equals(property, SymbolEqualityComparer.Default)) | ||||
|             .ToArray(); | ||||
|  | ||||
|         foreach (var otherProperty in otherProperties) | ||||
|         { | ||||
|             if (!CommandParameterSymbol.IsParameterProperty(otherProperty)) | ||||
|                 continue; | ||||
|  | ||||
|             if (!IsScalar(otherProperty.Type)) | ||||
|             { | ||||
|                 context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation())); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandlePropertyDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										64
									
								
								CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| 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 | ||||
| { | ||||
|     public ParameterMustHaveUniqueNameAnalyzer() | ||||
|         : base( | ||||
|             "Parameters must have unique names", | ||||
|             "This parameter's name must be unique within the command (comparison IS NOT case sensitive).") | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     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, SymbolEqualityComparer.Default)) | ||||
|             .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.GetLocation())); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandlePropertyDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										57
									
								
								CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| 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 | ||||
| { | ||||
|     public ParameterMustHaveUniqueOrderAnalyzer() | ||||
|         : base( | ||||
|             "Parameters must have unique order", | ||||
|             "This parameter's order must be unique within the command.") | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     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, SymbolEqualityComparer.Default)) | ||||
|             .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.GetLocation())); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandlePropertyDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										49
									
								
								CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.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 ParameterMustHaveValidConverterAnalyzer : AnalyzerBase | ||||
| { | ||||
|     public ParameterMustHaveValidConverterAnalyzer() | ||||
|         : base( | ||||
|             $"Parameter converters must derive from `{SymbolNames.CliFxBindingConverterClass}`", | ||||
|             $"Converter specified for this parameter must derive from `{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; | ||||
|  | ||||
|         // We check against an internal interface because checking against a generic class is a pain | ||||
|         var converterImplementsInterface = parameter | ||||
|             .ConverterType | ||||
|             .AllInterfaces | ||||
|             .Any(s => s.DisplayNameMatches(SymbolNames.CliFxBindingConverterInterface)); | ||||
|  | ||||
|         if (!converterImplementsInterface) | ||||
|         { | ||||
|             context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation())); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         base.Initialize(context); | ||||
|         context.HandlePropertyDeclaration(Analyze); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										51
									
								
								CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| 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 | ||||
| { | ||||
|     public ParameterMustHaveValidValidatorsAnalyzer() | ||||
|         : base( | ||||
|             $"Parameter validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`", | ||||
|             $"All validators specified for this parameter must derive from `{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) | ||||
|         { | ||||
|             // We check against an internal interface because checking against a generic class is a pain | ||||
|             var validatorImplementsInterface = validatorType | ||||
|                 .AllInterfaces | ||||
|                 .Any(s => s.DisplayNameMatches(SymbolNames.CliFxBindingValidatorInterface)); | ||||
|  | ||||
|             if (!validatorImplementsInterface) | ||||
|             { | ||||
|                 context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										77
									
								
								CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| 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 | ||||
| { | ||||
|     public SystemConsoleShouldBeAvoidedAnalyzer() | ||||
|         : base( | ||||
|             $"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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										52
									
								
								CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| using System; | ||||
| 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 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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										17
									
								
								CliFx.Analyzers/Utils/Extensions/StringExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								CliFx.Analyzers/Utils/Extensions/StringExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| 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.Substring(0, str.Length - sub.Length); | ||||
|  | ||||
|         return str; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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>()); | ||||
| } | ||||
| @@ -1,6 +1,9 @@ | ||||
| using clipr; | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using clipr; | ||||
| 
 | ||||
| namespace CliFx.Benchmarks.Commands | ||||
| namespace CliFx.Benchmarks; | ||||
| 
 | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     public class CliprCommand | ||||
|     { | ||||
| @@ -17,4 +20,7 @@ namespace CliFx.Benchmarks.Commands | ||||
|         { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     [Benchmark(Description = "Clipr")] | ||||
|     public void ExecuteWithClipr() => CliParser.Parse<CliprCommand>(Arguments).Execute(); | ||||
| } | ||||
| @@ -1,6 +1,9 @@ | ||||
| using Cocona; | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using Cocona; | ||||
| 
 | ||||
| namespace CliFx.Benchmarks.Commands | ||||
| namespace CliFx.Benchmarks; | ||||
| 
 | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     public class CoconaCommand | ||||
|     { | ||||
| @@ -14,4 +17,7 @@ namespace CliFx.Benchmarks.Commands | ||||
|         { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     [Benchmark(Description = "Cocona")] | ||||
|     public void ExecuteWithCocona() => CoconaApp.Run<CoconaCommand>(Arguments); | ||||
| } | ||||
							
								
								
									
										29
									
								
								CliFx.Benchmarks/Benchmarks.CommandLineParser.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								CliFx.Benchmarks/Benchmarks.CommandLineParser.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| 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); | ||||
| } | ||||
| @@ -1,6 +1,9 @@ | ||||
| using PowerArgs; | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using PowerArgs; | ||||
| 
 | ||||
| namespace CliFx.Benchmarks.Commands | ||||
| namespace CliFx.Benchmarks; | ||||
| 
 | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     public class PowerArgsCommand | ||||
|     { | ||||
| @@ -17,4 +20,7 @@ namespace CliFx.Benchmarks.Commands | ||||
|         { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     [Benchmark(Description = "PowerArgs")] | ||||
|     public void ExecuteWithPowerArgs() => Args.InvokeMain<PowerArgsCommand>(Arguments); | ||||
| } | ||||
| @@ -1,8 +1,11 @@ | ||||
| using System.CommandLine; | ||||
| using System.CommandLine.Invocation; | ||||
| using System.Threading.Tasks; | ||||
| using BenchmarkDotNet.Attributes; | ||||
| 
 | ||||
| namespace CliFx.Benchmarks.Commands | ||||
| namespace CliFx.Benchmarks; | ||||
| 
 | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     public class SystemCommandLineCommand | ||||
|     { | ||||
| @@ -26,9 +29,15 @@ namespace CliFx.Benchmarks.Commands | ||||
|                 } | ||||
|             }; | ||||
| 
 | ||||
|             command.Handler = CommandHandler.Create(typeof(SystemCommandLineCommand).GetMethod(nameof(ExecuteHandler))); | ||||
|             command.Handler = CommandHandler.Create( | ||||
|                 typeof(SystemCommandLineCommand).GetMethod(nameof(ExecuteHandler))! | ||||
|             ); | ||||
| 
 | ||||
|             return command.InvokeAsync(args); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     [Benchmark(Description = "System.CommandLine")] | ||||
|     public async Task<int> ExecuteWithSystemCommandLine() => | ||||
|         await new SystemCommandLineCommand().ExecuteAsync(Arguments); | ||||
| } | ||||
| @@ -1,52 +1,19 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using BenchmarkDotNet.Configs; | ||||
| using BenchmarkDotNet.Order; | ||||
| using BenchmarkDotNet.Running; | ||||
| using CliFx.Benchmarks.Commands; | ||||
| using CommandLine; | ||||
|  | ||||
| namespace CliFx.Benchmarks | ||||
| { | ||||
|     [SimpleJob] | ||||
| namespace CliFx.Benchmarks; | ||||
|  | ||||
| [RankColumn] | ||||
| [Orderer(SummaryOrderPolicy.FastestToSlowest)] | ||||
|     public class Benchmarks | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"}; | ||||
|  | ||||
|         [Benchmark(Description = "CliFx", Baseline = true)] | ||||
|         public async ValueTask<int> ExecuteWithCliFx() => | ||||
|             await new CliApplicationBuilder().AddCommand<CliFxCommand>().Build().RunAsync(Arguments, new Dictionary<string, string>()); | ||||
|  | ||||
|         [Benchmark(Description = "System.CommandLine")] | ||||
|         public async Task<int> ExecuteWithSystemCommandLine() => | ||||
|             await new SystemCommandLineCommand().ExecuteAsync(Arguments); | ||||
|  | ||||
|         [Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] | ||||
|         public int ExecuteWithMcMaster() => | ||||
|             McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute<McMasterCommand>(Arguments); | ||||
|  | ||||
|         [Benchmark(Description = "CommandLineParser")] | ||||
|         public void ExecuteWithCommandLineParser() => | ||||
|             new Parser() | ||||
|                 .ParseArguments(Arguments, typeof(CommandLineParserCommand)) | ||||
|                 .WithParsed<CommandLineParserCommand>(c => c.Execute()); | ||||
|  | ||||
|         [Benchmark(Description = "PowerArgs")] | ||||
|         public void ExecuteWithPowerArgs() => | ||||
|             PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments); | ||||
|  | ||||
|         [Benchmark(Description = "Clipr")] | ||||
|         public void ExecuteWithClipr() => | ||||
|             clipr.CliParser.Parse<CliprCommand>(Arguments).Execute(); | ||||
|  | ||||
|         [Benchmark(Description = "Cocona")] | ||||
|         public void ExecuteWithCocona() => | ||||
|             Cocona.CoconaApp.Run<CoconaCommand>(Arguments); | ||||
|  | ||||
|         public static void Main() => | ||||
|             BenchmarkRunner.Run<Benchmarks>(DefaultConfig.Instance.With(ConfigOptions.DisableOptimizationsValidator)); | ||||
|     } | ||||
|     public static void Main() => BenchmarkRunner.Run<Benchmarks>( | ||||
|         DefaultConfig | ||||
|             .Instance | ||||
|             .WithOptions(ConfigOptions.DisableOptimizationsValidator) | ||||
|     ); | ||||
| } | ||||
| @@ -1,19 +1,18 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|   <Import Project="../CliFx.props" /> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <OutputType>Exe</OutputType> | ||||
|     <TargetFramework>net5.0</TargetFramework> | ||||
|     <TargetFramework>net6.0</TargetFramework> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="BenchmarkDotNet" Version="0.12.0" /> | ||||
|     <PackageReference Include="BenchmarkDotNet" Version="0.13.1" /> | ||||
|     <PackageReference Include="clipr" Version="1.6.1" /> | ||||
|     <PackageReference Include="Cocona" Version="1.5.0" /> | ||||
|     <PackageReference Include="Cocona" Version="1.6.0" /> | ||||
|     <PackageReference Include="CommandLineParser" Version="2.8.0" /> | ||||
|     <PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="3.0.0" /> | ||||
|     <PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.0.0" /> | ||||
|     <PackageReference Include="PowerArgs" Version="3.6.0" /> | ||||
|     <PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" /> | ||||
|     <PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20574.7" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|   | ||||
| @@ -1,20 +0,0 @@ | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
|  | ||||
| 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 ValueTask ExecuteAsync(IConsole console) => default; | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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 | | ||||
| @@ -1,14 +1,13 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|   <Import Project="../CliFx.props" /> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <OutputType>Exe</OutputType> | ||||
|     <TargetFramework>net5.0</TargetFramework> | ||||
|     <TargetFramework>net6.0</TargetFramework> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" /> | ||||
|     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> | ||||
|     <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|   | ||||
| @@ -1,45 +1,45 @@ | ||||
| using System; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Demo.Internal; | ||||
| using CliFx.Demo.Models; | ||||
| using CliFx.Demo.Services; | ||||
| using CliFx.Demo.Domain; | ||||
| using CliFx.Demo.Utils; | ||||
| using CliFx.Exceptions; | ||||
| using CliFx.Infrastructure; | ||||
|  | ||||
| namespace CliFx.Demo.Commands; | ||||
|  | ||||
| namespace CliFx.Demo.Commands | ||||
| { | ||||
| [Command("book add", Description = "Add a book to the library.")] | ||||
| public partial class BookAddCommand : ICommand | ||||
| { | ||||
|         private readonly LibraryService _libraryService; | ||||
|     private readonly LibraryProvider _libraryProvider; | ||||
|  | ||||
|     [CommandParameter(0, Name = "title", Description = "Book title.")] | ||||
|         public string Title { get; set; } = ""; | ||||
|     public string Title { get; init; } = ""; | ||||
|  | ||||
|     [CommandOption("author", 'a', IsRequired = true, Description = "Book author.")] | ||||
|         public string Author { get; set; } = ""; | ||||
|     public string Author { get; init; } = ""; | ||||
|  | ||||
|     [CommandOption("published", 'p', Description = "Book publish date.")] | ||||
|         public DateTimeOffset Published { get; set; } = CreateRandomDate(); | ||||
|     public DateTimeOffset Published { get; init; } = CreateRandomDate(); | ||||
|  | ||||
|     [CommandOption("isbn", 'n', Description = "Book ISBN.")] | ||||
|         public Isbn Isbn { get; set; } = CreateRandomIsbn(); | ||||
|     public Isbn Isbn { get; init; } = CreateRandomIsbn(); | ||||
|  | ||||
|         public BookAddCommand(LibraryService libraryService) | ||||
|     public BookAddCommand(LibraryProvider libraryProvider) | ||||
|     { | ||||
|             _libraryService = libraryService; | ||||
|         _libraryProvider = libraryProvider; | ||||
|     } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|             if (_libraryService.GetBook(Title) != null) | ||||
|                 throw new CommandException("Book already exists.", 1); | ||||
|         if (_libraryProvider.TryGetBook(Title) is not null) | ||||
|             throw new CommandException("Book already exists.", 10); | ||||
|  | ||||
|         var book = new Book(Title, Author, Published, Isbn); | ||||
|             _libraryService.AddBook(book); | ||||
|         _libraryProvider.AddBook(book); | ||||
|  | ||||
|         console.Output.WriteLine("Book added."); | ||||
|             console.RenderBook(book); | ||||
|         console.Output.WriteBook(book); | ||||
|  | ||||
|         return default; | ||||
|     } | ||||
| @@ -47,22 +47,23 @@ namespace CliFx.Demo.Commands | ||||
|  | ||||
| public partial class BookAddCommand | ||||
| { | ||||
|         private static readonly Random Random = new Random(); | ||||
|     private static readonly Random Random = new(); | ||||
|  | ||||
|         private static DateTimeOffset CreateRandomDate() => new DateTimeOffset( | ||||
|     private static DateTimeOffset CreateRandomDate() => new( | ||||
|         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); | ||||
|         TimeSpan.Zero | ||||
|     ); | ||||
|  | ||||
|         private static Isbn CreateRandomIsbn() => new Isbn( | ||||
|     private static Isbn CreateRandomIsbn() => new( | ||||
|         Random.Next(0, 999), | ||||
|         Random.Next(0, 99), | ||||
|         Random.Next(0, 99999), | ||||
|         Random.Next(0, 99), | ||||
|             Random.Next(0, 9)); | ||||
|     } | ||||
|         Random.Next(0, 9) | ||||
|     ); | ||||
| } | ||||
| @@ -1,34 +1,34 @@ | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Demo.Internal; | ||||
| using CliFx.Demo.Services; | ||||
| using CliFx.Demo.Domain; | ||||
| using CliFx.Demo.Utils; | ||||
| using CliFx.Exceptions; | ||||
| using CliFx.Infrastructure; | ||||
|  | ||||
| namespace CliFx.Demo.Commands | ||||
| { | ||||
|     [Command("book", Description = "View, list, add or remove books.")] | ||||
| namespace CliFx.Demo.Commands; | ||||
|  | ||||
| [Command("book", Description = "Retrieve a book from the library.")] | ||||
| public class BookCommand : ICommand | ||||
| { | ||||
|         private readonly LibraryService _libraryService; | ||||
|     private readonly LibraryProvider _libraryProvider; | ||||
|  | ||||
|         [CommandParameter(0, Name = "title", Description = "Book title.")] | ||||
|         public string Title { get; set; } = ""; | ||||
|     [CommandParameter(0, Name = "title", Description = "Title of the book to retrieve.")] | ||||
|     public string Title { get; init; } = ""; | ||||
|  | ||||
|         public BookCommand(LibraryService libraryService) | ||||
|     public BookCommand(LibraryProvider libraryProvider) | ||||
|     { | ||||
|             _libraryService = libraryService; | ||||
|         _libraryProvider = libraryProvider; | ||||
|     } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|             var book = _libraryService.GetBook(Title); | ||||
|         var book = _libraryProvider.TryGetBook(Title); | ||||
|  | ||||
|             if (book == null) | ||||
|                 throw new CommandException("Book not found.", 1); | ||||
|         if (book is null) | ||||
|             throw new CommandException("Book not found.", 10); | ||||
|  | ||||
|             console.RenderBook(book); | ||||
|         console.Output.WriteBook(book); | ||||
|  | ||||
|         return default; | ||||
|     } | ||||
| } | ||||
| } | ||||
| @@ -1,37 +1,36 @@ | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Demo.Internal; | ||||
| using CliFx.Demo.Services; | ||||
| using CliFx.Demo.Domain; | ||||
| using CliFx.Demo.Utils; | ||||
| using CliFx.Infrastructure; | ||||
|  | ||||
| namespace CliFx.Demo.Commands; | ||||
|  | ||||
| namespace CliFx.Demo.Commands | ||||
| { | ||||
| [Command("book list", Description = "List all books in the library.")] | ||||
| public class BookListCommand : ICommand | ||||
| { | ||||
|         private readonly LibraryService _libraryService; | ||||
|     private readonly LibraryProvider _libraryProvider; | ||||
|  | ||||
|         public BookListCommand(LibraryService libraryService) | ||||
|     public BookListCommand(LibraryProvider libraryProvider) | ||||
|     { | ||||
|             _libraryService = libraryService; | ||||
|         _libraryProvider = libraryProvider; | ||||
|     } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|             var library = _libraryService.GetLibrary(); | ||||
|         var library = _libraryProvider.GetLibrary(); | ||||
|  | ||||
|             var isFirst = true; | ||||
|             foreach (var book in library.Books) | ||||
|         for (var i = 0; i < library.Books.Count; i++) | ||||
|         { | ||||
|                 // Margin | ||||
|                 if (!isFirst) | ||||
|             // Add margin | ||||
|             if (i != 0) | ||||
|                 console.Output.WriteLine(); | ||||
|                 isFirst = false; | ||||
|  | ||||
|             // Render book | ||||
|                 console.RenderBook(book); | ||||
|             var book = library.Books[i]; | ||||
|             console.Output.WriteBook(book); | ||||
|         } | ||||
|  | ||||
|         return default; | ||||
|     } | ||||
| } | ||||
| } | ||||
| @@ -1,35 +1,35 @@ | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Demo.Services; | ||||
| using CliFx.Demo.Domain; | ||||
| using CliFx.Exceptions; | ||||
| using CliFx.Infrastructure; | ||||
|  | ||||
| namespace CliFx.Demo.Commands; | ||||
|  | ||||
| namespace CliFx.Demo.Commands | ||||
| { | ||||
| [Command("book remove", Description = "Remove a book from the library.")] | ||||
| public class BookRemoveCommand : ICommand | ||||
| { | ||||
|         private readonly LibraryService _libraryService; | ||||
|     private readonly LibraryProvider _libraryProvider; | ||||
|  | ||||
|         [CommandParameter(0, Name = "title", Description = "Book title.")] | ||||
|         public string Title { get; set; } = ""; | ||||
|     [CommandParameter(0, Name = "title", Description = "Title of the book to remove.")] | ||||
|     public string Title { get; init; } = ""; | ||||
|  | ||||
|         public BookRemoveCommand(LibraryService libraryService) | ||||
|     public BookRemoveCommand(LibraryProvider libraryProvider) | ||||
|     { | ||||
|             _libraryService = libraryService; | ||||
|         _libraryProvider = libraryProvider; | ||||
|     } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|             var book = _libraryService.GetBook(Title); | ||||
|         var book = _libraryProvider.TryGetBook(Title); | ||||
|  | ||||
|             if (book == null) | ||||
|                 throw new CommandException("Book not found.", 1); | ||||
|         if (book is null) | ||||
|             throw new CommandException("Book not found.", 10); | ||||
|  | ||||
|             _libraryService.RemoveBook(book); | ||||
|         _libraryProvider.RemoveBook(book); | ||||
|  | ||||
|         console.Output.WriteLine($"Book {Title} removed."); | ||||
|  | ||||
|         return default; | ||||
|     } | ||||
| } | ||||
| } | ||||
							
								
								
									
										22
									
								
								CliFx.Demo/Domain/Book.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								CliFx.Demo/Domain/Book.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| using System; | ||||
|  | ||||
| namespace CliFx.Demo.Domain; | ||||
|  | ||||
| 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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										44
									
								
								CliFx.Demo/Domain/Isbn.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								CliFx.Demo/Domain/Isbn.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| using System; | ||||
|  | ||||
| namespace CliFx.Demo.Domain; | ||||
|  | ||||
| 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, 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) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										35
									
								
								CliFx.Demo/Domain/Library.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								CliFx.Demo/Domain/Library.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
|  | ||||
| namespace CliFx.Demo.Domain; | ||||
|  | ||||
| public partial class Library | ||||
| { | ||||
|     public IReadOnlyList<Book> Books { get; } | ||||
|  | ||||
|     public Library(IReadOnlyList<Book> books) | ||||
|     { | ||||
|         Books = 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 class Library | ||||
| { | ||||
|     public static Library Empty { get; } = new(Array.Empty<Book>()); | ||||
| } | ||||
							
								
								
									
										40
									
								
								CliFx.Demo/Domain/LibraryProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								CliFx.Demo/Domain/LibraryProvider.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using Newtonsoft.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 = 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) ?? 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,29 +0,0 @@ | ||||
| using System; | ||||
| using CliFx.Demo.Models; | ||||
|  | ||||
| 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,45 +0,0 @@ | ||||
| using System; | ||||
|  | ||||
| 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, 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) | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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,20 +1,13 @@ | ||||
| using System; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx; | ||||
| using CliFx.Demo.Commands; | ||||
| using CliFx.Demo.Services; | ||||
| using CliFx.Demo.Domain; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
|  | ||||
| namespace CliFx.Demo | ||||
| { | ||||
|     public static class Program | ||||
|     { | ||||
|         private static IServiceProvider GetServiceProvider() | ||||
|         { | ||||
| // We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands | ||||
| var services = new ServiceCollection(); | ||||
|  | ||||
| // Register services | ||||
|             services.AddSingleton<LibraryService>(); | ||||
| services.AddSingleton<LibraryProvider>(); | ||||
|  | ||||
| // Register commands | ||||
| services.AddTransient<BookCommand>(); | ||||
| @@ -22,14 +15,11 @@ namespace CliFx.Demo | ||||
| services.AddTransient<BookRemoveCommand>(); | ||||
| services.AddTransient<BookListCommand>(); | ||||
|  | ||||
|             return services.BuildServiceProvider(); | ||||
|         } | ||||
| var serviceProvider = services.BuildServiceProvider(); | ||||
|  | ||||
|         public static async Task<int> Main() => | ||||
|             await new CliApplicationBuilder() | ||||
| return await new CliApplicationBuilder() | ||||
|     .SetDescription("Demo application showcasing CliFx features.") | ||||
|     .AddCommandsFromThisAssembly() | ||||
|                 .UseTypeActivator(GetServiceProvider().GetRequiredService) | ||||
|     .UseTypeActivator(serviceProvider.GetRequiredService) | ||||
|     .Build() | ||||
|     .RunAsync(); | ||||
|     } | ||||
| } | ||||
| @@ -2,6 +2,4 @@ | ||||
|  | ||||
| Sample command line interface for managing a library of books. | ||||
|  | ||||
| This demo project shows basic CliFx functionality such as command routing, argument parsing, autogenerated help text, and some other things. | ||||
|  | ||||
| You can get a list of available commands by running `CliFx.Demo --help`. | ||||
| This demo project showcases basic CliFx functionality such as command routing, argument parsing, autogenerated help text. | ||||
| @@ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										36
									
								
								CliFx.Demo/Utils/ConsoleExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								CliFx.Demo/Utils/ConsoleExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| 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); | ||||
|     } | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user