45 Commits
2.3.3 ... 2.3.5

Author SHA1 Message Date
Tyrrrz
566dd4a9a7 Update version 2023-11-16 00:46:26 +02:00
Tyrrrz
9beb439323 Update NuGet packages 2023-11-16 00:46:16 +02:00
Tyrrrz
029257a915 Update to .NET 8 2023-11-14 20:26:29 +02:00
AliReZa Sabouri
d330fbbb63 Expect same environment variables with different case on Windows (#138) 2023-11-14 18:41:20 +02:00
Tyrrrz
236867f547 Formatting 2023-11-12 20:25:01 +02:00
Tyrrrz
b41e9b4929 Update NuGet packages 2023-11-12 19:08:54 +02:00
Tyrrrz
ff06b8896f More I_can_try pattern in tests 2023-11-11 22:51:42 +02:00
Tyrrrz
0fe9c89fa0 Use the I_can_try_to... naming pattern in tests 2023-11-11 22:35:00 +02:00
Tyrrrz
8646c9de5e Remove some shared project properties 2023-11-11 16:21:53 +02:00
Tyrrrz
a33c42a163 Update gitignore file 2023-11-09 13:41:55 +02:00
Tyrrrz
55cea48cbd Simplify workflows 2023-10-29 00:49:52 +03:00
Tyrrrz
e67eda3515 Make package deployment off by default 2023-10-29 00:39:14 +03:00
Tyrrrz
4412c20e97 Clean up workflow file 2023-10-29 00:32:17 +03:00
Tyrrrz
9eb84c6649 Simplify workflows 2023-10-29 00:25:19 +03:00
Tyrrrz
2ef37ab6d9 Use console instead of sh for syntax highlighting 2023-09-29 13:30:56 +03:00
Tyrrrz
38a73772fc Treat all warnings as errors 2023-09-20 21:32:01 +03:00
Tyrrrz
aed53eb090 Fix formatting 2023-08-22 21:36:05 +03:00
Tyrrrz
21b601da66 Use CSharpier 2023-08-22 21:20:04 +03:00
Tyrrrz
a4726fcefd Normalize injected language tags 2023-08-20 22:14:38 +03:00
Tyrrrz
ab24ca8604 Update issue forms 2023-08-18 01:40:36 +03:00
Tyrrrz
3533bff344 Update issue forms 2023-08-13 13:40:52 +03:00
Tyrrrz
1b096b679e Update issue forms 2023-08-12 21:07:26 +03:00
Tyrrrz
cb61b31e9d Update NuGet packages 2023-08-07 21:56:23 +03:00
Tyrrrz
d8f183c404 Better error messages in CliApplicationBuilder 2023-08-07 21:55:48 +03:00
Tyrrrz
c95b6c32d5 Run test dummy through the app host 2023-08-07 21:55:37 +03:00
Tyrrrz
d2e390c691 Update NuGet packages 2023-08-06 00:20:24 +03:00
Tyrrrz
66ef221586 Add dispatchable workflow for pre-releases 2023-07-26 22:28:37 +03:00
Tyrrrz
2d3bb30125 Update readme 2023-07-17 00:53:16 +03:00
Tyrrrz
5d72692aa5 Update NuGet packages 2023-07-17 00:52:32 +03:00
Tyrrrz
3be17db784 Update NuGet packages 2023-06-26 23:00:43 +03:00
Tyrrrz
4aef8ce8fb Clean up 2023-06-26 22:57:57 +03:00
Tyrrrz
8c1cff3bb7 Add favicon to demo 2023-05-27 13:38:20 +03:00
Tyrrrz
669d8bfe20 Update favicon 2023-05-27 12:41:15 +03:00
Tyrrrz
4dce7bddb4 Update NuGet packages 2023-05-25 08:56:22 +03:00
Tyrrrz
a621e89e89 Consistency in command descriptions 2023-05-20 05:50:04 +03:00
Tyrrrz
5ea11e3a23 Update readme 2023-05-20 03:14:54 +03:00
Tyrrrz
7cb61182d2 Remove unused usings 2023-05-20 03:07:15 +03:00
Tyrrrz
99c59431c4 Update version 2023-05-18 07:30:32 +03:00
Tyrrrz
f376081489 Update NuGet packages 2023-05-18 07:20:27 +03:00
Tyrrrz
00a1e12b5c Test naming consistency 2023-05-17 23:51:21 +03:00
Tyrrrz
81f8b17451 Fix cancellation tests 2023-05-16 02:43:47 +03:00
Tyrrrz
aa8315b68d Clean up 2023-05-16 01:56:28 +03:00
Tyrrrz
e52781c25a Refactor 2023-05-16 01:36:03 +03:00
Tyrrrz
01f29a5375 Fix tests 2023-05-15 05:50:29 +03:00
Tyrrrz
013cb8f66b Add an overload of UseTypeActivator(...) that takes a list of added command types 2023-05-15 05:29:46 +03:00
161 changed files with 3155 additions and 3203 deletions

View File

@@ -1,42 +1,75 @@
name: 🐞 Bug report name: 🐛 Bug report
description: Report broken functionality. description: Report broken functionality.
labels: [bug] labels: [bug]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
🧐 **Guidelines:** - Avoid generic or vague titles such as "Something's not working" or "A couple of problems" — be as descriptive as possible.
- Keep your issue focused on one single problem. If you have multiple bug reports, please create a separate issue for each of them.
- Issues should represent **complete and actionable** work items. If you are unsure about something or have a question, please start a [discussion](https://github.com/Tyrrrz/CliFx/discussions/new) instead.
- Remember that **CliFx** is an open-source project funded by the community. If you find it useful, **please consider [donating](https://tyrrrz.me/donate) to support its development**.
- 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 - type: input
attributes: attributes:
label: Version label: Version
description: Which version of CliFx does this bug affect? description: Which version of the package does this bug affect? Make sure you're not using an outdated version.
placeholder: ver X.Y.Z placeholder: v1.0.0
validations: validations:
required: true required: true
- type: textarea - type: input
attributes: attributes:
label: Details label: Platform
description: Clear and thorough explanation of the bug. description: Which platform do you experience this bug on?
placeholder: I was doing X expecting Y to happen, but Z happened instead. placeholder: .NET 7.0 / Windows 11
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Steps to reproduce label: Steps to reproduce
description: Minimum steps required to reproduce the bug. description: >
placeholder: | Minimum steps required to reproduce the bug, including prerequisites, code snippets, or other relevant items.
- Step 1 The information provided in this field must be readily actionable, meaning that anyone should be able to reproduce the bug by following these steps.
- Step 2 If the reproduction steps are too complex to fit in this field, please provide a link to a repository instead.
- Step 3 placeholder: |
validations: - Step 1
required: true - Step 2
- Step 3
validations:
required: true
- type: textarea
attributes:
label: Details
description: Clear and thorough explanation of the bug, including any additional information you may find relevant.
placeholder: |
- Expected behavior: ...
- Actual behavior: ...
validations:
required: true
- type: checkboxes
attributes:
label: Checklist
description: Quick list of checks to ensure that everything is in order.
options:
- label: I have looked through existing issues to make sure that this bug has not been reported before
required: true
- label: I have provided a descriptive title for this issue
required: true
- label: I have made sure that that this bug is reproducible on the latest version of the package
required: true
- label: I have provided all the information needed to reproduce this bug as efficiently as possible
required: true
- label: I have sponsored this project
required: false
- type: markdown
attributes:
value: |
If you are struggling to provide actionable reproduction steps, or if something else is preventing you from creating a complete bug report, please start a [discussion](https://github.com/Tyrrrz/CliFx/discussions/new) instead.

View File

@@ -3,9 +3,9 @@ contact_links:
- name: ⚠ Feature request - name: ⚠ Feature request
url: https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md url: https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md
about: Sorry, but this project is in maintenance mode and no longer accepts new feature requests. about: Sorry, but this project is in maintenance mode and no longer accepts new feature requests.
- name: 🗨 Discussions
url: https://github.com/Tyrrrz/CliFx/discussions/new
about: Ask and answer questions.
- name: 💬 Discord server - name: 💬 Discord server
url: https://discord.gg/2SUWKFnHSm url: https://discord.gg/2SUWKFnHSm
about: Chat with the project community. about: Chat with the project community.
- name: 🗨 Discussions
url: https://github.com/Tyrrrz/CliFx/discussions/new
about: Ask and answer questions.

View File

@@ -1,13 +1,29 @@
name: main name: main
on: [push, pull_request] on:
workflow_dispatch:
inputs:
package-version:
type: string
description: Package version
required: false
deploy:
type: boolean
description: Deploy package
required: false
default: false
push:
pull_request:
jobs: jobs:
main: main:
uses: Tyrrrz/.github/.github/workflows/nuget.yml@master uses: Tyrrrz/.github/.github/workflows/nuget.yml@master
with: with:
dotnet-version: 7.0.x dotnet-version: 8.0.x
package-version: ${{ inputs.package-version }}
# Deploy only on tags by default, unless deploy is explicitly requested
deploy-on-tags-only: ${{ !(github.event_name == 'workflow_dispatch' && inputs.deploy) }}
secrets: secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}

21
.gitignore vendored
View File

@@ -1,21 +1,12 @@
# User-specific files # User-specific files
.vs/
.idea/
*.suo *.suo
*.user *.user
*.userosscache
*.sln.docstates
.idea/
# Build results # Build results
[Dd]ebug/ bin/
[Dd]ebugPublic/ obj/
[Rr]elease/
[Rr]eleases/
[Xx]64/
[Xx]86/
[Bb]uild/
bld/
[Bb]in/
[Oo]bj/
# Coverage # Test results
*.opencover.xml TestResults/

View File

@@ -1,5 +1,13 @@
# Changelog # Changelog
## v2.3.5 (16-Nov-2023)
- Fixed an issue where calling `CliApplication.RunAsync(IReadOnlyList<string>)` could fail in very specific scenarios on Windows, if there were two global environment variables with the same name but different casing. (Thanks [@alirezanet](https://github.com/alirezanet))
## v2.3.4 (18-May-2023)
- Added an overload of `CliApplicationBuilder.UseTypeActivator(...)` that accepts a `Func<IReadOnlyList<Type>, IServiceProvider>` delegate. The first parameter in the delegate is the list of all command types registered in the application. You can use this overload to more easily add the commands to a DI container. See the readme for an [updated example](https://github.com/Tyrrrz/CliFx/tree/2.3.4#type-activation).
## v2.3.3 (28-Apr-2023) ## v2.3.3 (28-Apr-2023)
- Fixed an issue where the analyzers failed to load in some projects, due to targeting a Roslyn version that was too high. - Fixed an issue where the analyzers failed to load in some projects, due to targeting a Roslyn version that was too high.

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -9,14 +9,15 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Basic.Reference.Assemblies.Net70" Version="1.4.1" /> <PackageReference Include="Basic.Reference.Assemblies.Net70" Version="1.4.5" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.2" PrivateAssets="all" /> <PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all" />
<PackageReference Include="FluentAssertions" Version="6.11.0" /> <PackageReference Include="CSharpier.MsBuild" Version="0.26.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" /> <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" /> <PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="xunit" Version="2.4.2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageReference Include="coverlet.collector" Version="3.2.0" PrivateAssets="all" /> <PackageReference Include="xunit" Version="2.6.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -12,9 +12,8 @@ public class CommandMustBeAnnotatedAnalyzerSpecs
public void Analyzer_reports_an_error_if_a_command_is_not_annotated_with_the_command_attribute() public void Analyzer_reports_an_error_if_a_command_is_not_annotated_with_the_command_attribute()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
@@ -29,9 +28,8 @@ public class CommandMustBeAnnotatedAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public abstract class MyCommand : ICommand public abstract class MyCommand : ICommand
{ {
@@ -47,9 +45,8 @@ public class CommandMustBeAnnotatedAnalyzerSpecs
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 // lang=csharp
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;
@@ -64,16 +61,15 @@ public class CommandMustBeAnnotatedAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
public class Foo public class Foo
{ {
public int Bar { get; set; } = 5; public int Bar { get; init; } = 5;
} }
"""; """;
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,15 +6,15 @@ namespace CliFx.Analyzers.Tests;
public class CommandMustImplementInterfaceAnalyzerSpecs public class CommandMustImplementInterfaceAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new CommandMustImplementInterfaceAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new CommandMustImplementInterfaceAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_a_command_does_not_implement_ICommand_interface() public void Analyzer_reports_an_error_if_a_command_does_not_implement_ICommand_interface()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand public class MyCommand
{ {
@@ -30,9 +30,8 @@ public class CommandMustImplementInterfaceAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -48,16 +47,15 @@ public class CommandMustImplementInterfaceAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
public class Foo public class Foo
{ {
public int Bar { get; set; } = 5; public int Bar { get; init; } = 5;
} }
"""; """;
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -16,7 +16,7 @@ public class GeneralSpecs
.Assembly .Assembly
.GetTypes() .GetTypes()
.Where(t => !t.IsAbstract && t.IsAssignableTo(typeof(DiagnosticAnalyzer))) .Where(t => !t.IsAbstract && t.IsAssignableTo(typeof(DiagnosticAnalyzer)))
.Select(t => (DiagnosticAnalyzer) Activator.CreateInstance(t)!) .Select(t => (DiagnosticAnalyzer)Activator.CreateInstance(t)!)
.ToArray(); .ToArray();
// Act // Act
@@ -27,4 +27,4 @@ public class GeneralSpecs
// Assert // Assert
diagnosticIds.Should().OnlyHaveUniqueItems(); diagnosticIds.Should().OnlyHaveUniqueItems();
} }
} }

View File

@@ -12,13 +12,12 @@ public class OptionMustBeInsideCommandAnalyzerSpecs
public void Analyzer_reports_an_error_if_an_option_is_inside_a_class_that_is_not_a_command() public void Analyzer_reports_an_error_if_an_option_is_inside_a_class_that_is_not_a_command()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
public class MyClass public class MyClass
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public string? Foo { get; init; }
} }
"""; """;
@@ -30,14 +29,13 @@ public class OptionMustBeInsideCommandAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -51,13 +49,12 @@ public class OptionMustBeInsideCommandAnalyzerSpecs
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 // lang=csharp
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; init; }
} }
"""; """;
@@ -69,13 +66,12 @@ public class OptionMustBeInsideCommandAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -84,4 +80,4 @@ public class OptionMustBeInsideCommandAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,20 +6,20 @@ namespace CliFx.Analyzers.Tests;
public class OptionMustBeRequiredIfPropertyRequiredAnalyzerSpecs public class OptionMustBeRequiredIfPropertyRequiredAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustBeRequiredIfPropertyRequiredAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new OptionMustBeRequiredIfPropertyRequiredAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_a_non_required_option_is_bound_to_a_required_property() public void Analyzer_reports_an_error_if_a_non_required_option_is_bound_to_a_required_property()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption('f', IsRequired = false)] [CommandOption('f', IsRequired = false)]
public required string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -33,14 +33,13 @@ public class OptionMustBeRequiredIfPropertyRequiredAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_required_option_is_bound_to_a_required_property() public void Analyzer_does_not_report_an_error_if_a_required_option_is_bound_to_a_required_property()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption('f', IsRequired = true)] [CommandOption('f')]
public required string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -54,14 +53,13 @@ public class OptionMustBeRequiredIfPropertyRequiredAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_non_required_option_is_bound_to_a_non_required_property() public void Analyzer_does_not_report_an_error_if_a_non_required_option_is_bound_to_a_non_required_property()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption('f', IsRequired = false)] [CommandOption('f', IsRequired = false)]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -75,14 +73,13 @@ public class OptionMustBeRequiredIfPropertyRequiredAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_required_option_is_bound_to_a_non_required_property() public void Analyzer_does_not_report_an_error_if_a_required_option_is_bound_to_a_non_required_property()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption('f', IsRequired = true)] [CommandOption('f')]
public string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -96,13 +93,12 @@ public class OptionMustBeRequiredIfPropertyRequiredAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public required string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -111,4 +107,4 @@ public class OptionMustBeRequiredIfPropertyRequiredAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,20 +6,20 @@ namespace CliFx.Analyzers.Tests;
public class OptionMustHaveNameOrShortNameAnalyzerSpecs public class OptionMustHaveNameOrShortNameAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveNameOrShortNameAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new OptionMustHaveNameOrShortNameAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_an_option_does_not_have_a_name_or_short_name() public void Analyzer_reports_an_error_if_an_option_does_not_have_a_name_or_short_name()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption(null)] [CommandOption(null)]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -33,14 +33,13 @@ public class OptionMustHaveNameOrShortNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -54,14 +53,13 @@ public class OptionMustHaveNameOrShortNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -75,13 +73,12 @@ public class OptionMustHaveNameOrShortNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -90,4 +87,4 @@ public class OptionMustHaveNameOrShortNameAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -12,17 +12,16 @@ public class OptionMustHaveUniqueNameAnalyzerSpecs
public void Analyzer_reports_an_error_if_an_option_has_the_same_name_as_another_option() public void Analyzer_reports_an_error_if_an_option_has_the_same_name_as_another_option()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public string? Foo { get; init; }
[CommandOption("foo")] [CommandOption("foo")]
public string Bar { get; set; } public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -36,17 +35,16 @@ public class OptionMustHaveUniqueNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public string? Foo { get; init; }
[CommandOption("bar")] [CommandOption("bar")]
public string Bar { get; set; } public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -60,14 +58,13 @@ public class OptionMustHaveUniqueNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -81,13 +78,12 @@ public class OptionMustHaveUniqueNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -96,4 +92,4 @@ public class OptionMustHaveUniqueNameAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,23 +6,23 @@ namespace CliFx.Analyzers.Tests;
public class OptionMustHaveUniqueShortNameAnalyzerSpecs public class OptionMustHaveUniqueShortNameAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveUniqueShortNameAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new OptionMustHaveUniqueShortNameAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_an_option_has_the_same_short_name_as_another_option() public void Analyzer_reports_an_error_if_an_option_has_the_same_short_name_as_another_option()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public string Foo { get; set; } public string? Foo { get; init; }
[CommandOption('f')] [CommandOption('f')]
public string Bar { get; set; } public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -36,17 +36,16 @@ public class OptionMustHaveUniqueShortNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public string Foo { get; set; } public string? Foo { get; init; }
[CommandOption('b')] [CommandOption('b')]
public string Bar { get; set; } public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -60,17 +59,16 @@ public class OptionMustHaveUniqueShortNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public string Foo { get; set; } public string? Foo { get; init; }
[CommandOption('F')] [CommandOption('F')]
public string Bar { get; set; } public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -84,14 +82,13 @@ public class OptionMustHaveUniqueShortNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -105,13 +102,12 @@ public class OptionMustHaveUniqueShortNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -120,4 +116,4 @@ public class OptionMustHaveUniqueShortNameAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,26 +6,26 @@ namespace CliFx.Analyzers.Tests;
public class OptionMustHaveValidConverterAnalyzerSpecs public class OptionMustHaveValidConverterAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidConverterAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new OptionMustHaveValidConverterAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_an_option_has_a_converter_that_does_not_derive_from_BindingConverter() public void Analyzer_reports_an_error_if_an_option_has_a_converter_that_does_not_derive_from_BindingConverter()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
public class MyConverter public class MyConverter
{ {
public string Convert(string rawValue) => rawValue; public string Convert(string? rawValue) => rawValue;
} }
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo", Converter = typeof(MyConverter))] [CommandOption("foo", Converter = typeof(MyConverter))]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -38,20 +38,19 @@ public class OptionMustHaveValidConverterAnalyzerSpecs
public void Analyzer_reports_an_error_if_an_option_has_a_converter_that_does_not_derive_from_a_compatible_BindingConverter() public void Analyzer_reports_an_error_if_an_option_has_a_converter_that_does_not_derive_from_a_compatible_BindingConverter()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
public class MyConverter : BindingConverter<int> public class MyConverter : BindingConverter<int>
{ {
public override int Convert(string rawValue) => 42; public override int Convert(string? rawValue) => 42;
} }
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo", Converter = typeof(MyConverter))] [CommandOption("foo", Converter = typeof(MyConverter))]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -64,20 +63,19 @@ public class OptionMustHaveValidConverterAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_an_option_has_a_converter_that_derives_from_a_compatible_BindingConverter() public void Analyzer_does_not_report_an_error_if_an_option_has_a_converter_that_derives_from_a_compatible_BindingConverter()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
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;
} }
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo", Converter = typeof(MyConverter))] [CommandOption("foo", Converter = typeof(MyConverter))]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -90,20 +88,19 @@ public class OptionMustHaveValidConverterAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_nullable_option_has_a_converter_that_derives_from_a_compatible_BindingConverter() public void Analyzer_does_not_report_an_error_if_a_nullable_option_has_a_converter_that_derives_from_a_compatible_BindingConverter()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
public class MyConverter : BindingConverter<int> public class MyConverter : BindingConverter<int>
{ {
public override int Convert(string rawValue) => 42; public override int Convert(string? rawValue) => 42;
} }
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo", Converter = typeof(MyConverter))] [CommandOption("foo", Converter = typeof(MyConverter))]
public int? Foo { get; set; } public int? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -116,20 +113,19 @@ public class OptionMustHaveValidConverterAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_non_scalar_option_has_a_converter_that_derives_from_a_compatible_BindingConverter() public void Analyzer_does_not_report_an_error_if_a_non_scalar_option_has_a_converter_that_derives_from_a_compatible_BindingConverter()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
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;
} }
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo", Converter = typeof(MyConverter))] [CommandOption("foo", Converter = typeof(MyConverter))]
public IReadOnlyList<string> Foo { get; set; } public IReadOnlyList<string>? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -142,15 +138,14 @@ public class OptionMustHaveValidConverterAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -163,14 +158,13 @@ public class OptionMustHaveValidConverterAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -178,4 +172,4 @@ public class OptionMustHaveValidConverterAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -12,14 +12,13 @@ public class OptionMustHaveValidNameAnalyzerSpecs
public void Analyzer_reports_an_error_if_an_option_has_a_name_which_is_too_short() public void Analyzer_reports_an_error_if_an_option_has_a_name_which_is_too_short()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("f")] [CommandOption("f")]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -33,14 +32,13 @@ public class OptionMustHaveValidNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("1foo")] [CommandOption("1foo")]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -54,14 +52,13 @@ public class OptionMustHaveValidNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -75,14 +72,13 @@ public class OptionMustHaveValidNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -96,13 +92,12 @@ public class OptionMustHaveValidNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -111,4 +106,4 @@ public class OptionMustHaveValidNameAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,20 +6,20 @@ namespace CliFx.Analyzers.Tests;
public class OptionMustHaveValidShortNameAnalyzerSpecs public class OptionMustHaveValidShortNameAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidShortNameAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new OptionMustHaveValidShortNameAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_an_option_has_a_short_name_which_is_not_a_letter_character() public void Analyzer_reports_an_error_if_an_option_has_a_short_name_which_is_not_a_letter_character()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption('1')] [CommandOption('1')]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -33,14 +33,13 @@ public class OptionMustHaveValidShortNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -54,14 +53,13 @@ public class OptionMustHaveValidShortNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -75,13 +73,12 @@ public class OptionMustHaveValidShortNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -90,4 +87,4 @@ public class OptionMustHaveValidShortNameAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,15 +6,15 @@ namespace CliFx.Analyzers.Tests;
public class OptionMustHaveValidValidatorsAnalyzerSpecs public class OptionMustHaveValidValidatorsAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidValidatorsAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new OptionMustHaveValidValidatorsAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_an_option_has_a_validator_that_does_not_derive_from_BindingValidator() public void Analyzer_reports_an_error_if_an_option_has_a_validator_that_does_not_derive_from_BindingValidator()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
public class MyValidator public class MyValidator
{ {
public void Validate(string value) {} public void Validate(string value) {}
@@ -24,7 +24,7 @@ public class OptionMustHaveValidValidatorsAnalyzerSpecs
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo", Validators = new[] {typeof(MyValidator)})] [CommandOption("foo", Validators = new[] {typeof(MyValidator)})]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -38,9 +38,8 @@ public class OptionMustHaveValidValidatorsAnalyzerSpecs
public void Analyzer_reports_an_error_if_an_option_has_a_validator_that_does_not_derive_from_a_compatible_BindingValidator() public void Analyzer_reports_an_error_if_an_option_has_a_validator_that_does_not_derive_from_a_compatible_BindingValidator()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
public class MyValidator : BindingValidator<int> public class MyValidator : BindingValidator<int>
{ {
public override BindingValidationError Validate(int value) => Ok(); public override BindingValidationError Validate(int value) => Ok();
@@ -50,7 +49,7 @@ public class OptionMustHaveValidValidatorsAnalyzerSpecs
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo", Validators = new[] {typeof(MyValidator)})] [CommandOption("foo", Validators = new[] {typeof(MyValidator)})]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -64,9 +63,8 @@ public class OptionMustHaveValidValidatorsAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_an_option_has_validators_that_all_derive_from_compatible_BindingValidators() public void Analyzer_does_not_report_an_error_if_an_option_has_validators_that_all_derive_from_compatible_BindingValidators()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
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();
@@ -76,7 +74,7 @@ public class OptionMustHaveValidValidatorsAnalyzerSpecs
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo", Validators = new[] {typeof(MyValidator)})] [CommandOption("foo", Validators = new[] {typeof(MyValidator)})]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -90,14 +88,13 @@ public class OptionMustHaveValidValidatorsAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -111,13 +108,12 @@ public class OptionMustHaveValidValidatorsAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -126,4 +122,4 @@ public class OptionMustHaveValidValidatorsAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,19 +6,19 @@ namespace CliFx.Analyzers.Tests;
public class ParameterMustBeInsideCommandAnalyzerSpecs public class ParameterMustBeInsideCommandAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeInsideCommandAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new ParameterMustBeInsideCommandAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_a_parameter_is_inside_a_class_that_is_not_a_command() public void Analyzer_reports_an_error_if_a_parameter_is_inside_a_class_that_is_not_a_command()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
public class MyClass public class MyClass
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
} }
"""; """;
@@ -30,15 +30,14 @@ public class ParameterMustBeInsideCommandAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -51,13 +50,12 @@ public class ParameterMustBeInsideCommandAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
public abstract class MyCommand public abstract class MyCommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
} }
"""; """;
@@ -69,14 +67,13 @@ public class ParameterMustBeInsideCommandAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -84,4 +81,4 @@ public class ParameterMustBeInsideCommandAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,24 +6,24 @@ namespace CliFx.Analyzers.Tests;
public class ParameterMustBeLastIfNonRequiredAnalyzerSpecs public class ParameterMustBeLastIfNonRequiredAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeLastIfNonRequiredAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new ParameterMustBeLastIfNonRequiredAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_a_non_required_parameter_is_not_the_last_in_order() public void Analyzer_reports_an_error_if_a_non_required_parameter_is_not_the_last_in_order()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, IsRequired = false)] [CommandParameter(0, IsRequired = false)]
public string Foo { get; set; } public string? Foo { get; init; }
[CommandParameter(1)] [CommandParameter(1)]
public string Bar { get; set; } public required string Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -36,18 +36,17 @@ public class ParameterMustBeLastIfNonRequiredAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_non_required_parameter_is_the_last_in_order() public void Analyzer_does_not_report_an_error_if_a_non_required_parameter_is_the_last_in_order()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1, IsRequired = false)] [CommandParameter(1, IsRequired = false)]
public string Bar { get; set; } public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -60,18 +59,17 @@ public class ParameterMustBeLastIfNonRequiredAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_no_non_required_parameters_are_defined() public void Analyzer_does_not_report_an_error_if_no_non_required_parameters_are_defined()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1, IsRequired = true)] [CommandParameter(1)]
public string Bar { get; set; } public required string Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -84,14 +82,13 @@ public class ParameterMustBeLastIfNonRequiredAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -99,4 +96,4 @@ public class ParameterMustBeLastIfNonRequiredAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,24 +6,24 @@ namespace CliFx.Analyzers.Tests;
public class ParameterMustBeLastIfNonScalarAnalyzerSpecs public class ParameterMustBeLastIfNonScalarAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeLastIfNonScalarAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new ParameterMustBeLastIfNonScalarAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_a_non_scalar_parameter_is_not_the_last_in_order() public void Analyzer_reports_an_error_if_a_non_scalar_parameter_is_not_the_last_in_order()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string[] Foo { get; set; } public required string[] Foo { get; init; }
[CommandParameter(1)] [CommandParameter(1)]
public string Bar { get; set; } public required string Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -36,18 +36,17 @@ public class ParameterMustBeLastIfNonScalarAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_is_the_last_in_order() public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_is_the_last_in_order()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1)] [CommandParameter(1)]
public string[] Bar { get; set; } public required string[] Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -60,18 +59,17 @@ public class ParameterMustBeLastIfNonScalarAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1)] [CommandParameter(1)]
public string Bar { get; set; } public required string Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -84,14 +82,13 @@ public class ParameterMustBeLastIfNonScalarAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -99,4 +96,4 @@ public class ParameterMustBeLastIfNonScalarAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,20 +6,20 @@ namespace CliFx.Analyzers.Tests;
public class ParameterMustBeRequiredIfPropertyRequiredAnalyzerSpecs public class ParameterMustBeRequiredIfPropertyRequiredAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeRequiredIfPropertyRequiredAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new ParameterMustBeRequiredIfPropertyRequiredAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_a_non_required_parameter_is_bound_to_a_required_property() public void Analyzer_reports_an_error_if_a_non_required_parameter_is_bound_to_a_required_property()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, IsRequired = false)] [CommandParameter(0, IsRequired = false)]
public required string Foo { get; set; } public required string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -33,14 +33,13 @@ public class ParameterMustBeRequiredIfPropertyRequiredAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_required_parameter_is_bound_to_a_required_property() public void Analyzer_does_not_report_an_error_if_a_required_parameter_is_bound_to_a_required_property()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, IsRequired = true)] [CommandParameter(0)]
public required string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -54,14 +53,13 @@ public class ParameterMustBeRequiredIfPropertyRequiredAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_non_required_parameter_is_bound_to_a_non_required_property() public void Analyzer_does_not_report_an_error_if_a_non_required_parameter_is_bound_to_a_non_required_property()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, IsRequired = false)] [CommandParameter(0, IsRequired = false)]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -75,14 +73,13 @@ public class ParameterMustBeRequiredIfPropertyRequiredAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_required_parameter_is_bound_to_a_non_required_property() public void Analyzer_does_not_report_an_error_if_a_required_parameter_is_bound_to_a_non_required_property()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, IsRequired = true)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -96,13 +93,12 @@ public class ParameterMustBeRequiredIfPropertyRequiredAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public required string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -111,4 +107,4 @@ public class ParameterMustBeRequiredIfPropertyRequiredAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,24 +6,24 @@ namespace CliFx.Analyzers.Tests;
public class ParameterMustBeSingleIfNonRequiredAnalyzerSpecs public class ParameterMustBeSingleIfNonRequiredAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeSingleIfNonRequiredAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new ParameterMustBeSingleIfNonRequiredAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_more_than_one_non_required_parameters_are_defined() public void Analyzer_reports_an_error_if_more_than_one_non_required_parameters_are_defined()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, IsRequired = false)] [CommandParameter(0, IsRequired = false)]
public string Foo { get; set; } public string? Foo { get; init; }
[CommandParameter(1, IsRequired = false)] [CommandParameter(1, IsRequired = false)]
public string Bar { get; set; } public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -36,18 +36,17 @@ public class ParameterMustBeSingleIfNonRequiredAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_only_one_non_required_parameter_is_defined() public void Analyzer_does_not_report_an_error_if_only_one_non_required_parameter_is_defined()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1, IsRequired = false)] [CommandParameter(1, IsRequired = false)]
public string Bar { get; set; } public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -60,18 +59,17 @@ public class ParameterMustBeSingleIfNonRequiredAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_no_non_required_parameters_are_defined() public void Analyzer_does_not_report_an_error_if_no_non_required_parameters_are_defined()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1, IsRequired = true)] [CommandParameter(1)]
public string Bar { get; set; } public required string Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -84,14 +82,13 @@ public class ParameterMustBeSingleIfNonRequiredAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -99,4 +96,4 @@ public class ParameterMustBeSingleIfNonRequiredAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,24 +6,24 @@ namespace CliFx.Analyzers.Tests;
public class ParameterMustBeSingleIfNonScalarAnalyzerSpecs public class ParameterMustBeSingleIfNonScalarAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeSingleIfNonScalarAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new ParameterMustBeSingleIfNonScalarAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_more_than_one_non_scalar_parameters_are_defined() public void Analyzer_reports_an_error_if_more_than_one_non_scalar_parameters_are_defined()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string[] Foo { get; set; } public required string[] Foo { get; init; }
[CommandParameter(1)] [CommandParameter(1)]
public string[] Bar { get; set; } public required string[] Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -36,18 +36,17 @@ public class ParameterMustBeSingleIfNonScalarAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1)] [CommandParameter(1)]
public string[] Bar { get; set; } public required string[] Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -60,18 +59,17 @@ public class ParameterMustBeSingleIfNonScalarAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1)] [CommandParameter(1)]
public string Bar { get; set; } public required string Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -84,14 +82,13 @@ public class ParameterMustBeSingleIfNonScalarAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -99,4 +96,4 @@ public class ParameterMustBeSingleIfNonScalarAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -12,18 +12,17 @@ public class ParameterMustHaveUniqueNameAnalyzerSpecs
public void Analyzer_reports_an_error_if_a_parameter_has_the_same_name_as_another_parameter() public void Analyzer_reports_an_error_if_a_parameter_has_the_same_name_as_another_parameter()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, Name = "foo")] [CommandParameter(0, Name = "foo")]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1, Name = "foo")] [CommandParameter(1, Name = "foo")]
public string Bar { get; set; } public required string Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -36,18 +35,17 @@ public class ParameterMustHaveUniqueNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, Name = "foo")] [CommandParameter(0, Name = "foo")]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1, Name = "bar")] [CommandParameter(1, Name = "bar")]
public string Bar { get; set; } public required string Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -60,14 +58,13 @@ public class ParameterMustHaveUniqueNameAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -75,4 +72,4 @@ public class ParameterMustHaveUniqueNameAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,24 +6,24 @@ namespace CliFx.Analyzers.Tests;
public class ParameterMustHaveUniqueOrderAnalyzerSpecs public class ParameterMustHaveUniqueOrderAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveUniqueOrderAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new ParameterMustHaveUniqueOrderAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_a_parameter_has_the_same_order_as_another_parameter() public void Analyzer_reports_an_error_if_a_parameter_has_the_same_order_as_another_parameter()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(0)] [CommandParameter(0)]
public string Bar { get; set; } public required string Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -36,18 +36,17 @@ public class ParameterMustHaveUniqueOrderAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1)] [CommandParameter(1)]
public string Bar { get; set; } public required string Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -60,14 +59,13 @@ public class ParameterMustHaveUniqueOrderAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -75,4 +73,4 @@ public class ParameterMustHaveUniqueOrderAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,26 +6,26 @@ namespace CliFx.Analyzers.Tests;
public class ParameterMustHaveValidConverterAnalyzerSpecs public class ParameterMustHaveValidConverterAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveValidConverterAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new ParameterMustHaveValidConverterAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_a_parameter_has_a_converter_that_does_not_derive_from_BindingConverter() public void Analyzer_reports_an_error_if_a_parameter_has_a_converter_that_does_not_derive_from_BindingConverter()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
public class MyConverter public class MyConverter
{ {
public string Convert(string rawValue) => rawValue; public string Convert(string? rawValue) => rawValue;
} }
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, Converter = typeof(MyConverter))] [CommandParameter(0, Converter = typeof(MyConverter))]
public string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -38,25 +38,23 @@ public class ParameterMustHaveValidConverterAnalyzerSpecs
public void Analyzer_reports_an_error_if_a_parameter_has_a_converter_that_does_not_derive_from_a_compatible_BindingConverter() public void Analyzer_reports_an_error_if_a_parameter_has_a_converter_that_does_not_derive_from_a_compatible_BindingConverter()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
public class MyConverter : BindingConverter<int> public class MyConverter : BindingConverter<int>
{ {
public override int Convert(string rawValue) => 42; public override int Convert(string? rawValue) => 42;
} }
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, Converter = typeof(MyConverter))] [CommandParameter(0, Converter = typeof(MyConverter))]
public string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
// Act & assert // Act & assert
Analyzer.Should().ProduceDiagnostics(code); Analyzer.Should().ProduceDiagnostics(code);
} }
@@ -65,20 +63,19 @@ public class ParameterMustHaveValidConverterAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter() public void Analyzer_does_not_report_an_error_if_a_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
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;
} }
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, Converter = typeof(MyConverter))] [CommandParameter(0, Converter = typeof(MyConverter))]
public string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -91,20 +88,19 @@ public class ParameterMustHaveValidConverterAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_nullable_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter() public void Analyzer_does_not_report_an_error_if_a_nullable_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
public class MyConverter : BindingConverter<int> public class MyConverter : BindingConverter<int>
{ {
public override int Convert(string rawValue) => 42; public override int Convert(string? rawValue) => 42;
} }
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandOption("foo", Converter = typeof(MyConverter))] [CommandOption("foo", Converter = typeof(MyConverter))]
public int? Foo { get; set; } public int? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -117,20 +113,19 @@ public class ParameterMustHaveValidConverterAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter() public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
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;
} }
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, Converter = typeof(MyConverter))] [CommandParameter(0, Converter = typeof(MyConverter))]
public IReadOnlyList<string> Foo { get; set; } public required IReadOnlyList<string> Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -143,15 +138,14 @@ public class ParameterMustHaveValidConverterAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -164,14 +158,13 @@ public class ParameterMustHaveValidConverterAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -179,4 +172,4 @@ public class ParameterMustHaveValidConverterAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,26 +6,26 @@ namespace CliFx.Analyzers.Tests;
public class ParameterMustHaveValidValidatorsAnalyzerSpecs public class ParameterMustHaveValidValidatorsAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveValidValidatorsAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new ParameterMustHaveValidValidatorsAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_a_parameter_has_a_validator_that_does_not_derive_from_BindingValidator() public void Analyzer_reports_an_error_a_parameter_has_a_validator_that_does_not_derive_from_BindingValidator()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
public class MyValidator public class MyValidator
{ {
public void Validate(string value) {} public void Validate(string value) {}
} }
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, Validators = new[] {typeof(MyValidator)})] [CommandParameter(0, Validators = new[] {typeof(MyValidator)})]
public string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -38,20 +38,19 @@ public class ParameterMustHaveValidValidatorsAnalyzerSpecs
public void Analyzer_reports_an_error_if_a_parameter_has_a_validator_that_does_not_derive_from_a_compatible_BindingValidator() public void Analyzer_reports_an_error_if_a_parameter_has_a_validator_that_does_not_derive_from_a_compatible_BindingValidator()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
public class MyValidator : BindingValidator<int> public class MyValidator : BindingValidator<int>
{ {
public override BindingValidationError Validate(int value) => Ok(); public override BindingValidationError Validate(int value) => Ok();
} }
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, Validators = new[] {typeof(MyValidator)})] [CommandParameter(0, Validators = new[] {typeof(MyValidator)})]
public string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -64,20 +63,19 @@ public class ParameterMustHaveValidValidatorsAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_parameter_has_validators_that_all_derive_from_compatible_BindingValidators() public void Analyzer_does_not_report_an_error_if_a_parameter_has_validators_that_all_derive_from_compatible_BindingValidators()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
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();
} }
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0, Validators = new[] {typeof(MyValidator)})] [CommandParameter(0, Validators = new[] {typeof(MyValidator)})]
public string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -90,15 +88,14 @@ public class ParameterMustHaveValidValidatorsAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -111,14 +108,13 @@ public class ParameterMustHaveValidValidatorsAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
"""; """;
@@ -126,4 +122,4 @@ public class ParameterMustHaveValidValidatorsAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -6,15 +6,15 @@ namespace CliFx.Analyzers.Tests;
public class SystemConsoleShouldBeAvoidedAnalyzerSpecs public class SystemConsoleShouldBeAvoidedAnalyzerSpecs
{ {
private static DiagnosticAnalyzer Analyzer { get; } = new SystemConsoleShouldBeAvoidedAnalyzer(); private static DiagnosticAnalyzer Analyzer { get; } =
new SystemConsoleShouldBeAvoidedAnalyzer();
[Fact] [Fact]
public void Analyzer_reports_an_error_if_a_command_calls_a_method_on_SystemConsole() public void Analyzer_reports_an_error_if_a_command_calls_a_method_on_SystemConsole()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -34,9 +34,8 @@ public class SystemConsoleShouldBeAvoidedAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -56,9 +55,8 @@ public class SystemConsoleShouldBeAvoidedAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -78,9 +76,8 @@ public class SystemConsoleShouldBeAvoidedAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -100,9 +97,8 @@ public class SystemConsoleShouldBeAvoidedAnalyzerSpecs
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 // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -120,9 +116,8 @@ public class SystemConsoleShouldBeAvoidedAnalyzerSpecs
public void Analyzer_does_not_report_an_error_if_a_command_does_not_access_SystemConsole() public void Analyzer_does_not_report_an_error_if_a_command_does_not_access_SystemConsole()
{ {
// Arrange // Arrange
// language=cs // lang=csharp
const string code = const string code = """
"""
[Command] [Command]
public class MyCommand : ICommand public class MyCommand : ICommand
{ {
@@ -136,4 +131,4 @@ public class SystemConsoleShouldBeAvoidedAnalyzerSpecs
// Act & assert // Act & assert
Analyzer.Should().NotProduceDiagnostics(code); Analyzer.Should().NotProduceDiagnostics(code);
} }
} }

View File

@@ -18,9 +18,7 @@ internal class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer,
protected override string Identifier { get; } = "analyzer"; protected override string Identifier { get; } = "analyzer";
public AnalyzerAssertions(DiagnosticAnalyzer analyzer) public AnalyzerAssertions(DiagnosticAnalyzer analyzer)
: base(analyzer) : base(analyzer) { }
{
}
private Compilation Compile(string sourceCode) private Compilation Compile(string sourceCode)
{ {
@@ -43,10 +41,10 @@ internal class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer,
// 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 + + Environment.NewLine
sourceCode; + sourceCode;
// Parse the source code // Parse the source code
var ast = SyntaxFactory.ParseSyntaxTree( var ast = SyntaxFactory.ParseSyntaxTree(
@@ -58,7 +56,9 @@ internal class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer,
var compilation = CSharpCompilation.Create( var compilation = CSharpCompilation.Create(
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(), "CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
new[] { ast }, new[] { ast },
Net70.References.All Net70
.References
.All
.Append(MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location)), .Append(MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location)),
// DLL to avoid having to define the Main() method // DLL to avoid having to define the Main() method
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
@@ -103,44 +103,47 @@ internal class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer,
var producedDiagnosticIds = producedDiagnostics.Select(d => d.Id).Distinct().ToArray(); var producedDiagnosticIds = producedDiagnostics.Select(d => d.Id).Distinct().ToArray();
var isSuccessfulAssertion = var isSuccessfulAssertion =
expectedDiagnosticIds.Intersect(producedDiagnosticIds).Count() == expectedDiagnosticIds.Intersect(producedDiagnosticIds).Count()
expectedDiagnosticIds.Length; == expectedDiagnosticIds.Length;
Execute.Assertion.ForCondition(isSuccessfulAssertion).FailWith(() => Execute
{ .Assertion
var buffer = new StringBuilder(); .ForCondition(isSuccessfulAssertion)
.FailWith(() =>
buffer.AppendLine("Expected and produced diagnostics do not match.");
buffer.AppendLine();
buffer.AppendLine("Expected diagnostics:");
foreach (var expectedDiagnostic in expectedDiagnostics)
{ {
buffer.Append(" - "); var buffer = new StringBuilder();
buffer.Append(expectedDiagnostic.Id);
buffer.AppendLine("Expected and produced diagnostics do not match.");
buffer.AppendLine(); buffer.AppendLine();
}
buffer.AppendLine(); buffer.AppendLine("Expected diagnostics:");
buffer.AppendLine("Produced diagnostics:"); foreach (var expectedDiagnostic in expectedDiagnostics)
if (producedDiagnostics.Any())
{
foreach (var producedDiagnostic in producedDiagnostics)
{ {
buffer.Append(" - "); buffer.Append(" - ");
buffer.Append(producedDiagnostic); buffer.Append(expectedDiagnostic.Id);
buffer.AppendLine();
} }
}
else
{
buffer.AppendLine(" < none >");
}
return new FailReason(buffer.ToString()); buffer.AppendLine();
});
buffer.AppendLine("Produced diagnostics:");
if (producedDiagnostics.Any())
{
foreach (var producedDiagnostic in producedDiagnostics)
{
buffer.Append(" - ");
buffer.Append(producedDiagnostic);
}
}
else
{
buffer.AppendLine(" < none >");
}
return new FailReason(buffer.ToString());
});
} }
public void NotProduceDiagnostics(string sourceCode) public void NotProduceDiagnostics(string sourceCode)
@@ -148,27 +151,30 @@ internal class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer,
var producedDiagnostics = GetProducedDiagnostics(sourceCode); var producedDiagnostics = GetProducedDiagnostics(sourceCode);
var isSuccessfulAssertion = !producedDiagnostics.Any(); var isSuccessfulAssertion = !producedDiagnostics.Any();
Execute.Assertion.ForCondition(isSuccessfulAssertion).FailWith(() => Execute
{ .Assertion
var buffer = new StringBuilder(); .ForCondition(isSuccessfulAssertion)
.FailWith(() =>
buffer.AppendLine("Expected no produced diagnostics.");
buffer.AppendLine();
buffer.AppendLine("Produced diagnostics:");
foreach (var producedDiagnostic in producedDiagnostics)
{ {
buffer.Append(" - "); var buffer = new StringBuilder();
buffer.Append(producedDiagnostic);
}
return new FailReason(buffer.ToString()); 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 internal static class AnalyzerAssertionsExtensions
{ {
public static AnalyzerAssertions Should(this DiagnosticAnalyzer analyzer) => new(analyzer); public static AnalyzerAssertions Should(this DiagnosticAnalyzer analyzer) => new(analyzer);
} }

View File

@@ -14,7 +14,8 @@ public abstract class AnalyzerBase : DiagnosticAnalyzer
protected AnalyzerBase( protected AnalyzerBase(
string diagnosticTitle, string diagnosticTitle,
string diagnosticMessage, string diagnosticMessage,
DiagnosticSeverity diagnosticSeverity = DiagnosticSeverity.Error) DiagnosticSeverity diagnosticSeverity = DiagnosticSeverity.Error
)
{ {
SupportedDiagnostic = new DiagnosticDescriptor( SupportedDiagnostic = new DiagnosticDescriptor(
"CliFx_" + GetType().Name.TrimEnd("Analyzer"), "CliFx_" + GetType().Name.TrimEnd("Analyzer"),
@@ -36,4 +37,4 @@ public abstract class AnalyzerBase : DiagnosticAnalyzer
context.EnableConcurrentExecution(); context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
} }
} }

View File

@@ -17,10 +17,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CSharpier.MsBuild" Version="0.26.1" PrivateAssets="all" />
<!-- Make sure to target the lowest possible version of the compiler for wider support --> <!-- Make sure to target the lowest possible version of the compiler for wider support -->
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.0.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis" Version="3.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.0.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.0.0" PrivateAssets="all" />
<PackageReference Include="PolyShim" Version="1.2.0" PrivateAssets="all" /> <PackageReference Include="PolyShim" Version="1.8.0" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -13,14 +13,14 @@ public class CommandMustBeAnnotatedAnalyzer : AnalyzerBase
public CommandMustBeAnnotatedAnalyzer() public CommandMustBeAnnotatedAnalyzer()
: base( : base(
$"Commands must be annotated with `{SymbolNames.CliFxCommandAttribute}`", $"Commands must be annotated with `{SymbolNames.CliFxCommandAttribute}`",
$"This type must be annotated with `{SymbolNames.CliFxCommandAttribute}` in order to be a valid command.") $"This type must be annotated with `{SymbolNames.CliFxCommandAttribute}` in order to be a valid command."
{ ) { }
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
ClassDeclarationSyntax classDeclaration, ClassDeclarationSyntax classDeclaration,
ITypeSymbol type) ITypeSymbol type
)
{ {
// Ignore abstract classes, because they may be used to define // Ignore abstract classes, because they may be used to define
// base implementations for commands, in which case the command // base implementations for commands, in which case the command
@@ -28,12 +28,11 @@ public class CommandMustBeAnnotatedAnalyzer : AnalyzerBase
if (type.IsAbstract) if (type.IsAbstract)
return; return;
var implementsCommandInterface = type var implementsCommandInterface = type.AllInterfaces.Any(
.AllInterfaces i => i.DisplayNameMatches(SymbolNames.CliFxCommandInterface)
.Any(i => i.DisplayNameMatches(SymbolNames.CliFxCommandInterface)); );
var hasCommandAttribute = type var hasCommandAttribute = type.GetAttributes()
.GetAttributes()
.Select(a => a.AttributeClass) .Select(a => a.AttributeClass)
.Any(c => c.DisplayNameMatches(SymbolNames.CliFxCommandAttribute)); .Any(c => c.DisplayNameMatches(SymbolNames.CliFxCommandAttribute));
@@ -41,9 +40,7 @@ public class CommandMustBeAnnotatedAnalyzer : AnalyzerBase
// then it's very likely a user error. // then it's very likely a user error.
if (implementsCommandInterface && !hasCommandAttribute) if (implementsCommandInterface && !hasCommandAttribute)
{ {
context.ReportDiagnostic( context.ReportDiagnostic(CreateDiagnostic(classDeclaration.Identifier.GetLocation()));
CreateDiagnostic(classDeclaration.Identifier.GetLocation())
);
} }
} }
@@ -52,4 +49,4 @@ public class CommandMustBeAnnotatedAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandleClassDeclaration(Analyze); context.HandleClassDeclaration(Analyze);
} }
} }

View File

@@ -13,31 +13,28 @@ public class CommandMustImplementInterfaceAnalyzer : AnalyzerBase
public CommandMustImplementInterfaceAnalyzer() public CommandMustImplementInterfaceAnalyzer()
: base( : base(
$"Commands must implement `{SymbolNames.CliFxCommandInterface}` interface", $"Commands must implement `{SymbolNames.CliFxCommandInterface}` interface",
$"This type must implement `{SymbolNames.CliFxCommandInterface}` interface in order to be a valid command.") $"This type must implement `{SymbolNames.CliFxCommandInterface}` interface in order to be a valid command."
{ ) { }
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
ClassDeclarationSyntax classDeclaration, ClassDeclarationSyntax classDeclaration,
ITypeSymbol type) ITypeSymbol type
)
{ {
var hasCommandAttribute = type var hasCommandAttribute = type.GetAttributes()
.GetAttributes()
.Select(a => a.AttributeClass) .Select(a => a.AttributeClass)
.Any(c => c.DisplayNameMatches(SymbolNames.CliFxCommandAttribute)); .Any(c => c.DisplayNameMatches(SymbolNames.CliFxCommandAttribute));
var implementsCommandInterface = type var implementsCommandInterface = type.AllInterfaces.Any(
.AllInterfaces i => i.DisplayNameMatches(SymbolNames.CliFxCommandInterface)
.Any(i => i.DisplayNameMatches(SymbolNames.CliFxCommandInterface)); );
// If the attribute is present, but the interface is not implemented, // If the attribute is present, but the interface is not implemented,
// it's very likely a user error. // it's very likely a user error.
if (hasCommandAttribute && !implementsCommandInterface) if (hasCommandAttribute && !implementsCommandInterface)
{ {
context.ReportDiagnostic( context.ReportDiagnostic(CreateDiagnostic(classDeclaration.Identifier.GetLocation()));
CreateDiagnostic(classDeclaration.Identifier.GetLocation())
);
} }
} }
@@ -46,4 +43,4 @@ public class CommandMustImplementInterfaceAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandleClassDeclaration(Analyze); context.HandleClassDeclaration(Analyze);
} }
} }

