60 Commits
2.0 ... 2.1

Author SHA1 Message Date
Tyrrrz
05a70175cc Update version 2022-01-04 22:35:59 +02:00
Tyrrrz
33ec2eb3a0 Cleanup 2022-01-04 22:31:50 +02:00
David Fallah
f6ef6cd4c0 Fix ordering of parameters within command help usage (#118) 2022-01-04 12:12:17 -08:00
Tyrrrz
a9ef693dc1 Share more stuff 2021-12-11 00:11:20 +02:00
Tyrrrz
98bbd666dc Update badges 2021-12-10 23:22:22 +02:00
Tyrrrz
4e7ed830f8 Move to shared workflows 2021-12-10 23:21:38 +02:00
Tyrrrz
ef87ff76fc Use top-level statements in demo 2021-12-08 23:52:33 +02:00
Tyrrrz
2feeb21270 C#10ify 2021-12-08 23:43:35 +02:00
Tyrrrz
9990387cfa Update readme 2021-12-05 22:20:00 +02:00
Tyrrrz
bc1bdca7c6 Update nuget packages 2021-12-05 22:05:21 +02:00
Tyrrrz
2a992d37df Update readme 2021-12-05 21:59:53 +02:00
Tyrrrz
15c87aecbb Update CI to .NET 6 2021-11-08 23:34:14 +02:00
Alexey Golub
10a46451ac Update Readme.md 2021-09-30 15:21:11 -07:00
Alexey Golub
e4c6a4174b Update readme 2021-09-04 04:20:38 -07:00
Alexey Golub
4c65f7bbee Update readme 2021-08-30 18:34:26 -07:00
Alexey Golub
5f21de0df5 Refactor webhook in CD 2021-08-28 10:30:58 -07:00
Alexey Golub
9b01b67d98 Update CD 2021-07-28 15:13:09 -07:00
Tyrrrz
4508f5e211 Add Discord 2021-07-26 19:30:54 +03:00
Alex Rosenfeld
f0cbc46df4 Add ReadKey to IConsole (#111)
Co-authored-by: Alexey Golub <tyrrrrrr@gmail.com>
2021-07-23 11:46:00 -07:00
Alex Rosenfeld
6c96e9e173 Add a clear console function (#110) 2021-07-19 04:33:07 -07:00
Tyrrrz
51cca36d2a Update version 2021-07-17 21:37:32 +03:00
Tyrrrz
84672c92f6 Unwrap TargetInvocationException to provide more user-friendly errors when binding fails 2021-07-17 21:32:15 +03:00
Tyrrrz
b1d01898b6 Add test for preamble omission 2021-07-10 19:43:21 +03:00
Tyrrrz
441a47a1a8 Update version 2021-07-09 22:23:46 +03:00
Tyrrrz
8abd7219a1 Better shimming in NoPreambleEncoding 2021-07-09 22:00:31 +03:00
Tyrrrz
df73a0bfe8 Update GitHib issue forms 2021-06-24 21:40:08 +03:00
Tyrrrz
55d12dc721 Add readme to package 2021-06-17 20:36:35 +03:00
Alexey Golub
a6ee44c1bb Fix typo in readme 2021-06-13 06:19:26 -07:00
Tyrrrz
76816e22f1 Use Basic.Reference.Assemblies to simplify reference resolving for dynamic assemblies in tests
Note: bumped `Microsoft.CodeAnalysis.CSharp` in test projects, but didn't touch the one in CliFx.Analyzers as it may have unintended side-effects.
2021-05-10 21:10:42 +03:00
Tyrrrz
daf25e59d6 Fix deprecation warning 2021-05-10 21:06:36 +03:00
Tyrrrz
f2b4e53615 Update version 2021-04-24 20:59:10 +03:00
Tyrrrz
2d519ab190 Remove the usage of ConsoleColor.DarkGray because it looks bad in some terminals
Fixes #104
2021-04-24 20:48:06 +03:00
Tyrrrz
2d479c9cb6 Refactor 2021-04-24 20:43:35 +03:00
Tyrrrz
2bb7e13e51 Use issue forms 2021-04-22 22:11:39 +03:00
Tyrrrz
6e1dfdcdd4 Update readme 2021-04-22 21:08:16 +03:00
Tyrrrz
5ba647e5c1 Update readme 2021-04-22 21:05:35 +03:00
Tyrrrz
853492695f Update readme 2021-04-22 21:04:36 +03:00
Robert Dailey
d5d72c7c50 Show choices for nullable enums in enumerable (#105) 2021-04-22 15:28:33 +03:00
Tyrrrz
d676b5832e Fix discrepancies in unicode handling between ConsoleWriter and Console.Write(...) 2021-04-21 03:16:18 +03:00
Tyrrrz
28097afc1e Update NuGet packages 2021-04-18 19:38:35 +03:00
Tyrrrz
fda96586f3 Update NuGet.config 2021-04-17 21:23:19 +03:00
Tyrrrz
fc5af8dbbc Don't write default value in help text for types that don't override ToString() 2021-04-16 23:28:39 +03:00
Tyrrrz
4835e64388 Remove GHA workarounds 2021-04-13 22:29:15 +03:00
Tyrrrz
0999c33f93 Add NuGet.config 2021-04-13 22:20:07 +03:00
Tyrrrz
595805255a Update version 2021-04-09 22:24:06 +03:00
Tyrrrz
65eaa912cf Refactor 2021-04-08 20:53:48 +03:00
Robert Dailey
038f48b78e Show choices on non-scalar enum parameters and options (#102) 2021-04-08 20:51:17 +03:00
Tyrrrz
d7460244b7 Update version 2021-03-31 12:11:50 +03:00
Tyrrrz
02766868fc Streamline analyzer packaging 2021-03-31 12:11:36 +03:00
Tyrrrz
8d7d25a144 Cleanup 2021-03-31 11:40:37 +03:00
Tyrrrz
17ded54e24 Update readme 2021-03-31 11:29:31 +03:00
Tyrrrz
54a4c32ddf Fix nullref in SystemConsoleShouldBeAvoidedAnalyzer 2021-03-31 11:28:48 +03:00
Tyrrrz
6d46e82145 Add test for SystemConsoleShouldBeAvoidedAnalyzer for when System.Console isn't used 2021-03-31 00:11:36 +03:00
Tyrrrz
fd4a2a18fe Improve comment on IConsole.RegisterCancellationHandler() 2021-03-30 16:22:10 +03:00
Tyrrrz
bfe99d620e Refactor IConsole.WithColors(...) 2021-03-30 16:10:11 +03:00
Tyrrrz
c5a111207f Seal attributes 2021-03-25 06:01:46 +02:00
Tyrrrz
544945c0e6 Don't reference analyzer assembly from main assembly 2021-03-24 02:42:03 +02:00
Tyrrrz
c616cdd750 Update version 2021-03-24 02:40:10 +02:00
Tyrrrz
d3c396956d Fix StackFrame.ParseMany(...) being too paranoid about its own failure 2021-03-24 02:34:36 +02:00
Tyrrrz
d0cbbc6d9a Don't highlight valid values in help text 2021-03-23 01:56:28 +02:00
151 changed files with 8425 additions and 8041 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
github: Tyrrrz
patreon: Tyrrrz
custom: ['buymeacoffee.com/Tyrrrz', 'tyrrrz.me/donate']

42
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: 🐞 Bug report
description: Report broken functionality.
labels: [bug]
body:
- type: markdown
attributes:
value: |
🧐 **Guidelines:**
- Search through [existing issues](https://github.com/Tyrrrz/CliFx/issues?q=is%3Aissue) first to ensure that this bug has not been reported before.
- Write a descriptive title for your issue. Avoid generic or vague titles such as "Something's not working" or "A couple of problems".
- Keep your issue focused on one single problem. If you have multiple bug reports, please create separate issues for each of them.
- Provide as much context as possible in the details section. Include screenshots, screen recordings, links, references, or anything else you may consider relevant.
- If you want to ask a question instead of reporting a bug, please use [discussions](https://github.com/Tyrrrz/CliFx/discussions/new) instead.
- type: input
attributes:
label: Version
description: Which version of CliFx does this bug affect?
placeholder: ver X.Y.Z
validations:
required: true
- type: textarea
attributes:
label: Details
description: Clear and thorough explanation of the bug.
placeholder: I was doing X expecting Y to happen, but Z happened instead.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: Minimum steps required to reproduce the bug.
placeholder: |
- Step 1
- Step 2
- Step 3
validations:
required: true

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 💬 Discord server
url: https://discord.gg/2SUWKFnHSm
about: Chat with the project community.
- name: 🗨 Discussions
url: https://github.com/Tyrrrz/CliFx/discussions/new
about: Ask and answer questions.

View 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

View File

@@ -1,27 +0,0 @@
name: CD
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2.3.3
- name: Install .NET
uses: actions/setup-dotnet@v1.7.2
with:
dotnet-version: 5.0.x
- name: Pack
run: |
dotnet nuget locals all --clear
dotnet pack CliFx --configuration Release
- name: Deploy
run: dotnet nuget push CliFx/bin/Release/*.nupkg -s nuget.org -k ${{ secrets.NUGET_TOKEN }}

View File

@@ -1,30 +0,0 @@
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout
uses: actions/checkout@v2.3.3
- name: Install .NET
uses: actions/setup-dotnet@v1.7.2
with:
dotnet-version: 5.0.x
- name: Build & test
run: |
dotnet nuget locals all --clear
dotnet test --configuration Release --logger GitHubActions
- name: Upload coverage
uses: codecov/codecov-action@v1.0.5
with:
token: ${{ secrets.CODECOV_TOKEN }}

11
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
name: main
on: [push, pull_request]
jobs:
main:
uses: Tyrrrz/.github/.github/workflows/NuGet.yml@master
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}

View File

@@ -1,3 +1,39 @@
### v2.1 (04-Jan-2022)
- Added `IConsole.Clear()` with corresponding implementations in `SystemConsole`, `FakeConsole`, and `FakeInMemoryConsole`. (Thanks [@Alex Rosenfeld](https://github.com/alexrosenfeld10))
- Added `IConsole.ReadKey()` with corresponding implementations in `SystemConsole`, `FakeConsole`, and `FakeInMemoryConsole`. (Thanks [@Alex Rosenfeld](https://github.com/alexrosenfeld10))
- Fixed an issue that caused parameters to appear out of order in the usage format section of the help text. (Thanks [@David Fallah](https://github.com/TAGC))
### v2.0.6 (17-Jul-2021)
- Fixed an issue where an exception thrown via reflection during parameter or option binding resulted in `Exception has been thrown by the target of an invocation` error instead of a more useful message. Such exceptions will now be unwrapped to provide better user experience.
### v2.0.5 (09-Jul-2021)
- Fixed an issue where calling `IConsole.Output.Encoding.EncodingName` and some other members threw an exception.
- Added readme file to the package.
### v2.0.4 (24-Apr-2021)
- Fixed an issue where output and error streams in `SystemConsole` defaulted to UTF8 encoding with BOM when the application was running with UTF8 codepage. `ConsoleWriter` will now discard preamble from the specified encoding. This fix brings the behavior of `SystemConsole` in line with .NET's own `System.Console` which also discards preamble for output and error streams.
- Fixed an issue where help text tried to show default values for parameters and options whose type does not override `ToString()` method.
- Fixed an issue where help text didn't show default values for parameters and options whose type is an enumerable of nullable enums. (Thanks [@Robert Dailey](https://github.com/rcdailey))
- Fixed an issue where specific parts of the help text weren't legible in some terminals due to low color resolution. Removed the usage of `ConsoleColor.DarkGray` in help text.
### v2.0.3 (09-Apr-2021)
- Improved help text by showing valid values for non-scalar enum parameters and options. (Thanks [@Robert Dailey](https://github.com/rcdailey))
### v2.0.2 (31-Mar-2021)
- Fixed an issue where having a transitive reference to CliFx sometimes resulted in `SystemConsoleShouldBeAvoidedAnalyzer` throwing `NullReferenceException` during build.
- Fixed some documentation typos and inconsistencies.
### v2.0.1 (24-Mar-2021)
- Fixed an issue where some exceptions with async stack traces generated on .NET 3.1 or earlier were not parsed and formatted correctly.
- Fixed an issue where help text applied slightly incorrect formatting when displaying choices for enum-based parameters and properties.
### v2.0 (21-Mar-2021) ### v2.0 (21-Mar-2021)
> Note: this major release includes many breaking changes. > Note: this major release includes many breaking changes.

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage> <CollectCoverage>true</CollectCoverage>
@@ -13,13 +13,14 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Basic.Reference.Assemblies" Version="1.2.4" />
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" /> <PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
<PackageReference Include="FluentAssertions" Version="5.10.3" /> <PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
<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="3.0.3" PrivateAssets="all" /> <PackageReference Include="coverlet.msbuild" Version="3.1.0" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -2,71 +2,70 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class CommandMustBeAnnotatedAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new CommandMustBeAnnotatedAnalyzer();
[Fact] public class CommandMustBeAnnotatedAnalyzerSpecs
public void Analyzer_reports_an_error_if_a_command_is_not_annotated_with_the_command_attribute() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new CommandMustBeAnnotatedAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" 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 class MyCommand : ICommand
{ {
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_a_command_is_annotated_with_the_command_attribute() public void Analyzer_does_not_report_an_error_if_a_command_is_annotated_with_the_command_attribute()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public abstract class MyCommand : ICommand public abstract class MyCommand : ICommand
{ {
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_a_command_is_implemented_as_an_abstract_class() public void Analyzer_does_not_report_an_error_if_a_command_is_implemented_as_an_abstract_class()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
public abstract class MyCommand : ICommand public abstract class MyCommand : ICommand
{ {
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_class_that_is_not_a_command() public void Analyzer_does_not_report_an_error_on_a_class_that_is_not_a_command()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
public class Foo public class Foo
{ {
public int Bar { get; set; } = 5; public int Bar { get; set; } = 5;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,57 +2,56 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class CommandMustImplementInterfaceAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new CommandMustImplementInterfaceAnalyzer();
[Fact] public class CommandMustImplementInterfaceAnalyzerSpecs
public void Analyzer_reports_an_error_if_a_command_does_not_implement_ICommand_interface() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new CommandMustImplementInterfaceAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" public void Analyzer_reports_an_error_if_a_command_does_not_implement_ICommand_interface()
{
// Arrange
// language=cs
const string code = @"
[Command] [Command]
public class MyCommand public class MyCommand
{ {
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_a_command_implements_ICommand_interface() public void Analyzer_does_not_report_an_error_if_a_command_implements_ICommand_interface()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_class_that_is_not_a_command() public void Analyzer_does_not_report_an_error_on_a_class_that_is_not_a_command()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
public class Foo public class Foo
{ {
public int Bar { get; set; } = 5; public int Bar { get; set; } = 5;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -4,28 +4,27 @@ using FluentAssertions;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
public class GeneralSpecs
{ {
public class GeneralSpecs [Fact]
public void All_analyzers_have_unique_diagnostic_IDs()
{ {
[Fact] // Arrange
public void All_analyzers_have_unique_diagnostic_IDs() var analyzers = typeof(AnalyzerBase)
{ .Assembly
// Arrange .GetTypes()
var analyzers = typeof(AnalyzerBase) .Where(t => !t.IsAbstract && t.IsAssignableTo(typeof(DiagnosticAnalyzer)))
.Assembly .Select(t => (DiagnosticAnalyzer) Activator.CreateInstance(t)!)
.GetTypes() .ToArray();
.Where(t => !t.IsAbstract && t.IsAssignableTo(typeof(DiagnosticAnalyzer)))
.Select(t => (DiagnosticAnalyzer) Activator.CreateInstance(t)!)
.ToArray();
// Act // Act
var diagnosticIds = analyzers var diagnosticIds = analyzers
.SelectMany(a => a.SupportedDiagnostics.Select(d => d.Id)) .SelectMany(a => a.SupportedDiagnostics.Select(d => d.Id))
.ToArray(); .ToArray();
// Assert // Assert
diagnosticIds.Should().OnlyHaveUniqueItems(); diagnosticIds.Should().OnlyHaveUniqueItems();
}
} }
} }

View File

@@ -2,34 +2,34 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class OptionMustBeInsideCommandAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustBeInsideCommandAnalyzer();
[Fact] public class OptionMustBeInsideCommandAnalyzerSpecs
public void Analyzer_reports_an_error_if_an_option_is_inside_a_class_that_is_not_a_command() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustBeInsideCommandAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" 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 public class MyClass
{ {
[CommandOption(""foo"")] [CommandOption(""foo"")]
public string Foo { get; set; } public string Foo { get; set; }
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_is_inside_a_command() public void Analyzer_does_not_report_an_error_if_an_option_is_inside_a_command()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -39,32 +39,32 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_is_inside_an_abstract_class() public void Analyzer_does_not_report_an_error_if_an_option_is_inside_an_abstract_class()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
public abstract class MyCommand public abstract class MyCommand
{ {
[CommandOption(""foo"")] [CommandOption(""foo"")]
public string Foo { get; set; } public string Foo { get; set; }
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -73,8 +73,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,18 +2,18 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class OptionMustHaveNameOrShortNameAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveNameOrShortNameAnalyzer();
[Fact] public class OptionMustHaveNameOrShortNameAnalyzerSpecs
public void Analyzer_reports_an_error_if_an_option_does_not_have_a_name_or_short_name() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveNameOrShortNameAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" public void Analyzer_reports_an_error_if_an_option_does_not_have_a_name_or_short_name()
{
// Arrange
// language=cs
const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -23,16 +23,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_has_a_name() public void Analyzer_does_not_report_an_error_if_an_option_has_a_name()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -42,16 +42,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_has_a_short_name() public void Analyzer_does_not_report_an_error_if_an_option_has_a_short_name()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -61,16 +61,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -79,8 +79,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,18 +2,18 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class OptionMustHaveUniqueNameAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveUniqueNameAnalyzer();
[Fact] public class OptionMustHaveUniqueNameAnalyzerSpecs
public void Analyzer_reports_an_error_if_an_option_has_the_same_name_as_another_option() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveUniqueNameAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" public void Analyzer_reports_an_error_if_an_option_has_the_same_name_as_another_option()
{
// Arrange
// language=cs
const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -26,16 +26,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_has_a_unique_name() public void Analyzer_does_not_report_an_error_if_an_option_has_a_unique_name()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -48,16 +48,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_name() public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_name()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -67,16 +67,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -85,8 +85,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,18 +2,18 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class OptionMustHaveUniqueShortNameAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveUniqueShortNameAnalyzer();
[Fact] public class OptionMustHaveUniqueShortNameAnalyzerSpecs
public void Analyzer_reports_an_error_if_an_option_has_the_same_short_name_as_another_option() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveUniqueShortNameAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" public void Analyzer_reports_an_error_if_an_option_has_the_same_short_name_as_another_option()
{
// Arrange
// language=cs
const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -26,16 +26,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_has_a_unique_short_name() public void Analyzer_does_not_report_an_error_if_an_option_has_a_unique_short_name()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -48,16 +48,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_has_a_short_name_which_is_unique_only_in_casing() public void Analyzer_does_not_report_an_error_if_an_option_has_a_short_name_which_is_unique_only_in_casing()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -70,16 +70,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_short_name() public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_short_name()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -89,16 +89,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -107,8 +107,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,18 +2,18 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class OptionMustHaveValidConverterAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidConverterAnalyzer();
[Fact] public class OptionMustHaveValidConverterAnalyzerSpecs
public void Analyzer_reports_an_error_if_the_specified_option_converter_does_not_derive_from_BindingConverter() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidConverterAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" 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 class MyConverter
{ {
public string Convert(string rawValue) => rawValue; public string Convert(string rawValue) => rawValue;
@@ -28,16 +28,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_the_specified_option_converter_derives_from_BindingConverter() public void Analyzer_does_not_report_an_error_if_the_specified_option_converter_derives_from_BindingConverter()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
public class MyConverter : BindingConverter<string> public class MyConverter : BindingConverter<string>
{ {
public override string Convert(string rawValue) => rawValue; public override string Convert(string rawValue) => rawValue;
@@ -52,16 +52,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_converter() public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_converter()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -71,16 +71,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -89,8 +89,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,18 +2,18 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class OptionMustHaveValidNameAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidNameAnalyzer();
[Fact] public class OptionMustHaveValidNameAnalyzerSpecs
public void Analyzer_reports_an_error_if_an_option_has_a_name_which_is_too_short() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidNameAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" public void Analyzer_reports_an_error_if_an_option_has_a_name_which_is_too_short()
{
// Arrange
// language=cs
const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -23,16 +23,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_reports_an_error_if_an_option_has_a_name_that_starts_with_a_non_letter_character() public void Analyzer_reports_an_error_if_an_option_has_a_name_that_starts_with_a_non_letter_character()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -42,16 +42,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_has_a_valid_name() public void Analyzer_does_not_report_an_error_if_an_option_has_a_valid_name()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -61,16 +61,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_name() public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_name()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -80,16 +80,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -98,8 +98,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,18 +2,18 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class OptionMustHaveValidShortNameAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidShortNameAnalyzer();
[Fact] public class OptionMustHaveValidShortNameAnalyzerSpecs
public void Analyzer_reports_an_error_if_an_option_has_a_short_name_which_is_not_a_letter_character() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidShortNameAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" 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] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -23,16 +23,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_has_a_valid_short_name() public void Analyzer_does_not_report_an_error_if_an_option_has_a_valid_short_name()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -42,16 +42,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_short_name() public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_short_name()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -61,16 +61,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -79,8 +79,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,18 +2,18 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class OptionMustHaveValidValidatorsAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidValidatorsAnalyzer();
[Fact] public class OptionMustHaveValidValidatorsAnalyzerSpecs
public void Analyzer_reports_an_error_if_one_of_the_specified_option_validators_does_not_derive_from_BindingValidator() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidValidatorsAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" 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 class MyValidator
{ {
public void Validate(string value) {} public void Validate(string value) {}
@@ -28,16 +28,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_all_specified_option_validators_derive_from_BindingValidator() public void Analyzer_does_not_report_an_error_if_all_specified_option_validators_derive_from_BindingValidator()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
public class MyValidator : BindingValidator<string> public class MyValidator : BindingValidator<string>
{ {
public override BindingValidationError Validate(string value) => Ok(); public override BindingValidationError Validate(string value) => Ok();
@@ -52,16 +52,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_validators() public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_validators()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -71,16 +71,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -89,8 +89,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,34 +2,34 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class ParameterMustBeInsideCommandAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeInsideCommandAnalyzer();
[Fact] public class ParameterMustBeInsideCommandAnalyzerSpecs
public void Analyzer_reports_an_error_if_a_parameter_is_inside_a_class_that_is_not_a_command() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeInsideCommandAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" 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 public class MyClass
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public string Foo { get; set; }
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_a_parameter_is_inside_a_command() public void Analyzer_does_not_report_an_error_if_a_parameter_is_inside_a_command()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -39,32 +39,32 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_a_parameter_is_inside_an_abstract_class() public void Analyzer_does_not_report_an_error_if_a_parameter_is_inside_an_abstract_class()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
public abstract class MyCommand public abstract class MyCommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public string Foo { get; set; }
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -73,8 +73,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,18 +2,18 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class ParameterMustBeLastIfNonScalarAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeLastIfNonScalarAnalyzer();
[Fact] public class ParameterMustBeLastIfNonScalarAnalyzerSpecs
public void Analyzer_reports_an_error_if_a_non_scalar_parameter_is_not_last_in_order() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeLastIfNonScalarAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" public void Analyzer_reports_an_error_if_a_non_scalar_parameter_is_not_last_in_order()
{
// Arrange
// language=cs
const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -26,16 +26,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_is_last_in_order() public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_is_last_in_order()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -48,16 +48,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_no_non_scalar_parameters_are_defined() public void Analyzer_does_not_report_an_error_if_no_non_scalar_parameters_are_defined()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -70,16 +70,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -88,8 +88,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,18 +2,18 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class ParameterMustBeSingleIfNonScalarAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeSingleIfNonScalarAnalyzer();
[Fact] public class ParameterMustBeSingleIfNonScalarAnalyzerSpecs
public void Analyzer_reports_an_error_if_more_than_one_non_scalar_parameters_are_defined() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeSingleIfNonScalarAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" public void Analyzer_reports_an_error_if_more_than_one_non_scalar_parameters_are_defined()
{
// Arrange
// language=cs
const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -26,16 +26,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_only_one_non_scalar_parameter_is_defined() public void Analyzer_does_not_report_an_error_if_only_one_non_scalar_parameter_is_defined()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -48,16 +48,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_no_non_scalar_parameters_are_defined() public void Analyzer_does_not_report_an_error_if_no_non_scalar_parameters_are_defined()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -70,16 +70,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -88,8 +88,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,18 +2,18 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class ParameterMustHaveUniqueNameAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveUniqueNameAnalyzer();
[Fact] public class ParameterMustHaveUniqueNameAnalyzerSpecs
public void Analyzer_reports_an_error_if_a_parameter_has_the_same_name_as_another_parameter() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveUniqueNameAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" public void Analyzer_reports_an_error_if_a_parameter_has_the_same_name_as_another_parameter()
{
// Arrange
// language=cs
const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -26,16 +26,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_a_parameter_has_unique_name() public void Analyzer_does_not_report_an_error_if_a_parameter_has_unique_name()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -48,16 +48,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -66,8 +66,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,18 +2,18 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class ParameterMustHaveUniqueOrderAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveUniqueOrderAnalyzer();
[Fact] public class ParameterMustHaveUniqueOrderAnalyzerSpecs
public void Analyzer_reports_an_error_if_a_parameter_has_the_same_order_as_another_parameter() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveUniqueOrderAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" public void Analyzer_reports_an_error_if_a_parameter_has_the_same_order_as_another_parameter()
{
// Arrange
// language=cs
const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -26,16 +26,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_a_parameter_has_unique_order() public void Analyzer_does_not_report_an_error_if_a_parameter_has_unique_order()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -48,16 +48,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -66,8 +66,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,18 +2,18 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class ParameterMustHaveValidConverterAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveValidConverterAnalyzer();
[Fact] public class ParameterMustHaveValidConverterAnalyzerSpecs
public void Analyzer_reports_an_error_if_the_specified_parameter_converter_does_not_derive_from_BindingConverter() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveValidConverterAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" 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 class MyConverter
{ {
public string Convert(string rawValue) => rawValue; public string Convert(string rawValue) => rawValue;
@@ -28,16 +28,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_the_specified_parameter_converter_derives_from_BindingConverter() public void Analyzer_does_not_report_an_error_if_the_specified_parameter_converter_derives_from_BindingConverter()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
public class MyConverter : BindingConverter<string> public class MyConverter : BindingConverter<string>
{ {
public override string Convert(string rawValue) => rawValue; public override string Convert(string rawValue) => rawValue;
@@ -52,16 +52,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_a_parameter_does_not_have_a_converter() public void Analyzer_does_not_report_an_error_if_a_parameter_does_not_have_a_converter()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -71,16 +71,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -89,8 +89,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,18 +2,18 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class ParameterMustHaveValidValidatorsAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveValidValidatorsAnalyzer();
[Fact] public class ParameterMustHaveValidValidatorsAnalyzerSpecs
public void Analyzer_reports_an_error_if_one_of_the_specified_parameter_validators_does_not_derive_from_BindingValidator() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveValidValidatorsAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" 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 class MyValidator
{ {
public void Validate(string value) {} public void Validate(string value) {}
@@ -28,16 +28,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_all_specified_parameter_validators_derive_from_BindingValidator() public void Analyzer_does_not_report_an_error_if_all_specified_parameter_validators_derive_from_BindingValidator()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
public class MyValidator : BindingValidator<string> public class MyValidator : BindingValidator<string>
{ {
public override BindingValidationError Validate(string value) => Ok(); public override BindingValidationError Validate(string value) => Ok();
@@ -52,16 +52,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_a_parameter_does_not_have_validators() public void Analyzer_does_not_report_an_error_if_a_parameter_does_not_have_validators()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -71,16 +71,16 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -89,8 +89,7 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
}
} }
} }

View File

@@ -2,18 +2,18 @@
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Xunit; using Xunit;
namespace CliFx.Analyzers.Tests namespace CliFx.Analyzers.Tests;
{
public class SystemConsoleShouldBeAvoidedAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new SystemConsoleShouldBeAvoidedAnalyzer();
[Fact] public class SystemConsoleShouldBeAvoidedAnalyzerSpecs
public void Analyzer_reports_an_error_if_a_command_calls_a_method_on_SystemConsole() {
{ private static DiagnosticAnalyzer Analyzer { get; } = new SystemConsoleShouldBeAvoidedAnalyzer();
// Arrange
// language=cs [Fact]
const string code = @" public void Analyzer_reports_an_error_if_a_command_calls_a_method_on_SystemConsole()
{
// Arrange
// language=cs
const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -24,16 +24,16 @@ public class MyCommand : ICommand
} }
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_reports_an_error_if_a_command_accesses_a_property_on_SystemConsole() public void Analyzer_reports_an_error_if_a_command_accesses_a_property_on_SystemConsole()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -43,16 +43,16 @@ public class MyCommand : ICommand
return default; return default;
} }
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_reports_an_error_if_a_command_calls_a_method_on_a_property_of_SystemConsole() public void Analyzer_reports_an_error_if_a_command_calls_a_method_on_a_property_of_SystemConsole()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -63,16 +63,16 @@ public class MyCommand : ICommand
} }
}"; }";
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_a_command_interacts_with_the_console_through_IConsole() public void Analyzer_does_not_report_an_error_if_a_command_interacts_with_the_console_through_IConsole()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -83,16 +83,16 @@ public class MyCommand : ICommand
} }
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
[Fact] [Fact]
public void Analyzer_does_not_report_an_error_if_IConsole_is_not_available_in_the_current_method() public void Analyzer_does_not_report_an_error_if_IConsole_is_not_available_in_the_current_method()
{ {
// Arrange // Arrange
// language=cs // language=cs
const string code = @" const string code = @"
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -101,8 +101,26 @@ public class MyCommand : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"; }";
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); 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);
} }
} }

View File

@@ -2,8 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Text; using System.Text;
using Basic.Reference.Assemblies;
using FluentAssertions.Execution; using FluentAssertions.Execution;
using FluentAssertions.Primitives; using FluentAssertions.Primitives;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
@@ -11,164 +11,157 @@ using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text;
namespace CliFx.Analyzers.Tests.Utils namespace CliFx.Analyzers.Tests.Utils;
{
internal class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer, AnalyzerAssertions>
{
protected override string Identifier { get; } = "analyzer";
public AnalyzerAssertions(DiagnosticAnalyzer analyzer) internal class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer, AnalyzerAssertions>
: base(analyzer) {
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()))
);
} }
private Compilation Compile(string sourceCode) 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(() =>
{ {
// Get default system namespaces var buffer = new StringBuilder();
var defaultSystemNamespaces = new[]
buffer.AppendLine("Expected and produced diagnostics do not match.");
buffer.AppendLine();
buffer.AppendLine("Expected diagnostics:");
foreach (var expectedDiagnostic in expectedDiagnostics)
{ {
"System", buffer.Append(" - ");
"System.Collections.Generic", buffer.Append(expectedDiagnostic.Id);
"System.Threading.Tasks" buffer.AppendLine();
};
// 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},
new[]
{
MetadataReference.CreateFromFile(Assembly.Load("netstandard").Location),
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
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; buffer.AppendLine();
}
private IReadOnlyList<Diagnostic> GetProducedDiagnostics(string sourceCode) buffer.AppendLine("Produced diagnostics:");
{
var analyzers = ImmutableArray.Create(Subject);
var compilation = Compile(sourceCode);
return compilation foreach (var producedDiagnostic in producedDiagnostics)
.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.Append(" - ");
buffer.Append(producedDiagnostic);
}
buffer.AppendLine("Expected and produced diagnostics do not match."); return new FailReason(buffer.ToString());
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 void NotProduceDiagnostics(string sourceCode)
{ {
public static AnalyzerAssertions Should(this DiagnosticAnalyzer analyzer) => new(analyzer); 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);
}

View File

@@ -3,38 +3,37 @@ using CliFx.Analyzers.Utils.Extensions;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
public abstract class AnalyzerBase : DiagnosticAnalyzer
{ {
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)
{ {
public DiagnosticDescriptor SupportedDiagnostic { get; } SupportedDiagnostic = new DiagnosticDescriptor(
"CliFx_" + GetType().Name.TrimEnd("Analyzer"),
diagnosticTitle,
diagnosticMessage,
"CliFx",
diagnosticSeverity,
true
);
public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } SupportedDiagnostics = ImmutableArray.Create(SupportedDiagnostic);
}
protected AnalyzerBase( protected Diagnostic CreateDiagnostic(Location location) =>
string diagnosticTitle, Diagnostic.Create(SupportedDiagnostic, location);
string diagnosticMessage,
DiagnosticSeverity diagnosticSeverity = DiagnosticSeverity.Error)
{
SupportedDiagnostic = new DiagnosticDescriptor(
"CliFx_" + GetType().Name.TrimEnd("Analyzer"),
diagnosticTitle,
diagnosticMessage,
"CliFx",
diagnosticSeverity,
true
);
SupportedDiagnostics = ImmutableArray.Create(SupportedDiagnostic); public override void Initialize(AnalysisContext context)
} {
context.EnableConcurrentExecution();
protected Diagnostic CreateDiagnostic(Location location) => context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
Diagnostic.Create(SupportedDiagnostic, location);
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
}
} }
} }

View File

@@ -5,50 +5,49 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CommandMustBeAnnotatedAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public CommandMustBeAnnotatedAnalyzer()
public class CommandMustBeAnnotatedAnalyzer : AnalyzerBase : base(
$"Commands must be annotated with `{SymbolNames.CliFxCommandAttribute}`",
$"This type must be annotated with `{SymbolNames.CliFxCommandAttribute}` in order to be a valid command.")
{ {
public CommandMustBeAnnotatedAnalyzer() }
: base(
$"Commands must be annotated with `{SymbolNames.CliFxCommandAttribute}`", private void Analyze(
$"This type must be annotated with `{SymbolNames.CliFxCommandAttribute}` in order to be a valid command.") 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()));
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);
} }
} }
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandleClassDeclaration(Analyze);
}
} }

View File

@@ -5,44 +5,43 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CommandMustImplementInterfaceAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public CommandMustImplementInterfaceAnalyzer()
public class CommandMustImplementInterfaceAnalyzer : AnalyzerBase : base(
$"Commands must implement `{SymbolNames.CliFxCommandInterface}` interface",
$"This type must implement `{SymbolNames.CliFxCommandInterface}` interface in order to be a valid command.")
{ {
public CommandMustImplementInterfaceAnalyzer() }
: base(
$"Commands must implement `{SymbolNames.CliFxCommandInterface}` interface", private void Analyze(
$"This type must implement `{SymbolNames.CliFxCommandInterface}` interface in order to be a valid command.") 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()));
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);
} }
} }
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandleClassDeclaration(Analyze);
}
} }

View File

@@ -3,81 +3,80 @@ using Microsoft.CodeAnalysis;
using System.Linq; using System.Linq;
using CliFx.Analyzers.Utils.Extensions; using CliFx.Analyzers.Utils.Extensions;
namespace CliFx.Analyzers.ObjectModel namespace CliFx.Analyzers.ObjectModel;
internal partial class CommandOptionSymbol
{ {
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)
{ {
public string? Name { get; } Name = name;
ShortName = shortName;
public char? ShortName { get; } ConverterType = converterType;
ValidatorTypes = validatorTypes;
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;
} }
} }
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;
}

View File

@@ -3,80 +3,79 @@ using System.Linq;
using CliFx.Analyzers.Utils.Extensions; using CliFx.Analyzers.Utils.Extensions;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
namespace CliFx.Analyzers.ObjectModel namespace CliFx.Analyzers.ObjectModel;
internal partial class CommandParameterSymbol
{ {
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)
{ {
public int Order { get; } Order = order;
Name = name;
public string? Name { get; } ConverterType = converterType;
ValidatorTypes = validatorTypes;
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;
} }
} }
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;
}

View File

@@ -1,15 +1,14 @@
namespace CliFx.Analyzers.ObjectModel namespace CliFx.Analyzers.ObjectModel;
internal static class SymbolNames
{ {
internal static class SymbolNames public const string CliFxCommandInterface = "CliFx.ICommand";
{ public const string CliFxCommandAttribute = "CliFx.Attributes.CommandAttribute";
public const string CliFxCommandInterface = "CliFx.ICommand"; public const string CliFxCommandParameterAttribute = "CliFx.Attributes.CommandParameterAttribute";
public const string CliFxCommandAttribute = "CliFx.Attributes.CommandAttribute"; public const string CliFxCommandOptionAttribute = "CliFx.Attributes.CommandOptionAttribute";
public const string CliFxCommandParameterAttribute = "CliFx.Attributes.CommandParameterAttribute"; public const string CliFxConsoleInterface = "CliFx.Infrastructure.IConsole";
public const string CliFxCommandOptionAttribute = "CliFx.Attributes.CommandOptionAttribute"; public const string CliFxBindingConverterInterface = "CliFx.Extensibility.IBindingConverter";
public const string CliFxConsoleInterface = "CliFx.Infrastructure.IConsole"; public const string CliFxBindingConverterClass = "CliFx.Extensibility.BindingConverter<T>";
public const string CliFxBindingConverterInterface = "CliFx.Extensibility.IBindingConverter"; public const string CliFxBindingValidatorInterface = "CliFx.Extensibility.IBindingValidator";
public const string CliFxBindingConverterClass = "CliFx.Extensibility.BindingConverter<T>"; public const string CliFxBindingValidatorClass = "CliFx.Extensibility.BindingValidator<T>";
public const string CliFxBindingValidatorInterface = "CliFx.Extensibility.IBindingValidator";
public const string CliFxBindingValidatorClass = "CliFx.Extensibility.BindingValidator<T>";
}
} }

View File

@@ -5,44 +5,46 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustBeInsideCommandAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public OptionMustBeInsideCommandAnalyzer()
public class OptionMustBeInsideCommandAnalyzer : AnalyzerBase : base(
"Options must be defined inside commands",
$"This option must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`.")
{ {
public OptionMustBeInsideCommandAnalyzer() }
: base(
"Options must be defined inside commands", private void Analyze(
$"This option must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`.") 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()));
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
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);
} }
} }
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
} }

View File

@@ -4,37 +4,36 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveNameOrShortNameAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public OptionMustHaveNameOrShortNameAnalyzer()
public class OptionMustHaveNameOrShortNameAnalyzer : AnalyzerBase : base(
"Options must have either a name or short name specified",
"This option must have either a name or short name specified.")
{ {
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( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property)
{ {
var option = CommandOptionSymbol.TryResolve(property); var option = CommandOptionSymbol.TryResolve(property);
if (option is null) if (option is null)
return; return;
if (string.IsNullOrWhiteSpace(option.Name) && option.ShortName is null) if (string.IsNullOrWhiteSpace(option.Name) && option.ShortName is null)
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{ {
base.Initialize(context); context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
context.HandlePropertyDeclaration(Analyze);
} }
} }
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
} }

View File

@@ -6,57 +6,59 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveUniqueNameAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public OptionMustHaveUniqueNameAnalyzer()
public class OptionMustHaveUniqueNameAnalyzer : AnalyzerBase : base(
"Options must have unique names",
"This option's name must be unique within the command (comparison IS NOT case sensitive).")
{ {
public OptionMustHaveUniqueNameAnalyzer() }
: base(
"Options must have unique names", private void Analyze(
"This option's name must be unique within the command (comparison IS NOT case sensitive).") 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;
private void Analyze( if (string.IsNullOrWhiteSpace(otherOption.Name))
SyntaxNodeAnalysisContext context, continue;
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
var option = CommandOptionSymbol.TryResolve(property);
if (option is null)
return;
if (string.IsNullOrWhiteSpace(option.Name)) if (string.Equals(option.Name, otherOption.Name, StringComparison.OrdinalIgnoreCase))
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); context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
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) public override void Initialize(AnalysisContext context)
{ {
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
}
} }
} }

View File

@@ -5,57 +5,59 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveUniqueShortNameAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public OptionMustHaveUniqueShortNameAnalyzer()
public class OptionMustHaveUniqueShortNameAnalyzer : AnalyzerBase : base(
"Options must have unique short names",
"This option's short name must be unique within the command (comparison IS case sensitive).")
{ {
public OptionMustHaveUniqueShortNameAnalyzer() }
: base(
"Options must have unique short names", private void Analyze(
"This option's short name must be unique within the command (comparison IS case sensitive).") 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;
private void Analyze( if (otherOption.ShortName is null)
SyntaxNodeAnalysisContext context, continue;
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
var option = CommandOptionSymbol.TryResolve(property);
if (option is null)
return;
if (option.ShortName is null) if (option.ShortName == otherOption.ShortName)
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); context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
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) public override void Initialize(AnalysisContext context)
{ {
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
}
} }
} }

View File

@@ -5,46 +5,45 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveValidConverterAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public OptionMustHaveValidConverterAnalyzer()
public class OptionMustHaveValidConverterAnalyzer : AnalyzerBase : base(
$"Option converters must derive from `{SymbolNames.CliFxBindingConverterClass}`",
$"Converter specified for this option must derive from `{SymbolNames.CliFxBindingConverterClass}`.")
{ {
public OptionMustHaveValidConverterAnalyzer() }
: base(
$"Option converters must derive from `{SymbolNames.CliFxBindingConverterClass}`", private void Analyze(
$"Converter specified for this option must derive from `{SymbolNames.CliFxBindingConverterClass}`.") 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()));
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);
} }
} }
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
} }

View File

@@ -4,40 +4,39 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveValidNameAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public OptionMustHaveValidNameAnalyzer()
public class OptionMustHaveValidNameAnalyzer : AnalyzerBase : base(
"Options must have valid names",
"This option's name must be at least 2 characters long and must start with a letter.")
{ {
public OptionMustHaveValidNameAnalyzer() }
: base(
"Options must have valid names", private void Analyze(
"This option's name must be at least 2 characters long and must start with a letter.") 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()));
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);
} }
} }
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
} }

View File

@@ -4,40 +4,39 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveValidShortNameAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public OptionMustHaveValidShortNameAnalyzer()
public class OptionMustHaveValidShortNameAnalyzer : AnalyzerBase : base(
"Option short names must be letter characters",
"This option's short name must be a single letter character.")
{ {
public OptionMustHaveValidShortNameAnalyzer() }
: base(
"Option short names must be letter characters", private void Analyze(
"This option's short name must be a single letter character.") 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()));
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);
} }
} }
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
} }

View File

@@ -5,48 +5,47 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveValidValidatorsAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public OptionMustHaveValidValidatorsAnalyzer()
public class OptionMustHaveValidValidatorsAnalyzer : AnalyzerBase : base(
$"Option validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`",
$"All validators specified for this option must derive from `{SymbolNames.CliFxBindingValidatorClass}`.")
{ {
public OptionMustHaveValidValidatorsAnalyzer() }
: base(
$"Option validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`",
$"All validators specified for this option must derive from `{SymbolNames.CliFxBindingValidatorClass}`.")
{
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property)
{ {
var option = CommandOptionSymbol.TryResolve(property); var option = CommandOptionSymbol.TryResolve(property);
if (option is null) if (option is null)
return; return;
foreach (var validatorType in option.ValidatorTypes) 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)
{ {
// We check against an internal interface because checking against a generic class is a pain context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
var validatorImplementsInterface = validatorType
.AllInterfaces
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxBindingValidatorInterface));
if (!validatorImplementsInterface) // No need to report multiple identical diagnostics on the same node
{ break;
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
// No need to report multiple identical diagnostics on the same node
break;
}
} }
} }
}
public override void Initialize(AnalysisContext context) public override void Initialize(AnalysisContext context)
{ {
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
}
} }
} }

View File

@@ -5,44 +5,46 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustBeInsideCommandAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public ParameterMustBeInsideCommandAnalyzer()
public class ParameterMustBeInsideCommandAnalyzer : AnalyzerBase : base(
"Parameters must be defined inside commands",
$"This parameter must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`.")
{ {
public ParameterMustBeInsideCommandAnalyzer() }
: base(
"Parameters must be defined inside commands", private void Analyze(
$"This parameter must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`.") 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()));
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
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);
} }
} }
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
} }

View File

@@ -5,61 +5,63 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustBeLastIfNonScalarAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public ParameterMustBeLastIfNonScalarAnalyzer()
public class ParameterMustBeLastIfNonScalarAnalyzer : AnalyzerBase : 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).")
{ {
public ParameterMustBeLastIfNonScalarAnalyzer() }
: base(
"Parameters of non-scalar types must be last in order", private static bool IsScalar(ITypeSymbol type) =>
"This parameter has a non-scalar type so it must be last in order (its order must be highest within the command).") 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;
private static bool IsScalar(ITypeSymbol type) => if (otherParameter.Order > parameter.Order)
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 (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); context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
if (otherParameter is null)
continue;
if (otherParameter.Order > parameter.Order)
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
} }
} }
}
public override void Initialize(AnalysisContext context) public override void Initialize(AnalysisContext context)
{ {
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
}
} }
} }

View File

@@ -5,59 +5,61 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustBeSingleIfNonScalarAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public ParameterMustBeSingleIfNonScalarAnalyzer()
public class ParameterMustBeSingleIfNonScalarAnalyzer : AnalyzerBase : 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.")
{ {
public ParameterMustBeSingleIfNonScalarAnalyzer() }
: base(
"Parameters of non-scalar types are limited to one per command", private static bool IsScalar(ITypeSymbol type) =>
"This parameter has a non-scalar type so it must be the only such parameter in the command.") 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;
private static bool IsScalar(ITypeSymbol type) => if (!IsScalar(otherProperty.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 (!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)) context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
continue;
if (!IsScalar(otherProperty.Type))
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
} }
} }
}
public override void Initialize(AnalysisContext context) public override void Initialize(AnalysisContext context)
{ {
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
}
} }
} }

View File

@@ -6,57 +6,59 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustHaveUniqueNameAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public ParameterMustHaveUniqueNameAnalyzer()
public class ParameterMustHaveUniqueNameAnalyzer : AnalyzerBase : base(
"Parameters must have unique names",
"This parameter's name must be unique within the command (comparison IS NOT case sensitive).")
{ {
public ParameterMustHaveUniqueNameAnalyzer() }
: base(
"Parameters must have unique names", private void Analyze(
"This parameter's name must be unique within the command (comparison IS NOT case sensitive).") 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;
private void Analyze( if (string.IsNullOrWhiteSpace(otherParameter.Name))
SyntaxNodeAnalysisContext context, continue;
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null)
return;
if (string.IsNullOrWhiteSpace(parameter.Name)) if (string.Equals(parameter.Name, otherParameter.Name, StringComparison.OrdinalIgnoreCase))
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); context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
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) public override void Initialize(AnalysisContext context)
{ {
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
}
} }
} }

View File

@@ -5,51 +5,53 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustHaveUniqueOrderAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public ParameterMustHaveUniqueOrderAnalyzer()
public class ParameterMustHaveUniqueOrderAnalyzer : AnalyzerBase : base(
"Parameters must have unique order",
"This parameter's order must be unique within the command.")
{ {
public ParameterMustHaveUniqueOrderAnalyzer() }
: base(
"Parameters must have unique order", private void Analyze(
"This parameter's order must be unique within the command.") 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;
private void Analyze( if (parameter.Order == otherParameter.Order)
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
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); context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
if (otherParameter is null)
continue;
if (parameter.Order == otherParameter.Order)
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
} }
} }
}
public override void Initialize(AnalysisContext context) public override void Initialize(AnalysisContext context)
{ {
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
}
} }
} }

View File

@@ -5,46 +5,45 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustHaveValidConverterAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public ParameterMustHaveValidConverterAnalyzer()
public class ParameterMustHaveValidConverterAnalyzer : AnalyzerBase : base(
$"Parameter converters must derive from `{SymbolNames.CliFxBindingConverterClass}`",
$"Converter specified for this parameter must derive from `{SymbolNames.CliFxBindingConverterClass}`.")
{ {
public ParameterMustHaveValidConverterAnalyzer() }
: base(
$"Parameter converters must derive from `{SymbolNames.CliFxBindingConverterClass}`", private void Analyze(
$"Converter specified for this parameter must derive from `{SymbolNames.CliFxBindingConverterClass}`.") 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()));
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);
} }
} }
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
} }

View File

@@ -5,48 +5,47 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustHaveValidValidatorsAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public ParameterMustHaveValidValidatorsAnalyzer()
public class ParameterMustHaveValidValidatorsAnalyzer : AnalyzerBase : base(
$"Parameter validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`",
$"All validators specified for this parameter must derive from `{SymbolNames.CliFxBindingValidatorClass}`.")
{ {
public ParameterMustHaveValidValidatorsAnalyzer() }
: base(
$"Parameter validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`",
$"All validators specified for this parameter must derive from `{SymbolNames.CliFxBindingValidatorClass}`.")
{
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property)
{ {
var parameter = CommandParameterSymbol.TryResolve(property); var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null) if (parameter is null)
return; return;
foreach (var validatorType in parameter.ValidatorTypes) 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)
{ {
// We check against an internal interface because checking against a generic class is a pain context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
var validatorImplementsInterface = validatorType
.AllInterfaces
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxBindingValidatorInterface));
if (!validatorImplementsInterface) // No need to report multiple identical diagnostics on the same node
{ break;
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
// No need to report multiple identical diagnostics on the same node
break;
}
} }
} }
}
public override void Initialize(AnalysisContext context) public override void Initialize(AnalysisContext context)
{ {
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
}
} }
} }

View File

@@ -6,72 +6,72 @@ using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class SystemConsoleShouldBeAvoidedAnalyzer : AnalyzerBase
{ {
[DiagnosticAnalyzer(LanguageNames.CSharp)] public SystemConsoleShouldBeAvoidedAnalyzer()
public class SystemConsoleShouldBeAvoidedAnalyzer : AnalyzerBase : 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)
{ {
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( private MemberAccessExpressionSyntax? TryGetSystemConsoleMemberAccess(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
SyntaxNode node) SyntaxNode node)
{ {
var currentNode = node; var currentNode = node;
while (currentNode is MemberAccessExpressionSyntax memberAccess) while (currentNode is MemberAccessExpressionSyntax memberAccess)
{
var member = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
if (member?.ContainingType?.DisplayNameMatches("System.Console") == true)
{ {
var symbol = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol; return memberAccess;
if (symbol is not null && symbol.ContainingType.DisplayNameMatches("System.Console"))
{
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; // 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;
} }
private void Analyze(SyntaxNodeAnalysisContext context) 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)
{ {
// Try to get a member access on System.Console in the current expression, context.ReportDiagnostic(CreateDiagnostic(systemConsoleMemberAccess.GetLocation()));
// 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);
} }
} }
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.SimpleMemberAccessExpression);
}
} }

View File

@@ -4,45 +4,49 @@ using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers.Utils.Extensions namespace CliFx.Analyzers.Utils.Extensions;
internal static class RoslynExtensions
{ {
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)
{ {
public static bool DisplayNameMatches(this ISymbol symbol, string name) => analysisContext.RegisterSyntaxNodeAction(ctx =>
string.Equals(symbol.ToDisplayString(), name, StringComparison.Ordinal);
public static void HandleClassDeclaration(
this AnalysisContext analysisContext,
Action<SyntaxNodeAnalysisContext, ClassDeclarationSyntax, ITypeSymbol> handler)
{ {
analysisContext.RegisterSyntaxNodeAction(ctx => if (ctx.Node is not ClassDeclarationSyntax classDeclaration)
{ return;
if (ctx.Node is not ClassDeclarationSyntax classDeclaration)
return;
var type = ctx.SemanticModel.GetDeclaredSymbol(classDeclaration); var type = ctx.SemanticModel.GetDeclaredSymbol(classDeclaration);
if (type is null) if (type is null)
return; return;
handler(ctx, classDeclaration, type); analyze(ctx, classDeclaration, type);
}, SyntaxKind.ClassDeclaration); }, SyntaxKind.ClassDeclaration);
} }
public static void HandlePropertyDeclaration( public static void HandlePropertyDeclaration(
this AnalysisContext analysisContext, this AnalysisContext analysisContext,
Action<SyntaxNodeAnalysisContext, PropertyDeclarationSyntax, IPropertySymbol> handler) Action<SyntaxNodeAnalysisContext, PropertyDeclarationSyntax, IPropertySymbol> analyze)
{
analysisContext.RegisterSyntaxNodeAction(ctx =>
{ {
analysisContext.RegisterSyntaxNodeAction(ctx => if (ctx.Node is not PropertyDeclarationSyntax propertyDeclaration)
{ return;
if (ctx.Node is not PropertyDeclarationSyntax propertyDeclaration)
return;
var property = ctx.SemanticModel.GetDeclaredSymbol(propertyDeclaration); var property = ctx.SemanticModel.GetDeclaredSymbol(propertyDeclaration);
if (property is null) if (property is null)
return; return;
handler(ctx, propertyDeclaration, property); analyze(ctx, propertyDeclaration, property);
}, SyntaxKind.PropertyDeclaration); }, SyntaxKind.PropertyDeclaration);
}
} }
} }

View File

@@ -1,18 +1,17 @@
using System; using System;
namespace CliFx.Analyzers.Utils.Extensions 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; 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;
} }
} }

View File

@@ -4,30 +4,29 @@ using BenchmarkDotNet.Attributes;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Infrastructure; using CliFx.Infrastructure;
namespace CliFx.Benchmarks namespace CliFx.Benchmarks;
public partial class Benchmarks
{ {
public partial class Benchmarks [Command]
public class CliFxCommand : ICommand
{ {
[Command] [CommandOption("str", 's')]
public class CliFxCommand : ICommand public string? StrOption { get; set; }
{
[CommandOption("str", 's')]
public string? StrOption { get; set; }
[CommandOption("int", 'i')] [CommandOption("int", 'i')]
public int IntOption { get; set; } public int IntOption { get; set; }
[CommandOption("bool", 'b')] [CommandOption("bool", 'b')]
public bool BoolOption { get; set; } public bool BoolOption { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default; 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>());
} }
[Benchmark(Description = "CliFx", Baseline = true)]
public async ValueTask<int> ExecuteWithCliFx() =>
await new CliApplicationBuilder()
.AddCommand<CliFxCommand>()
.Build()
.RunAsync(Arguments, new Dictionary<string, string>());
} }

View File

@@ -1,27 +1,26 @@
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using clipr; using clipr;
namespace CliFx.Benchmarks namespace CliFx.Benchmarks;
public partial class Benchmarks
{ {
public partial class Benchmarks public class CliprCommand
{ {
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()
{ {
[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();
} }
[Benchmark(Description = "Clipr")]
public void ExecuteWithClipr() => CliParser.Parse<CliprCommand>(Arguments).Execute();
} }

View File

@@ -1,24 +1,23 @@
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using Cocona; using Cocona;
namespace CliFx.Benchmarks 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 partial class Benchmarks
public void ExecuteWithCocona() => CoconaApp.Run<CoconaCommand>(Arguments); {
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);
} }

View File

@@ -1,30 +1,29 @@
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using CommandLine; using CommandLine;
namespace CliFx.Benchmarks namespace CliFx.Benchmarks;
public partial class Benchmarks
{ {
public partial class Benchmarks public class CommandLineParserCommand
{ {
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()
{ {
[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());
} }
[Benchmark(Description = "CommandLineParser")]
public void ExecuteWithCommandLineParser() =>
new Parser()
.ParseArguments(Arguments, typeof(CommandLineParserCommand))
.WithParsed<CommandLineParserCommand>(c => c.Execute());
} }

View File

@@ -1,25 +1,24 @@
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using McMaster.Extensions.CommandLineUtils; using McMaster.Extensions.CommandLineUtils;
namespace CliFx.Benchmarks namespace CliFx.Benchmarks;
public partial class Benchmarks
{ {
public partial class Benchmarks public class McMasterCommand
{ {
public class McMasterCommand [Option("--str|-s")]
{ public string? StrOption { get; set; }
[Option("--str|-s")]
public string? StrOption { get; set; }
[Option("--int|-i")] [Option("--int|-i")]
public int IntOption { get; set; } public int IntOption { get; set; }
[Option("--bool|-b")] [Option("--bool|-b")]
public bool BoolOption { get; set; } public bool BoolOption { get; set; }
public int OnExecute() => 0; public int OnExecute() => 0;
}
[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
public int ExecuteWithMcMaster() => CommandLineApplication.Execute<McMasterCommand>(Arguments);
} }
[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
public int ExecuteWithMcMaster() => CommandLineApplication.Execute<McMasterCommand>(Arguments);
} }

View File

@@ -1,27 +1,26 @@
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using PowerArgs; using PowerArgs;
namespace CliFx.Benchmarks namespace CliFx.Benchmarks;
public partial class Benchmarks
{ {
public partial class Benchmarks public class PowerArgsCommand
{ {
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()
{ {
[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);
} }
[Benchmark(Description = "PowerArgs")]
public void ExecuteWithPowerArgs() => Args.InvokeMain<PowerArgsCommand>(Arguments);
} }

View File

@@ -3,42 +3,41 @@ using System.CommandLine.Invocation;
using System.Threading.Tasks; using System.Threading.Tasks;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
namespace CliFx.Benchmarks namespace CliFx.Benchmarks;
public partial class Benchmarks
{ {
public partial class Benchmarks public class SystemCommandLineCommand
{ {
public class SystemCommandLineCommand public static int ExecuteHandler(string s, int i, bool b) => 0;
public Task<int> ExecuteAsync(string[] args)
{ {
public static int ExecuteHandler(string s, int i, bool b) => 0; var command = new RootCommand
public Task<int> ExecuteAsync(string[] args)
{ {
var command = new RootCommand new Option(new[] {"--str", "-s"})
{ {
new Option(new[] {"--str", "-s"}) Argument = new Argument<string?>()
{ },
Argument = new Argument<string?>() new Option(new[] {"--int", "-i"})
}, {
new Option(new[] {"--int", "-i"}) Argument = new Argument<int>()
{ },
Argument = new Argument<int>() new Option(new[] {"--bool", "-b"})
}, {
new Option(new[] {"--bool", "-b"}) Argument = new Argument<bool>()
{ }
Argument = new Argument<bool>() };
}
};
command.Handler = CommandHandler.Create( command.Handler = CommandHandler.Create(
typeof(SystemCommandLineCommand).GetMethod(nameof(ExecuteHandler))! typeof(SystemCommandLineCommand).GetMethod(nameof(ExecuteHandler))!
); );
return command.InvokeAsync(args); return command.InvokeAsync(args);
}
} }
[Benchmark(Description = "System.CommandLine")]
public async Task<int> ExecuteWithSystemCommandLine() =>
await new SystemCommandLineCommand().ExecuteAsync(Arguments);
} }
[Benchmark(Description = "System.CommandLine")]
public async Task<int> ExecuteWithSystemCommandLine() =>
await new SystemCommandLineCommand().ExecuteAsync(Arguments);
} }

View File

@@ -3,18 +3,17 @@ using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Order; using BenchmarkDotNet.Order;
using BenchmarkDotNet.Running; using BenchmarkDotNet.Running;
namespace CliFx.Benchmarks namespace CliFx.Benchmarks;
{
[RankColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public partial class Benchmarks
{
private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"};
public static void Main() => BenchmarkRunner.Run<Benchmarks>( [RankColumn]
DefaultConfig [Orderer(SummaryOrderPolicy.FastestToSlowest)]
.Instance public partial class Benchmarks
.With(ConfigOptions.DisableOptimizationsValidator) {
); private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"};
}
public static void Main() => BenchmarkRunner.Run<Benchmarks>(
DefaultConfig
.Instance
.WithOptions(ConfigOptions.DisableOptimizationsValidator)
);
} }

View File

@@ -2,13 +2,13 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" /> <PackageReference Include="BenchmarkDotNet" Version="0.13.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.6.0" />
<PackageReference Include="CommandLineParser" Version="2.8.0" /> <PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="3.1.0" /> <PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="3.1.0" />
<PackageReference Include="PowerArgs" Version="3.6.0" /> <PackageReference Include="PowerArgs" Version="3.6.0" />

View File

@@ -2,12 +2,12 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -6,65 +6,64 @@ using CliFx.Demo.Utils;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Infrastructure; using CliFx.Infrastructure;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands;
[Command("book add", Description = "Add a book to the library.")]
public partial class BookAddCommand : ICommand
{ {
[Command("book add", Description = "Add a book to the library.")] private readonly LibraryProvider _libraryProvider;
public partial class BookAddCommand : ICommand
[CommandParameter(0, Name = "title", Description = "Book title.")]
public string Title { get; init; } = "";
[CommandOption("author", 'a', IsRequired = true, Description = "Book author.")]
public string Author { get; init; } = "";
[CommandOption("published", 'p', Description = "Book publish date.")]
public DateTimeOffset Published { get; init; } = CreateRandomDate();
[CommandOption("isbn", 'n', Description = "Book ISBN.")]
public Isbn Isbn { get; init; } = CreateRandomIsbn();
public BookAddCommand(LibraryProvider libraryProvider)
{ {
private readonly LibraryProvider _libraryProvider; _libraryProvider = libraryProvider;
[CommandParameter(0, Name = "title", Description = "Book title.")]
public string Title { get; init; } = "";
[CommandOption("author", 'a', IsRequired = true, Description = "Book author.")]
public string Author { get; init; } = "";
[CommandOption("published", 'p', Description = "Book publish date.")]
public DateTimeOffset Published { get; init; } = CreateRandomDate();
[CommandOption("isbn", 'n', Description = "Book ISBN.")]
public Isbn Isbn { get; init; } = CreateRandomIsbn();
public BookAddCommand(LibraryProvider libraryProvider)
{
_libraryProvider = libraryProvider;
}
public ValueTask ExecuteAsync(IConsole console)
{
if (_libraryProvider.TryGetBook(Title) is not null)
throw new CommandException("Book already exists.", 10);
var book = new Book(Title, Author, Published, Isbn);
_libraryProvider.AddBook(book);
console.Output.WriteLine("Book added.");
console.Output.WriteBook(book);
return default;
}
} }
public partial class BookAddCommand public ValueTask ExecuteAsync(IConsole console)
{ {
private static readonly Random Random = new(); if (_libraryProvider.TryGetBook(Title) is not null)
throw new CommandException("Book already exists.", 10);
private static DateTimeOffset CreateRandomDate() => new( var book = new Book(Title, Author, Published, Isbn);
Random.Next(1800, 2020), _libraryProvider.AddBook(book);
Random.Next(1, 12),
Random.Next(1, 28),
Random.Next(1, 23),
Random.Next(1, 59),
Random.Next(1, 59),
TimeSpan.Zero
);
private static Isbn CreateRandomIsbn() => new( console.Output.WriteLine("Book added.");
Random.Next(0, 999), console.Output.WriteBook(book);
Random.Next(0, 99),
Random.Next(0, 99999), return default;
Random.Next(0, 99),
Random.Next(0, 9)
);
} }
} }
public partial class BookAddCommand
{
private static readonly Random Random = new();
private static DateTimeOffset CreateRandomDate() => new(
Random.Next(1800, 2020),
Random.Next(1, 12),
Random.Next(1, 28),
Random.Next(1, 23),
Random.Next(1, 59),
Random.Next(1, 59),
TimeSpan.Zero
);
private static Isbn CreateRandomIsbn() => new(
Random.Next(0, 999),
Random.Next(0, 99),
Random.Next(0, 99999),
Random.Next(0, 99),
Random.Next(0, 9)
);
}

View File

@@ -5,31 +5,30 @@ using CliFx.Demo.Utils;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Infrastructure; using CliFx.Infrastructure;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands;
[Command("book", Description = "Retrieve a book from the library.")]
public class BookCommand : ICommand
{ {
[Command("book", Description = "Retrieve a book from the library.")] private readonly LibraryProvider _libraryProvider;
public class BookCommand : ICommand
[CommandParameter(0, Name = "title", Description = "Title of the book to retrieve.")]
public string Title { get; init; } = "";
public BookCommand(LibraryProvider libraryProvider)
{ {
private readonly LibraryProvider _libraryProvider; _libraryProvider = libraryProvider;
}
[CommandParameter(0, Name = "title", Description = "Title of the book to retrieve.")] public ValueTask ExecuteAsync(IConsole console)
public string Title { get; init; } = ""; {
var book = _libraryProvider.TryGetBook(Title);
public BookCommand(LibraryProvider libraryProvider) if (book is null)
{ throw new CommandException("Book not found.", 10);
_libraryProvider = libraryProvider;
}
public ValueTask ExecuteAsync(IConsole console) console.Output.WriteBook(book);
{
var book = _libraryProvider.TryGetBook(Title);
if (book is null) return default;
throw new CommandException("Book not found.", 10);
console.Output.WriteBook(book);
return default;
}
} }
} }

View File

@@ -4,34 +4,33 @@ using CliFx.Demo.Domain;
using CliFx.Demo.Utils; using CliFx.Demo.Utils;
using CliFx.Infrastructure; using CliFx.Infrastructure;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands;
[Command("book list", Description = "List all books in the library.")]
public class BookListCommand : ICommand
{ {
[Command("book list", Description = "List all books in the library.")] private readonly LibraryProvider _libraryProvider;
public class BookListCommand : ICommand
public BookListCommand(LibraryProvider libraryProvider)
{ {
private readonly LibraryProvider _libraryProvider; _libraryProvider = libraryProvider;
}
public BookListCommand(LibraryProvider libraryProvider) public ValueTask ExecuteAsync(IConsole console)
{
var library = _libraryProvider.GetLibrary();
for (var i = 0; i < library.Books.Count; i++)
{ {
_libraryProvider = libraryProvider; // Add margin
if (i != 0)
console.Output.WriteLine();
// Render book
var book = library.Books[i];
console.Output.WriteBook(book);
} }
public ValueTask ExecuteAsync(IConsole console) return default;
{
var library = _libraryProvider.GetLibrary();
for (var i = 0; i < library.Books.Count; i++)
{
// Add margin
if (i != 0)
console.Output.WriteLine();
// Render book
var book = library.Books[i];
console.Output.WriteBook(book);
}
return default;
}
} }
} }

View File

@@ -4,33 +4,32 @@ using CliFx.Demo.Domain;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Infrastructure; using CliFx.Infrastructure;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands;
[Command("book remove", Description = "Remove a book from the library.")]
public class BookRemoveCommand : ICommand
{ {
[Command("book remove", Description = "Remove a book from the library.")] private readonly LibraryProvider _libraryProvider;
public class BookRemoveCommand : ICommand
[CommandParameter(0, Name = "title", Description = "Title of the book to remove.")]
public string Title { get; init; } = "";
public BookRemoveCommand(LibraryProvider libraryProvider)
{ {
private readonly LibraryProvider _libraryProvider; _libraryProvider = libraryProvider;
}
[CommandParameter(0, Name = "title", Description = "Title of the book to remove.")] public ValueTask ExecuteAsync(IConsole console)
public string Title { get; init; } = ""; {
var book = _libraryProvider.TryGetBook(Title);
public BookRemoveCommand(LibraryProvider libraryProvider) if (book is null)
{ throw new CommandException("Book not found.", 10);
_libraryProvider = libraryProvider;
}
public ValueTask ExecuteAsync(IConsole console) _libraryProvider.RemoveBook(book);
{
var book = _libraryProvider.TryGetBook(Title);
if (book is null) console.Output.WriteLine($"Book {Title} removed.");
throw new CommandException("Book not found.", 10);
_libraryProvider.RemoveBook(book); return default;
console.Output.WriteLine($"Book {Title} removed.");
return default;
}
} }
} }

View File

@@ -1,23 +1,22 @@
using System; using System;
namespace CliFx.Demo.Domain namespace CliFx.Demo.Domain;
public class Book
{ {
public class Book public string Title { get; }
public string Author { get; }
public DateTimeOffset Published { get; }
public Isbn Isbn { get; }
public Book(string title, string author, DateTimeOffset published, Isbn isbn)
{ {
public string Title { get; } Title = title;
Author = author;
public string Author { get; } Published = published;
Isbn = isbn;
public DateTimeOffset Published { get; }
public Isbn Isbn { get; }
public Book(string title, string author, DateTimeOffset published, Isbn isbn)
{
Title = title;
Author = author;
Published = published;
Isbn = isbn;
}
} }
} }

View File

@@ -1,45 +1,44 @@
using System; using System;
namespace CliFx.Demo.Domain namespace CliFx.Demo.Domain;
public partial class Isbn
{ {
public partial class Isbn public int EanPrefix { get; }
public int RegistrationGroup { get; }
public int Registrant { get; }
public int Publication { get; }
public int CheckDigit { get; }
public Isbn(int eanPrefix, int registrationGroup, int registrant, int publication, int checkDigit)
{ {
public int EanPrefix { get; } EanPrefix = eanPrefix;
RegistrationGroup = registrationGroup;
public int RegistrationGroup { get; } Registrant = registrant;
Publication = publication;
public int Registrant { get; } CheckDigit = checkDigit;
public int Publication { get; }
public int CheckDigit { get; }
public Isbn(int eanPrefix, int registrationGroup, int registrant, int publication, int checkDigit)
{
EanPrefix = eanPrefix;
RegistrationGroup = registrationGroup;
Registrant = registrant;
Publication = publication;
CheckDigit = checkDigit;
}
public override string ToString() =>
$"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}";
} }
public partial class Isbn public override string ToString() =>
{ $"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}";
public static Isbn Parse(string value, IFormatProvider formatProvider) }
{
var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries);
return new Isbn( public partial class Isbn
int.Parse(components[0], formatProvider), {
int.Parse(components[1], formatProvider), public static Isbn Parse(string value, IFormatProvider formatProvider)
int.Parse(components[2], formatProvider), {
int.Parse(components[3], formatProvider), var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries);
int.Parse(components[4], formatProvider)
); return new Isbn(
} int.Parse(components[0], formatProvider),
int.Parse(components[1], formatProvider),
int.Parse(components[2], formatProvider),
int.Parse(components[3], formatProvider),
int.Parse(components[4], formatProvider)
);
} }
} }

View File

@@ -2,35 +2,34 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace CliFx.Demo.Domain namespace CliFx.Demo.Domain;
public partial class Library
{ {
public partial class Library public IReadOnlyList<Book> Books { get; }
public Library(IReadOnlyList<Book> books)
{ {
public IReadOnlyList<Book> Books { get; } Books = books;
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 Library WithBook(Book book)
{ {
public static Library Empty { get; } = new(Array.Empty<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>());
}

View File

@@ -2,40 +2,39 @@
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace CliFx.Demo.Domain namespace CliFx.Demo.Domain;
public class LibraryProvider
{ {
public class LibraryProvider private static string StorageFilePath { get; } = Path.Combine(Directory.GetCurrentDirectory(), "Library.json");
private void StoreLibrary(Library library)
{ {
private static string StorageFilePath { get; } = Path.Combine(Directory.GetCurrentDirectory(), "Library.json"); var data = JsonConvert.SerializeObject(library);
File.WriteAllText(StorageFilePath, data);
}
private void StoreLibrary(Library library) public Library GetLibrary()
{ {
var data = JsonConvert.SerializeObject(library); if (!File.Exists(StorageFilePath))
File.WriteAllText(StorageFilePath, data); return Library.Empty;
}
public Library GetLibrary() var data = File.ReadAllText(StorageFilePath);
{
if (!File.Exists(StorageFilePath))
return Library.Empty;
var data = File.ReadAllText(StorageFilePath); return JsonConvert.DeserializeObject<Library>(data) ?? Library.Empty;
}
return JsonConvert.DeserializeObject<Library>(data); public Book? TryGetBook(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)
{
var updatedLibrary = GetLibrary().WithBook(book);
StoreLibrary(updatedLibrary);
}
public void AddBook(Book book) public void RemoveBook(Book book)
{ {
var updatedLibrary = GetLibrary().WithBook(book); var updatedLibrary = GetLibrary().WithoutBook(book);
StoreLibrary(updatedLibrary); StoreLibrary(updatedLibrary);
}
public void RemoveBook(Book book)
{
var updatedLibrary = GetLibrary().WithoutBook(book);
StoreLibrary(updatedLibrary);
}
} }
} }

View File

@@ -1,36 +1,25 @@
using System; using CliFx;
using System.Threading.Tasks;
using CliFx.Demo.Commands; using CliFx.Demo.Commands;
using CliFx.Demo.Domain; using CliFx.Demo.Domain;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace CliFx.Demo // We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
{ var services = new ServiceCollection();
public static class Program
{
private static IServiceProvider GetServiceProvider()
{
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
var services = new ServiceCollection();
// Register services // Register services
services.AddSingleton<LibraryProvider>(); services.AddSingleton<LibraryProvider>();
// Register commands // Register commands
services.AddTransient<BookCommand>(); services.AddTransient<BookCommand>();
services.AddTransient<BookAddCommand>(); services.AddTransient<BookAddCommand>();
services.AddTransient<BookRemoveCommand>(); services.AddTransient<BookRemoveCommand>();
services.AddTransient<BookListCommand>(); services.AddTransient<BookListCommand>();
return services.BuildServiceProvider(); var serviceProvider = services.BuildServiceProvider();
}
public static async Task<int> Main() => return await new CliApplicationBuilder()
await new CliApplicationBuilder() .SetDescription("Demo application showcasing CliFx features.")
.SetDescription("Demo application showcasing CliFx features.") .AddCommandsFromThisAssembly()
.AddCommandsFromThisAssembly() .UseTypeActivator(serviceProvider.GetRequiredService)
.UseTypeActivator(GetServiceProvider().GetRequiredService) .Build()
.Build() .RunAsync();
.RunAsync();
}
}

View File

@@ -2,36 +2,35 @@
using CliFx.Demo.Domain; using CliFx.Demo.Domain;
using CliFx.Infrastructure; using CliFx.Infrastructure;
namespace CliFx.Demo.Utils namespace CliFx.Demo.Utils;
internal static class ConsoleExtensions
{ {
internal static class ConsoleExtensions public static void WriteBook(this ConsoleWriter writer, Book book)
{ {
public static void WriteBook(this ConsoleWriter writer, Book book) // Title
{ using (writer.Console.WithForegroundColor(ConsoleColor.White))
// Title writer.WriteLine(book.Title);
using (writer.Console.WithForegroundColor(ConsoleColor.White))
writer.WriteLine(book.Title);
// Author // Author
writer.Write(" "); writer.Write(" ");
writer.Write("Author: "); writer.Write("Author: ");
using (writer.Console.WithForegroundColor(ConsoleColor.White)) using (writer.Console.WithForegroundColor(ConsoleColor.White))
writer.WriteLine(book.Author); writer.WriteLine(book.Author);
// Published // Published
writer.Write(" "); writer.Write(" ");
writer.Write("Published: "); writer.Write("Published: ");
using (writer.Console.WithForegroundColor(ConsoleColor.White)) using (writer.Console.WithForegroundColor(ConsoleColor.White))
writer.WriteLine($"{book.Published:d}"); writer.WriteLine($"{book.Published:d}");
// ISBN // ISBN
writer.Write(" "); writer.Write(" ");
writer.Write("ISBN: "); writer.Write("ISBN: ");
using (writer.Console.WithForegroundColor(ConsoleColor.White)) using (writer.Console.WithForegroundColor(ConsoleColor.White))
writer.WriteLine(book.Isbn); writer.WriteLine(book.Isbn);
}
} }
} }

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -3,22 +3,21 @@ using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Infrastructure; using CliFx.Infrastructure;
namespace CliFx.Tests.Dummy.Commands namespace CliFx.Tests.Dummy.Commands;
[Command("console-test")]
public class ConsoleTestCommand : ICommand
{ {
[Command("console-test")] public ValueTask ExecuteAsync(IConsole console)
public class ConsoleTestCommand : ICommand
{ {
public ValueTask ExecuteAsync(IConsole console) var input = console.Input.ReadToEnd();
using (console.WithColors(ConsoleColor.Black, ConsoleColor.White))
{ {
var input = console.Input.ReadToEnd(); console.Output.WriteLine(input);
console.Error.WriteLine(input);
using (console.WithColors(ConsoleColor.Black, ConsoleColor.White))
{
console.Output.WriteLine(input);
console.Error.WriteLine(input);
}
return default;
} }
return default;
} }
} }

View File

@@ -2,19 +2,18 @@
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Infrastructure; using CliFx.Infrastructure;
namespace CliFx.Tests.Dummy.Commands namespace CliFx.Tests.Dummy.Commands;
[Command("env-test")]
public class EnvironmentTestCommand : ICommand
{ {
[Command("env-test")] [CommandOption("target", EnvironmentVariable = "ENV_TARGET")]
public class EnvironmentTestCommand : ICommand public string GreetingTarget { get; set; } = "World";
public ValueTask ExecuteAsync(IConsole console)
{ {
[CommandOption("target", EnvironmentVariable = "ENV_TARGET")] console.Output.WriteLine($"Hello {GreetingTarget}!");
public string GreetingTarget { get; set; } = "World";
public ValueTask ExecuteAsync(IConsole console) return default;
{
console.Output.WriteLine($"Hello {GreetingTarget}!");
return default;
}
} }
} }

View File

@@ -1,24 +1,22 @@
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace CliFx.Tests.Dummy namespace CliFx.Tests.Dummy;
// This dummy application is used in tests for scenarios
// that require an external process to properly verify.
public static partial class Program
{ {
// This dummy application is used in tests for scenarios public static Assembly Assembly { get; } = typeof(Program).Assembly;
// that require an external process to properly verify.
public static partial class Program public static string Location { get; } = Assembly.Location;
{ }
public static Assembly Assembly { get; } = typeof(Program).Assembly;
public static partial class Program
public static string Location { get; } = Assembly.Location; {
} public static async Task Main() =>
await new CliApplicationBuilder()
public static partial class Program .AddCommandsFromThisAssembly()
{ .Build()
public static async Task Main() => .RunAsync();
await new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.Build()
.RunAsync();
}
} }

View File

@@ -6,81 +6,80 @@ using FluentAssertions;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace CliFx.Tests namespace CliFx.Tests;
public class ApplicationSpecs : SpecsBase
{ {
public class ApplicationSpecs : SpecsBase public ApplicationSpecs(ITestOutputHelper testOutput)
: base(testOutput)
{ {
public ApplicationSpecs(ITestOutputHelper testOutput) }
: base(testOutput)
{
}
[Fact] [Fact]
public async Task Application_can_be_created_with_minimal_configuration() public async Task Application_can_be_created_with_minimal_configuration()
{ {
// Act // Act
var app = new CliApplicationBuilder() var app = new CliApplicationBuilder()
.AddCommandsFromThisAssembly() .AddCommandsFromThisAssembly()
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
var exitCode = await app.RunAsync( var exitCode = await app.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
} }
[Fact] [Fact]
public async Task Application_can_be_created_with_a_fully_customized_configuration() public async Task Application_can_be_created_with_a_fully_customized_configuration()
{ {
// Act // Act
var app = new CliApplicationBuilder() var app = new CliApplicationBuilder()
.AddCommand<NoOpCommand>() .AddCommand<NoOpCommand>()
.AddCommandsFrom(typeof(NoOpCommand).Assembly) .AddCommandsFrom(typeof(NoOpCommand).Assembly)
.AddCommands(new[] {typeof(NoOpCommand)}) .AddCommands(new[] {typeof(NoOpCommand)})
.AddCommandsFrom(new[] {typeof(NoOpCommand).Assembly}) .AddCommandsFrom(new[] {typeof(NoOpCommand).Assembly})
.AddCommandsFromThisAssembly() .AddCommandsFromThisAssembly()
.AllowDebugMode() .AllowDebugMode()
.AllowPreviewMode() .AllowPreviewMode()
.SetTitle("test") .SetTitle("test")
.SetExecutableName("test") .SetExecutableName("test")
.SetVersion("test") .SetVersion("test")
.SetDescription("test") .SetDescription("test")
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.UseTypeActivator(Activator.CreateInstance!) .UseTypeActivator(Activator.CreateInstance!)
.Build(); .Build();
var exitCode = await app.RunAsync( var exitCode = await app.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
} }
[Fact] [Fact]
public async Task Application_configuration_fails_if_an_invalid_command_is_registered() public async Task Application_configuration_fails_if_an_invalid_command_is_registered()
{ {
// Act // Act
var app = new CliApplicationBuilder() var app = new CliApplicationBuilder()
.AddCommand(typeof(ApplicationSpecs)) .AddCommand(typeof(ApplicationSpecs))
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
var exitCode = await app.RunAsync( var exitCode = await app.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdErr.Should().Contain("not a valid command"); stdErr.Should().Contain("not a valid command");
}
} }
} }

View File

@@ -6,22 +6,22 @@ using FluentAssertions;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace CliFx.Tests namespace CliFx.Tests;
{
public class CancellationSpecs : SpecsBase
{
public CancellationSpecs(ITestOutputHelper testOutput)
: base(testOutput)
{
}
[Fact] public class CancellationSpecs : SpecsBase
public async Task Command_can_register_to_receive_a_cancellation_signal_from_the_console() {
{ public CancellationSpecs(ITestOutputHelper testOutput)
// Arrange : base(testOutput)
var commandType = DynamicCommandBuilder.Compile( {
// language=cs }
@"
[Fact]
public async Task Command_can_register_to_receive_a_cancellation_signal_from_the_console()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -44,24 +44,23 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
FakeConsole.RequestCancellation(TimeSpan.FromSeconds(0.2)); FakeConsole.RequestCancellation(TimeSpan.FromSeconds(0.2));
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdOut.Trim().Should().Be("Cancelled"); stdOut.Trim().Should().Be("Cancelled");
}
} }
} }

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage> <CollectCoverage>true</CollectCoverage>
@@ -13,14 +13,15 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CliWrap" Version="3.3.1" /> <PackageReference Include="Basic.Reference.Assemblies" Version="1.2.4" />
<PackageReference Include="FluentAssertions" Version="5.10.3" /> <PackageReference Include="CliWrap" Version="3.3.3" />
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" /> <PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" /> <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="3.0.3" PrivateAssets="all" /> <PackageReference Include="coverlet.msbuild" Version="3.1.0" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,6 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Infrastructure;
using CliFx.Tests.Utils; using CliFx.Tests.Utils;
using CliWrap; using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;
@@ -8,42 +11,42 @@ using FluentAssertions;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace CliFx.Tests namespace CliFx.Tests;
public class ConsoleSpecs : SpecsBase
{ {
public class ConsoleSpecs : SpecsBase public ConsoleSpecs(ITestOutputHelper testOutput)
: base(testOutput)
{ {
public ConsoleSpecs(ITestOutputHelper testOutput) }
: base(testOutput)
{
}
[Fact] [Fact]
public async Task Real_console_maps_directly_to_system_console() public async Task Real_console_maps_directly_to_system_console()
{ {
// Can't verify our own console output, so using an // Can't verify our own console output, so using an
// external process for this test. // external process for this test.
// Arrange // Arrange
var command = "Hello world" | Cli.Wrap("dotnet") var command = "Hello world" | Cli.Wrap("dotnet")
.WithArguments(a => a .WithArguments(a => a
.Add(Dummy.Program.Location) .Add(Dummy.Program.Location)
.Add("console-test")); .Add("console-test"));
// Act // Act
var result = await command.ExecuteBufferedAsync(); var result = await command.ExecuteBufferedAsync();
// Assert // Assert
result.StandardOutput.Trim().Should().Be("Hello world"); result.StandardOutput.Trim().Should().Be("Hello world");
result.StandardError.Trim().Should().Be("Hello world"); result.StandardError.Trim().Should().Be("Hello world");
} }
[Fact] [Fact]
public async Task Fake_console_does_not_leak_to_system_console() public async Task Fake_console_does_not_leak_to_system_console()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -63,43 +66,43 @@ public class Command : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
Console.OpenStandardInput().Should().NotBe(FakeConsole.Input.BaseStream); Console.OpenStandardInput().Should().NotBeSameAs(FakeConsole.Input.BaseStream);
Console.OpenStandardOutput().Should().NotBe(FakeConsole.Output.BaseStream); Console.OpenStandardOutput().Should().NotBeSameAs(FakeConsole.Output.BaseStream);
Console.OpenStandardError().Should().NotBe(FakeConsole.Error.BaseStream); Console.OpenStandardError().Should().NotBeSameAs(FakeConsole.Error.BaseStream);
Console.ForegroundColor.Should().NotBe(ConsoleColor.DarkMagenta); Console.ForegroundColor.Should().NotBe(ConsoleColor.DarkMagenta);
Console.BackgroundColor.Should().NotBe(ConsoleColor.DarkMagenta); Console.BackgroundColor.Should().NotBe(ConsoleColor.DarkMagenta);
// This fails because tests don't spawn a console window // This fails because tests don't spawn a console window
//Console.CursorLeft.Should().NotBe(42); //Console.CursorLeft.Should().NotBe(42);
//Console.CursorTop.Should().NotBe(24); //Console.CursorTop.Should().NotBe(24);
FakeConsole.IsInputRedirected.Should().BeTrue(); FakeConsole.IsInputRedirected.Should().BeTrue();
FakeConsole.IsOutputRedirected.Should().BeTrue(); FakeConsole.IsOutputRedirected.Should().BeTrue();
FakeConsole.IsErrorRedirected.Should().BeTrue(); FakeConsole.IsErrorRedirected.Should().BeTrue();
} }
[Fact] [Fact]
public async Task Fake_console_can_be_used_with_an_in_memory_backing_store() public async Task Fake_console_can_be_used_with_an_in_memory_backing_store()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -114,26 +117,42 @@ public class Command : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
FakeConsole.WriteInput("Hello world"); FakeConsole.WriteInput("Hello world");
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be("Hello world"); stdOut.Trim().Should().Be("Hello world");
stdErr.Trim().Should().Be("Hello world"); stdErr.Trim().Should().Be("Hello world");
} }
[Fact]
public void Console_does_not_emit_preamble_when_used_with_encoding_that_has_it()
{
// Arrange
using var buffer = new MemoryStream();
using var consoleWriter = new ConsoleWriter(FakeConsole, buffer, Encoding.UTF8);
// Act
consoleWriter.Write("Hello world");
consoleWriter.Flush();
var output = consoleWriter.Encoding.GetString(buffer.ToArray());
// Assert
output.Should().Be("Hello world");
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -10,71 +10,71 @@ using FluentAssertions;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace CliFx.Tests namespace CliFx.Tests;
public class DirectivesSpecs : SpecsBase
{ {
public class DirectivesSpecs : SpecsBase public DirectivesSpecs(ITestOutputHelper testOutput)
: base(testOutput)
{ {
public DirectivesSpecs(ITestOutputHelper testOutput) }
: base(testOutput)
[Fact]
public async Task Debug_directive_can_be_specified_to_interrupt_execution_until_a_debugger_is_attached()
{
// Arrange
var stdOutBuffer = new StringBuilder();
var command = Cli.Wrap("dotnet")
.WithArguments(a => a
.Add(Dummy.Program.Location)
.Add("[debug]")) | stdOutBuffer;
// Act
try
{ {
} // This has a timeout just in case the execution hangs forever
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
[Fact] var task = command.ExecuteAsync(cts.Token);
public async Task Debug_directive_can_be_specified_to_interrupt_execution_until_a_debugger_is_attached()
{
// Arrange
var stdOutBuffer = new StringBuilder();
var command = Cli.Wrap("dotnet") // We can't attach a debugger programmatically, so the application
.WithArguments(a => a // will hang indefinitely.
.Add(Dummy.Program.Location) // To work around it, we will wait until the application writes
.Add("[debug]")) | stdOutBuffer; // something to the standard output and then kill it.
while (true)
// Act
try
{ {
// This has a timeout just in case the execution hangs forever if (stdOutBuffer.Length > 0)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var task = command.ExecuteAsync(cts.Token);
// We can't attach a debugger programmatically, so the application
// will hang indefinitely.
// To work around it, we will wait until the application writes
// something to the standard output and then kill it.
while (true)
{ {
if (stdOutBuffer.Length > 0) cts.Cancel();
{ break;
cts.Cancel();
break;
}
await Task.Delay(100, cts.Token);
} }
await task; await Task.Delay(100, cts.Token);
}
catch (OperationCanceledException)
{
// It's expected to fail
} }
var stdOut = stdOutBuffer.ToString(); await task;
}
// Assert catch (OperationCanceledException)
stdOut.Should().Contain("Attach debugger to"); {
// It's expected to fail
TestOutput.WriteLine(stdOut);
} }
[Fact] var stdOut = stdOutBuffer.ToString();
public async Task Preview_directive_can_be_specified_to_print_command_input()
{ // Assert
// Arrange stdOut.Should().Contain("Attach debugger to");
var commandType = DynamicCommandBuilder.Compile(
// language=cs TestOutput.WriteLine(stdOut);
@" }
[Fact]
public async Task Preview_directive_can_be_specified_to_print_command_input()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
[Command(""cmd"")] [Command(""cmd"")]
public class Command : ICommand public class Command : ICommand
{ {
@@ -82,31 +82,30 @@ public class Command : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.AllowPreviewMode() .AllowPreviewMode()
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"[preview]", "cmd", "param", "-abc", "--option", "foo"}, new[] {"[preview]", "cmd", "param", "-abc", "--option", "foo"},
new Dictionary<string, string> new Dictionary<string, string>
{ {
["ENV_QOP"] = "hello", ["ENV_QOP"] = "hello",
["ENV_KIL"] = "world" ["ENV_KIL"] = "world"
} }
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ContainAllInOrder( stdOut.Should().ContainAllInOrder(
"cmd", "<param>", "[-a]", "[-b]", "[-c]", "[--option \"foo\"]", "cmd", "<param>", "[-a]", "[-b]", "[-c]", "[--option \"foo\"]",
"ENV_QOP", "=", "\"hello\"", "ENV_QOP", "=", "\"hello\"",
"ENV_KIL", "=", "\"world\"" "ENV_KIL", "=", "\"world\""
); );
}
} }
} }

View File

@@ -10,22 +10,22 @@ using FluentAssertions;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace CliFx.Tests namespace CliFx.Tests;
{
public class EnvironmentSpecs : SpecsBase
{
public EnvironmentSpecs(ITestOutputHelper testOutput)
: base(testOutput)
{
}
[Fact] public class EnvironmentSpecs : SpecsBase
public async Task Option_can_fall_back_to_an_environment_variable() {
{ public EnvironmentSpecs(ITestOutputHelper testOutput)
// Arrange : base(testOutput)
var commandType = DynamicCommandBuilder.Compile( {
// language=cs }
@"
[Fact]
public async Task Option_can_fall_back_to_an_environment_variable()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -40,34 +40,34 @@ public class Command : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string> new Dictionary<string, string>
{ {
["ENV_FOO"] = "bar" ["ENV_FOO"] = "bar"
} }
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be("bar"); stdOut.Trim().Should().Be("bar");
} }
[Fact] [Fact]
public async Task Option_does_not_fall_back_to_an_environment_variable_if_a_value_is_provided_through_arguments() public async Task Option_does_not_fall_back_to_an_environment_variable_if_a_value_is_provided_through_arguments()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -82,34 +82,34 @@ public class Command : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"--foo", "baz"}, new[] {"--foo", "baz"},
new Dictionary<string, string> new Dictionary<string, string>
{ {
["ENV_FOO"] = "bar" ["ENV_FOO"] = "bar"
} }
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be("baz"); stdOut.Trim().Should().Be("baz");
} }
[Fact] [Fact]
public async Task Option_of_non_scalar_type_can_receive_multiple_values_from_an_environment_variable() public async Task Option_of_non_scalar_type_can_receive_multiple_values_from_an_environment_variable()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -126,37 +126,37 @@ public class Command : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string> new Dictionary<string, string>
{ {
["ENV_FOO"] = $"bar{Path.PathSeparator}baz" ["ENV_FOO"] = $"bar{Path.PathSeparator}baz"
} }
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines( stdOut.Should().ConsistOfLines(
"bar", "bar",
"baz" "baz"
); );
} }
[Fact] [Fact]
public async Task Option_of_scalar_type_always_receives_a_single_value_from_an_environment_variable() public async Task Option_of_scalar_type_always_receives_a_single_value_from_an_environment_variable()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -171,34 +171,34 @@ public class Command : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string> new Dictionary<string, string>
{ {
["ENV_FOO"] = $"bar{Path.PathSeparator}baz" ["ENV_FOO"] = $"bar{Path.PathSeparator}baz"
} }
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be($"bar{Path.PathSeparator}baz"); stdOut.Trim().Should().Be($"bar{Path.PathSeparator}baz");
} }
[Fact] [Fact]
public async Task Environment_variables_are_matched_case_sensitively() public async Task Environment_variables_are_matched_case_sensitively()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -213,48 +213,47 @@ public class Command : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string> new Dictionary<string, string>
{ {
["ENV_foo"] = "baz", ["ENV_foo"] = "baz",
["ENV_FOO"] = "bar", ["ENV_FOO"] = "bar",
["env_FOO"] = "qop" ["env_FOO"] = "qop"
} }
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be("bar"); stdOut.Trim().Should().Be("bar");
} }
[Fact] [Fact]
public async Task Environment_variables_are_extracted_automatically() public async Task Environment_variables_are_extracted_automatically()
{ {
// Ensures that the environment variables are properly obtained from // Ensures that the environment variables are properly obtained from
// System.Environment when they are not provided explicitly to CliApplication. // System.Environment when they are not provided explicitly to CliApplication.
// Arrange // Arrange
var command = Cli.Wrap("dotnet") var command = Cli.Wrap("dotnet")
.WithArguments(a => a .WithArguments(a => a
.Add(Dummy.Program.Location) .Add(Dummy.Program.Location)
.Add("env-test")) .Add("env-test"))
.WithEnvironmentVariables(e => e .WithEnvironmentVariables(e => e
.Set("ENV_TARGET", "Mars")); .Set("ENV_TARGET", "Mars"));
// Act // Act
var result = await command.ExecuteBufferedAsync(); var result = await command.ExecuteBufferedAsync();
// Assert // Assert
result.StandardOutput.Trim().Should().Be("Hello Mars!"); result.StandardOutput.Trim().Should().Be("Hello Mars!");
}
} }
} }

View File

@@ -7,22 +7,22 @@ using FluentAssertions;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace CliFx.Tests namespace CliFx.Tests;
{
public class ErrorReportingSpecs : SpecsBase
{
public ErrorReportingSpecs(ITestOutputHelper testOutput)
: base(testOutput)
{
}
[Fact] public class ErrorReportingSpecs : SpecsBase
public async Task Command_can_throw_an_exception_which_exits_with_a_stacktrace() {
{ public ErrorReportingSpecs(ITestOutputHelper testOutput)
// Arrange : base(testOutput)
var commandType = DynamicCommandBuilder.Compile( {
// language=cs }
@"
[Fact]
public async Task Command_can_throw_an_exception_which_exits_with_a_stacktrace()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -31,36 +31,36 @@ public class Command : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdOut.Should().BeEmpty(); stdOut.Should().BeEmpty();
stdErr.Should().ContainAllInOrder( stdErr.Should().ContainAllInOrder(
"System.Exception", "Something went wrong", "System.Exception", "Something went wrong",
"at", "CliFx." "at", "CliFx."
); );
} }
[Fact] [Fact]
public async Task Command_can_throw_an_exception_with_an_inner_exception_which_exits_with_a_stacktrace() public async Task Command_can_throw_an_exception_with_an_inner_exception_which_exits_with_a_stacktrace()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -69,37 +69,37 @@ public class Command : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdOut.Should().BeEmpty(); stdOut.Should().BeEmpty();
stdErr.Should().ContainAllInOrder( stdErr.Should().ContainAllInOrder(
"System.Exception", "Something went wrong", "System.Exception", "Something went wrong",
"System.Exception", "Another exception", "System.Exception", "Another exception",
"at", "CliFx." "at", "CliFx."
); );
} }
[Fact] [Fact]
public async Task Command_can_throw_a_special_exception_which_exits_with_specified_code_and_message() public async Task Command_can_throw_a_special_exception_which_exits_with_specified_code_and_message()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -108,33 +108,33 @@ public class Command : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().Be(69); exitCode.Should().Be(69);
stdOut.Should().BeEmpty(); stdOut.Should().BeEmpty();
stdErr.Trim().Should().Be("Something went wrong"); stdErr.Trim().Should().Be("Something went wrong");
} }
[Fact] [Fact]
public async Task Command_can_throw_a_special_exception_without_message_which_exits_with_a_stacktrace() public async Task Command_can_throw_a_special_exception_without_message_which_exits_with_a_stacktrace()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -143,36 +143,36 @@ public class Command : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().Be(69); exitCode.Should().Be(69);
stdOut.Should().BeEmpty(); stdOut.Should().BeEmpty();
stdErr.Should().ContainAllInOrder( stdErr.Should().ContainAllInOrder(
"CliFx.Exceptions.CommandException", "CliFx.Exceptions.CommandException",
"at", "CliFx." "at", "CliFx."
); );
} }
[Fact] [Fact]
public async Task Command_can_throw_a_special_exception_which_prints_help_text_before_exiting() public async Task Command_can_throw_a_special_exception_which_prints_help_text_before_exiting()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -181,25 +181,24 @@ public class Command : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.SetDescription("This will be in help text") .SetDescription("This will be in help text")
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().Be(69); exitCode.Should().Be(69);
stdOut.Should().Contain("This will be in help text"); stdOut.Should().Contain("This will be in help text");
stdErr.Trim().Should().Be("Something went wrong"); stdErr.Trim().Should().Be("Something went wrong");
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -7,22 +7,22 @@ using FluentAssertions;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace CliFx.Tests namespace CliFx.Tests;
{
public class OptionBindingSpecs : SpecsBase
{
public OptionBindingSpecs(ITestOutputHelper testOutput)
: base(testOutput)
{
}
[Fact] public class OptionBindingSpecs : SpecsBase
public async Task Option_is_bound_from_an_argument_matching_its_name() {
{ public OptionBindingSpecs(ITestOutputHelper testOutput)
// Arrange : base(testOutput)
var commandType = DynamicCommandBuilder.Compile( {
// language=cs }
@"
[Fact]
public async Task Option_is_bound_from_an_argument_matching_its_name()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -36,31 +36,31 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"--foo"}, new[] {"--foo"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be("True"); stdOut.Trim().Should().Be("True");
} }
[Fact] [Fact]
public async Task Option_is_bound_from_an_argument_matching_its_short_name() public async Task Option_is_bound_from_an_argument_matching_its_short_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -74,31 +74,31 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f"}, new[] {"-f"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be("True"); stdOut.Trim().Should().Be("True");
} }
[Fact] [Fact]
public async Task Option_is_bound_from_a_set_of_arguments_matching_its_name() public async Task Option_is_bound_from_a_set_of_arguments_matching_its_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -117,34 +117,34 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"--foo", "one", "--bar", "two"}, new[] {"--foo", "one", "--bar", "two"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines( stdOut.Should().ConsistOfLines(
"Foo = one", "Foo = one",
"Bar = two" "Bar = two"
); );
} }
[Fact] [Fact]
public async Task Option_is_bound_from_a_set_of_arguments_matching_its_short_name() public async Task Option_is_bound_from_a_set_of_arguments_matching_its_short_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -163,34 +163,34 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "one", "-b", "two"}, new[] {"-f", "one", "-b", "two"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines( stdOut.Should().ConsistOfLines(
"Foo = one", "Foo = one",
"Bar = two" "Bar = two"
); );
} }
[Fact] [Fact]
public async Task Option_is_bound_from_a_stack_of_arguments_matching_its_short_name() public async Task Option_is_bound_from_a_stack_of_arguments_matching_its_short_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -209,34 +209,34 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-fb", "value"}, new[] {"-fb", "value"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines( stdOut.Should().ConsistOfLines(
"Foo = ", "Foo = ",
"Bar = value" "Bar = value"
); );
} }
[Fact] [Fact]
public async Task Option_of_non_scalar_type_is_bound_from_a_set_of_arguments_matching_its_name() public async Task Option_of_non_scalar_type_is_bound_from_a_set_of_arguments_matching_its_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -252,35 +252,35 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"--foo", "one", "two", "three"}, new[] {"--foo", "one", "two", "three"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines( stdOut.Should().ConsistOfLines(
"one", "one",
"two", "two",
"three" "three"
); );
} }
[Fact] [Fact]
public async Task Option_of_non_scalar_type_is_bound_from_a_set_of_arguments_matching_its_short_name() public async Task Option_of_non_scalar_type_is_bound_from_a_set_of_arguments_matching_its_short_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -296,35 +296,35 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "one", "two", "three"}, new[] {"-f", "one", "two", "three"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines( stdOut.Should().ConsistOfLines(
"one", "one",
"two", "two",
"three" "three"
); );
} }
[Fact] [Fact]
public async Task Option_of_non_scalar_type_is_bound_from_multiple_sets_of_arguments_matching_its_name() public async Task Option_of_non_scalar_type_is_bound_from_multiple_sets_of_arguments_matching_its_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -340,35 +340,35 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"--foo", "one", "--foo", "two", "--foo", "three"}, new[] {"--foo", "one", "--foo", "two", "--foo", "three"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines( stdOut.Should().ConsistOfLines(
"one", "one",
"two", "two",
"three" "three"
); );
} }
[Fact] [Fact]
public async Task Option_of_non_scalar_type_is_bound_from_multiple_sets_of_arguments_matching_its_short_name() public async Task Option_of_non_scalar_type_is_bound_from_multiple_sets_of_arguments_matching_its_short_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -384,35 +384,35 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "one", "-f", "two", "-f", "three"}, new[] {"-f", "one", "-f", "two", "-f", "three"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines( stdOut.Should().ConsistOfLines(
"one", "one",
"two", "two",
"three" "three"
); );
} }
[Fact] [Fact]
public async Task Option_of_non_scalar_type_is_bound_from_multiple_sets_of_arguments_matching_its_name_or_short_name() public async Task Option_of_non_scalar_type_is_bound_from_multiple_sets_of_arguments_matching_its_name_or_short_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -428,35 +428,35 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"--foo", "one", "-f", "two", "--foo", "three"}, new[] {"--foo", "one", "-f", "two", "--foo", "three"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines( stdOut.Should().ConsistOfLines(
"one", "one",
"two", "two",
"three" "three"
); );
} }
[Fact] [Fact]
public async Task Option_is_not_bound_if_there_are_no_arguments_matching_its_name_or_short_name() public async Task Option_is_not_bound_if_there_are_no_arguments_matching_its_name_or_short_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -475,34 +475,34 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"--foo", "one"}, new[] {"--foo", "one"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines( stdOut.Should().ConsistOfLines(
"Foo = one", "Foo = one",
"Bar = hello" "Bar = hello"
); );
} }
[Fact] [Fact]
public async Task Option_binding_does_not_consider_a_negative_number_as_an_option_name_or_short_name() public async Task Option_binding_does_not_consider_a_negative_number_as_an_option_name_or_short_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -517,31 +517,31 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"--foo", "-13"}, new[] {"--foo", "-13"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be("-13"); stdOut.Trim().Should().Be("-13");
} }
[Fact] [Fact]
public async Task Option_binding_fails_if_a_required_option_has_not_been_provided() public async Task Option_binding_fails_if_a_required_option_has_not_been_provided()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -551,31 +551,31 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdErr.Should().Contain("Missing required option(s)"); stdErr.Should().Contain("Missing required option(s)");
} }
[Fact] [Fact]
public async Task Option_binding_fails_if_a_required_option_has_been_provided_with_an_empty_value() public async Task Option_binding_fails_if_a_required_option_has_been_provided_with_an_empty_value()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -585,31 +585,31 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"--foo"}, new[] {"--foo"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdErr.Should().Contain("Missing required option(s)"); stdErr.Should().Contain("Missing required option(s)");
} }
[Fact] [Fact]
public async Task Option_binding_fails_if_a_required_option_of_non_scalar_type_has_not_been_provided_with_at_least_one_value() public async Task Option_binding_fails_if_a_required_option_of_non_scalar_type_has_not_been_provided_with_at_least_one_value()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -619,31 +619,31 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"--foo"}, new[] {"--foo"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdErr.Should().Contain("Missing required option(s)"); stdErr.Should().Contain("Missing required option(s)");
} }
[Fact] [Fact]
public async Task Option_binding_fails_if_one_of_the_provided_option_names_is_not_recognized() public async Task Option_binding_fails_if_one_of_the_provided_option_names_is_not_recognized()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -653,31 +653,31 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"--foo", "one", "--bar", "two"}, new[] {"--foo", "one", "--bar", "two"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdErr.Should().Contain("Unrecognized option(s)"); stdErr.Should().Contain("Unrecognized option(s)");
} }
[Fact] [Fact]
public async Task Option_binding_fails_if_an_option_of_scalar_type_has_been_provided_with_multiple_values() public async Task Option_binding_fails_if_an_option_of_scalar_type_has_been_provided_with_multiple_values()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -687,22 +687,21 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"--foo", "one", "two", "three"}, new[] {"--foo", "one", "two", "three"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdErr.Should().Contain("expects a single argument, but provided with multiple"); stdErr.Should().Contain("expects a single argument, but provided with multiple");
}
} }
} }

View File

@@ -6,22 +6,22 @@ using FluentAssertions;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace CliFx.Tests namespace CliFx.Tests;
{
public class ParameterBindingSpecs : SpecsBase
{
public ParameterBindingSpecs(ITestOutputHelper testOutput)
: base(testOutput)
{
}
[Fact] public class ParameterBindingSpecs : SpecsBase
public async Task Parameter_is_bound_from_an_argument_matching_its_order() {
{ public ParameterBindingSpecs(ITestOutputHelper testOutput)
// Arrange : base(testOutput)
var commandType = DynamicCommandBuilder.Compile( {
// language=cs }
@"
[Fact]
public async Task Parameter_is_bound_from_an_argument_matching_its_order()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -40,34 +40,34 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"one", "two"}, new[] {"one", "two"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines( stdOut.Should().ConsistOfLines(
"Foo = one", "Foo = one",
"Bar = two" "Bar = two"
); );
} }
[Fact] [Fact]
public async Task Parameter_of_non_scalar_type_is_bound_from_remaining_non_option_arguments() public async Task Parameter_of_non_scalar_type_is_bound_from_remaining_non_option_arguments()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -95,37 +95,37 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"one", "two", "three", "four", "five", "--boo", "xxx"}, new[] {"one", "two", "three", "four", "five", "--boo", "xxx"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines( stdOut.Should().ConsistOfLines(
"Foo = one", "Foo = one",
"Bar = two", "Bar = two",
"Baz = three", "Baz = three",
"Baz = four", "Baz = four",
"Baz = five" "Baz = five"
); );
} }
[Fact] [Fact]
public async Task Parameter_binding_fails_if_one_of_the_parameters_has_not_been_provided() public async Task Parameter_binding_fails_if_one_of_the_parameters_has_not_been_provided()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -138,31 +138,31 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"one"}, new[] {"one"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdErr.Should().Contain("Missing parameter(s)"); stdErr.Should().Contain("Missing parameter(s)");
} }
[Fact] [Fact]
public async Task Parameter_binding_fails_if_a_parameter_of_non_scalar_type_has_not_been_provided_with_at_least_one_value() public async Task Parameter_binding_fails_if_a_parameter_of_non_scalar_type_has_not_been_provided_with_at_least_one_value()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -175,31 +175,31 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"one"}, new[] {"one"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdErr.Should().Contain("Missing parameter(s)"); stdErr.Should().Contain("Missing parameter(s)");
} }
[Fact] [Fact]
public async Task Parameter_binding_fails_if_one_of_the_provided_parameters_is_unexpected() public async Task Parameter_binding_fails_if_one_of_the_provided_parameters_is_unexpected()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -212,22 +212,21 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"one", "two", "three"}, new[] {"one", "two", "three"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdErr.Should().Contain("Unexpected parameter(s)"); stdErr.Should().Contain("Unexpected parameter(s)");
}
} }
} }

View File

@@ -6,22 +6,22 @@ using FluentAssertions;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace CliFx.Tests namespace CliFx.Tests;
{
public class RoutingSpecs : SpecsBase
{
public RoutingSpecs(ITestOutputHelper testOutput)
: base(testOutput)
{
}
[Fact] public class RoutingSpecs : SpecsBase
public async Task Default_command_is_executed_if_provided_arguments_do_not_match_any_named_command() {
{ public RoutingSpecs(ITestOutputHelper testOutput)
// Arrange : base(testOutput)
var commandTypes = DynamicCommandBuilder.CompileMany( {
// language=cs }
@"
[Fact]
public async Task Default_command_is_executed_if_provided_arguments_do_not_match_any_named_command()
{
// Arrange
var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs
@"
[Command] [Command]
public class DefaultCommand : ICommand public class DefaultCommand : ICommand
{ {
@@ -53,31 +53,31 @@ public class NamedChildCommand : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommands(commandTypes) .AddCommands(commandTypes)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be("default"); stdOut.Trim().Should().Be("default");
} }
[Fact] [Fact]
public async Task Specific_named_command_is_executed_if_provided_arguments_match_its_name() public async Task Specific_named_command_is_executed_if_provided_arguments_match_its_name()
{ {
// Arrange // Arrange
var commandTypes = DynamicCommandBuilder.CompileMany( var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs // language=cs
@" @"
[Command] [Command]
public class DefaultCommand : ICommand public class DefaultCommand : ICommand
{ {
@@ -109,31 +109,31 @@ public class NamedChildCommand : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommands(commandTypes) .AddCommands(commandTypes)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"cmd"}, new[] {"cmd"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be("cmd"); stdOut.Trim().Should().Be("cmd");
} }
[Fact] [Fact]
public async Task Specific_named_child_command_is_executed_if_provided_arguments_match_its_name() public async Task Specific_named_child_command_is_executed_if_provided_arguments_match_its_name()
{ {
// Arrange // Arrange
var commandTypes = DynamicCommandBuilder.CompileMany( var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs // language=cs
@" @"
[Command] [Command]
public class DefaultCommand : ICommand public class DefaultCommand : ICommand
{ {
@@ -165,22 +165,21 @@ public class NamedChildCommand : ICommand
} }
"); ");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommands(commandTypes) .AddCommands(commandTypes)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"cmd", "child"}, new[] {"cmd", "child"},
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be("cmd child"); stdOut.Trim().Should().Be("cmd child");
}
} }
} }

View File

@@ -3,21 +3,20 @@ using CliFx.Infrastructure;
using CliFx.Tests.Utils.Extensions; using CliFx.Tests.Utils.Extensions;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace CliFx.Tests namespace CliFx.Tests;
public abstract class SpecsBase : IDisposable
{ {
public abstract class SpecsBase : IDisposable public ITestOutputHelper TestOutput { get; }
public FakeInMemoryConsole FakeConsole { get; } = new();
protected SpecsBase(ITestOutputHelper testOutput) =>
TestOutput = testOutput;
public void Dispose()
{ {
public ITestOutputHelper TestOutput { get; } FakeConsole.DumpToTestOutput(TestOutput);
FakeConsole.Dispose();
public FakeInMemoryConsole FakeConsole { get; } = new();
protected SpecsBase(ITestOutputHelper testOutput) =>
TestOutput = testOutput;
public void Dispose()
{
FakeConsole.DumpToTestOutput(TestOutput);
FakeConsole.Dispose();
}
} }
} }

View File

@@ -7,22 +7,22 @@ using FluentAssertions;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace CliFx.Tests namespace CliFx.Tests;
{
public class TypeActivationSpecs : SpecsBase
{
public TypeActivationSpecs(ITestOutputHelper testOutput)
: base(testOutput)
{
}
[Fact] public class TypeActivationSpecs : SpecsBase
public async Task Default_type_activator_can_initialize_a_type_if_it_has_a_parameterless_constructor() {
{ public TypeActivationSpecs(ITestOutputHelper testOutput)
// Arrange : base(testOutput)
var commandType = DynamicCommandBuilder.Compile( {
// language=cs }
@"
[Fact]
public async Task Default_type_activator_can_initialize_a_type_if_it_has_a_parameterless_constructor()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -33,32 +33,32 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.UseTypeActivator(new DefaultTypeActivator()) .UseTypeActivator(new DefaultTypeActivator())
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be("foo"); stdOut.Trim().Should().Be("foo");
} }
[Fact] [Fact]
public async Task Default_type_activator_fails_if_the_type_does_not_have_a_parameterless_constructor() public async Task Default_type_activator_fails_if_the_type_does_not_have_a_parameterless_constructor()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -67,32 +67,32 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.UseTypeActivator(new DefaultTypeActivator()) .UseTypeActivator(new DefaultTypeActivator())
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdErr.Should().Contain("Failed to create an instance of type"); stdErr.Should().Contain("Failed to create an instance of type");
} }
[Fact] [Fact]
public async Task Delegate_type_activator_can_initialize_a_type_using_a_custom_function() public async Task Delegate_type_activator_can_initialize_a_type_using_a_custom_function()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -107,32 +107,32 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.UseTypeActivator(type => Activator.CreateInstance(type, "hello world")!) .UseTypeActivator(type => Activator.CreateInstance(type, "hello world")!)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be("hello world"); stdOut.Trim().Should().Be("hello world");
} }
[Fact] [Fact]
public async Task Delegate_type_activator_fails_if_the_underlying_function_returns_null() public async Task Delegate_type_activator_fails_if_the_underlying_function_returns_null()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // language=cs
@" @"
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
@@ -143,23 +143,22 @@ public class Command : ICommand
} }
}"); }");
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.UseTypeActivator(_ => null!) .UseTypeActivator(_ => null!)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), Array.Empty<string>(),
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdErr.Should().Contain("Failed to create an instance of type"); stdErr.Should().Contain("Failed to create an instance of type");
}
} }
} }

View File

@@ -3,140 +3,133 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Basic.Reference.Assemblies;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text;
namespace CliFx.Tests.Utils namespace CliFx.Tests.Utils;
// This class uses Roslyn to compile commands dynamically.
//
// It allows us to collocate commands with tests more
// easily, which helps a lot when reasoning about them.
// Unfortunately, this comes at a cost of static typing,
// but this is still a worthwhile trade off.
//
// Maybe one day C# will allow declaring classes inside
// methods and doing this will no longer be necessary.
// Language proposal: https://github.com/dotnet/csharplang/discussions/130
internal static class DynamicCommandBuilder
{ {
// This class uses Roslyn to compile commands dynamically. public static IReadOnlyList<Type> CompileMany(string sourceCode)
//
// It allows us to collocate commands with tests more
// easily, which helps a lot when reasoning about them.
// Unfortunately, this comes at a cost of static typing,
// but this is still a worthwhile trade off.
//
// Maybe one day C# will allow declaring classes inside
// methods and doing this will no longer be necessary.
// Language proposal: https://github.com/dotnet/csharplang/discussions/130
internal static class DynamicCommandBuilder
{ {
public static IReadOnlyList<Type> CompileMany(string sourceCode) // Get default system namespaces
var defaultSystemNamespaces = new[]
{ {
// Get default system namespaces "System",
var defaultSystemNamespaces = new[] "System.Collections",
{ "System.Collections.Generic",
"System", "System.Linq",
"System.Collections", "System.Threading.Tasks",
"System.Collections.Generic", "System.Globalization"
"System.Linq", };
"System.Threading.Tasks",
"System.Globalization"
};
// Get default CliFx namespaces // Get default CliFx namespaces
var defaultCliFxNamespaces = typeof(ICommand) var defaultCliFxNamespaces = typeof(ICommand)
.Assembly .Assembly
.GetTypes() .GetTypes()
.Where(t => t.IsPublic) .Where(t => t.IsPublic)
.Select(t => t.Namespace) .Select(t => t.Namespace)
.Distinct() .Distinct()
.ToArray(); .ToArray();
// Append default imports to the source code // Append default imports to the source code
var sourceCodeWithUsings = var sourceCodeWithUsings =
string.Join(Environment.NewLine, defaultSystemNamespaces.Select(n => $"using {n};")) + string.Join(Environment.NewLine, defaultSystemNamespaces.Select(n => $"using {n};")) +
string.Join(Environment.NewLine, defaultCliFxNamespaces.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))
.Append(MetadataReference.CreateFromFile(typeof(DynamicCommandBuilder).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 + Environment.NewLine +
sourceCode; string.Join(Environment.NewLine, compilationErrors.Select(e => e.ToString()))
// 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},
new[]
{
MetadataReference.CreateFromFile(Assembly.Load("netstandard").Location),
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location),
MetadataReference.CreateFromFile(typeof(DynamicCommandBuilder).Assembly.Location),
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()))
);
}
// Emit the code to an in-memory buffer
using var buffer = new MemoryStream();
var emit = compilation.Emit(buffer);
var emitErrors = emit
.Diagnostics
.Where(d => d.Severity >= DiagnosticSeverity.Error)
.ToArray();
if (emitErrors.Any())
{
throw new InvalidOperationException(
"Failed to emit code." +
Environment.NewLine +
string.Join(Environment.NewLine, emitErrors.Select(e => e.ToString()))
);
}
// Load the generated assembly
var generatedAssembly = Assembly.Load(buffer.ToArray());
// Return all defined commands
var commandTypes = generatedAssembly
.GetTypes()
.Where(t => t.IsAssignableTo(typeof(ICommand)))
.ToArray();
if (commandTypes.Length <= 0)
{
throw new InvalidOperationException(
"There are no command definitions in the provide source code."
);
}
return commandTypes;
} }
public static Type Compile(string sourceCode) // Emit the code to an in-memory buffer
using var buffer = new MemoryStream();
var emit = compilation.Emit(buffer);
var emitErrors = emit
.Diagnostics
.Where(d => d.Severity >= DiagnosticSeverity.Error)
.ToArray();
if (emitErrors.Any())
{ {
var commandTypes = CompileMany(sourceCode); throw new InvalidOperationException(
"Failed to emit code." +
if (commandTypes.Count > 1) Environment.NewLine +
{ string.Join(Environment.NewLine, emitErrors.Select(e => e.ToString()))
throw new InvalidOperationException( );
"There are more than one command definitions in the provide source code."
);
}
return commandTypes.Single();
} }
// Load the generated assembly
var generatedAssembly = Assembly.Load(buffer.ToArray());
// Return all defined commands
var commandTypes = generatedAssembly
.GetTypes()
.Where(t => t.IsAssignableTo(typeof(ICommand)) && !t.IsAbstract)
.ToArray();
if (commandTypes.Length <= 0)
{
throw new InvalidOperationException(
"There are no command definitions in the provided source code."
);
}
return commandTypes;
}
public static Type Compile(string sourceCode)
{
var commandTypes = CompileMany(sourceCode);
if (commandTypes.Count > 1)
{
throw new InvalidOperationException(
"There are more than one command definitions in the provided source code."
);
}
return commandTypes.Single();
} }
} }

View File

@@ -1,54 +1,51 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using FluentAssertions; using FluentAssertions;
using FluentAssertions.Collections;
using FluentAssertions.Execution; using FluentAssertions.Execution;
using FluentAssertions.Primitives; using FluentAssertions.Primitives;
namespace CliFx.Tests.Utils.Extensions namespace CliFx.Tests.Utils.Extensions;
internal static class AssertionExtensions
{ {
internal static class AssertionExtensions public static void ConsistOfLines(
this StringAssertions assertions,
IEnumerable<string> lines)
{ {
public static AndConstraint<StringCollectionAssertions> ConsistOfLines( var actualLines = assertions.Subject.Split(new[] {'\n', '\r'}, StringSplitOptions.RemoveEmptyEntries);
this StringAssertions assertions, actualLines.Should().Equal(lines);
IEnumerable<string> lines) }
public static void ConsistOfLines(
this StringAssertions assertions,
params string[] lines) =>
assertions.ConsistOfLines((IEnumerable<string>) lines);
public static AndConstraint<StringAssertions> ContainAllInOrder(
this StringAssertions assertions,
IEnumerable<string> values)
{
var lastIndex = 0;
foreach (var value in values)
{ {
var actualLines = assertions.Subject.Split(new[] {'\n', '\r'}, StringSplitOptions.RemoveEmptyEntries); var index = assertions.Subject.IndexOf(value, lastIndex, StringComparison.Ordinal);
return actualLines.Should().Equal(lines); if (index < 0)
}
public static AndConstraint<StringCollectionAssertions> ConsistOfLines(
this StringAssertions assertions,
params string[] lines) =>
assertions.ConsistOfLines((IEnumerable<string>) lines);
public static AndConstraint<StringAssertions> ContainAllInOrder(
this StringAssertions assertions,
IEnumerable<string> values)
{
var lastIndex = 0;
foreach (var value in values)
{ {
var index = assertions.Subject.IndexOf(value, lastIndex, StringComparison.Ordinal); Execute.Assertion.FailWith(
$"Expected string '{assertions.Subject}' to contain '{value}' after position {lastIndex}."
if (index < 0) );
{
Execute.Assertion.FailWith(
$"Expected string '{assertions.Subject}' to contain '{value}' after position {lastIndex}."
);
}
lastIndex = index;
} }
return new(assertions); lastIndex = index;
} }
public static AndConstraint<StringAssertions> ContainAllInOrder( return new(assertions);
this StringAssertions assertions,
params string[] values) =>
assertions.ContainAllInOrder((IEnumerable<string>) values);
} }
public static AndConstraint<StringAssertions> ContainAllInOrder(
this StringAssertions assertions,
params string[] values) =>
assertions.ContainAllInOrder((IEnumerable<string>) values);
} }

View File

@@ -1,17 +1,16 @@
using CliFx.Infrastructure; using CliFx.Infrastructure;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace CliFx.Tests.Utils.Extensions namespace CliFx.Tests.Utils.Extensions;
{
internal static class ConsoleExtensions
{
public static void DumpToTestOutput(this FakeInMemoryConsole console, ITestOutputHelper testOutputHelper)
{
testOutputHelper.WriteLine("[*] Captured standard output:");
testOutputHelper.WriteLine(console.ReadOutputString());
testOutputHelper.WriteLine("[*] Captured standard error:"); internal static class ConsoleExtensions
testOutputHelper.WriteLine(console.ReadErrorString()); {
} public static void DumpToTestOutput(this FakeInMemoryConsole console, ITestOutputHelper testOutputHelper)
{
testOutputHelper.WriteLine("[*] Captured standard output:");
testOutputHelper.WriteLine(console.ReadOutputString());
testOutputHelper.WriteLine("[*] Captured standard error:");
testOutputHelper.WriteLine(console.ReadErrorString());
} }
} }

View File

@@ -2,11 +2,10 @@
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Infrastructure; using CliFx.Infrastructure;
namespace CliFx.Tests.Utils namespace CliFx.Tests.Utils;
[Command]
public class NoOpCommand : ICommand
{ {
[Command] public ValueTask ExecuteAsync(IConsole console) => default;
public class NoOpCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
} }

View File

@@ -1,39 +1,38 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace CliFx namespace CliFx;
/// <summary>
/// Configuration of an application.
/// </summary>
public class ApplicationConfiguration
{ {
/// <summary> /// <summary>
/// Configuration of an application. /// Command types defined in this application.
/// </summary> /// </summary>
public class ApplicationConfiguration public IReadOnlyList<Type> CommandTypes { get; }
/// <summary>
/// Whether debug mode is allowed in this application.
/// </summary>
public bool IsDebugModeAllowed { get; }
/// <summary>
/// Whether preview mode is allowed in this application.
/// </summary>
public bool IsPreviewModeAllowed { get; }
/// <summary>
/// Initializes an instance of <see cref="ApplicationConfiguration"/>.
/// </summary>
public ApplicationConfiguration(
IReadOnlyList<Type> commandTypes,
bool isDebugModeAllowed,
bool isPreviewModeAllowed)
{ {
/// <summary> CommandTypes = commandTypes;
/// Command types defined in this application. IsDebugModeAllowed = isDebugModeAllowed;
/// </summary> IsPreviewModeAllowed = isPreviewModeAllowed;
public IReadOnlyList<Type> CommandTypes { get; }
/// <summary>
/// Whether debug mode is allowed in this application.
/// </summary>
public bool IsDebugModeAllowed { get; }
/// <summary>
/// Whether preview mode is allowed in this application.
/// </summary>
public bool IsPreviewModeAllowed { get; }
/// <summary>
/// Initializes an instance of <see cref="ApplicationConfiguration"/>.
/// </summary>
public ApplicationConfiguration(
IReadOnlyList<Type> commandTypes,
bool isDebugModeAllowed,
bool isPreviewModeAllowed)
{
CommandTypes = commandTypes;
IsDebugModeAllowed = isDebugModeAllowed;
IsPreviewModeAllowed = isPreviewModeAllowed;
}
} }
} }

View File

@@ -1,43 +1,42 @@
namespace CliFx namespace CliFx;
/// <summary>
/// Metadata associated with an application.
/// </summary>
public class ApplicationMetadata
{ {
/// <summary> /// <summary>
/// Metadata associated with an application. /// Application title.
/// </summary> /// </summary>
public class ApplicationMetadata public string Title { get; }
/// <summary>
/// Application executable name.
/// </summary>
public string ExecutableName { get; }
/// <summary>
/// Application version.
/// </summary>
public string Version { get; }
/// <summary>
/// Application description.
/// </summary>
public string? Description { get; }
/// <summary>
/// Initializes an instance of <see cref="ApplicationMetadata"/>.
/// </summary>
public ApplicationMetadata(
string title,
string executableName,
string version,
string? description)
{ {
/// <summary> Title = title;
/// Application title. ExecutableName = executableName;
/// </summary> Version = version;
public string Title { get; } Description = description;
/// <summary>
/// Application executable name.
/// </summary>
public string ExecutableName { get; }
/// <summary>
/// Application version text.
/// </summary>
public string Version { get; }
/// <summary>
/// Application description.
/// </summary>
public string? Description { get; }
/// <summary>
/// Initializes an instance of <see cref="ApplicationMetadata"/>.
/// </summary>
public ApplicationMetadata(
string title,
string executableName,
string version,
string? description)
{
Title = title;
ExecutableName = executableName;
Version = version;
Description = description;
}
} }
} }

View File

@@ -1,43 +1,42 @@
using System; using System;
namespace CliFx.Attributes namespace CliFx.Attributes;
/// <summary>
/// Annotates a type that defines a command.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class CommandAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Annotates a type that defines a command. /// Command's name.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)] /// <remarks>
public class CommandAttribute : Attribute /// Command can have no name, in which case it's treated as the default command.
///
/// All commands registered in an application must have unique names (comparison IS NOT case-sensitive).
/// Only one command without a name is allowed in an application.
/// </remarks>
public string? Name { get; }
/// <summary>
/// Command description.
/// This is shown to the user in the help text.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Initializes an instance of <see cref="CommandAttribute"/>.
/// </summary>
public CommandAttribute(string name)
{ {
/// <summary> Name = name;
/// Command's name. }
/// </summary>
/// <remarks>
/// Command can have no name, in which case it's treated as the default command.
///
/// All commands registered in an application must have unique names (comparison IS NOT case-sensitive).
/// Only one command without a name is allowed in an application.
/// </remarks>
public string? Name { get; }
/// <summary> /// <summary>
/// Command description. /// Initializes an instance of <see cref="CommandAttribute"/>.
/// This is shown to the user in the help text. /// </summary>
/// </summary> public CommandAttribute()
public string? Description { get; set; } {
/// <summary>
/// Initializes an instance of <see cref="CommandAttribute"/>.
/// </summary>
public CommandAttribute(string name)
{
Name = name;
}
/// <summary>
/// Initializes an instance of <see cref="CommandAttribute"/>.
/// </summary>
public CommandAttribute()
{
}
} }
} }

View File

@@ -1,100 +1,99 @@
using System; using System;
using CliFx.Extensibility; using CliFx.Extensibility;
namespace CliFx.Attributes namespace CliFx.Attributes;
/// <summary>
/// Annotates a property that defines a command option.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class CommandOptionAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Annotates a property that defines a command option. /// Option name.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Property)] /// <remarks>
public class CommandOptionAttribute : Attribute /// Must contain at least two characters and start with a letter.
/// Either <see cref="Name"/> or <see cref="ShortName"/> must be set.
/// All options in a command must have unique names (comparison IS NOT case-sensitive).
/// </remarks>
public string? Name { get; }
/// <summary>
/// Option short name.
/// </summary>
/// <remarks>
/// Either <see cref="Name"/> or <see cref="ShortName"/> must be set.
/// All options in a command must have unique short names (comparison IS case-sensitive).
/// </remarks>
public char? ShortName { get; }
/// <summary>
/// Whether this option is required.
/// If an option is required, the user will get an error if they don't set it.
/// </summary>
public bool IsRequired { get; set; }
/// <summary>
/// Environment variable whose value will be used as a fallback if the option
/// has not been explicitly set through command line arguments.
/// </summary>
public string? EnvironmentVariable { get; set; }
/// <summary>
/// Option description.
/// This is shown to the user in the help text.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Custom converter used for mapping the raw command line argument into
/// a value expected by the underlying property.
/// </summary>
/// <remarks>
/// Converter must derive from <see cref="BindingConverter{T}"/>.
/// </remarks>
public Type? Converter { get; set; }
/// <summary>
/// Custom validators used for verifying the value of the underlying
/// property, after it has been bound.
/// </summary>
/// <remarks>
/// Validators must derive from <see cref="BindingValidator{T}"/>.
/// </remarks>
public Type[] Validators { get; set; } = Array.Empty<Type>();
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary>
private CommandOptionAttribute(string? name, char? shortName)
{ {
/// <summary> Name = name;
/// Option name. ShortName = shortName;
/// </summary> }
/// <remarks>
/// Must contain at least two characters and start with a letter.
/// Either <see cref="Name"/> or <see cref="ShortName"/> must be set.
/// All options in a command must have unique names (comparison IS NOT case-sensitive).
/// </remarks>
public string? Name { get; }
/// <summary> /// <summary>
/// Option short name. /// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary> /// </summary>
/// <remarks> public CommandOptionAttribute(string name, char shortName)
/// Either <see cref="Name"/> or <see cref="ShortName"/> must be set. : this(name, (char?) shortName)
/// All options in a command must have unique short names (comparison IS case-sensitive). {
/// </remarks> }
public char? ShortName { get; }
/// <summary> /// <summary>
/// Whether this option is required. /// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// If an option is required, the user will get an error if they don't set it. /// </summary>
/// </summary> public CommandOptionAttribute(string name)
public bool IsRequired { get; set; } : this(name, null)
{
}
/// <summary> /// <summary>
/// Environment variable whose value will be used as a fallback if the option /// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// has not been explicitly set through command line arguments. /// </summary>
/// </summary> public CommandOptionAttribute(char shortName)
public string? EnvironmentVariable { get; set; } : this(null, (char?) shortName)
{
/// <summary>
/// Option description.
/// This is shown to the user in the help text.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Custom converter used for mapping the raw command line argument into
/// a value expected by the underlying property.
/// </summary>
/// <remarks>
/// Converter must derive from <see cref="BindingConverter{T}"/>.
/// </remarks>
public Type? Converter { get; set; }
/// <summary>
/// Custom validators used for verifying the value of the underlying
/// property, after it has been bound.
/// </summary>
/// <remarks>
/// Validators must derive from <see cref="BindingValidator{T}"/>.
/// </remarks>
public Type[] Validators { get; set; } = Array.Empty<Type>();
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary>
private CommandOptionAttribute(string? name, char? shortName)
{
Name = name;
ShortName = shortName;
}
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary>
public CommandOptionAttribute(string name, char shortName)
: this(name, (char?) shortName)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary>
public CommandOptionAttribute(string name)
: this(name, null)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary>
public CommandOptionAttribute(char shortName)
: this(null, (char?) shortName)
{
}
} }
} }

View File

@@ -1,67 +1,66 @@
using System; using System;
using CliFx.Extensibility; using CliFx.Extensibility;
namespace CliFx.Attributes namespace CliFx.Attributes;
/// <summary>
/// Annotates a property that defines a command parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class CommandParameterAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Annotates a property that defines a command parameter. /// Parameter order.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Property)] /// <remarks>
public class CommandParameterAttribute : Attribute /// Higher order means the parameter appears later, lower order means
/// it appears earlier.
///
/// All parameters in a command must have unique order.
///
/// Parameter whose type is a non-scalar (e.g. array), must always be the last in order.
/// Only one non-scalar parameter is allowed in a command.
/// </remarks>
public int Order { get; }
/// <summary>
/// Parameter name.
/// This is shown to the user in the help text.
/// </summary>
/// <remarks>
/// If this isn't specified, parameter name is inferred from the property name.
/// </remarks>
public string? Name { get; set; }
/// <summary>
/// Parameter description.
/// This is shown to the user in the help text.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Custom converter used for mapping the raw command line argument into
/// a value expected by the underlying property.
/// </summary>
/// <remarks>
/// Converter must derive from <see cref="BindingConverter{T}"/>.
/// </remarks>
public Type? Converter { get; set; }
/// <summary>
/// Custom validators used for verifying the value of the underlying
/// property, after it has been bound.
/// </summary>
/// <remarks>
/// Validators must derive from <see cref="BindingValidator{T}"/>.
/// </remarks>
public Type[] Validators { get; set; } = Array.Empty<Type>();
/// <summary>
/// Initializes an instance of <see cref="CommandParameterAttribute"/>.
/// </summary>
public CommandParameterAttribute(int order)
{ {
/// <summary> Order = order;
/// Parameter order.
/// </summary>
/// <remarks>
/// Higher order means the parameter appears later, lower order means
/// it appears earlier.
///
/// All parameters in a command must have unique order.
///
/// Parameter whose type is a non-scalar (e.g. array), must always be the last in order.
/// Only one non-scalar parameter is allowed in a command.
/// </remarks>
public int Order { get; }
/// <summary>
/// Parameter name.
/// This is shown to the user in the help text.
/// </summary>
/// <remarks>
/// If this isn't specified, parameter name is inferred from the property name.
/// </remarks>
public string? Name { get; set; }
/// <summary>
/// Parameter description.
/// This is shown to the user in the help text.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Custom converter used for mapping the raw command line argument into
/// a value expected by the underlying property.
/// </summary>
/// <remarks>
/// Converter must derive from <see cref="BindingConverter{T}"/>.
/// </remarks>
public Type? Converter { get; set; }
/// <summary>
/// Custom validators used for verifying the value of the underlying
/// property, after it has been bound.
/// </summary>
/// <remarks>
/// Validators must derive from <see cref="BindingValidator{T}"/>.
/// </remarks>
public Type[] Validators { get; set; } = Array.Empty<Type>();
/// <summary>
/// Initializes an instance of <see cref="CommandParameterAttribute"/>.
/// </summary>
public CommandParameterAttribute(int order)
{
Order = order;
}
} }
} }

Some files were not shown because too many files have changed in this diff Show More