mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- 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
|
||||||
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!--
|
||||||
|
|
||||||
|
**Important:**
|
||||||
|
|
||||||
|
Please make sure that there is an existing issue that describes the problem solved by your pull request. If there isn't one, consider creating it first.
|
||||||
|
An open issue offers a good place to iron out requirements, discuss possible solutions, and ask questions.
|
||||||
|
|
||||||
|
Remember to also:
|
||||||
|
|
||||||
|
- Keep your pull request focused and as small as possible. If you want to contribute multiple unrelated changes, please create separate pull requests for them.
|
||||||
|
- Follow the coding style and conventions already established by the project. When in doubt about which style to use, ask in the comments to your pull request.
|
||||||
|
- If you want to start a discussion regarding a specific change you've made, add a review comment to your own code. This can be used to highlight something important or to seek further input from others.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Please specify the issue addressed by this pull request -->
|
||||||
|
Closes #ISSUE_NUMBER
|
||||||
2
.github/workflows/CD.yml
vendored
2
.github/workflows/CD.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Install .NET
|
- name: Install .NET
|
||||||
uses: actions/setup-dotnet@v1.7.2
|
uses: actions/setup-dotnet@v1.7.2
|
||||||
with:
|
with:
|
||||||
dotnet-version: 5.0.100
|
dotnet-version: 5.0.x
|
||||||
|
|
||||||
- name: Pack
|
- name: Pack
|
||||||
run: dotnet pack CliFx --configuration Release
|
run: dotnet pack CliFx --configuration Release
|
||||||
|
|||||||
2
.github/workflows/CI.yml
vendored
2
.github/workflows/CI.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Install .NET
|
- name: Install .NET
|
||||||
uses: actions/setup-dotnet@v1.7.2
|
uses: actions/setup-dotnet@v1.7.2
|
||||||
with:
|
with:
|
||||||
dotnet-version: 5.0.100
|
dotnet-version: 5.0.x
|
||||||
|
|
||||||
- name: Build & test
|
- name: Build & test
|
||||||
run: dotnet test --configuration Release --logger GitHubActions
|
run: dotnet test --configuration Release --logger GitHubActions
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 14 KiB |
66
Changelog.md
66
Changelog.md
@@ -1,3 +1,67 @@
|
|||||||
|
### 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)
|
### 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))
|
- 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 +81,7 @@
|
|||||||
|
|
||||||
### v1.3.2 (31-Jul-2020)
|
### 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))
|
- 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)
|
### 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,5 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="../CliFx.props" />
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
@@ -10,14 +9,18 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Gu.Roslyn.Asserts" Version="3.3.1" />
|
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.1.2" />
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Basic.Reference.Assemblies" Version="1.1.2" />
|
||||||
|
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
|
||||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.4.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
|
||||||
<PackageReference Include="xunit" Version="2.4.0" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
|
<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.0.3" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
72
CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs
Normal file
72
CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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,58 @@
|
|||||||
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
31
CliFx.Analyzers.Tests/GeneralSpecs.cs
Normal file
31
CliFx.Analyzers.Tests/GeneralSpecs.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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,80 @@
|
|||||||
|
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,86 @@
|
|||||||
|
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,92 @@
|
|||||||
|
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,114 @@
|
|||||||
|
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,96 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs
Normal file
105
CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
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,86 @@
|
|||||||
|
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,96 @@
|
|||||||
|
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,80 @@
|
|||||||
|
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,95 @@
|
|||||||
|
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_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_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,95 @@
|
|||||||
|
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,73 @@
|
|||||||
|
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,73 @@
|
|||||||
|
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,96 @@
|
|||||||
|
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,96 @@
|
|||||||
|
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,127 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
168
CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs
Normal file
168
CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
40
CliFx.Analyzers/AnalyzerBase.cs
Normal file
40
CliFx.Analyzers/AnalyzerBase.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers
|
||||||
|
{
|
||||||
|
public abstract class AnalyzerBase : DiagnosticAnalyzer
|
||||||
|
{
|
||||||
|
public DiagnosticDescriptor SupportedDiagnostic { get; }
|
||||||
|
|
||||||
|
public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
|
||||||
|
|
||||||
|
protected AnalyzerBase(
|
||||||
|
string diagnosticTitle,
|
||||||
|
string diagnosticMessage,
|
||||||
|
DiagnosticSeverity diagnosticSeverity = DiagnosticSeverity.Error)
|
||||||
|
{
|
||||||
|
SupportedDiagnostic = new DiagnosticDescriptor(
|
||||||
|
"CliFx_" + GetType().Name.TrimEnd("Analyzer"),
|
||||||
|
diagnosticTitle,
|
||||||
|
diagnosticMessage,
|
||||||
|
"CliFx",
|
||||||
|
diagnosticSeverity,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
SupportedDiagnostics = ImmutableArray.Create(SupportedDiagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Diagnostic CreateDiagnostic(Location location) =>
|
||||||
|
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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="../CliFx.props" />
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<Nullable>annotations</Nullable>
|
<Nullable>annotations</Nullable>
|
||||||
|
<NoWarn>$(NoWarn);RS1025;RS1026</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.4.0" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
54
CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs
Normal file
54
CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs
Normal file
48
CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
83
CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs
Normal file
83
CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
if (attribute is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return FromAttribute(attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsOptionProperty(IPropertySymbol property) =>
|
||||||
|
TryGetOptionAttribute(property) is not null;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs
Normal file
82
CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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 ITypeSymbol? ConverterType { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; }
|
||||||
|
|
||||||
|
public CommandParameterSymbol(
|
||||||
|
int order,
|
||||||
|
string? name,
|
||||||
|
ITypeSymbol? converterType,
|
||||||
|
IReadOnlyList<ITypeSymbol> validatorTypes)
|
||||||
|
{
|
||||||
|
Order = order;
|
||||||
|
Name = name;
|
||||||
|
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 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, converter, validators);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CommandParameterSymbol? TryResolve(IPropertySymbol property)
|
||||||
|
{
|
||||||
|
var attribute = TryGetParameterAttribute(property);
|
||||||
|
|
||||||
|
if (attribute is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return FromAttribute(attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsParameterProperty(IPropertySymbol property) =>
|
||||||
|
TryGetParameterAttribute(property) is not null;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
CliFx.Analyzers/ObjectModel/SymbolNames.cs
Normal file
15
CliFx.Analyzers/ObjectModel/SymbolNames.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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>";
|
||||||
|
}
|
||||||
|
}
|
||||||
51
CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.cs
Normal file
51
CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
CliFx.Analyzers/OptionMustHaveNameOrShortNameAnalyzer.cs
Normal file
40
CliFx.Analyzers/OptionMustHaveNameOrShortNameAnalyzer.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs
Normal file
65
CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.cs
Normal file
64
CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs
Normal file
50
CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs
Normal file
43
CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers
|
||||||
|
{
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class OptionMustHaveValidNameAnalyzer : AnalyzerBase
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs
Normal file
43
CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers
|
||||||
|
{
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class OptionMustHaveValidShortNameAnalyzer : AnalyzerBase
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs
Normal file
52
CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.cs
Normal file
51
CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs
Normal file
68
CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers
|
||||||
|
{
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class ParameterMustBeLastIfNonScalarAnalyzer : AnalyzerBase
|
||||||
|
{
|
||||||
|
public ParameterMustBeLastIfNonScalarAnalyzer()
|
||||||
|
: base(
|
||||||
|
"Parameters of non-scalar types must be last in order",
|
||||||
|
"This parameter has a non-scalar type so it must be 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs
Normal file
66
CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs
Normal file
65
CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs
Normal file
58
CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Analyzers.ObjectModel;
|
||||||
|
using CliFx.Analyzers.Utils.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers
|
||||||
|
{
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public class 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs
Normal file
50
CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs
Normal file
52
CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs
Normal file
78
CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs
Normal file
53
CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
CliFx.Analyzers/Utils/Extensions/StringExtensions.cs
Normal file
18
CliFx.Analyzers/Utils/Extensions/StringExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CliFx.Analyzers.Utils.Extensions
|
||||||
|
{
|
||||||
|
internal static class StringExtensions
|
||||||
|
{
|
||||||
|
public static string TrimEnd(
|
||||||
|
this string str,
|
||||||
|
string sub,
|
||||||
|
StringComparison comparison = StringComparison.Ordinal)
|
||||||
|
{
|
||||||
|
while (str.EndsWith(sub, comparison))
|
||||||
|
str = str.Substring(0, str.Length - sub.Length);
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
CliFx.Benchmarks/Benchmarks.CliFx.cs
Normal file
33
CliFx.Benchmarks/Benchmarks.CliFx.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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>());
|
||||||
|
}
|
||||||
|
}
|
||||||
27
CliFx.Benchmarks/Benchmarks.Clipr.cs
Normal file
27
CliFx.Benchmarks/Benchmarks.Clipr.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using clipr;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks
|
||||||
|
{
|
||||||
|
public partial class Benchmarks
|
||||||
|
{
|
||||||
|
public class CliprCommand
|
||||||
|
{
|
||||||
|
[NamedArgument('s', "str")]
|
||||||
|
public string? StrOption { get; set; }
|
||||||
|
|
||||||
|
[NamedArgument('i', "int")]
|
||||||
|
public int IntOption { get; set; }
|
||||||
|
|
||||||
|
[NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)]
|
||||||
|
public bool BoolOption { get; set; }
|
||||||
|
|
||||||
|
public void Execute()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Description = "Clipr")]
|
||||||
|
public void ExecuteWithClipr() => CliParser.Parse<CliprCommand>(Arguments).Execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
CliFx.Benchmarks/Benchmarks.Cocona.cs
Normal file
24
CliFx.Benchmarks/Benchmarks.Cocona.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using Cocona;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks
|
||||||
|
{
|
||||||
|
public partial class Benchmarks
|
||||||
|
{
|
||||||
|
public class CoconaCommand
|
||||||
|
{
|
||||||
|
public void Execute(
|
||||||
|
[Option("str", new []{'s'})]
|
||||||
|
string? strOption,
|
||||||
|
[Option("int", new []{'i'})]
|
||||||
|
int intOption,
|
||||||
|
[Option("bool", new []{'b'})]
|
||||||
|
bool boolOption)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Description = "Cocona")]
|
||||||
|
public void ExecuteWithCocona() => CoconaApp.Run<CoconaCommand>(Arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
CliFx.Benchmarks/Benchmarks.CommandLineParser.cs
Normal file
30
CliFx.Benchmarks/Benchmarks.CommandLineParser.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
25
CliFx.Benchmarks/Benchmarks.McMaster.cs
Normal file
25
CliFx.Benchmarks/Benchmarks.McMaster.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
CliFx.Benchmarks/Benchmarks.PowerArgs.cs
Normal file
27
CliFx.Benchmarks/Benchmarks.PowerArgs.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using PowerArgs;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks
|
||||||
|
{
|
||||||
|
public partial class Benchmarks
|
||||||
|
{
|
||||||
|
public class PowerArgsCommand
|
||||||
|
{
|
||||||
|
[ArgShortcut("--str"), ArgShortcut("-s")]
|
||||||
|
public string? StrOption { get; set; }
|
||||||
|
|
||||||
|
[ArgShortcut("--int"), ArgShortcut("-i")]
|
||||||
|
public int IntOption { get; set; }
|
||||||
|
|
||||||
|
[ArgShortcut("--bool"), ArgShortcut("-b")]
|
||||||
|
public bool BoolOption { get; set; }
|
||||||
|
|
||||||
|
public void Main()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Description = "PowerArgs")]
|
||||||
|
public void ExecuteWithPowerArgs() => Args.InvokeMain<PowerArgsCommand>(Arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
CliFx.Benchmarks/Benchmarks.SystemCommandLine.cs
Normal file
44
CliFx.Benchmarks/Benchmarks.SystemCommandLine.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using System.CommandLine.Invocation;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks
|
||||||
|
{
|
||||||
|
public partial class Benchmarks
|
||||||
|
{
|
||||||
|
public class SystemCommandLineCommand
|
||||||
|
{
|
||||||
|
public static int ExecuteHandler(string s, int i, bool b) => 0;
|
||||||
|
|
||||||
|
public Task<int> ExecuteAsync(string[] args)
|
||||||
|
{
|
||||||
|
var command = new RootCommand
|
||||||
|
{
|
||||||
|
new Option(new[] {"--str", "-s"})
|
||||||
|
{
|
||||||
|
Argument = new Argument<string?>()
|
||||||
|
},
|
||||||
|
new Option(new[] {"--int", "-i"})
|
||||||
|
{
|
||||||
|
Argument = new Argument<int>()
|
||||||
|
},
|
||||||
|
new Option(new[] {"--bool", "-b"})
|
||||||
|
{
|
||||||
|
Argument = new Argument<bool>()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
command.Handler = CommandHandler.Create(
|
||||||
|
typeof(SystemCommandLineCommand).GetMethod(nameof(ExecuteHandler))!
|
||||||
|
);
|
||||||
|
|
||||||
|
return command.InvokeAsync(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Description = "System.CommandLine")]
|
||||||
|
public async Task<int> ExecuteWithSystemCommandLine() =>
|
||||||
|
await new SystemCommandLineCommand().ExecuteAsync(Arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +1,20 @@
|
|||||||
using System.Collections.Generic;
|
using BenchmarkDotNet.Attributes;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using BenchmarkDotNet.Attributes;
|
|
||||||
using BenchmarkDotNet.Configs;
|
using BenchmarkDotNet.Configs;
|
||||||
using BenchmarkDotNet.Order;
|
using BenchmarkDotNet.Order;
|
||||||
using BenchmarkDotNet.Running;
|
using BenchmarkDotNet.Running;
|
||||||
using CliFx.Benchmarks.Commands;
|
|
||||||
using CommandLine;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks
|
namespace CliFx.Benchmarks
|
||||||
{
|
{
|
||||||
[SimpleJob]
|
|
||||||
[RankColumn]
|
[RankColumn]
|
||||||
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
||||||
public class Benchmarks
|
public partial class Benchmarks
|
||||||
{
|
{
|
||||||
private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"};
|
private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"};
|
||||||
|
|
||||||
[Benchmark(Description = "CliFx", Baseline = true)]
|
public static void Main() => BenchmarkRunner.Run<Benchmarks>(
|
||||||
public async ValueTask<int> ExecuteWithCliFx() =>
|
DefaultConfig
|
||||||
await new CliApplicationBuilder().AddCommand<CliFxCommand>().Build().RunAsync(Arguments, new Dictionary<string, string>());
|
.Instance
|
||||||
|
.WithOptions(ConfigOptions.DisableOptimizationsValidator)
|
||||||
[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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="../CliFx.props" />
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
@@ -7,13 +6,13 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
|
||||||
<PackageReference Include="clipr" Version="1.6.1" />
|
<PackageReference Include="clipr" Version="1.6.1" />
|
||||||
<PackageReference Include="Cocona" Version="1.5.0" />
|
<PackageReference Include="Cocona" Version="1.5.0" />
|
||||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||||
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="3.0.0" />
|
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="3.1.0" />
|
||||||
<PackageReference Include="PowerArgs" Version="3.6.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>
|
||||||
|
|
||||||
<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 clipr;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
public class CliprCommand
|
|
||||||
{
|
|
||||||
[NamedArgument('s', "str")]
|
|
||||||
public string? StrOption { get; set; }
|
|
||||||
|
|
||||||
[NamedArgument('i', "int")]
|
|
||||||
public int IntOption { get; set; }
|
|
||||||
|
|
||||||
[NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)]
|
|
||||||
public bool BoolOption { get; set; }
|
|
||||||
|
|
||||||
public void Execute()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
using Cocona;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
public class CoconaCommand
|
|
||||||
{
|
|
||||||
public void Execute(
|
|
||||||
[Option("str", new []{'s'})]
|
|
||||||
string? strOption,
|
|
||||||
[Option("int", new []{'i'})]
|
|
||||||
int intOption,
|
|
||||||
[Option("bool", new []{'b'})]
|
|
||||||
bool boolOption)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using CommandLine;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
public class CommandLineParserCommand
|
|
||||||
{
|
|
||||||
[Option('s', "str")]
|
|
||||||
public string? StrOption { get; set; }
|
|
||||||
|
|
||||||
[Option('i', "int")]
|
|
||||||
public int IntOption { get; set; }
|
|
||||||
|
|
||||||
[Option('b', "bool")]
|
|
||||||
public bool BoolOption { get; set; }
|
|
||||||
|
|
||||||
public void Execute()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using McMaster.Extensions.CommandLineUtils;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
public class McMasterCommand
|
|
||||||
{
|
|
||||||
[Option("--str|-s")]
|
|
||||||
public string? StrOption { get; set; }
|
|
||||||
|
|
||||||
[Option("--int|-i")]
|
|
||||||
public int IntOption { get; set; }
|
|
||||||
|
|
||||||
[Option("--bool|-b")]
|
|
||||||
public bool BoolOption { get; set; }
|
|
||||||
|
|
||||||
public int OnExecute() => 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using PowerArgs;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
public class PowerArgsCommand
|
|
||||||
{
|
|
||||||
[ArgShortcut("--str"), ArgShortcut("-s")]
|
|
||||||
public string? StrOption { get; set; }
|
|
||||||
|
|
||||||
[ArgShortcut("--int"), ArgShortcut("-i")]
|
|
||||||
public int IntOption { get; set; }
|
|
||||||
|
|
||||||
[ArgShortcut("--bool"), ArgShortcut("-b")]
|
|
||||||
public bool BoolOption { get; set; }
|
|
||||||
|
|
||||||
public void Main()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using System.CommandLine;
|
|
||||||
using System.CommandLine.Invocation;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
public class SystemCommandLineCommand
|
|
||||||
{
|
|
||||||
public static int ExecuteHandler(string s, int i, bool b) => 0;
|
|
||||||
|
|
||||||
public Task<int> ExecuteAsync(string[] args)
|
|
||||||
{
|
|
||||||
var command = new RootCommand
|
|
||||||
{
|
|
||||||
new Option(new[] {"--str", "-s"})
|
|
||||||
{
|
|
||||||
Argument = new Argument<string?>()
|
|
||||||
},
|
|
||||||
new Option(new[] {"--int", "-i"})
|
|
||||||
{
|
|
||||||
Argument = new Argument<int>()
|
|
||||||
},
|
|
||||||
new Option(new[] {"--bool", "-b"})
|
|
||||||
{
|
|
||||||
Argument = new Argument<bool>()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
command.Handler = CommandHandler.Create(typeof(SystemCommandLineCommand).GetMethod(nameof(ExecuteHandler)));
|
|
||||||
|
|
||||||
return command.InvokeAsync(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,5 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="../CliFx.props" />
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
@@ -7,8 +6,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Demo.Internal;
|
using CliFx.Demo.Domain;
|
||||||
using CliFx.Demo.Models;
|
using CliFx.Demo.Utils;
|
||||||
using CliFx.Demo.Services;
|
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace CliFx.Demo.Commands
|
namespace CliFx.Demo.Commands
|
||||||
{
|
{
|
||||||
[Command("book add", Description = "Add a book to the library.")]
|
[Command("book add", Description = "Add a book to the library.")]
|
||||||
public partial class BookAddCommand : ICommand
|
public partial class BookAddCommand : ICommand
|
||||||
{
|
{
|
||||||
private readonly LibraryService _libraryService;
|
private readonly LibraryProvider _libraryProvider;
|
||||||
|
|
||||||
[CommandParameter(0, Name = "title", Description = "Book title.")]
|
[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.")]
|
[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.")]
|
[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.")]
|
[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)
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
if (_libraryService.GetBook(Title) != null)
|
if (_libraryProvider.TryGetBook(Title) is not null)
|
||||||
throw new CommandException("Book already exists.", 1);
|
throw new CommandException("Book already exists.", 10);
|
||||||
|
|
||||||
var book = new Book(Title, Author, Published, Isbn);
|
var book = new Book(Title, Author, Published, Isbn);
|
||||||
_libraryService.AddBook(book);
|
_libraryProvider.AddBook(book);
|
||||||
|
|
||||||
console.Output.WriteLine("Book added.");
|
console.Output.WriteLine("Book added.");
|
||||||
console.RenderBook(book);
|
console.Output.WriteBook(book);
|
||||||
|
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
@@ -47,22 +47,24 @@ namespace CliFx.Demo.Commands
|
|||||||
|
|
||||||
public partial class BookAddCommand
|
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(1800, 2020),
|
||||||
Random.Next(1, 12),
|
Random.Next(1, 12),
|
||||||
Random.Next(1, 28),
|
Random.Next(1, 28),
|
||||||
Random.Next(1, 23),
|
Random.Next(1, 23),
|
||||||
Random.Next(1, 59),
|
Random.Next(1, 59),
|
||||||
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, 999),
|
||||||
Random.Next(0, 99),
|
Random.Next(0, 99),
|
||||||
Random.Next(0, 99999),
|
Random.Next(0, 99999),
|
||||||
Random.Next(0, 99),
|
Random.Next(0, 99),
|
||||||
Random.Next(0, 9));
|
Random.Next(0, 9)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,33 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Demo.Internal;
|
using CliFx.Demo.Domain;
|
||||||
using CliFx.Demo.Services;
|
using CliFx.Demo.Utils;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace CliFx.Demo.Commands
|
namespace CliFx.Demo.Commands
|
||||||
{
|
{
|
||||||
[Command("book", Description = "View, list, add or remove books.")]
|
[Command("book", Description = "Retrieve a book from the library.")]
|
||||||
public class BookCommand : ICommand
|
public class BookCommand : ICommand
|
||||||
{
|
{
|
||||||
private readonly LibraryService _libraryService;
|
private readonly LibraryProvider _libraryProvider;
|
||||||
|
|
||||||
[CommandParameter(0, Name = "title", Description = "Book title.")]
|
[CommandParameter(0, Name = "title", Description = "Title of the book to retrieve.")]
|
||||||
public string Title { get; set; } = "";
|
public string Title { get; init; } = "";
|
||||||
|
|
||||||
public BookCommand(LibraryService libraryService)
|
public BookCommand(LibraryProvider libraryProvider)
|
||||||
{
|
{
|
||||||
_libraryService = libraryService;
|
_libraryProvider = libraryProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console)
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
var book = _libraryService.GetBook(Title);
|
var book = _libraryProvider.TryGetBook(Title);
|
||||||
|
|
||||||
if (book == null)
|
if (book is null)
|
||||||
throw new CommandException("Book not found.", 1);
|
throw new CommandException("Book not found.", 10);
|
||||||
|
|
||||||
console.RenderBook(book);
|
console.Output.WriteBook(book);
|
||||||
|
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Demo.Internal;
|
using CliFx.Demo.Domain;
|
||||||
using CliFx.Demo.Services;
|
using CliFx.Demo.Utils;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace CliFx.Demo.Commands
|
namespace CliFx.Demo.Commands
|
||||||
{
|
{
|
||||||
[Command("book list", Description = "List all books in the library.")]
|
[Command("book list", Description = "List all books in the library.")]
|
||||||
public class BookListCommand : ICommand
|
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)
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
var library = _libraryService.GetLibrary();
|
var library = _libraryProvider.GetLibrary();
|
||||||
|
|
||||||
var isFirst = true;
|
for (var i = 0; i < library.Books.Count; i++)
|
||||||
foreach (var book in library.Books)
|
|
||||||
{
|
{
|
||||||
// Margin
|
// Add margin
|
||||||
if (!isFirst)
|
if (i != 0)
|
||||||
console.Output.WriteLine();
|
console.Output.WriteLine();
|
||||||
isFirst = false;
|
|
||||||
|
|
||||||
// Render book
|
// Render book
|
||||||
console.RenderBook(book);
|
var book = library.Books[i];
|
||||||
|
console.Output.WriteBook(book);
|
||||||
}
|
}
|
||||||
|
|
||||||
return default;
|
return default;
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Demo.Services;
|
using CliFx.Demo.Domain;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace CliFx.Demo.Commands
|
namespace CliFx.Demo.Commands
|
||||||
{
|
{
|
||||||
[Command("book remove", Description = "Remove a book from the library.")]
|
[Command("book remove", Description = "Remove a book from the library.")]
|
||||||
public class BookRemoveCommand : ICommand
|
public class BookRemoveCommand : ICommand
|
||||||
{
|
{
|
||||||
private readonly LibraryService _libraryService;
|
private readonly LibraryProvider _libraryProvider;
|
||||||
|
|
||||||
[CommandParameter(0, Name = "title", Description = "Book title.")]
|
[CommandParameter(0, Name = "title", Description = "Title of the book to remove.")]
|
||||||
public string Title { get; set; } = "";
|
public string Title { get; init; } = "";
|
||||||
|
|
||||||
public BookRemoveCommand(LibraryService libraryService)
|
public BookRemoveCommand(LibraryProvider libraryProvider)
|
||||||
{
|
{
|
||||||
_libraryService = libraryService;
|
_libraryProvider = libraryProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console)
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
var book = _libraryService.GetBook(Title);
|
var book = _libraryProvider.TryGetBook(Title);
|
||||||
|
|
||||||
if (book == null)
|
if (book is null)
|
||||||
throw new CommandException("Book not found.", 1);
|
throw new CommandException("Book not found.", 10);
|
||||||
|
|
||||||
_libraryService.RemoveBook(book);
|
_libraryProvider.RemoveBook(book);
|
||||||
|
|
||||||
console.Output.WriteLine($"Book {Title} removed.");
|
console.Output.WriteLine($"Book {Title} removed.");
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CliFx.Demo.Models
|
namespace CliFx.Demo.Domain
|
||||||
{
|
{
|
||||||
public class Book
|
public class Book
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CliFx.Demo.Models
|
namespace CliFx.Demo.Domain
|
||||||
{
|
{
|
||||||
public partial class Isbn
|
public partial class Isbn
|
||||||
{
|
{
|
||||||
36
CliFx.Demo/Domain/Library.cs
Normal file
36
CliFx.Demo/Domain/Library.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CliFx.Demo.Models;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace CliFx.Demo.Services
|
namespace CliFx.Demo.Domain
|
||||||
{
|
{
|
||||||
public class LibraryService
|
public class LibraryProvider
|
||||||
{
|
{
|
||||||
private string StorageFilePath => Path.Combine(Directory.GetCurrentDirectory(), "Data.json");
|
private static string StorageFilePath { get; } = Path.Combine(Directory.GetCurrentDirectory(), "Library.json");
|
||||||
|
|
||||||
private void StoreLibrary(Library library)
|
private void StoreLibrary(Library library)
|
||||||
{
|
{
|
||||||
@@ -22,10 +21,10 @@ namespace CliFx.Demo.Services
|
|||||||
|
|
||||||
var data = File.ReadAllText(StorageFilePath);
|
var data = File.ReadAllText(StorageFilePath);
|
||||||
|
|
||||||
return JsonConvert.DeserializeObject<Library>(data);
|
return JsonConvert.DeserializeObject<Library>(data) ?? Library.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Book? GetBook(string title) => GetLibrary().Books.FirstOrDefault(b => b.Title == title);
|
public Book? TryGetBook(string title) => GetLibrary().Books.FirstOrDefault(b => b.Title == title);
|
||||||
|
|
||||||
public void AddBook(Book book)
|
public void AddBook(Book book)
|
||||||
{
|
{
|
||||||
@@ -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,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,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,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Demo.Commands;
|
using CliFx.Demo.Commands;
|
||||||
using CliFx.Demo.Services;
|
using CliFx.Demo.Domain;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace CliFx.Demo
|
namespace CliFx.Demo
|
||||||
@@ -14,7 +14,7 @@ namespace CliFx.Demo
|
|||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
// Register services
|
// Register services
|
||||||
services.AddSingleton<LibraryService>();
|
services.AddSingleton<LibraryProvider>();
|
||||||
|
|
||||||
// Register commands
|
// Register commands
|
||||||
services.AddTransient<BookCommand>();
|
services.AddTransient<BookCommand>();
|
||||||
@@ -27,6 +27,7 @@ namespace CliFx.Demo
|
|||||||
|
|
||||||
public static async Task<int> Main() =>
|
public static async Task<int> Main() =>
|
||||||
await new CliApplicationBuilder()
|
await new CliApplicationBuilder()
|
||||||
|
.SetDescription("Demo application showcasing CliFx features.")
|
||||||
.AddCommandsFromThisAssembly()
|
.AddCommandsFromThisAssembly()
|
||||||
.UseTypeActivator(GetServiceProvider().GetRequiredService)
|
.UseTypeActivator(GetServiceProvider().GetRequiredService)
|
||||||
.Build()
|
.Build()
|
||||||
|
|||||||
@@ -2,6 +2,4 @@
|
|||||||
|
|
||||||
Sample command line interface for managing a library of books.
|
Sample command line interface for managing a library of books.
|
||||||
|
|
||||||
This demo project shows basic CliFx functionality such as command routing, argument parsing, autogenerated help text, and some other things.
|
This demo project showcases basic CliFx functionality such as command routing, argument parsing, autogenerated help text.
|
||||||
|
|
||||||
You can get a list of available commands by running `CliFx.Demo --help`.
|
|
||||||
37
CliFx.Demo/Utils/ConsoleExtensions.cs
Normal file
37
CliFx.Demo/Utils/ConsoleExtensions.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="../CliFx.props" />
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace CliFx.Tests.Dummy.Commands
|
namespace CliFx.Tests.Dummy.Commands
|
||||||
{
|
{
|
||||||
@@ -11,11 +12,11 @@ namespace CliFx.Tests.Dummy.Commands
|
|||||||
{
|
{
|
||||||
var input = console.Input.ReadToEnd();
|
var input = console.Input.ReadToEnd();
|
||||||
|
|
||||||
console.WithColors(ConsoleColor.Black, ConsoleColor.White, () =>
|
using (console.WithColors(ConsoleColor.Black, ConsoleColor.White))
|
||||||
{
|
{
|
||||||
console.Output.WriteLine(input);
|
console.Output.WriteLine(input);
|
||||||
console.Error.WriteLine(input);
|
console.Error.WriteLine(input);
|
||||||
});
|
}
|
||||||
|
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|||||||
20
CliFx.Tests.Dummy/Commands/EnvironmentTestCommand.cs
Normal file
20
CliFx.Tests.Dummy/Commands/EnvironmentTestCommand.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
|
namespace CliFx.Tests.Dummy.Commands
|
||||||
|
{
|
||||||
|
[Command("env-test")]
|
||||||
|
public class EnvironmentTestCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("target", EnvironmentVariable = "ENV_TARGET")]
|
||||||
|
public string GreetingTarget { get; set; } = "World";
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
|
{
|
||||||
|
console.Output.WriteLine($"Hello {GreetingTarget}!");
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.Dummy.Commands
|
|
||||||
{
|
|
||||||
[Command]
|
|
||||||
public class HelloWorldCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("target", EnvironmentVariableName = "ENV_TARGET")]
|
|
||||||
public string Target { get; set; } = "World";
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
console.Output.WriteLine($"Hello {Target}!");
|
|
||||||
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user