View File

@@ -25,7 +25,8 @@ internal partial class CommandOptionSymbol : ICommandMemberSymbol
char? shortName, char? shortName,
bool? isRequired, bool? isRequired,
ITypeSymbol? converterType, ITypeSymbol? converterType,
IReadOnlyList<ITypeSymbol> validatorTypes) IReadOnlyList<ITypeSymbol> validatorTypes
)
{ {
Property = property; Property = property;
Name = name; Name = name;
@@ -38,9 +39,14 @@ internal partial class CommandOptionSymbol : ICommandMemberSymbol
internal partial class CommandOptionSymbol internal partial class CommandOptionSymbol
{ {
private static AttributeData? TryGetOptionAttribute(IPropertySymbol property) => property private static AttributeData? TryGetOptionAttribute(IPropertySymbol property) =>
.GetAttributes() property
.FirstOrDefault(a => a.AttributeClass?.DisplayNameMatches(SymbolNames.CliFxCommandOptionAttribute) == true); .GetAttributes()
.FirstOrDefault(
a =>
a.AttributeClass?.DisplayNameMatches(SymbolNames.CliFxCommandOptionAttribute)
== true
);
public static CommandOptionSymbol? TryResolve(IPropertySymbol property) public static CommandOptionSymbol? TryResolve(IPropertySymbol property)
{ {
@@ -48,23 +54,26 @@ internal partial class CommandOptionSymbol
if (attribute is null) if (attribute is null)
return null; return null;
var name = attribute var name =
.ConstructorArguments attribute
.Where(a => a.Type?.SpecialType == SpecialType.System_String) .ConstructorArguments
.Select(a => a.Value) .Where(a => a.Type?.SpecialType == SpecialType.System_String)
.FirstOrDefault() as string; .Select(a => a.Value)
.FirstOrDefault() as string;
var shortName = attribute var shortName =
.ConstructorArguments attribute
.Where(a => a.Type?.SpecialType == SpecialType.System_Char) .ConstructorArguments
.Select(a => a.Value) .Where(a => a.Type?.SpecialType == SpecialType.System_Char)
.FirstOrDefault() as char?; .Select(a => a.Value)
.FirstOrDefault() as char?;
var isRequired = attribute var isRequired =
.NamedArguments attribute
.Where(a => a.Key == "IsRequired") .NamedArguments
.Select(a => a.Value.Value) .Where(a => a.Key == "IsRequired")
.FirstOrDefault() as bool?; .Select(a => a.Value.Value)
.FirstOrDefault() as bool?;
var converter = attribute var converter = attribute
.NamedArguments .NamedArguments
@@ -81,9 +90,16 @@ internal partial class CommandOptionSymbol
.Cast<ITypeSymbol>() .Cast<ITypeSymbol>()
.ToArray(); .ToArray();
return new CommandOptionSymbol(property, name, shortName, isRequired, converter, validators); return new CommandOptionSymbol(
property,
name,
shortName,
isRequired,
converter,
validators
);
} }
public static bool IsOptionProperty(IPropertySymbol property) => public static bool IsOptionProperty(IPropertySymbol property) =>
TryGetOptionAttribute(property) is not null; TryGetOptionAttribute(property) is not null;
} }

View File

@@ -25,7 +25,8 @@ internal partial class CommandParameterSymbol : ICommandMemberSymbol
string? name, string? name,
bool? isRequired, bool? isRequired,
ITypeSymbol? converterType, ITypeSymbol? converterType,
IReadOnlyList<ITypeSymbol> validatorTypes) IReadOnlyList<ITypeSymbol> validatorTypes
)
{ {
Property = property; Property = property;
Order = order; Order = order;
@@ -38,9 +39,14 @@ internal partial class CommandParameterSymbol : ICommandMemberSymbol
internal partial class CommandParameterSymbol internal partial class CommandParameterSymbol
{ {
private static AttributeData? TryGetParameterAttribute(IPropertySymbol property) => property private static AttributeData? TryGetParameterAttribute(IPropertySymbol property) =>
.GetAttributes() property
.FirstOrDefault(a => a.AttributeClass?.DisplayNameMatches(SymbolNames.CliFxCommandParameterAttribute) == true); .GetAttributes()
.FirstOrDefault(
a =>
a.AttributeClass?.DisplayNameMatches(SymbolNames.CliFxCommandParameterAttribute)
== true
);
public static CommandParameterSymbol? TryResolve(IPropertySymbol property) public static CommandParameterSymbol? TryResolve(IPropertySymbol property)
{ {
@@ -48,22 +54,21 @@ internal partial class CommandParameterSymbol
if (attribute is null) if (attribute is null)
return null; return null;
var order = (int)attribute var order = (int)attribute.ConstructorArguments.Select(a => a.Value).First()!;
.ConstructorArguments
.Select(a => a.Value)
.First()!;
var name = attribute var name =
.NamedArguments attribute
.Where(a => a.Key == "Name") .NamedArguments
.Select(a => a.Value.Value) .Where(a => a.Key == "Name")
.FirstOrDefault() as string; .Select(a => a.Value.Value)
.FirstOrDefault() as string;
var isRequired = attribute var isRequired =
.NamedArguments attribute
.Where(a => a.Key == "IsRequired") .NamedArguments
.Select(a => a.Value.Value) .Where(a => a.Key == "IsRequired")
.FirstOrDefault() as bool?; .Select(a => a.Value.Value)
.FirstOrDefault() as bool?;
var converter = attribute var converter = attribute
.NamedArguments .NamedArguments
@@ -85,4 +90,4 @@ internal partial class CommandParameterSymbol
public static bool IsParameterProperty(IPropertySymbol property) => public static bool IsParameterProperty(IPropertySymbol property) =>
TryGetParameterAttribute(property) is not null; TryGetParameterAttribute(property) is not null;
} }

View File

@@ -16,6 +16,6 @@ internal interface ICommandMemberSymbol
internal static class CommandMemberSymbolExtensions internal static class CommandMemberSymbolExtensions
{ {
public static bool IsScalar(this ICommandMemberSymbol member) => public static bool IsScalar(this ICommandMemberSymbol member) =>
member.Property.Type.SpecialType == SpecialType.System_String || member.Property.Type.SpecialType == SpecialType.System_String
member.Property.Type.TryGetEnumerableUnderlyingType() is null; || member.Property.Type.TryGetEnumerableUnderlyingType() is null;
} }

View File

@@ -4,9 +4,10 @@ internal static class SymbolNames
{ {
public const string CliFxCommandInterface = "CliFx.ICommand"; public const string CliFxCommandInterface = "CliFx.ICommand";
public const string CliFxCommandAttribute = "CliFx.Attributes.CommandAttribute"; public const string CliFxCommandAttribute = "CliFx.Attributes.CommandAttribute";
public const string CliFxCommandParameterAttribute = "CliFx.Attributes.CommandParameterAttribute"; public const string CliFxCommandParameterAttribute =
"CliFx.Attributes.CommandParameterAttribute";
public const string CliFxCommandOptionAttribute = "CliFx.Attributes.CommandOptionAttribute"; public const string CliFxCommandOptionAttribute = "CliFx.Attributes.CommandOptionAttribute";
public const string CliFxConsoleInterface = "CliFx.Infrastructure.IConsole"; public const string CliFxConsoleInterface = "CliFx.Infrastructure.IConsole";
public const string CliFxBindingConverterClass = "CliFx.Extensibility.BindingConverter<T>"; public const string CliFxBindingConverterClass = "CliFx.Extensibility.BindingConverter<T>";
public const string CliFxBindingValidatorClass = "CliFx.Extensibility.BindingValidator<T>"; public const string CliFxBindingValidatorClass = "CliFx.Extensibility.BindingValidator<T>";
} }

View File

@@ -13,14 +13,14 @@ public class OptionMustBeInsideCommandAnalyzer : AnalyzerBase
public OptionMustBeInsideCommandAnalyzer() public OptionMustBeInsideCommandAnalyzer()
: base( : base(
"Options must be defined inside commands", "Options must be defined inside commands",
$"This option must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`.") $"This option must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`."
{ ) { }
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property
)
{ {
if (property.ContainingType is null) if (property.ContainingType is null)
return; return;
@@ -49,4 +49,4 @@ public class OptionMustBeInsideCommandAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -12,14 +12,14 @@ public class OptionMustBeRequiredIfPropertyRequiredAnalyzer : AnalyzerBase
public OptionMustBeRequiredIfPropertyRequiredAnalyzer() public OptionMustBeRequiredIfPropertyRequiredAnalyzer()
: base( : base(
"Options bound to required properties cannot be marked as non-required", "Options bound to required properties cannot be marked as non-required",
"This option cannot be marked as non-required because it's bound to a required property.") "This option cannot be marked as non-required because it's bound to a required property."
{ ) { }
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property
)
{ {
if (property.ContainingType is null) if (property.ContainingType is null)
return; return;
@@ -34,11 +34,7 @@ public class OptionMustBeRequiredIfPropertyRequiredAnalyzer : AnalyzerBase
if (option.IsRequired != false) if (option.IsRequired != false)
return; return;
context.ReportDiagnostic( context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.Identifier.GetLocation()));
CreateDiagnostic(
propertyDeclaration.Identifier.GetLocation()
)
);
} }
public override void Initialize(AnalysisContext context) public override void Initialize(AnalysisContext context)
@@ -46,4 +42,4 @@ public class OptionMustBeRequiredIfPropertyRequiredAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -12,14 +12,14 @@ public class OptionMustHaveNameOrShortNameAnalyzer : AnalyzerBase
public OptionMustHaveNameOrShortNameAnalyzer() public OptionMustHaveNameOrShortNameAnalyzer()
: base( : base(
"Options must have either a name or short name specified", "Options must have either a name or short name specified",
"This option 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)
@@ -38,4 +38,4 @@ public class OptionMustHaveNameOrShortNameAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -14,16 +14,16 @@ public class OptionMustHaveUniqueNameAnalyzer : AnalyzerBase
public OptionMustHaveUniqueNameAnalyzer() public OptionMustHaveUniqueNameAnalyzer()
: base( : base(
"Options must have unique names", "Options must have unique names",
"This option's name must be unique within the command (comparison IS NOT case sensitive). " + "This option's name must be unique within the command (comparison IS NOT case sensitive). "
"Specified name: `{0}`. " + + "Specified name: `{0}`. "
"Property bound to another option with the same name: `{1}`.") + "Property bound to another option with the same name: `{1}`."
{ ) { }
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property
)
{ {
if (property.ContainingType is null) if (property.ContainingType is null)
return; return;
@@ -69,4 +69,4 @@ public class OptionMustHaveUniqueNameAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -13,16 +13,16 @@ public class OptionMustHaveUniqueShortNameAnalyzer : AnalyzerBase
public OptionMustHaveUniqueShortNameAnalyzer() public OptionMustHaveUniqueShortNameAnalyzer()
: base( : base(
"Options must have unique short names", "Options must have unique short names",
"This option's short name must be unique within the command (comparison IS case sensitive). " + "This option's short name must be unique within the command (comparison IS case sensitive). "
"Specified short name: `{0}` " + + "Specified short name: `{0}` "
"Property bound to another option with the same short name: `{1}`.") + "Property bound to another option with the same short name: `{1}`."
{ ) { }
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property
)
{ {
if (property.ContainingType is null) if (property.ContainingType is null)
return; return;
@@ -68,4 +68,4 @@ public class OptionMustHaveUniqueShortNameAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -13,14 +13,14 @@ public class OptionMustHaveValidConverterAnalyzer : AnalyzerBase
public OptionMustHaveValidConverterAnalyzer() public OptionMustHaveValidConverterAnalyzer()
: base( : base(
$"Option converters must derive from `{SymbolNames.CliFxBindingConverterClass}`", $"Option converters must derive from `{SymbolNames.CliFxBindingConverterClass}`",
$"Converter specified for this option must derive from a compatible `{SymbolNames.CliFxBindingConverterClass}`.") $"Converter specified for this option must derive from a compatible `{SymbolNames.CliFxBindingConverterClass}`."
{ ) { }
}
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)
@@ -32,18 +32,24 @@ public class OptionMustHaveValidConverterAnalyzer : AnalyzerBase
var converterValueType = option var converterValueType = option
.ConverterType .ConverterType
.GetBaseTypes() .GetBaseTypes()
.FirstOrDefault(t => t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingConverterClass))? .FirstOrDefault(
.TypeArguments t => t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingConverterClass)
)
?.TypeArguments
.FirstOrDefault(); .FirstOrDefault();
// Value returned by the converter must be assignable to the property type // Value returned by the converter must be assignable to the property type
var isCompatible = var isCompatible =
converterValueType is not null && (option.IsScalar() converterValueType is not null
// Scalar && (
? context.Compilation.IsAssignable(converterValueType, property.Type) option.IsScalar()
// Non-scalar (assume we can handle all IEnumerable types for simplicity) // Scalar
: property.Type.TryGetEnumerableUnderlyingType() is { } enumerableUnderlyingType && ? context.Compilation.IsAssignable(converterValueType, property.Type)
context.Compilation.IsAssignable(converterValueType, enumerableUnderlyingType) // Non-scalar (assume we can handle all IEnumerable types for simplicity)
: property.Type.TryGetEnumerableUnderlyingType() is { } enumerableUnderlyingType
&& context
.Compilation
.IsAssignable(converterValueType, enumerableUnderlyingType)
); );
if (!isCompatible) if (!isCompatible)
@@ -59,4 +65,4 @@ public class OptionMustHaveValidConverterAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -12,15 +12,15 @@ public class OptionMustHaveValidNameAnalyzer : AnalyzerBase
public OptionMustHaveValidNameAnalyzer() public OptionMustHaveValidNameAnalyzer()
: base( : base(
"Options must have valid names", "Options must have valid names",
"This option's name must be at least 2 characters long and must start with a letter. " + "This option's name must be at least 2 characters long and must start with a letter. "
"Specified name: `{0}`.") + "Specified name: `{0}`."
{ ) { }
}
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)
@@ -32,10 +32,7 @@ public class OptionMustHaveValidNameAnalyzer : AnalyzerBase
if (option.Name.Length < 2 || !char.IsLetter(option.Name[0])) if (option.Name.Length < 2 || !char.IsLetter(option.Name[0]))
{ {
context.ReportDiagnostic( context.ReportDiagnostic(
CreateDiagnostic( CreateDiagnostic(propertyDeclaration.Identifier.GetLocation(), option.Name)
propertyDeclaration.Identifier.GetLocation(),
option.Name
)
); );
} }
} }
@@ -45,4 +42,4 @@ public class OptionMustHaveValidNameAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -12,15 +12,15 @@ public class OptionMustHaveValidShortNameAnalyzer : AnalyzerBase
public OptionMustHaveValidShortNameAnalyzer() public OptionMustHaveValidShortNameAnalyzer()
: base( : base(
"Option short names must be letter characters", "Option short names must be letter characters",
"This option's short name must be a single letter character. " + "This option's short name must be a single letter character. "
"Specified short name: `{0}`.") + "Specified short name: `{0}`."
{ ) { }
}
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)
@@ -32,10 +32,7 @@ public class OptionMustHaveValidShortNameAnalyzer : AnalyzerBase
if (!char.IsLetter(option.ShortName.Value)) if (!char.IsLetter(option.ShortName.Value))
{ {
context.ReportDiagnostic( context.ReportDiagnostic(
CreateDiagnostic( CreateDiagnostic(propertyDeclaration.Identifier.GetLocation(), option.ShortName)
propertyDeclaration.Identifier.GetLocation(),
option.ShortName
)
); );
} }
} }
@@ -45,4 +42,4 @@ public class OptionMustHaveValidShortNameAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -13,14 +13,14 @@ public class OptionMustHaveValidValidatorsAnalyzer : AnalyzerBase
public OptionMustHaveValidValidatorsAnalyzer() public OptionMustHaveValidValidatorsAnalyzer()
: base( : base(
$"Option validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`", $"Option validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`",
$"Each validator specified for this option must derive from a compatible `{SymbolNames.CliFxBindingValidatorClass}`.") $"Each validator specified for this option must derive from a compatible `{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)
@@ -30,14 +30,17 @@ public class OptionMustHaveValidValidatorsAnalyzer : AnalyzerBase
{ {
var validatorValueType = validatorType var validatorValueType = validatorType
.GetBaseTypes() .GetBaseTypes()
.FirstOrDefault(t => t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingValidatorClass))? .FirstOrDefault(
.TypeArguments t =>
t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingValidatorClass)
)
?.TypeArguments
.FirstOrDefault(); .FirstOrDefault();
// Value passed to the validator must be assignable from the property type // Value passed to the validator must be assignable from the property type
var isCompatible = var isCompatible =
validatorValueType is not null && validatorValueType is not null
context.Compilation.IsAssignable(property.Type, validatorValueType); && context.Compilation.IsAssignable(property.Type, validatorValueType);
if (!isCompatible) if (!isCompatible)
{ {
@@ -56,4 +59,4 @@ public class OptionMustHaveValidValidatorsAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -13,14 +13,14 @@ public class ParameterMustBeInsideCommandAnalyzer : AnalyzerBase
public ParameterMustBeInsideCommandAnalyzer() public ParameterMustBeInsideCommandAnalyzer()
: base( : base(
"Parameters must be defined inside commands", "Parameters must be defined inside commands",
$"This parameter must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`.") $"This parameter must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`."
{ ) { }
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property
)
{ {
if (property.ContainingType is null) if (property.ContainingType is null)
return; return;
@@ -49,4 +49,4 @@ public class ParameterMustBeInsideCommandAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -13,15 +13,15 @@ public class ParameterMustBeLastIfNonRequiredAnalyzer : AnalyzerBase
public ParameterMustBeLastIfNonRequiredAnalyzer() public ParameterMustBeLastIfNonRequiredAnalyzer()
: base( : base(
"Parameters marked as non-required must be the last in order", "Parameters marked as non-required must be the last in order",
"This parameter is non-required so it must be the last in order (its order must be highest within the command). " + "This parameter is non-required so it must be the last in order (its order must be highest within the command). "
"Property bound to another non-required parameter: `{0}`.") + "Property bound to another non-required parameter: `{0}`."
{ ) { }
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property
)
{ {
if (property.ContainingType is null) if (property.ContainingType is null)
return; return;
@@ -63,4 +63,4 @@ public class ParameterMustBeLastIfNonRequiredAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -13,15 +13,15 @@ public class ParameterMustBeLastIfNonScalarAnalyzer : AnalyzerBase
public ParameterMustBeLastIfNonScalarAnalyzer() public ParameterMustBeLastIfNonScalarAnalyzer()
: base( : base(
"Parameters of non-scalar types must be the last in order", "Parameters of non-scalar types must be the last in order",
"This parameter has a non-scalar type so it must be the last in order (its order must be highest within the command). " + "This parameter has a non-scalar type so it must be the last in order (its order must be highest within the command). "
"Property bound to another non-scalar parameter: `{0}`.") + "Property bound to another non-scalar parameter: `{0}`."
{ ) { }
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property
)
{ {
if (property.ContainingType is null) if (property.ContainingType is null)
return; return;
@@ -63,4 +63,4 @@ public class ParameterMustBeLastIfNonScalarAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -12,14 +12,14 @@ public class ParameterMustBeRequiredIfPropertyRequiredAnalyzer : AnalyzerBase
public ParameterMustBeRequiredIfPropertyRequiredAnalyzer() public ParameterMustBeRequiredIfPropertyRequiredAnalyzer()
: base( : base(
"Parameters bound to required properties cannot be marked as non-required", "Parameters bound to required properties cannot be marked as non-required",
"This parameter cannot be marked as non-required because it's bound to a required property.") "This parameter cannot be marked as non-required because it's bound to a required property."
{ ) { }
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property
)
{ {
if (property.ContainingType is null) if (property.ContainingType is null)
return; return;
@@ -34,11 +34,7 @@ public class ParameterMustBeRequiredIfPropertyRequiredAnalyzer : AnalyzerBase
if (parameter.IsRequired != false) if (parameter.IsRequired != false)
return; return;
context.ReportDiagnostic( context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.Identifier.GetLocation()));
CreateDiagnostic(
propertyDeclaration.Identifier.GetLocation()
)
);
} }
public override void Initialize(AnalysisContext context) public override void Initialize(AnalysisContext context)
@@ -46,4 +42,4 @@ public class ParameterMustBeRequiredIfPropertyRequiredAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -13,15 +13,15 @@ public class ParameterMustBeSingleIfNonRequiredAnalyzer : AnalyzerBase
public ParameterMustBeSingleIfNonRequiredAnalyzer() public ParameterMustBeSingleIfNonRequiredAnalyzer()
: base( : base(
"Parameters marked as non-required are limited to one per command", "Parameters marked as non-required are limited to one per command",
"This parameter is non-required so it must be the only such parameter in the command. " + "This parameter is non-required so it must be the only such parameter in the command. "
"Property bound to another non-required parameter: `{0}`.") + "Property bound to another non-required parameter: `{0}`."
{ ) { }
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property
)
{ {
if (property.ContainingType is null) if (property.ContainingType is null)
return; return;
@@ -63,4 +63,4 @@ public class ParameterMustBeSingleIfNonRequiredAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -13,15 +13,15 @@ public class ParameterMustBeSingleIfNonScalarAnalyzer : AnalyzerBase
public ParameterMustBeSingleIfNonScalarAnalyzer() public ParameterMustBeSingleIfNonScalarAnalyzer()
: base( : base(
"Parameters of non-scalar types are limited to one per command", "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. " + "This parameter has a non-scalar type so it must be the only such parameter in the command. "
"Property bound to another non-scalar parameter: `{0}`.") + "Property bound to another non-scalar parameter: `{0}`."
{ ) { }
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property
)
{ {
if (property.ContainingType is null) if (property.ContainingType is null)
return; return;
@@ -63,4 +63,4 @@ public class ParameterMustBeSingleIfNonScalarAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -14,16 +14,16 @@ public class ParameterMustHaveUniqueNameAnalyzer : AnalyzerBase
public ParameterMustHaveUniqueNameAnalyzer() public ParameterMustHaveUniqueNameAnalyzer()
: base( : base(
"Parameters must have unique names", "Parameters must have unique names",
"This parameter's name must be unique within the command (comparison IS NOT case sensitive). " + "This parameter's name must be unique within the command (comparison IS NOT case sensitive). "
"Specified name: `{0}`. " + + "Specified name: `{0}`. "
"Property bound to another parameter with the same name: `{1}`.") + "Property bound to another parameter with the same name: `{1}`."
{ ) { }
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property
)
{ {
if (property.ContainingType is null) if (property.ContainingType is null)
return; return;
@@ -51,7 +51,13 @@ public class ParameterMustHaveUniqueNameAnalyzer : AnalyzerBase
if (string.IsNullOrWhiteSpace(otherParameter.Name)) if (string.IsNullOrWhiteSpace(otherParameter.Name))
continue; continue;
if (string.Equals(parameter.Name, otherParameter.Name, StringComparison.OrdinalIgnoreCase)) if (
string.Equals(
parameter.Name,
otherParameter.Name,
StringComparison.OrdinalIgnoreCase
)
)
{ {
context.ReportDiagnostic( context.ReportDiagnostic(
CreateDiagnostic( CreateDiagnostic(
@@ -69,4 +75,4 @@ public class ParameterMustHaveUniqueNameAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -13,16 +13,16 @@ public class ParameterMustHaveUniqueOrderAnalyzer : AnalyzerBase
public ParameterMustHaveUniqueOrderAnalyzer() public ParameterMustHaveUniqueOrderAnalyzer()
: base( : base(
"Parameters must have unique order", "Parameters must have unique order",
"This parameter's order must be unique within the command. " + "This parameter's order must be unique within the command. "
"Specified order: {0}. " + + "Specified order: {0}. "
"Property bound to another parameter with the same order: `{1}`.") + "Property bound to another parameter with the same order: `{1}`."
{ ) { }
}
private void Analyze( private void Analyze(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration, PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property) IPropertySymbol property
)
{ {
if (property.ContainingType is null) if (property.ContainingType is null)
return; return;
@@ -62,4 +62,4 @@ public class ParameterMustHaveUniqueOrderAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -13,14 +13,14 @@ public class ParameterMustHaveValidConverterAnalyzer : AnalyzerBase
public ParameterMustHaveValidConverterAnalyzer() public ParameterMustHaveValidConverterAnalyzer()
: base( : base(
$"Parameter converters must derive from `{SymbolNames.CliFxBindingConverterClass}`", $"Parameter converters must derive from `{SymbolNames.CliFxBindingConverterClass}`",
$"Converter specified for this parameter must derive from a compatible `{SymbolNames.CliFxBindingConverterClass}`.") $"Converter specified for this parameter must derive from a compatible `{SymbolNames.CliFxBindingConverterClass}`."
{ ) { }
}
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)
@@ -32,18 +32,24 @@ public class ParameterMustHaveValidConverterAnalyzer : AnalyzerBase
var converterValueType = parameter var converterValueType = parameter
.ConverterType .ConverterType
.GetBaseTypes() .GetBaseTypes()
.FirstOrDefault(t => t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingConverterClass))? .FirstOrDefault(
.TypeArguments t => t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingConverterClass)
)
?.TypeArguments
.FirstOrDefault(); .FirstOrDefault();
// Value returned by the converter must be assignable to the property type // Value returned by the converter must be assignable to the property type
var isCompatible = var isCompatible =
converterValueType is not null && (parameter.IsScalar() converterValueType is not null
// Scalar && (
? context.Compilation.IsAssignable(converterValueType, property.Type) parameter.IsScalar()
// Non-scalar (assume we can handle all IEnumerable types for simplicity) // Scalar
: property.Type.TryGetEnumerableUnderlyingType() is { } enumerableUnderlyingType && ? context.Compilation.IsAssignable(converterValueType, property.Type)
context.Compilation.IsAssignable(converterValueType, enumerableUnderlyingType) // Non-scalar (assume we can handle all IEnumerable types for simplicity)
: property.Type.TryGetEnumerableUnderlyingType() is { } enumerableUnderlyingType
&& context
.Compilation
.IsAssignable(converterValueType, enumerableUnderlyingType)
); );
if (!isCompatible) if (!isCompatible)
@@ -59,4 +65,4 @@ public class ParameterMustHaveValidConverterAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -13,14 +13,14 @@ public class ParameterMustHaveValidValidatorsAnalyzer : AnalyzerBase
public ParameterMustHaveValidValidatorsAnalyzer() public ParameterMustHaveValidValidatorsAnalyzer()
: base( : base(
$"Parameter validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`", $"Parameter validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`",
$"Each validator specified for this parameter must derive from a compatible `{SymbolNames.CliFxBindingValidatorClass}`.") $"Each validator specified for this parameter must derive from a compatible `{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)
@@ -30,14 +30,17 @@ public class ParameterMustHaveValidValidatorsAnalyzer : AnalyzerBase
{ {
var validatorValueType = validatorType var validatorValueType = validatorType
.GetBaseTypes() .GetBaseTypes()
.FirstOrDefault(t => t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingValidatorClass))? .FirstOrDefault(
.TypeArguments t =>
t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingValidatorClass)
)
?.TypeArguments
.FirstOrDefault(); .FirstOrDefault();
// Value passed to the validator must be assignable from the property type // Value passed to the validator must be assignable from the property type
var isCompatible = var isCompatible =
validatorValueType is not null && validatorValueType is not null
context.Compilation.IsAssignable(property.Type, validatorValueType); && context.Compilation.IsAssignable(property.Type, validatorValueType);
if (!isCompatible) if (!isCompatible)
{ {
@@ -56,4 +59,4 @@ public class ParameterMustHaveValidValidatorsAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.HandlePropertyDeclaration(Analyze); context.HandlePropertyDeclaration(Analyze);
} }
} }

View File

@@ -15,13 +15,13 @@ public class SystemConsoleShouldBeAvoidedAnalyzer : AnalyzerBase
: base( : base(
$"Avoid calling `System.Console` where `{SymbolNames.CliFxConsoleInterface}` is available", $"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.", $"Use the provided `{SymbolNames.CliFxConsoleInterface}` abstraction instead of `System.Console` to ensure that the command can be tested in isolation.",
DiagnosticSeverity.Warning) DiagnosticSeverity.Warning
{ ) { }
}
private MemberAccessExpressionSyntax? TryGetSystemConsoleMemberAccess( private MemberAccessExpressionSyntax? TryGetSystemConsoleMemberAccess(
SyntaxNodeAnalysisContext context, SyntaxNodeAnalysisContext context,
SyntaxNode node) SyntaxNode node
)
{ {
var currentNode = node; var currentNode = node;
@@ -65,9 +65,7 @@ public class SystemConsoleShouldBeAvoidedAnalyzer : AnalyzerBase
if (isConsoleInterfaceAvailable) if (isConsoleInterfaceAvailable)
{ {
context.ReportDiagnostic( context.ReportDiagnostic(CreateDiagnostic(systemConsoleMemberAccess.GetLocation()));
CreateDiagnostic(systemConsoleMemberAccess.GetLocation())
);
} }
} }
@@ -76,4 +74,4 @@ public class SystemConsoleShouldBeAvoidedAnalyzer : AnalyzerBase
base.Initialize(context); base.Initialize(context);
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.SimpleMemberAccessExpression); context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.SimpleMemberAccessExpression);
} }
} }

View File

@@ -29,56 +29,72 @@ internal static class RoslynExtensions
} }
} }
public static ITypeSymbol? TryGetEnumerableUnderlyingType(this ITypeSymbol type) => type public static ITypeSymbol? TryGetEnumerableUnderlyingType(this ITypeSymbol type) =>
.AllInterfaces type.AllInterfaces
.FirstOrDefault(i => i.ConstructedFrom.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T)? .FirstOrDefault(
.TypeArguments[0]; i =>
i.ConstructedFrom.SpecialType
== SpecialType.System_Collections_Generic_IEnumerable_T
)
?.TypeArguments[0];
// Detect if the property is required through roundabout means so as to not have to take dependency // Detect if the property is required through roundabout means so as to not have to take dependency
// on higher versions of the C# compiler. // on higher versions of the C# compiler.
public static bool IsRequired(this IPropertySymbol property) => property public static bool IsRequired(this IPropertySymbol property) =>
// Can't rely on the RequiredMemberAttribute because it's generated by the compiler, not added by the user, property
// so we have to check for the presence of the `required` modifier in the syntax tree instead. // Can't rely on the RequiredMemberAttribute because it's generated by the compiler, not added by the user,
.DeclaringSyntaxReferences // so we have to check for the presence of the `required` modifier in the syntax tree instead.
.Select(r => r.GetSyntax()) .DeclaringSyntaxReferences
.OfType<PropertyDeclarationSyntax>() .Select(r => r.GetSyntax())
.SelectMany(p => p.Modifiers) .OfType<PropertyDeclarationSyntax>()
.Any(m => m.IsKind((SyntaxKind)8447)); .SelectMany(p => p.Modifiers)
.Any(m => m.IsKind((SyntaxKind)8447));
public static bool IsAssignable(this Compilation compilation, ITypeSymbol source, ITypeSymbol destination) => public static bool IsAssignable(
compilation.ClassifyConversion(source, destination).Exists; this Compilation compilation,
ITypeSymbol source,
ITypeSymbol destination
) => compilation.ClassifyConversion(source, destination).Exists;
public static void HandleClassDeclaration( public static void HandleClassDeclaration(
this AnalysisContext analysisContext, this AnalysisContext analysisContext,
Action<SyntaxNodeAnalysisContext, ClassDeclarationSyntax, ITypeSymbol> analyze) Action<SyntaxNodeAnalysisContext, ClassDeclarationSyntax, ITypeSymbol> analyze
)
{ {
analysisContext.RegisterSyntaxNodeAction(ctx => 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;
analyze(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> analyze) 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;
analyze(ctx, propertyDeclaration, property); analyze(ctx, propertyDeclaration, property);
}, SyntaxKind.PropertyDeclaration); },
SyntaxKind.PropertyDeclaration
);
} }
} }

View File

@@ -7,11 +7,12 @@ internal static class StringExtensions
public static string TrimEnd( public static string TrimEnd(
this string str, this string str,
string sub, string sub,
StringComparison comparison = StringComparison.Ordinal) StringComparison comparison = StringComparison.Ordinal
)
{ {
while (str.EndsWith(sub, comparison)) while (str.EndsWith(sub, comparison))
str = str[..^sub.Length]; str = str[..^sub.Length];
return str; return str;
} }
} }

View File

@@ -29,4 +29,4 @@ public partial class Benchmarks
.AddCommand<CliFxCommand>() .AddCommand<CliFxCommand>()
.Build() .Build()
.RunAsync(Arguments, new Dictionary<string, string>()); .RunAsync(Arguments, new Dictionary<string, string>());
} }

View File

@@ -16,11 +16,9 @@ public partial class Benchmarks
[NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)] [NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)]
public bool BoolOption { get; set; } public bool BoolOption { get; set; }
public void Execute() public void Execute() { }
{
}
} }
[Benchmark(Description = "Clipr")] [Benchmark(Description = "Clipr")]
public void ExecuteWithClipr() => CliParser.Parse<CliprCommand>(Arguments).Execute(); public void ExecuteWithClipr() => CliParser.Parse<CliprCommand>(Arguments).Execute();
} }

View File

@@ -8,16 +8,12 @@ public partial class Benchmarks
public class CoconaCommand public class CoconaCommand
{ {
public void Execute( public void Execute(
[Option("str", new []{'s'})] [Option("str", new[] { 's' })] string? strOption,
string? strOption, [Option("int", new[] { 'i' })] int intOption,
[Option("int", new []{'i'})] [Option("bool", new[] { 'b' })] bool boolOption
int intOption, ) { }
[Option("bool", new []{'b'})]
bool boolOption)
{
}
} }
[Benchmark(Description = "Cocona")] [Benchmark(Description = "Cocona")]
public void ExecuteWithCocona() => CoconaApp.Run<CoconaCommand>(Arguments); public void ExecuteWithCocona() => CoconaApp.Run<CoconaCommand>(Arguments);
} }

View File

@@ -16,9 +16,7 @@ public partial class Benchmarks
[Option('b', "bool")] [Option('b', "bool")]
public bool BoolOption { get; set; } public bool BoolOption { get; set; }
public void Execute() public void Execute() { }
{
}
} }
[Benchmark(Description = "CommandLineParser")] [Benchmark(Description = "CommandLineParser")]
@@ -26,4 +24,4 @@ public partial class Benchmarks
new Parser() new Parser()
.ParseArguments(Arguments, typeof(CommandLineParserCommand)) .ParseArguments(Arguments, typeof(CommandLineParserCommand))
.WithParsed<CommandLineParserCommand>(c => c.Execute()); .WithParsed<CommandLineParserCommand>(c => c.Execute());
} }

View File

@@ -21,4 +21,4 @@ public partial class Benchmarks
[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] [Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
public int ExecuteWithMcMaster() => CommandLineApplication.Execute<McMasterCommand>(Arguments); public int ExecuteWithMcMaster() => CommandLineApplication.Execute<McMasterCommand>(Arguments);
} }

View File

@@ -16,11 +16,9 @@ public partial class Benchmarks
[ArgShortcut("--bool"), ArgShortcut("-b")] [ArgShortcut("--bool"), ArgShortcut("-b")]
public bool BoolOption { get; set; } public bool BoolOption { get; set; }
public void Main() public void Main() { }
{
}
} }
[Benchmark(Description = "PowerArgs")] [Benchmark(Description = "PowerArgs")]
public void ExecuteWithPowerArgs() => Args.InvokeMain<PowerArgsCommand>(Arguments); public void ExecuteWithPowerArgs() => Args.InvokeMain<PowerArgsCommand>(Arguments);
} }

View File

@@ -15,18 +15,9 @@ public partial class Benchmarks
{ {
var command = new RootCommand var command = new RootCommand
{ {
new Option(new[] {"--str", "-s"}) new Option(new[] { "--str", "-s" }) { Argument = new Argument<string?>() },
{ new Option(new[] { "--int", "-i" }) { Argument = new Argument<int>() },
Argument = new Argument<string?>() new Option(new[] { "--bool", "-b" }) { Argument = new Argument<bool>() }
},
new Option(new[] {"--int", "-i"})
{
Argument = new Argument<int>()
},
new Option(new[] {"--bool", "-b"})
{
Argument = new Argument<bool>()
}
}; };
command.Handler = CommandHandler.Create( command.Handler = CommandHandler.Create(
@@ -40,4 +31,4 @@ public partial class Benchmarks
[Benchmark(Description = "System.CommandLine")] [Benchmark(Description = "System.CommandLine")]
public async Task<int> ExecuteWithSystemCommandLine() => public async Task<int> ExecuteWithSystemCommandLine() =>
await new SystemCommandLineCommand().ExecuteAsync(Arguments); await new SystemCommandLineCommand().ExecuteAsync(Arguments);
} }

View File

@@ -9,11 +9,10 @@ namespace CliFx.Benchmarks;
[Orderer(SummaryOrderPolicy.FastestToSlowest)] [Orderer(SummaryOrderPolicy.FastestToSlowest)]
public partial class Benchmarks public partial class Benchmarks
{ {
private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"}; private static readonly string[] Arguments = { "--str", "hello world", "-i", "13", "-b" };
public static void Main() => BenchmarkRunner.Run<Benchmarks>( public static void Main() =>
DefaultConfig BenchmarkRunner.Run<Benchmarks>(
.Instance DefaultConfig.Instance.WithOptions(ConfigOptions.DisableOptimizationsValidator)
.WithOptions(ConfigOptions.DisableOptimizationsValidator) );
); }
}

View File

@@ -2,16 +2,17 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.5" /> <PackageReference Include="BenchmarkDotNet" Version="0.13.10" />
<PackageReference Include="clipr" Version="1.6.1" /> <PackageReference Include="clipr" Version="1.6.1" />
<PackageReference Include="Cocona" Version="2.2.0" /> <PackageReference Include="Cocona" Version="2.2.0" />
<PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.0.2" /> <PackageReference Include="CSharpier.MsBuild" Version="0.26.1" PrivateAssets="all" />
<PackageReference Include="PowerArgs" Version="4.0.2" /> <PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.0" />
<PackageReference Include="PowerArgs" Version="4.0.3" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20574.7" /> <PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20574.7" />
</ItemGroup> </ItemGroup>

View File

@@ -2,11 +2,13 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ApplicationIcon>../favicon.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> <PackageReference Include="CSharpier.MsBuild" Version="0.26.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -8,7 +8,7 @@ using CliFx.Infrastructure;
namespace CliFx.Demo.Commands; namespace CliFx.Demo.Commands;
[Command("book add", Description = "Add a book to the library.")] [Command("book add", Description = "Adds a book to the library.")]
public partial class BookAddCommand : ICommand public partial class BookAddCommand : ICommand
{ {
private readonly LibraryProvider _libraryProvider; private readonly LibraryProvider _libraryProvider;
@@ -16,7 +16,7 @@ public partial class BookAddCommand : ICommand
[CommandParameter(0, Name = "title", Description = "Book title.")] [CommandParameter(0, Name = "title", Description = "Book title.")]
public required string Title { get; init; } public required string Title { get; init; }
[CommandOption("author", 'a', IsRequired = true, Description = "Book author.")] [CommandOption("author", 'a', Description = "Book author.")]
public required string Author { get; init; } public required string Author { get; init; }
[CommandOption("published", 'p', Description = "Book publish date.")] [CommandOption("published", 'p', Description = "Book publish date.")]
@@ -49,21 +49,23 @@ public partial class BookAddCommand
{ {
private static readonly Random Random = new(); private static readonly Random Random = new();
private static DateTimeOffset CreateRandomDate() => new( private static DateTimeOffset CreateRandomDate() =>
Random.Next(1800, 2020), new(
Random.Next(1, 12), Random.Next(1800, 2020),
Random.Next(1, 28), Random.Next(1, 12),
Random.Next(1, 23), Random.Next(1, 28),
Random.Next(1, 59), Random.Next(1, 23),
Random.Next(1, 59), Random.Next(1, 59),
TimeSpan.Zero Random.Next(1, 59),
); TimeSpan.Zero
);
private static Isbn CreateRandomIsbn() => new( private static Isbn CreateRandomIsbn() =>
Random.Next(0, 999), new(
Random.Next(0, 99), Random.Next(0, 999),
Random.Next(0, 99999), Random.Next(0, 99),
Random.Next(0, 99), Random.Next(0, 99999),
Random.Next(0, 9) Random.Next(0, 99),
); Random.Next(0, 9)
} );
}

View File

@@ -7,7 +7,7 @@ using CliFx.Infrastructure;
namespace CliFx.Demo.Commands; namespace CliFx.Demo.Commands;
[Command("book", Description = "Retrieve a book from the library.")] [Command("book", Description = "Retrieves a book from the library.")]
public class BookCommand : ICommand public class BookCommand : ICommand
{ {
private readonly LibraryProvider _libraryProvider; private readonly LibraryProvider _libraryProvider;
@@ -31,4 +31,4 @@ public class BookCommand : ICommand
return default; return default;
} }
} }

View File

@@ -6,7 +6,7 @@ using CliFx.Infrastructure;
namespace CliFx.Demo.Commands; namespace CliFx.Demo.Commands;
[Command("book list", Description = "List all books in the library.")] [Command("book list", Description = "Lists all books in the library.")]
public class BookListCommand : ICommand public class BookListCommand : ICommand
{ {
private readonly LibraryProvider _libraryProvider; private readonly LibraryProvider _libraryProvider;
@@ -33,4 +33,4 @@ public class BookListCommand : ICommand
return default; return default;
} }
} }

View File

@@ -6,7 +6,7 @@ using CliFx.Infrastructure;
namespace CliFx.Demo.Commands; namespace CliFx.Demo.Commands;
[Command("book remove", Description = "Remove a book from the library.")] [Command("book remove", Description = "Removes a book from the library.")]
public class BookRemoveCommand : ICommand public class BookRemoveCommand : ICommand
{ {
private readonly LibraryProvider _libraryProvider; private readonly LibraryProvider _libraryProvider;
@@ -32,4 +32,4 @@ public class BookRemoveCommand : ICommand
return default; return default;
} }
} }

View File

@@ -2,4 +2,4 @@
namespace CliFx.Demo.Domain; namespace CliFx.Demo.Domain;
public record Book(string Title, string Author, DateTimeOffset Published, Isbn Isbn); public record Book(string Title, string Author, DateTimeOffset Published, Isbn Isbn);

View File

@@ -2,7 +2,13 @@
namespace CliFx.Demo.Domain; namespace CliFx.Demo.Domain;
public partial record Isbn(int EanPrefix, int RegistrationGroup, int Registrant, int Publication, int CheckDigit) public partial record Isbn(
int EanPrefix,
int RegistrationGroup,
int Registrant,
int Publication,
int CheckDigit
)
{ {
public override string ToString() => public override string ToString() =>
$"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}"; $"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}";
@@ -22,4 +28,4 @@ public partial record Isbn
int.Parse(components[4], formatProvider) int.Parse(components[4], formatProvider)
); );
} }
} }

View File

@@ -25,4 +25,4 @@ public partial record Library(IReadOnlyList<Book> Books)
public partial record Library public partial record Library
{ {
public static Library Empty { get; } = new(Array.Empty<Book>()); public static Library Empty { get; } = new(Array.Empty<Book>());
} }

View File

@@ -6,7 +6,8 @@ namespace CliFx.Demo.Domain;
public class LibraryProvider public class LibraryProvider
{ {
private static string StorageFilePath { get; } = Path.Combine(Directory.GetCurrentDirectory(), "Library.json"); private static string StorageFilePath { get; } =
Path.Combine(Directory.GetCurrentDirectory(), "Library.json");
private void StoreLibrary(Library library) private void StoreLibrary(Library library)
{ {
@@ -24,7 +25,8 @@ public class LibraryProvider
return JsonSerializer.Deserialize<Library>(data) ?? Library.Empty; return JsonSerializer.Deserialize<Library>(data) ?? Library.Empty;
} }
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) public void AddBook(Book book)
{ {
@@ -37,4 +39,4 @@ public class LibraryProvider
var updatedLibrary = GetLibrary().WithoutBook(book); var updatedLibrary = GetLibrary().WithoutBook(book);
StoreLibrary(updatedLibrary); StoreLibrary(updatedLibrary);
} }
} }

View File

@@ -1,25 +1,21 @@
using CliFx; using CliFx;
using CliFx.Demo.Commands;
using CliFx.Demo.Domain; using CliFx.Demo.Domain;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
var services = new ServiceCollection();
// Register services
services.AddSingleton<LibraryProvider>();
// Register commands
services.AddTransient<BookCommand>();
services.AddTransient<BookAddCommand>();
services.AddTransient<BookRemoveCommand>();
services.AddTransient<BookListCommand>();
var serviceProvider = services.BuildServiceProvider();
return await new CliApplicationBuilder() return await new CliApplicationBuilder()
.SetDescription("Demo application showcasing CliFx features.") .SetDescription("Demo application showcasing CliFx features.")
.AddCommandsFromThisAssembly() .AddCommandsFromThisAssembly()
.UseTypeActivator(serviceProvider.GetRequiredService) .UseTypeActivator(commandTypes =>
{
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
var services = new ServiceCollection();
services.AddSingleton<LibraryProvider>();
// Register all commands as transient services
foreach (var commandType in commandTypes)
services.AddTransient(commandType);
return services.BuildServiceProvider();
})
.Build() .Build()
.RunAsync(); .RunAsync();

View File

@@ -33,4 +33,4 @@ internal static class ConsoleExtensions
using (writer.Console.WithForegroundColor(ConsoleColor.White)) using (writer.Console.WithForegroundColor(ConsoleColor.White))
writer.WriteLine(book.Isbn); writer.WriteLine(book.Isbn);
} }
} }

View File

@@ -2,9 +2,14 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ApplicationIcon>../favicon.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="CSharpier.MsBuild" Version="0.26.1" PrivateAssets="all" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\CliFx\CliFx.csproj" /> <ProjectReference Include="..\CliFx\CliFx.csproj" />
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" /> <ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />

View File

@@ -0,0 +1,27 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Infrastructure;
namespace CliFx.Tests.Dummy.Commands;
[Command("cancel-test")]
public class CancellationTestCommand : ICommand
{
public async ValueTask ExecuteAsync(IConsole console)
{
try
{
console.Output.WriteLine("Started.");
await Task.Delay(TimeSpan.FromSeconds(3), console.RegisterCancellationHandler());
console.Output.WriteLine("Completed.");
}
catch (OperationCanceledException)
{
console.Output.WriteLine("Cancelled.");
throw;
}
}
}

View File

@@ -20,4 +20,4 @@ public class ConsoleTestCommand : ICommand
return default; return default;
} }
} }

View File

@@ -8,7 +8,7 @@ namespace CliFx.Tests.Dummy.Commands;
public class EnvironmentTestCommand : ICommand public class EnvironmentTestCommand : ICommand
{ {
[CommandOption("target", EnvironmentVariable = "ENV_TARGET")] [CommandOption("target", EnvironmentVariable = "ENV_TARGET")]
public string GreetingTarget { get; set; } = "World"; public string GreetingTarget { get; init; } = "World";
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -16,4 +16,4 @@ public class EnvironmentTestCommand : ICommand
return default; return default;
} }
} }

View File

@@ -1,21 +1,21 @@
using System; using System;
using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices;
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 // This dummy application is used in tests for scenarios that require an external process to properly verify
// that require an external process to properly verify. public static class Program
public static partial class Program
{ {
public static Assembly Assembly { get; } = Assembly.GetExecutingAssembly(); // Path to the apphost
public static string FilePath { get; } =
Path.ChangeExtension(
Assembly.GetExecutingAssembly().Location,
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "exe" : null
);
public static string Location { get; } = Assembly.Location;
}
public static partial class Program
{
public static async Task Main() public static async Task Main()
{ {
// Make sure color codes are not produced because we rely on the output in tests // Make sure color codes are not produced because we rely on the output in tests
@@ -24,9 +24,6 @@ public static partial class Program
"false" "false"
); );
await new CliApplicationBuilder() await new CliApplicationBuilder().AddCommandsFromThisAssembly().Build().RunAsync();
.AddCommandsFromThisAssembly()
.Build()
.RunAsync();
} }
} }

View File

@@ -11,12 +11,10 @@ namespace CliFx.Tests;
public class ApplicationSpecs : SpecsBase public class ApplicationSpecs : SpecsBase
{ {
public ApplicationSpecs(ITestOutputHelper testOutput) public ApplicationSpecs(ITestOutputHelper testOutput)
: base(testOutput) : base(testOutput) { }
{
}
[Fact] [Fact]
public async Task Application_can_be_created_with_minimal_configuration() public async Task I_can_create_an_application_with_the_default_configuration()
{ {
// Act // Act
var app = new CliApplicationBuilder() var app = new CliApplicationBuilder()
@@ -24,24 +22,21 @@ public class ApplicationSpecs : SpecsBase
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
var exitCode = await app.RunAsync( var exitCode = await app.RunAsync(Array.Empty<string>(), new Dictionary<string, string>());
Array.Empty<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 I_can_create_an_application_with_a_custom_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()
@@ -53,17 +48,14 @@ public class ApplicationSpecs : SpecsBase
.UseTypeActivator(Activator.CreateInstance!) .UseTypeActivator(Activator.CreateInstance!)
.Build(); .Build();
var exitCode = await app.RunAsync( var exitCode = await app.RunAsync(Array.Empty<string>(), new Dictionary<string, string>());
Array.Empty<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 I_can_try_to_create_an_application_and_get_an_error_if_it_has_invalid_commands()
{ {
// Act // Act
var app = new CliApplicationBuilder() var app = new CliApplicationBuilder()
@@ -71,15 +63,12 @@ public class ApplicationSpecs : SpecsBase
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
var exitCode = await app.RunAsync( var exitCode = await app.RunAsync(Array.Empty<string>(), new Dictionary<string, string>());
Array.Empty<string>(),
new Dictionary<string, string>()
);
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("not a valid command"); stdErr.Should().Contain("not a valid command");
} }
} }

View File

@@ -1,7 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Tests.Utils; using CliFx.Tests.Utils;
using CliFx.Tests.Utils.Extensions;
using CliWrap;
using FluentAssertions; using FluentAssertions;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@@ -11,16 +15,52 @@ namespace CliFx.Tests;
public class CancellationSpecs : SpecsBase public class CancellationSpecs : SpecsBase
{ {
public CancellationSpecs(ITestOutputHelper testOutput) public CancellationSpecs(ITestOutputHelper testOutput)
: base(testOutput) : base(testOutput) { }
[Fact(Timeout = 15000)]
public async Task I_can_configure_the_command_to_listen_to_the_interrupt_signal()
{ {
// Arrange
using var cts = new CancellationTokenSource();
// We need to send the cancellation request right after the process has registered
// a handler for the interrupt signal, otherwise the default handler will trigger
// and just kill the process.
void HandleStdOut(string line)
{
if (string.Equals(line, "Started.", StringComparison.OrdinalIgnoreCase))
cts.CancelAfter(TimeSpan.FromSeconds(0.2));
}
var stdOutBuffer = new StringBuilder();
var pipeTarget = PipeTarget.Merge(
PipeTarget.ToDelegate(HandleStdOut),
PipeTarget.ToStringBuilder(stdOutBuffer)
);
var command = Cli.Wrap(Dummy.Program.FilePath).WithArguments("cancel-test") | pipeTarget;
// Act & assert
await Assert.ThrowsAnyAsync<OperationCanceledException>(
async () =>
await command.ExecuteAsync(
// Forceful cancellation (not required because we have a timeout)
CancellationToken.None,
// Graceful cancellation
cts.Token
)
);
stdOutBuffer.ToString().Trim().Should().ConsistOfLines("Started.", "Cancelled.");
} }
[Fact] [Fact]
public async Task Command_can_receive_a_cancellation_signal_from_the_console() public async Task I_can_configure_the_command_to_listen_to_the_interrupt_signal_when_running_in_isolation()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
@@ -29,16 +69,18 @@ public class CancellationSpecs : SpecsBase
{ {
try try
{ {
console.Output.WriteLine("Started.");
await Task.Delay( await Task.Delay(
TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3),
console.RegisterCancellationHandler() console.RegisterCancellationHandler()
); );
console.Output.WriteLine("Completed successfully"); console.Output.WriteLine("Completed.");
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
console.Output.WriteLine("Cancelled"); console.Output.WriteLine("Cancelled.");
throw; throw;
} }
} }
@@ -51,18 +93,18 @@ public class CancellationSpecs : SpecsBase
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.Build(); .Build();
// Act
FakeConsole.RequestCancellation(TimeSpan.FromSeconds(0.2)); FakeConsole.RequestCancellation(TimeSpan.FromSeconds(0.2));
// 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();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdOut.Trim().Should().Be("Cancelled");
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().ConsistOfLines("Started.", "Cancelled.");
} }
} }

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -9,16 +9,18 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Basic.Reference.Assemblies.Net70" Version="1.4.1" /> <PackageReference Include="Basic.Reference.Assemblies.Net70" Version="1.4.5" />
<PackageReference Include="CliWrap" Version="3.6.1" /> <PackageReference Include="CliWrap" Version="3.6.4" />
<PackageReference Include="FluentAssertions" Version="6.11.0" /> <PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.2" PrivateAssets="all" /> <PackageReference Include="CSharpier.MsBuild" Version="0.26.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" /> <PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageReference Include="xunit" Version="2.4.2" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="coverlet.collector" Version="3.2.0" PrivateAssets="all" /> <PackageReference Include="PolyShim" Version="1.8.0" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.6.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -17,22 +17,16 @@ namespace CliFx.Tests;
public class ConsoleSpecs : SpecsBase public class ConsoleSpecs : SpecsBase
{ {
public ConsoleSpecs(ITestOutputHelper testOutput) public ConsoleSpecs(ITestOutputHelper testOutput)
: base(testOutput) : base(testOutput) { }
{
}
[Fact(Timeout = 15000)] [Fact(Timeout = 15000)]
public async Task Real_console_maps_directly_to_system_console() public async Task I_can_run_the_application_with_the_default_console_implementation_to_interact_with_the_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 =
.WithArguments(a => a "Hello world" | Cli.Wrap(Dummy.Program.FilePath).WithArguments("console-test");
.Add(Dummy.Program.Location)
.Add("console-test")
);
// Act // Act
var result = await command.ExecuteBufferedAsync(); var result = await command.ExecuteBufferedAsync();
@@ -43,11 +37,30 @@ public class ConsoleSpecs : SpecsBase
} }
[Fact] [Fact]
public async Task Fake_console_does_not_leak_to_system_console() public void I_can_run_the_application_on_a_system_with_a_custom_console_encoding_and_not_get_corrupted_output()
{
// Arrange
using var buffer = new MemoryStream();
using var consoleWriter = new ConsoleWriter(FakeConsole, buffer, Encoding.UTF8);
// Act
consoleWriter.Write("Hello world");
consoleWriter.Flush();
// Assert
var outputBytes = buffer.ToArray();
outputBytes.Should().NotContain(Encoding.UTF8.GetPreamble());
var output = consoleWriter.Encoding.GetString(outputBytes);
output.Should().Be("Hello world");
}
[Fact]
public async Task I_can_run_the_application_with_the_fake_console_implementation_to_isolate_console_interactions()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
@@ -104,11 +117,11 @@ public class ConsoleSpecs : SpecsBase
} }
[Fact] [Fact]
public async Task Fake_console_can_be_used_with_an_in_memory_backing_store() public async Task I_can_run_the_application_with_the_fake_console_implementation_and_simulate_stream_interactions()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
@@ -138,21 +151,22 @@ public class ConsoleSpecs : SpecsBase
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("Hello world"); stdOut.Trim().Should().Be("Hello world");
var stdErr = FakeConsole.ReadErrorString();
stdErr.Trim().Should().Be("Hello world"); stdErr.Trim().Should().Be("Hello world");
} }
[Fact] [Fact]
public async Task Fake_console_can_read_key_presses() public async Task I_can_run_the_application_with_the_fake_console_implementation_and_simulate_key_presses()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
@@ -184,31 +198,10 @@ public class ConsoleSpecs : SpecsBase
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().ConsistOfLines(
"D0", var stdOut = FakeConsole.ReadOutputString();
"A", stdOut.Trim().Should().ConsistOfLines("D0", "A", "Backspace");
"Backspace"
);
} }
}
[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");
}
}

View File

@@ -11,22 +11,20 @@ namespace CliFx.Tests;
public class ConversionSpecs : SpecsBase public class ConversionSpecs : SpecsBase
{ {
public ConversionSpecs(ITestOutputHelper testOutput) public ConversionSpecs(ITestOutputHelper testOutput)
: base(testOutput) : base(testOutput) { }
{
}
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_a_string() public async Task I_can_bind_a_parameter_or_an_option_to_a_string_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -44,29 +42,29 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "xyz"}, new[] { "-f", "xyz" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("xyz"); stdOut.Trim().Should().Be("xyz");
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_an_object() public async Task I_can_bind_a_parameter_or_an_option_to_an_object_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public object Foo { get; set; } public object? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -84,37 +82,41 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "xyz"}, new[] { "-f", "xyz" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("xyz"); stdOut.Trim().Should().Be("xyz");
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_a_boolean() public async Task I_can_bind_a_parameter_or_an_option_to_a_boolean_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public bool Foo { get; set; } public bool Foo { get; init; }
[CommandOption('b')] [CommandOption('b')]
public bool Bar { get; set; } public bool Bar { get; init; }
[CommandOption('c')]
public bool Baz { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
console.Output.WriteLine("Foo = " + Foo); console.Output.WriteLine("Foo = " + Foo);
console.Output.WriteLine("Bar = " + Bar); console.Output.WriteLine("Bar = " + Bar);
console.Output.WriteLine("Baz = " + Baz);
return default; return default;
} }
@@ -129,32 +131,29 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "true", "-b", "false"}, new[] { "-f", "true", "-b", "false", "-c" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"Foo = True", var stdOut = FakeConsole.ReadOutputString();
"Bar = False" stdOut.Should().ConsistOfLines("Foo = True", "Bar = False", "Baz = True");
);
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_a_boolean_with_implicit_value() public async Task I_can_bind_a_parameter_or_an_option_to_an_integer_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public bool Foo { get; set; } public int Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -172,69 +171,29 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f"}, new[] { "-f", "32" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be("True");
}
[Fact]
public async Task Parameter_or_option_value_can_be_converted_to_an_integer()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
"""
[Command]
public class Command : ICommand
{
[CommandOption('f')]
public int Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(Foo);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"-f", "32"},
new Dictionary<string, string>()
);
var stdOut = FakeConsole.ReadOutputString(); var stdOut = FakeConsole.ReadOutputString();
// Assert
exitCode.Should().Be(0);
stdOut.Trim().Should().Be("32"); stdOut.Trim().Should().Be("32");
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_a_double() public async Task I_can_bind_a_parameter_or_an_option_to_a_double_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public double Foo { get; set; } public double Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -252,29 +211,29 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "32.14"}, new[] { "-f", "32.14" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("32.14"); stdOut.Trim().Should().Be("32.14");
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_DateTimeOffset() public async Task I_can_bind_a_parameter_or_an_option_to_a_DateTimeOffset_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public DateTimeOffset Foo { get; set; } public DateTimeOffset Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -292,29 +251,29 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "1995-04-28Z"}, new[] { "-f", "1995-04-28Z" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("1995-04-28 00:00:00Z"); stdOut.Trim().Should().Be("1995-04-28 00:00:00Z");
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_a_TimeSpan() public async Task I_can_bind_a_parameter_or_an_option_to_a_TimeSpan_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public TimeSpan Foo { get; set; } public TimeSpan Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -332,23 +291,23 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "12:34:56"}, new[] { "-f", "12:34:56" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("12:34:56"); stdOut.Trim().Should().Be("12:34:56");
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_an_enum() public async Task I_can_bind_a_parameter_or_an_option_to_an_enum_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
public enum CustomEnum { One = 1, Two = 2, Three = 3 } public enum CustomEnum { One = 1, Two = 2, Three = 3 }
@@ -356,7 +315,7 @@ public class ConversionSpecs : SpecsBase
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public CustomEnum Foo { get; set; } public CustomEnum Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -374,32 +333,32 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "two"}, new[] { "-f", "two" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("2"); stdOut.Trim().Should().Be("2");
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_a_nullable_integer() public async Task I_can_bind_a_parameter_or_an_option_to_a_nullable_integer_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public int? Foo { get; set; } public int? Foo { get; init; }
[CommandOption('b')] [CommandOption('b')]
public int? Bar { get; set; } public int? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -419,26 +378,23 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-b", "123"}, new[] { "-b", "123" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"Foo = ", var stdOut = FakeConsole.ReadOutputString();
"Bar = 123" stdOut.Should().ConsistOfLines("Foo = ", "Bar = 123");
);
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_a_nullable_enum() public async Task I_can_bind_a_parameter_or_an_option_to_a_nullable_enum_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
public enum CustomEnum { One = 1, Two = 2, Three = 3 } public enum CustomEnum { One = 1, Two = 2, Three = 3 }
@@ -446,10 +402,10 @@ public class ConversionSpecs : SpecsBase
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public CustomEnum? Foo { get; set; } public CustomEnum? Foo { get; init; }
[CommandOption('b')] [CommandOption('b')]
public CustomEnum? Bar { get; set; } public CustomEnum? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -469,26 +425,23 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-b", "two"}, new[] { "-b", "two" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"Foo = ", var stdOut = FakeConsole.ReadOutputString();
"Bar = 2" stdOut.Should().ConsistOfLines("Foo = ", "Bar = 2");
);
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_a_type_that_has_a_constructor_accepting_a_string() public async Task I_can_bind_a_parameter_or_an_option_to_a_string_constructable_object_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
public class CustomType public class CustomType
{ {
@@ -501,7 +454,7 @@ public class ConversionSpecs : SpecsBase
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public CustomType Foo { get; set; } public CustomType? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -519,23 +472,23 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "xyz"}, new[] { "-f", "xyz" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("xyz"); stdOut.Trim().Should().Be("xyz");
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_a_type_that_has_a_static_parse_method() public async Task I_can_bind_a_parameter_or_an_option_to_a_string_parsable_object_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
public class CustomTypeA public class CustomTypeA
{ {
@@ -561,10 +514,10 @@ public class ConversionSpecs : SpecsBase
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public CustomTypeA Foo { get; set; } public CustomTypeA? Foo { get; init; }
[CommandOption('b')] [CommandOption('b')]
public CustomTypeB Bar { get; set; } public CustomTypeB? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -584,26 +537,23 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "hello", "-b", "world"}, new[] { "-f", "hello", "-b", "world" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"Foo = hello", var stdOut = FakeConsole.ReadOutputString();
"Bar = world" stdOut.Should().ConsistOfLines("Foo = hello", "Bar = world");
);
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_using_a_custom_converter() public async Task I_can_bind_a_parameter_or_an_option_to_a_property_with_a_custom_converter()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
public class CustomConverter : BindingConverter<int> public class CustomConverter : BindingConverter<int>
{ {
@@ -615,7 +565,7 @@ public class ConversionSpecs : SpecsBase
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f', Converter = typeof(CustomConverter))] [CommandOption('f', Converter = typeof(CustomConverter))]
public int Foo { get; set; } public int Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -633,29 +583,29 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "hello world"}, new[] { "-f", "hello world" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("11"); stdOut.Trim().Should().Be("11");
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_an_array_of_strings() public async Task I_can_bind_a_parameter_or_an_option_to_a_string_array_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public string[] Foo { get; set; } public string[]? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -675,33 +625,29 @@ public class ConversionSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"one", var stdOut = FakeConsole.ReadOutputString();
"two", stdOut.Should().ConsistOfLines("one", "two", "three");
"three"
);
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_a_read_only_list_of_strings() public async Task I_can_bind_a_parameter_or_an_option_to_a_read_only_list_of_strings_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public IReadOnlyList<string> Foo { get; set; } public IReadOnlyList<string>? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -721,33 +667,29 @@ public class ConversionSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"one", var stdOut = FakeConsole.ReadOutputString();
"two", stdOut.Should().ConsistOfLines("one", "two", "three");
"three"
);
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_a_list_of_strings() public async Task I_can_bind_a_parameter_or_an_option_to_a_string_list_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public List<string> Foo { get; set; } public List<string>? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -767,33 +709,29 @@ public class ConversionSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"one", var stdOut = FakeConsole.ReadOutputString();
"two", stdOut.Should().ConsistOfLines("one", "two", "three");
"three"
);
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_can_be_converted_to_an_array_of_integers() public async Task I_can_bind_a_parameter_or_an_option_to_an_integer_array_property()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public int[] Foo { get; set; } public int[]? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -813,33 +751,33 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "1", "13", "27"}, new[] { "-f", "1", "13", "27" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"1", var stdOut = FakeConsole.ReadOutputString();
"13", stdOut.Should().ConsistOfLines("1", "13", "27");
"27"
);
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_conversion_fails_if_the_value_cannot_be_converted_to_the_target_type() public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_property_and_get_an_error_if_it_is_of_an_unsupported_type()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
public class CustomType
{
}
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public int Foo { get; set; } public CustomType? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -853,61 +791,23 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "12.34"}, new[] { "-f", "xyz" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdErr.Should().NotBeNullOrWhiteSpace();
}
[Fact]
public async Task Parameter_or_option_value_conversion_fails_if_the_target_type_is_not_supported()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
"""
public class CustomType {}
[Command]
public class Command : ICommand
{
[CommandOption('f')]
public CustomType Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"-f", "xyz"},
new Dictionary<string, string>()
);
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert
exitCode.Should().NotBe(0);
stdErr.Should().Contain("has an unsupported underlying property type"); stdErr.Should().Contain("has an unsupported underlying property type");
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_conversion_fails_if_the_target_non_scalar_type_is_not_supported() public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_non_scalar_property_and_get_an_error_if_it_is_of_an_unsupported_type()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
public class CustomType : IEnumerable<object> public class CustomType : IEnumerable<object>
{ {
@@ -920,7 +820,7 @@ public class ConversionSpecs : SpecsBase
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public CustomType Foo { get; set; } public CustomType? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -934,23 +834,59 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "one", "two"}, new[] { "-f", "one", "two" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("has an unsupported underlying property type"); stdErr.Should().Contain("has an unsupported underlying property type");
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_conversion_fails_if_one_of_the_validators_fail() public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_property_and_get_an_error_if_the_user_provides_an_invalid_value()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption('f')]
public int Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "-f", "12.34" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().NotBeNullOrWhiteSpace();
}
[Fact]
public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_property_and_get_an_error_if_a_custom_validator_fails()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
""" """
public class ValidatorA : BindingValidator<int> public class ValidatorA : BindingValidator<int>
{ {
@@ -966,7 +902,7 @@ public class ConversionSpecs : SpecsBase
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f', Validators = new[] {typeof(ValidatorA), typeof(ValidatorB)})] [CommandOption('f', Validators = new[] {typeof(ValidatorA), typeof(ValidatorB)})]
public int Foo { get; set; } public int Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -980,23 +916,23 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "12"}, new[] { "-f", "12" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("Hello world"); stdErr.Should().Contain("Hello world");
} }
[Fact] [Fact]
public async Task Parameter_or_option_value_conversion_fails_if_the_static_parse_method_throws() public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_string_parsable_property_and_get_an_error_if_the_parsing_fails()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
public class CustomType public class CustomType
{ {
@@ -1011,7 +947,7 @@ public class ConversionSpecs : SpecsBase
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public CustomType Foo { get; set; } public CustomType? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -1025,14 +961,14 @@ public class ConversionSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"-f", "bar"}, new[] { "-f", "bar" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("Hello world"); stdErr.Should().Contain("Hello world");
} }
} }

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Tests.Utils; using CliFx.Tests.Utils;
@@ -15,12 +14,10 @@ namespace CliFx.Tests;
public class DirectivesSpecs : SpecsBase public class DirectivesSpecs : SpecsBase
{ {
public DirectivesSpecs(ITestOutputHelper testOutput) public DirectivesSpecs(ITestOutputHelper testOutput)
: base(testOutput) : base(testOutput) { }
{
}
[Fact(Timeout = 15000)] [Fact(Timeout = 15000)]
public async Task Debug_directive_can_be_specified_to_interrupt_execution_until_a_debugger_is_attached() public async Task I_can_use_the_debug_directive_to_make_the_application_wait_for_the_debugger_to_attach()
{ {
// Arrange // Arrange
using var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource();
@@ -29,15 +26,11 @@ public class DirectivesSpecs : SpecsBase
void HandleStdOut(string line) void HandleStdOut(string line)
{ {
// Kill the process once it writes the output we expect // Kill the process once it writes the output we expect
if (line.Contains("Attach debugger to", StringComparison.OrdinalIgnoreCase)) if (line.Contains("Attach the debugger to", StringComparison.OrdinalIgnoreCase))
cts.Cancel(); cts.Cancel();
} }
var command = Cli.Wrap("dotnet") var command = Cli.Wrap(Dummy.Program.FilePath).WithArguments("[debug]") | HandleStdOut;
.WithArguments(a => a
.Add(Dummy.Program.Location)
.Add("[debug]")
) | HandleStdOut;
// Act & assert // Act & assert
try try
@@ -51,11 +44,11 @@ public class DirectivesSpecs : SpecsBase
} }
[Fact] [Fact]
public async Task Preview_directive_can_be_specified_to_print_command_input() public async Task I_can_use_the_preview_directive_to_make_the_application_print_the_parsed_command_input()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command("cmd")] [Command("cmd")]
public class Command : ICommand public class Command : ICommand
@@ -73,22 +66,29 @@ public class DirectivesSpecs : SpecsBase
// 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_KIL"] = "world" }
{
["ENV_QOP"] = "hello",
["ENV_KIL"] = "world"
}
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ContainAllInOrder(
"cmd", "<param>", "[-a]", "[-b]", "[-c]", "[--option \"foo\"]", var stdOut = FakeConsole.ReadOutputString();
"ENV_QOP", "=", "\"hello\"", stdOut
"ENV_KIL", "=", "\"world\"" .Should()
); .ContainAllInOrder(
"cmd",
"<param>",
"[-a]",
"[-b]",
"[-c]",
"[--option \"foo\"]",
"ENV_QOP",
"=",
"\"hello\"",
"ENV_KIL",
"=",
"\"world\""
);
} }
} }

View File

@@ -15,26 +15,29 @@ namespace CliFx.Tests;
public class EnvironmentSpecs : SpecsBase public class EnvironmentSpecs : SpecsBase
{ {
public EnvironmentSpecs(ITestOutputHelper testOutput) public EnvironmentSpecs(ITestOutputHelper testOutput)
: base(testOutput) : base(testOutput) { }
{
}
[Fact] [Fact]
public async Task Option_can_fall_back_to_an_environment_variable() public async Task I_can_configure_an_option_to_fall_back_to_an_environment_variable_if_the_user_does_not_provide_the_corresponding_argument()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption("foo", IsRequired = true, EnvironmentVariable = "ENV_FOO")] [CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
public string Foo { get; set; } public string? Foo { get; init; }
[CommandOption("bar", EnvironmentVariable = "ENV_BAR")]
public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
console.Output.WriteLine(Foo); console.Output.WriteLine(Foo);
console.Output.WriteLine(Bar);
return default; return default;
} }
} }
@@ -48,75 +51,29 @@ public class EnvironmentSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
Array.Empty<string>(), new[] { "--foo", "42" },
new Dictionary<string, string> new Dictionary<string, string> { ["ENV_FOO"] = "100", ["ENV_BAR"] = "200" }
{
["ENV_FOO"] = "bar"
}
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Trim().Should().Be("bar");
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().ConsistOfLines("42", "200");
} }
[Fact] [Fact]
public async Task Option_does_not_fall_back_to_an_environment_variable_if_a_value_is_provided_through_arguments() public async Task I_can_configure_an_option_bound_to_a_non_scalar_property_to_fall_back_to_an_environment_variable_if_the_user_does_not_provide_the_corresponding_argument()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")] [CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
public string Foo { get; set; } public IReadOnlyList<string>? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(Foo);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"--foo", "baz"},
new Dictionary<string, string>
{
["ENV_FOO"] = "bar"
}
);
var stdOut = FakeConsole.ReadOutputString();
// Assert
exitCode.Should().Be(0);
stdOut.Trim().Should().Be("baz");
}
[Fact]
public async Task Option_of_non_scalar_type_can_receive_multiple_values_from_an_environment_variable()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
public IReadOnlyList<string> Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -137,34 +94,28 @@ public class EnvironmentSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"bar", var stdOut = FakeConsole.ReadOutputString();
"baz" stdOut.Should().ConsistOfLines("bar", "baz");
);
} }
[Fact] [Fact]
public async Task Option_of_scalar_type_always_receives_a_single_value_from_an_environment_variable() public async Task I_can_configure_an_option_bound_to_a_scalar_property_to_fall_back_to_an_environment_variable_while_ignoring_path_separators()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")] [CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -183,79 +134,26 @@ public class EnvironmentSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be($"bar{Path.PathSeparator}baz"); stdOut.Trim().Should().Be($"bar{Path.PathSeparator}baz");
} }
[Fact]
public async Task Environment_variables_are_matched_case_sensitively()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(Foo);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
new Dictionary<string, string>
{
["ENV_foo"] = "baz",
["ENV_FOO"] = "bar",
["env_FOO"] = "qop"
}
);
var stdOut = FakeConsole.ReadOutputString();
// Assert
exitCode.Should().Be(0);
stdOut.Trim().Should().Be("bar");
}
[Fact(Timeout = 15000)] [Fact(Timeout = 15000)]
public async Task Environment_variables_are_extracted_automatically() public async Task I_can_run_the_application_and_it_will_resolve_all_required_environment_variables_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(Dummy.Program.FilePath)
.WithArguments(a => a .WithArguments("env-test")
.Add(Dummy.Program.Location) .WithEnvironmentVariables(e => e.Set("ENV_TARGET", "Mars"));
.Add("env-test")
)
.WithEnvironmentVariables(e => e
.Set("ENV_TARGET", "Mars")
);
// Act // Act
var result = await command.ExecuteBufferedAsync(); var result = await command.ExecuteBufferedAsync();
@@ -263,4 +161,4 @@ public class EnvironmentSpecs : SpecsBase
// Assert // Assert
result.StandardOutput.Trim().Should().Be("Hello Mars!"); result.StandardOutput.Trim().Should().Be("Hello Mars!");
} }
} }

View File

@@ -12,16 +12,14 @@ namespace CliFx.Tests;
public class ErrorReportingSpecs : SpecsBase public class ErrorReportingSpecs : SpecsBase
{ {
public ErrorReportingSpecs(ITestOutputHelper testOutput) public ErrorReportingSpecs(ITestOutputHelper testOutput)
: base(testOutput) : base(testOutput) { }
{
}
[Fact] [Fact]
public async Task Command_can_throw_an_exception_which_exits_with_a_stacktrace() public async Task I_can_throw_an_exception_in_a_command_to_report_an_error_with_a_stacktrace()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
@@ -43,24 +41,24 @@ public class ErrorReportingSpecs : SpecsBase
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().BeEmpty(); stdOut.Should().BeEmpty();
stdErr.Should().ContainAllInOrder(
"System.Exception", "Something went wrong", var stdErr = FakeConsole.ReadErrorString();
"at", "CliFx." stdErr
); .Should()
.ContainAllInOrder("System.Exception", "Something went wrong", "at", "CliFx.");
} }
[Fact] [Fact]
public async Task Command_can_throw_an_exception_with_an_inner_exception_which_exits_with_a_stacktrace() public async Task I_can_throw_an_exception_with_an_inner_exception_in_a_command_to_report_an_error_with_a_stacktrace()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
@@ -82,25 +80,31 @@ public class ErrorReportingSpecs : SpecsBase
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().BeEmpty(); stdOut.Should().BeEmpty();
stdErr.Should().ContainAllInOrder(
"System.Exception", "Something went wrong", var stdErr = FakeConsole.ReadErrorString();
"System.Exception", "Another exception", stdErr
"at", "CliFx." .Should()
); .ContainAllInOrder(
"System.Exception",
"Something went wrong",
"System.Exception",
"Another exception",
"at",
"CliFx."
);
} }
[Fact] [Fact]
public async Task Command_can_throw_a_special_exception_which_exits_with_specified_code_and_message() public async Task I_can_throw_an_exception_in_a_command_to_report_an_error_and_exit_with_the_specified_code()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
@@ -122,21 +126,22 @@ public class ErrorReportingSpecs : SpecsBase
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().Be(69); exitCode.Should().Be(69);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().BeEmpty(); stdOut.Should().BeEmpty();
var stdErr = FakeConsole.ReadErrorString();
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 I_can_throw_an_exception_without_a_message_in_a_command_to_report_an_error_with_a_stacktrace()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
@@ -158,24 +163,22 @@ public class ErrorReportingSpecs : SpecsBase
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().Be(69); exitCode.Should().Be(69);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().BeEmpty(); stdOut.Should().BeEmpty();
stdErr.Should().ContainAllInOrder(
"CliFx.Exceptions.CommandException", var stdErr = FakeConsole.ReadErrorString();
"at", "CliFx." stdErr.Should().ContainAllInOrder("CliFx.Exceptions.CommandException", "at", "CliFx.");
);
} }
[Fact] [Fact]
public async Task Command_can_throw_a_special_exception_which_prints_help_text_before_exiting() public async Task I_can_throw_an_exception_in_a_command_to_report_an_error_and_print_the_help_text()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
@@ -198,12 +201,13 @@ public class ErrorReportingSpecs : SpecsBase
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().Be(69); exitCode.Should().Be(69);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().Contain("This will be in help text"); stdOut.Should().Contain("This will be in help text");
var stdErr = FakeConsole.ReadErrorString();
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

@@ -12,22 +12,20 @@ namespace CliFx.Tests;
public class OptionBindingSpecs : SpecsBase public class OptionBindingSpecs : SpecsBase
{ {
public OptionBindingSpecs(ITestOutputHelper testOutput) public OptionBindingSpecs(ITestOutputHelper testOutput)
: base(testOutput) : base(testOutput) { }
{
}
[Fact] [Fact]
public async Task Option_is_bound_from_an_argument_matching_its_name() public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_by_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public bool Foo { get; set; } public bool Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -45,29 +43,29 @@ public class OptionBindingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
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 I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_by_short_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public bool Foo { get; set; } public bool Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -84,33 +82,30 @@ public class OptionBindingSpecs : SpecsBase
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(new[] { "-f" }, new Dictionary<string, string>());
new[] {"-f"},
new Dictionary<string, string>()
);
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
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 I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_set_by_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public string? Foo { get; init; }
[CommandOption("bar")] [CommandOption("bar")]
public string Bar { get; set; } public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -130,35 +125,32 @@ public class OptionBindingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"Foo = one", var stdOut = FakeConsole.ReadOutputString();
"Bar = two" stdOut.Should().ConsistOfLines("Foo = one", "Bar = two");
);
} }
[Fact] [Fact]
public async Task Option_is_bound_from_a_set_of_arguments_matching_its_short_name() public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_set_by_short_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public string Foo { get; set; } public string? Foo { get; init; }
[CommandOption('b')] [CommandOption('b')]
public string Bar { get; set; } public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -178,35 +170,32 @@ public class OptionBindingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"Foo = one", var stdOut = FakeConsole.ReadOutputString();
"Bar = two" stdOut.Should().ConsistOfLines("Foo = one", "Bar = two");
);
} }
[Fact] [Fact]
public async Task Option_is_bound_from_a_stack_of_arguments_matching_its_short_name() public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_stack_by_short_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public string Foo { get; set; } public string? Foo { get; init; }
[CommandOption('b')] [CommandOption('b')]
public string Bar { get; set; } public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -226,32 +215,29 @@ public class OptionBindingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"Foo = ", var stdOut = FakeConsole.ReadOutputString();
"Bar = value" stdOut.Should().ConsistOfLines("Foo = ", "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 I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_arguments_by_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption("Foo")] [CommandOption("Foo")]
public IReadOnlyList<string> Foo { get; set; } public IReadOnlyList<string>? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -271,33 +257,29 @@ public class OptionBindingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"one", var stdOut = FakeConsole.ReadOutputString();
"two", stdOut.Should().ConsistOfLines("one", "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 I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_arguments_by_short_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public IReadOnlyList<string> Foo { get; set; } public IReadOnlyList<string>? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -317,33 +299,29 @@ public class OptionBindingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"one", var stdOut = FakeConsole.ReadOutputString();
"two", stdOut.Should().ConsistOfLines("one", "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 I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_argument_sets_by_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public IReadOnlyList<string> Foo { get; set; } public IReadOnlyList<string>? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -363,33 +341,29 @@ public class OptionBindingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"one", var stdOut = FakeConsole.ReadOutputString();
"two", stdOut.Should().ConsistOfLines("one", "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 I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_argument_sets_by_short_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public IReadOnlyList<string> Foo { get; set; } public IReadOnlyList<string>? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -409,33 +383,29 @@ public class OptionBindingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"one", var stdOut = FakeConsole.ReadOutputString();
"two", stdOut.Should().ConsistOfLines("one", "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 I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_argument_sets_by_name_or_short_name()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption("foo", 'f')] [CommandOption("foo", 'f')]
public IReadOnlyList<string> Foo { get; set; } public IReadOnlyList<string>? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -455,36 +425,32 @@ public class OptionBindingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"one", var stdOut = FakeConsole.ReadOutputString();
"two", stdOut.Should().ConsistOfLines("one", "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 I_can_bind_an_option_to_a_property_and_get_no_value_if_the_user_does_not_provide_the_corresponding_argument()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public string? Foo { get; init; }
[CommandOption("bar")] [CommandOption("bar")]
public string Bar { get; set; } = "hello"; public string? Bar { get; init; } = "hello";
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -504,26 +470,23 @@ public class OptionBindingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"Foo = one", var stdOut = FakeConsole.ReadOutputString();
"Bar = hello" stdOut.Should().ConsistOfLines("Foo = one", "Bar = hello");
);
} }
[Fact] [Fact]
public async Task Option_binding_supports_multiple_inheritance_through_default_interface_members() public async Task I_can_bind_an_option_to_a_property_through_multiple_inheritance()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
public static class SharedContext public static class SharedContext
{ {
@@ -538,7 +501,7 @@ public class OptionBindingSpecs : SpecsBase
public int Foo public int Foo
{ {
get => SharedContext.Foo; get => SharedContext.Foo;
set => SharedContext.Foo = value; init => SharedContext.Foo = value;
} }
} }
@@ -548,20 +511,20 @@ public class OptionBindingSpecs : SpecsBase
public bool Bar public bool Bar
{ {
get => SharedContext.Bar; get => SharedContext.Bar;
set => SharedContext.Bar = value; init => SharedContext.Bar = value;
} }
} }
public interface IHasBaz : ICommand public interface IHasBaz : ICommand
{ {
public string Baz { get; set; } public string? Baz { get; init; }
} }
[Command] [Command]
public class Command : IHasFoo, IHasBar, IHasBaz public class Command : IHasFoo, IHasBar, IHasBaz
{ {
[CommandOption("baz")] [CommandOption("baz")]
public string Baz { get; set; } public string? Baz { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -581,38 +544,31 @@ public class OptionBindingSpecs : SpecsBase
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(new[] { "--foo", "42", "--bar", "--baz", "xyz" });
new[] { "--foo", "42", "--bar", "--baz", "xyz" }
);
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"Foo = 42", var stdOut = FakeConsole.ReadOutputString();
"Bar = True", stdOut.Should().ConsistOfLines("Foo = 42", "Bar = True", "Baz = xyz");
"Baz = xyz"
);
} }
[Fact] [Fact]
public async Task Option_binding_does_not_consider_a_negative_number_as_an_option_name_or_short_name() public async Task I_can_bind_an_option_to_a_property_and_get_the_correct_value_if_the_user_provides_an_argument_containing_a_negative_number()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
console.Output.WriteLine(Foo); console.Output.WriteLine(Foo);
return default; return default;
} }
} }
@@ -626,29 +582,29 @@ public class OptionBindingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
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 I_can_try_to_bind_a_required_option_to_a_property_and_get_an_error_if_the_user_does_not_provide_the_corresponding_argument()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption("foo", IsRequired = true)] [CommandOption("foo")]
public string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -666,97 +622,25 @@ public class OptionBindingSpecs : SpecsBase
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
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 I_can_try_to_bind_a_required_option_to_a_property_and_get_an_error_if_the_user_provides_an_empty_argument()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo", IsRequired = true)]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"--foo"},
new Dictionary<string, string>()
);
var stdErr = FakeConsole.ReadErrorString();
// Assert
exitCode.Should().NotBe(0);
stdErr.Should().Contain("Missing required option(s)");
}
[Fact]
public async Task Option_binding_fails_if_a_required_option_of_non_scalar_type_has_not_been_provided_with_at_least_one_value()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo", IsRequired = true)]
public IReadOnlyList<string> Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"--foo"},
new Dictionary<string, string>()
);
var stdErr = FakeConsole.ReadErrorString();
// Assert
exitCode.Should().NotBe(0);
stdErr.Should().Contain("Missing required option(s)");
}
[Fact]
public async Task Option_binding_fails_if_one_of_the_provided_option_names_is_not_recognized()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -770,29 +654,101 @@ public class OptionBindingSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"--foo", "one", "--bar", "two"}, new[] { "--foo" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
// Assert
exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("Missing required option(s)");
}
[Fact]
public async Task I_can_try_to_bind_an_option_to_a_non_scalar_property_and_get_an_error_if_the_user_does_not_provide_at_least_one_corresponding_argument()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo")]
public required IReadOnlyList<string> Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "--foo" },
new Dictionary<string, string>()
);
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("Missing required option(s)");
}
[Fact]
public async Task I_can_try_to_bind_options_and_get_an_error_if_the_user_provides_unrecognized_arguments()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo")]
public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "--foo", "one", "--bar", "two" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
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 I_can_try_to_bind_an_option_to_a_scalar_property_and_get_an_error_if_the_user_provides_too_many_arguments()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandOption("foo")] [CommandOption("foo")]
public string Foo { get; set; } public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -806,50 +762,14 @@ public class OptionBindingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("expects a single argument, but provided with multiple"); stdErr.Should().Contain("expects a single argument, but provided with multiple");
} }
}
[Fact]
public async Task Option_binding_fails_if_a_required_property_option_has_not_been_provided()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo")]
public required string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
new Dictionary<string, string>()
);
var stdErr = FakeConsole.ReadErrorString();
// Assert
exitCode.Should().NotBe(0);
stdErr.Should().Contain("Missing required option(s)");
}
}

View File

@@ -11,25 +11,23 @@ namespace CliFx.Tests;
public class ParameterBindingSpecs : SpecsBase public class ParameterBindingSpecs : SpecsBase
{ {
public ParameterBindingSpecs(ITestOutputHelper testOutput) public ParameterBindingSpecs(ITestOutputHelper testOutput)
: base(testOutput) : base(testOutput) { }
{
}
[Fact] [Fact]
public async Task Parameter_is_bound_from_an_argument_matching_its_order() public async Task I_can_bind_a_parameter_to_a_property_and_get_the_value_from_the_corresponding_argument()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1)] [CommandParameter(1)]
public string Bar { get; set; } public required string Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -49,41 +47,38 @@ public class ParameterBindingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"Foo = one", var stdOut = FakeConsole.ReadOutputString();
"Bar = two" stdOut.Should().ConsistOfLines("Foo = one", "Bar = two");
);
} }
[Fact] [Fact]
public async Task Parameter_of_non_scalar_type_is_bound_from_remaining_non_option_arguments() public async Task I_can_bind_a_parameter_to_a_non_scalar_property_and_get_values_from_the_remaining_non_option_arguments()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1)] [CommandParameter(1)]
public string Bar { get; set; } public required string Bar { get; init; }
[CommandParameter(2)] [CommandParameter(2)]
public IReadOnlyList<string> Baz { get; set; } public required IReadOnlyList<string> Baz { get; init; }
[CommandOption("boo")] [CommandOption("boo")]
public string Boo { get; set; } public string? Boo { get; init; }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -106,38 +101,112 @@ public class ParameterBindingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"Foo = one", var stdOut = FakeConsole.ReadOutputString();
"Bar = two", stdOut
"Baz = three", .Should()
"Baz = four", .ConsistOfLines("Foo = one", "Bar = two", "Baz = three", "Baz = four", "Baz = five");
"Baz = five"
);
} }
[Fact] [Fact]
public async Task Parameter_is_not_bound_if_there_are_no_arguments_matching_its_order() public async Task I_can_try_to_bind_a_parameter_to_a_property_and_get_an_error_if_the_user_does_not_provide_the_corresponding_argument()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1)]
public required string Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "one" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("Missing required parameter(s)");
}
[Fact]
public async Task I_can_try_to_bind_a_parameter_to_a_non_scalar_property_and_get_an_error_if_the_user_does_not_provide_at_least_one_corresponding_argument()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandParameter(0)]
public required string Foo { get; init; }
[CommandParameter(1)]
public required IReadOnlyList<string> Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "one" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("Missing required parameter(s)");
}
[Fact]
public async Task I_can_bind_a_non_required_parameter_to_a_property_and_get_no_value_if_the_user_does_not_provide_the_corresponding_argument()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandParameter(0)]
public required string Foo { get; init; }
[CommandParameter(1, IsRequired = false)] [CommandParameter(1, IsRequired = false)]
public string Bar { get; set; } = "xyz"; public string? Bar { get; init; } = "xyz";
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
@@ -157,35 +226,32 @@ public class ParameterBindingSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"abc"}, new[] { "abc" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"Foo = abc", var stdOut = FakeConsole.ReadOutputString();
"Bar = xyz" stdOut.Should().ConsistOfLines("Foo = abc", "Bar = xyz");
);
} }
[Fact] [Fact]
public async Task Parameter_binding_fails_if_a_required_parameter_has_not_been_provided() public async Task I_can_try_to_bind_parameters_and_get_an_error_if_the_user_provides_too_many_arguments()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
{ {
[CommandParameter(0)] [CommandParameter(0)]
public string Foo { get; set; } public required string Foo { get; init; }
[CommandParameter(1)] [CommandParameter(1)]
public string Bar { get; set; } public required string Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -199,92 +265,14 @@ public class ParameterBindingSpecs : SpecsBase
// Act // Act
var exitCode = await application.RunAsync( var exitCode = await application.RunAsync(
new[] {"one"}, new[] { "one", "two", "three" },
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stdErr.Should().Contain("Missing required parameter(s)");
}
[Fact]
public async Task Parameter_binding_fails_if_a_parameter_of_non_scalar_type_has_not_been_provided_with_at_least_one_value()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
"""
[Command]
public class Command : ICommand
{
[CommandParameter(0)]
public string Foo { get; set; }
[CommandParameter(1)]
public IReadOnlyList<string> Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"one"},
new Dictionary<string, string>()
);
var stdErr = FakeConsole.ReadErrorString(); var stdErr = FakeConsole.ReadErrorString();
// Assert
exitCode.Should().NotBe(0);
stdErr.Should().Contain("Missing required parameter(s)");
}
[Fact]
public async Task Parameter_binding_fails_if_one_of_the_provided_parameters_is_unexpected()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
"""
[Command]
public class Command : ICommand
{
[CommandParameter(0)]
public string Foo { get; set; }
[CommandParameter(1)]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"one", "two", "three"},
new Dictionary<string, string>()
);
var stdErr = FakeConsole.ReadErrorString();
// Assert
exitCode.Should().NotBe(0);
stdErr.Should().Contain("Unexpected parameter(s)"); stdErr.Should().Contain("Unexpected parameter(s)");
} }
} }

View File

@@ -11,16 +11,14 @@ namespace CliFx.Tests;
public class RoutingSpecs : SpecsBase public class RoutingSpecs : SpecsBase
{ {
public RoutingSpecs(ITestOutputHelper testOutput) public RoutingSpecs(ITestOutputHelper testOutput)
: base(testOutput) : base(testOutput) { }
{
}
[Fact] [Fact]
public async Task Default_command_is_executed_if_provided_arguments_do_not_match_any_named_command() public async Task I_can_configure_a_command_to_be_executed_by_default_when_the_user_does_not_specify_a_command_name()
{ {
// Arrange // Arrange
var commandTypes = DynamicCommandBuilder.CompileMany( var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class DefaultCommand : ICommand public class DefaultCommand : ICommand
@@ -65,19 +63,19 @@ public class RoutingSpecs : SpecsBase
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
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 I_can_configure_a_command_to_be_executed_when_the_user_specifies_its_name()
{ {
// Arrange // Arrange
var commandTypes = DynamicCommandBuilder.CompileMany( var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class DefaultCommand : ICommand public class DefaultCommand : ICommand
@@ -118,23 +116,23 @@ public class RoutingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
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 I_can_configure_a_nested_command_to_be_executed_when_the_user_specifies_its_name()
{ {
// Arrange // Arrange
var commandTypes = DynamicCommandBuilder.CompileMany( var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class DefaultCommand : ICommand public class DefaultCommand : ICommand
@@ -175,14 +173,14 @@ public class RoutingSpecs : SpecsBase
// 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();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("cmd child"); stdOut.Trim().Should().Be("cmd child");
} }
} }

View File

@@ -11,12 +11,11 @@ public abstract class SpecsBase : IDisposable
public FakeInMemoryConsole FakeConsole { get; } = new(); public FakeInMemoryConsole FakeConsole { get; } = new();
protected SpecsBase(ITestOutputHelper testOutput) => protected SpecsBase(ITestOutputHelper testOutput) => TestOutput = testOutput;
TestOutput = testOutput;
public void Dispose() public void Dispose()
{ {
FakeConsole.DumpToTestOutput(TestOutput); FakeConsole.DumpToTestOutput(TestOutput);
FakeConsole.Dispose(); FakeConsole.Dispose();
} }
} }

View File

@@ -13,16 +13,14 @@ namespace CliFx.Tests;
public class TypeActivationSpecs : SpecsBase public class TypeActivationSpecs : SpecsBase
{ {
public TypeActivationSpecs(ITestOutputHelper testOutput) public TypeActivationSpecs(ITestOutputHelper testOutput)
: base(testOutput) : base(testOutput) { }
{
}
[Fact] [Fact]
public async Task Default_type_activator_can_initialize_a_type_if_it_has_a_parameterless_constructor() public async Task I_can_configure_the_application_to_use_the_default_type_activator_to_initialize_types_through_parameterless_constructors()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
@@ -48,19 +46,19 @@ public class TypeActivationSpecs : SpecsBase
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
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 I_can_try_to_configure_the_application_to_use_the_default_type_activator_and_get_an_error_if_the_requested_type_does_not_have_a_parameterless_constructor()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
@@ -84,19 +82,19 @@ public class TypeActivationSpecs : SpecsBase
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
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 Custom_type_activator_can_initialize_a_type_using_a_given_function() public async Task I_can_configure_the_application_to_use_a_custom_type_activator_to_initialize_types_using_a_delegate()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
@@ -126,19 +124,19 @@ public class TypeActivationSpecs : SpecsBase
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("Hello world"); stdOut.Trim().Should().Be("Hello world");
} }
[Fact] [Fact]
public async Task Custom_type_activator_can_initialize_a_type_using_a_service_provider() public async Task I_can_configure_the_application_to_use_a_custom_type_activator_to_initialize_types_using_a_service_provider()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
@@ -156,14 +154,23 @@ public class TypeActivationSpecs : SpecsBase
""" """
); );
var serviceProvider = new ServiceCollection()
.AddSingleton(commandType, Activator.CreateInstance(commandType, "Hello world")!)
.BuildServiceProvider();
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.UseTypeActivator(serviceProvider) .UseTypeActivator(commandTypes =>
{
var services = new ServiceCollection();
foreach (var serviceType in commandTypes)
{
services.AddSingleton(
serviceType,
Activator.CreateInstance(serviceType, "Hello world")!
);
}
return services.BuildServiceProvider();
})
.Build(); .Build();
// Act // Act
@@ -172,19 +179,19 @@ public class TypeActivationSpecs : SpecsBase
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdOut = FakeConsole.ReadOutputString();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("Hello world"); stdOut.Trim().Should().Be("Hello world");
} }
[Fact] [Fact]
public async Task Custom_type_activator_fails_if_the_underlying_function_returns_null() public async Task I_can_try_to_configure_the_application_to_use_a_custom_type_activator_and_get_an_error_if_the_requested_type_cannot_be_initialized()
{ {
// Arrange // Arrange
var commandType = DynamicCommandBuilder.Compile( var commandType = DynamicCommandBuilder.Compile(
// language=cs // lang=csharp
""" """
[Command] [Command]
public class Command : ICommand public class Command : ICommand
@@ -201,7 +208,7 @@ public class TypeActivationSpecs : SpecsBase
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .UseConsole(FakeConsole)
.UseTypeActivator(_ => null!) .UseTypeActivator((Type _) => null!)
.Build(); .Build();
// Act // Act
@@ -210,10 +217,10 @@ public class TypeActivationSpecs : SpecsBase
new Dictionary<string, string>() new Dictionary<string, string>()
); );
var stdErr = FakeConsole.ReadErrorString();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("Failed to create an instance of type"); stdErr.Should().Contain("Failed to create an instance of type");
} }
} }

View File

@@ -11,14 +11,9 @@ using Microsoft.CodeAnalysis.Text;
namespace CliFx.Tests.Utils; namespace CliFx.Tests.Utils;
// This class uses Roslyn to compile commands dynamically. // 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.
// It allows us to collocate commands with tests more // Unfortunately, this comes at a cost of static typing, but this is still a worthwhile trade off.
// easily, which helps a lot when reasoning about them. // Maybe one day C# will allow declaring classes inside methods and doing this will no longer be necessary.
// 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 // Language proposal: https://github.com/dotnet/csharplang/discussions/130
internal static class DynamicCommandBuilder internal static class DynamicCommandBuilder
{ {
@@ -46,10 +41,10 @@ internal static class DynamicCommandBuilder
// 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 + + Environment.NewLine
sourceCode; + sourceCode;
// Parse the source code // Parse the source code
var ast = SyntaxFactory.ParseSyntaxTree( var ast = SyntaxFactory.ParseSyntaxTree(
@@ -60,10 +55,16 @@ internal static class DynamicCommandBuilder
// Compile the code to IL // Compile the code to IL
var compilation = CSharpCompilation.Create( var compilation = CSharpCompilation.Create(
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(), "CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
new[] {ast}, new[] { ast },
Net70.References.All Net70
.References
.All
.Append(MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location)) .Append(MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location))
.Append(MetadataReference.CreateFromFile(typeof(DynamicCommandBuilder).Assembly.Location)), .Append(
MetadataReference.CreateFromFile(
typeof(DynamicCommandBuilder).Assembly.Location
)
),
// DLL to avoid having to define the Main() method // DLL to avoid having to define the Main() method
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
); );
@@ -87,8 +88,7 @@ internal static class DynamicCommandBuilder
using var buffer = new MemoryStream(); using var buffer = new MemoryStream();
var emit = compilation.Emit(buffer); var emit = compilation.Emit(buffer);
var emitErrors = emit var emitErrors = emit.Diagnostics
.Diagnostics
.Where(d => d.Severity >= DiagnosticSeverity.Error) .Where(d => d.Severity >= DiagnosticSeverity.Error)
.ToArray(); .ToArray();
@@ -124,7 +124,6 @@ internal static class DynamicCommandBuilder
public static Type Compile(string sourceCode) public static Type Compile(string sourceCode)
{ {
var commandTypes = CompileMany(sourceCode); var commandTypes = CompileMany(sourceCode);
if (commandTypes.Count > 1) if (commandTypes.Count > 1)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
@@ -134,4 +133,4 @@ internal static class DynamicCommandBuilder
return commandTypes.Single(); return commandTypes.Single();
} }
} }

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