18 Commits
2.3 ... 2.3.1

Author SHA1 Message Date
Oleksii Holub
48f157a51e Update version 2022-12-08 21:57:42 +02:00
Oleksii Holub
b1995fa4f7 Update NuGet packages 2022-12-08 21:54:27 +02:00
Oleksii Holub
74bc973f77 Fix typo 2022-12-08 21:53:09 +02:00
Oleksii Holub
3420c3d039 Make test naming more consistent 2022-12-08 21:51:31 +02:00
Oleksii Holub
b10577fec5 Add integration with the new required keyword
Closes #132
2022-12-08 21:46:14 +02:00
Oleksii Holub
af96d0d31d Remove unused usings 2022-12-08 03:45:09 +02:00
Oleksii Holub
bd29ad31cc More raw string literals 2022-12-08 03:33:10 +02:00
Oleksii Holub
15150cb3ed Add required modifiers 2022-12-08 03:08:58 +02:00
Oleksii Holub
aac9c968eb Use raw string literals 2022-12-08 03:06:31 +02:00
Tyrrrz
85a9f157ad Update NuGet packages 2022-12-08 01:48:11 +02:00
Tyrrrz
d24a79361d Use net6.0 2022-12-08 01:47:30 +02:00
Oleksii Holub
5785720f31 Update readme 2022-11-30 14:23:22 +02:00
Oleksii Holub
3f6f0b9f1b Typo in readme: 'runtime' -> 'run-time' 2022-11-21 01:12:24 +02:00
Tyrrrz
128bb5be8b Clean up project items and update NuGet packages 2022-11-02 23:10:18 +02:00
Tyrrrz
36b3814f4e Cleanup 2022-10-12 22:53:18 +03:00
Tyrrrz
c4a975d5f1 Update NuGet Packages 2022-10-12 22:50:31 +03:00
Oleksii Holub
79d86d39c1 Move into maintenance mode 2022-08-26 18:00:10 +03:00
Oleksii Holub
c476700168 Update readme 2022-07-15 03:14:41 -07:00
84 changed files with 3129 additions and 2406 deletions

View File

@@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: ⚠ Feature request
url: https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md
about: Sorry, but this project is in maintenance mode and no longer accepts new feature requests.
- name: 💬 Discord server
url: https://discord.gg/2SUWKFnHSm
about: Chat with the project community.

View File

@@ -1,22 +0,0 @@
name: ✨ Feature request
description: Request a new feature.
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
🧐 **Guidelines:**
- Search through [existing issues](https://github.com/Tyrrrz/CliFx/issues?q=is%3Aissue) first to ensure that this feature has not been requested before.
- Write a descriptive title for your issue. Avoid generic or vague titles such as "Some suggestions" or "Ideas for improvement".
- Keep your issue focused on one single problem. If you have multiple feature requests, please create separate issues for each of them.
- Provide as much context as possible in the details section. Include screenshots, screen recordings, links, references, or anything else you may consider relevant.
- If you want to ask a question instead of requesting a feature, please use [discussions](https://github.com/Tyrrrz/CliFx/discussions/new) instead.
- type: textarea
attributes:
label: Details
description: Clear and thorough explanation of the feature you have in mind.
validations:
required: true

View File

@@ -6,7 +6,7 @@ jobs:
main:
uses: Tyrrrz/.github/.github/workflows/nuget.yml@master
with:
dotnet-version: 6.0.x
dotnet-version: 7.0.x
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}

View File

@@ -1,3 +1,7 @@
### v2.3.1 (08-Dec-2022)
- Added support for the `required` keyword introduced in C# 11. Command options bound to required properties are now marked as required by default (equivalent to setting `IsRequired = true` on the attribute). Also added analyzers that prevent marking an option or parameter non-required if it's bound to a required property.
### v2.3 (12-Jul-2022)
- Added console dimension properties `WindowWidth` and `WindowHeight` to `IConsole` interface and implementing classes.

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
@@ -9,14 +9,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Basic.Reference.Assemblies" Version="1.2.4" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.0" PrivateAssets="all" />
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="Basic.Reference.Assemblies.Net70" Version="1.4.1" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.1" PrivateAssets="all" />
<PackageReference Include="FluentAssertions" Version="6.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all" />
<PackageReference Include="coverlet.collector" Version="3.1.2" PrivateAssets="all" />
<PackageReference Include="coverlet.collector" Version="3.2.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>

View File

@@ -13,11 +13,13 @@ public class CommandMustBeAnnotatedAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -28,12 +30,14 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public abstract class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -44,11 +48,13 @@ public abstract class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public abstract class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -59,11 +65,13 @@ public abstract class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class Foo
{
public int Bar { get; set; } = 5;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,12 +13,14 @@ public class CommandMustImplementInterfaceAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -29,12 +31,14 @@ public class MyCommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -45,11 +49,13 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class Foo
{
public int Bar { get; set; } = 5;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,12 +13,14 @@ public class OptionMustBeInsideCommandAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyClass
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -29,15 +31,17 @@ public class MyClass
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -48,12 +52,14 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public abstract class MyCommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -64,14 +70,16 @@ public abstract class MyCommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -0,0 +1,114 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests;
public class OptionMustBeRequiredIfPropertyRequiredAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustBeRequiredIfPropertyRequiredAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_a_non_required_option_is_bound_to_a_required_property()
{
// Arrange
// language=cs
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption('f', IsRequired = false)]
public required string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_required_option_is_bound_to_a_required_property()
{
// Arrange
// language=cs
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption('f', IsRequired = true)]
public required string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_non_required_option_is_bound_to_a_non_required_property()
{
// Arrange
// language=cs
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption('f', IsRequired = false)]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_required_option_is_bound_to_a_non_required_property()
{
// Arrange
// language=cs
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption('f', IsRequired = true)]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
{
// Arrange
// language=cs
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public required string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}

View File

@@ -13,7 +13,8 @@ public class OptionMustHaveNameOrShortNameAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -21,7 +22,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -32,15 +34,17 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -51,7 +55,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -59,7 +64,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -70,14 +76,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,18 +13,20 @@ public class OptionMustHaveUniqueNameAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -35,18 +37,20 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
[CommandOption(""bar"")]
[CommandOption("bar")]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -57,7 +61,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -65,7 +70,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -76,14 +82,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,7 +13,8 @@ public class OptionMustHaveUniqueShortNameAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -24,7 +25,8 @@ public class MyCommand : ICommand
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -35,7 +37,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -46,7 +49,8 @@ public class MyCommand : ICommand
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -57,7 +61,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -68,7 +73,8 @@ public class MyCommand : ICommand
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -79,15 +85,17 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -98,14 +106,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,7 +13,8 @@ public class OptionMustHaveValidConverterAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyConverter
{
public string Convert(string rawValue) => rawValue;
@@ -22,11 +23,12 @@ public class MyConverter
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", Converter = typeof(MyConverter))]
[CommandOption("foo", Converter = typeof(MyConverter))]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -37,7 +39,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyConverter : BindingConverter<int>
{
public override int Convert(string rawValue) => 42;
@@ -46,11 +49,12 @@ public class MyConverter : BindingConverter<int>
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", Converter = typeof(MyConverter))]
[CommandOption("foo", Converter = typeof(MyConverter))]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -61,7 +65,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyConverter : BindingConverter<string>
{
public override string Convert(string rawValue) => rawValue;
@@ -70,11 +75,12 @@ public class MyConverter : BindingConverter<string>
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", Converter = typeof(MyConverter))]
[CommandOption("foo", Converter = typeof(MyConverter))]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -85,7 +91,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyConverter : BindingConverter<int>
{
public override int Convert(string rawValue) => 42;
@@ -94,11 +101,12 @@ public class MyConverter : BindingConverter<int>
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", Converter = typeof(MyConverter))]
[CommandOption("foo", Converter = typeof(MyConverter))]
public int? Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -109,7 +117,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyConverter : BindingConverter<string>
{
public override string Convert(string rawValue) => rawValue;
@@ -118,11 +127,12 @@ public class MyConverter : BindingConverter<string>
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", Converter = typeof(MyConverter))]
[CommandOption("foo", Converter = typeof(MyConverter))]
public IReadOnlyList<string> Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -133,15 +143,17 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -152,14 +164,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,15 +13,17 @@ public class OptionMustHaveValidNameAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption(""f"")]
[CommandOption("f")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -32,15 +34,17 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption(""1foo"")]
[CommandOption("1foo")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -51,15 +55,17 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -70,7 +76,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -78,7 +85,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -89,14 +97,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,7 +13,8 @@ public class OptionMustHaveValidShortNameAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -21,7 +22,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -32,7 +34,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -40,7 +43,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -51,15 +55,17 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -70,14 +76,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,7 +13,8 @@ public class OptionMustHaveValidValidatorsAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyValidator
{
public void Validate(string value) {}
@@ -22,11 +23,12 @@ public class MyValidator
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", Validators = new[] {typeof(MyValidator)})]
[CommandOption("foo", Validators = new[] {typeof(MyValidator)})]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -37,7 +39,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyValidator : BindingValidator<int>
{
public override BindingValidationError Validate(int value) => Ok();
@@ -46,11 +49,12 @@ public class MyValidator : BindingValidator<int>
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", Validators = new[] {typeof(MyValidator)})]
[CommandOption("foo", Validators = new[] {typeof(MyValidator)})]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -61,7 +65,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyValidator : BindingValidator<string>
{
public override BindingValidationError Validate(string value) => Ok();
@@ -70,11 +75,12 @@ public class MyValidator : BindingValidator<string>
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", Validators = new[] {typeof(MyValidator)})]
[CommandOption("foo", Validators = new[] {typeof(MyValidator)})]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -85,15 +91,17 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -104,14 +112,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,12 +13,14 @@ public class ParameterMustBeInsideCommandAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyClass
{
[CommandParameter(0)]
public string Foo { get; set; }
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -29,7 +31,8 @@ public class MyClass
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -37,7 +40,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -48,12 +52,14 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public abstract class MyCommand
{
[CommandParameter(0)]
public string Foo { get; set; }
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -64,14 +70,16 @@ public abstract class MyCommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,18 +13,20 @@ public class ParameterMustBeLastIfNonRequiredAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Name = ""foo"", IsRequired = false)]
[CommandParameter(0, IsRequired = false)]
public string Foo { get; set; }
[CommandParameter(1, Name = ""bar"")]
[CommandParameter(1)]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -35,18 +37,20 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Name = ""foo"")]
[CommandParameter(0)]
public string Foo { get; set; }
[CommandParameter(1, Name = ""bar"", IsRequired = false)]
[CommandParameter(1, IsRequired = false)]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -57,18 +61,20 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Name = ""foo"")]
[CommandParameter(0)]
public string Foo { get; set; }
[CommandParameter(1, Name = ""bar"", IsRequired = true)]
[CommandParameter(1, IsRequired = true)]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -79,14 +85,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,7 +13,8 @@ public class ParameterMustBeLastIfNonScalarAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -24,7 +25,8 @@ public class MyCommand : ICommand
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -35,7 +37,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -46,7 +49,8 @@ public class MyCommand : ICommand
public string[] Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -57,7 +61,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -68,7 +73,8 @@ public class MyCommand : ICommand
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -79,14 +85,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -0,0 +1,114 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests;
public class ParameterMustBeRequiredIfPropertyRequiredAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeRequiredIfPropertyRequiredAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_a_non_required_parameter_is_bound_to_a_required_property()
{
// Arrange
// language=cs
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, IsRequired = false)]
public required string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_required_parameter_is_bound_to_a_required_property()
{
// Arrange
// language=cs
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, IsRequired = true)]
public required string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_non_required_parameter_is_bound_to_a_non_required_property()
{
// Arrange
// language=cs
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, IsRequired = false)]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_required_parameter_is_bound_to_a_non_required_property()
{
// Arrange
// language=cs
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, IsRequired = true)]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{
// Arrange
// language=cs
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public required string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}

View File

@@ -13,7 +13,8 @@ public class ParameterMustBeSingleIfNonRequiredAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -24,7 +25,8 @@ public class MyCommand : ICommand
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -35,7 +37,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -46,7 +49,8 @@ public class MyCommand : ICommand
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -57,7 +61,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -68,7 +73,8 @@ public class MyCommand : ICommand
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -79,14 +85,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,7 +13,8 @@ public class ParameterMustBeSingleIfNonScalarAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -24,7 +25,8 @@ public class MyCommand : ICommand
public string[] Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -35,7 +37,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -46,7 +49,8 @@ public class MyCommand : ICommand
public string[] Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -57,7 +61,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -68,7 +73,8 @@ public class MyCommand : ICommand
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -79,14 +85,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,18 +13,20 @@ public class ParameterMustHaveUniqueNameAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Name = ""foo"")]
[CommandParameter(0, Name = "foo")]
public string Foo { get; set; }
[CommandParameter(1, Name = ""foo"")]
[CommandParameter(1, Name = "foo")]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -35,18 +37,20 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Name = ""foo"")]
[CommandParameter(0, Name = "foo")]
public string Foo { get; set; }
[CommandParameter(1, Name = ""bar"")]
[CommandParameter(1, Name = "bar")]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -57,14 +61,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,7 +13,8 @@ public class ParameterMustHaveUniqueOrderAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -24,7 +25,8 @@ public class MyCommand : ICommand
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -35,7 +37,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -46,7 +49,8 @@ public class MyCommand : ICommand
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -57,14 +61,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,7 +13,8 @@ public class ParameterMustHaveValidConverterAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyConverter
{
public string Convert(string rawValue) => rawValue;
@@ -26,7 +27,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -37,7 +39,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyConverter : BindingConverter<int>
{
public override int Convert(string rawValue) => 42;
@@ -50,7 +53,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
@@ -62,7 +66,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyConverter : BindingConverter<string>
{
public override string Convert(string rawValue) => rawValue;
@@ -75,7 +80,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -86,7 +92,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyConverter : BindingConverter<int>
{
public override int Convert(string rawValue) => 42;
@@ -95,11 +102,12 @@ public class MyConverter : BindingConverter<int>
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", Converter = typeof(MyConverter))]
[CommandOption("foo", Converter = typeof(MyConverter))]
public int? Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -110,7 +118,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyConverter : BindingConverter<string>
{
public override string Convert(string rawValue) => rawValue;
@@ -123,7 +132,8 @@ public class MyCommand : ICommand
public IReadOnlyList<string> Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -134,7 +144,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -142,7 +153,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -153,14 +165,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,7 +13,8 @@ public class ParameterMustHaveValidValidatorsAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyValidator
{
public void Validate(string value) {}
@@ -26,7 +27,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -37,7 +39,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyValidator : BindingValidator<int>
{
public override BindingValidationError Validate(int value) => Ok();
@@ -50,7 +53,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -61,7 +65,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
public class MyValidator : BindingValidator<string>
{
public override BindingValidationError Validate(string value) => Ok();
@@ -74,7 +79,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -85,7 +91,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -93,7 +100,8 @@ public class MyCommand : ICommand
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -104,14 +112,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -13,16 +13,18 @@ public class SystemConsoleShouldBeAvoidedAnalyzerSpecs
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
Console.WriteLine(""Hello world"");
Console.WriteLine("Hello world");
return default;
}
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -33,7 +35,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -42,7 +45,9 @@ public class MyCommand : ICommand
Console.ForegroundColor = ConsoleColor.Black;
return default;
}
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
@@ -52,16 +57,18 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
Console.Error.WriteLine(""Hello world"");
Console.Error.WriteLine("Hello world");
return default;
}
}";
}
""";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
@@ -72,16 +79,18 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Hello world"");
console.Output.WriteLine("Hello world");
return default;
}
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -92,14 +101,16 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
public void SomeOtherMethod() => Console.WriteLine(""Test"");
public void SomeOtherMethod() => Console.WriteLine("Test");
public ValueTask ExecuteAsync(IConsole console) => default;
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
@@ -110,7 +121,8 @@ public class MyCommand : ICommand
{
// Arrange
// language=cs
const string code = @"
const string code =
"""
[Command]
public class MyCommand : ICommand
{
@@ -118,7 +130,8 @@ public class MyCommand : ICommand
{
return default;
}
}";
}
""";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);

View File

@@ -58,7 +58,7 @@ internal class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer,
var compilation = CSharpCompilation.Create(
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
new[] { ast },
ReferenceAssemblies.Net50
Net70.References.All
.Append(MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location)),
// DLL to avoid having to define the Main() method
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
@@ -72,9 +72,10 @@ internal class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer,
if (compilationErrors.Any())
{
throw new InvalidOperationException(
"Failed to compile code." +
Environment.NewLine +
string.Join(Environment.NewLine, compilationErrors.Select(e => e.ToString()))
$"""
Failed to compile code.
{string.Join(Environment.NewLine, compilationErrors.Select(e => e.ToString()))}
"""
);
}

View File

@@ -3,14 +3,20 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<Nullable>annotations</Nullable>
<GenerateDependencyFile>true</GenerateDependencyFile>
<NoWarn>$(NoWarn);RS1025;RS1026</NoWarn>
</PropertyGroup>
<PropertyGroup>
<!--
Because this project only has a single target framework, the condition in
Directory.Build.props does not appear to work. This is a workaround for that.
-->
<Nullable>annotations</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- Keep this dependency as low as possible since we can't bundle it -->
<!-- https://github.com/Tyrrrz/CliFx/issues/127 -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
</ItemGroup>
</Project>

View File

@@ -13,6 +13,8 @@ internal partial class CommandOptionSymbol : ICommandMemberSymbol
public char? ShortName { get; }
public bool? IsRequired { get; }
public ITypeSymbol? ConverterType { get; }
public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; }
@@ -21,12 +23,14 @@ internal partial class CommandOptionSymbol : ICommandMemberSymbol
IPropertySymbol property,
string? name,
char? shortName,
bool? isRequired,
ITypeSymbol? converterType,
IReadOnlyList<ITypeSymbol> validatorTypes)
{
Property = property;
Name = name;
ShortName = shortName;
IsRequired = isRequired;
ConverterType = converterType;
ValidatorTypes = validatorTypes;
}
@@ -56,6 +60,12 @@ internal partial class CommandOptionSymbol
.Select(a => a.Value)
.FirstOrDefault() as char?;
var isRequired = attribute
.NamedArguments
.Where(a => a.Key == "IsRequired")
.Select(a => a.Value.Value)
.FirstOrDefault() as bool?;
var converter = attribute
.NamedArguments
.Where(a => a.Key == "Converter")
@@ -71,7 +81,7 @@ internal partial class CommandOptionSymbol
.Cast<ITypeSymbol>()
.ToArray();
return new CommandOptionSymbol(property, name, shortName, converter, validators);
return new CommandOptionSymbol(property, name, shortName, isRequired, converter, validators);
}
public static bool IsOptionProperty(IPropertySymbol property) =>

View File

@@ -0,0 +1,49 @@
using CliFx.Analyzers.ObjectModel;
using CliFx.Analyzers.Utils.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustBeRequiredIfPropertyRequiredAnalyzer : AnalyzerBase
{
public OptionMustBeRequiredIfPropertyRequiredAnalyzer()
: base(
"Options bound to required properties cannot be marked as non-required",
"This option cannot be marked as non-required because it's bound to a required property.")
{
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (!property.IsRequired)
return;
var option = CommandOptionSymbol.TryResolve(property);
if (option is null)
return;
if (option.IsRequired != false)
return;
context.ReportDiagnostic(
CreateDiagnostic(
propertyDeclaration.Identifier.GetLocation()
)
);
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
}

View File

@@ -0,0 +1,49 @@
using CliFx.Analyzers.ObjectModel;
using CliFx.Analyzers.Utils.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustBeRequiredIfPropertyRequiredAnalyzer : AnalyzerBase
{
public ParameterMustBeRequiredIfPropertyRequiredAnalyzer()
: base(
"Parameters bound to required properties cannot be marked as non-required",
"This parameter cannot be marked as non-required because it's bound to a required property.")
{
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (!property.IsRequired)
return;
var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null)
return;
if (parameter.IsRequired != false)
return;
context.ReportDiagnostic(
CreateDiagnostic(
propertyDeclaration.Identifier.GetLocation()
)
);
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
}

View File

@@ -2,16 +2,16 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
<PackageReference Include="clipr" Version="1.6.1" />
<PackageReference Include="Cocona" Version="2.0.3" />
<PackageReference Include="Cocona" Version="2.1.0" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.0.1" />
<PackageReference Include="PowerArgs" Version="3.6.0" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.0.2" />
<PackageReference Include="PowerArgs" Version="4.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20574.7" />
</ItemGroup>

View File

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

View File

@@ -14,10 +14,10 @@ public partial class BookAddCommand : ICommand
private readonly LibraryProvider _libraryProvider;
[CommandParameter(0, Name = "title", Description = "Book title.")]
public string Title { get; init; } = "";
public required string Title { get; init; }
[CommandOption("author", 'a', IsRequired = true, Description = "Book author.")]
public string Author { get; init; } = "";
public required string Author { get; init; }
[CommandOption("published", 'p', Description = "Book publish date.")]
public DateTimeOffset Published { get; init; } = CreateRandomDate();

View File

@@ -13,7 +13,7 @@ public class BookCommand : ICommand
private readonly LibraryProvider _libraryProvider;
[CommandParameter(0, Name = "title", Description = "Title of the book to retrieve.")]
public string Title { get; init; } = "";
public required string Title { get; init; }
public BookCommand(LibraryProvider libraryProvider)
{

View File

@@ -12,7 +12,7 @@ public class BookRemoveCommand : ICommand
private readonly LibraryProvider _libraryProvider;
[CommandParameter(0, Name = "title", Description = "Title of the book to remove.")]
public string Title { get; init; } = "";
public required string Title { get; init; }
public BookRemoveCommand(LibraryProvider libraryProvider)
{

View File

@@ -2,4 +2,4 @@
Sample command line interface for managing a library of books.
This demo project showcases basic CliFx functionality such as command routing, argument parsing, autogenerated help text.
This demo project showcases basic CliFx functionality such as command routing, argument parsing, and autogenerated help text.

View File

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

View File

@@ -21,7 +21,7 @@ public class CancellationSpecs : SpecsBase
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -34,15 +34,17 @@ public class Command : ICommand
console.RegisterCancellationHandler()
);
console.Output.WriteLine(""Completed successfully"");
console.Output.WriteLine("Completed successfully");
}
catch (OperationCanceledException)
{
console.Output.WriteLine(""Cancelled"");
console.Output.WriteLine("Cancelled");
throw;
}
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
@@ -9,16 +9,16 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Basic.Reference.Assemblies" Version="1.2.4" />
<PackageReference Include="CliWrap" Version="3.4.4" />
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="Basic.Reference.Assemblies.Net70" Version="1.4.1" />
<PackageReference Include="CliWrap" Version="3.5.0" />
<PackageReference Include="FluentAssertions" Version="6.8.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all" />
<PackageReference Include="coverlet.collector" Version="3.1.2" PrivateAssets="all" />
<PackageReference Include="coverlet.collector" Version="3.2.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
@@ -26,12 +26,4 @@
<ProjectReference Include="..\CliFx\CliFx.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="../CliFx.Tests.Dummy/bin/$(Configuration)/$(TargetFramework)/CliFx.Tests.Dummy.runtimeconfig.json">
<Link>CliFx.Tests.Dummy.runtimeconfig.json</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>false</Visible>
</None>
</ItemGroup>
</Project>

View File

@@ -48,7 +48,7 @@ public class ConsoleSpecs : SpecsBase
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -62,13 +62,14 @@ public class Command : ICommand
console.CursorLeft = 42;
console.CursorTop = 24;
console.Output.WriteLine(""Hello "");
console.Error.WriteLine(""world!"");
console.Output.WriteLine("Hello ");
console.Error.WriteLine("world!");
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -108,7 +109,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -121,7 +122,8 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -151,7 +153,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -164,7 +166,8 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)

View File

@@ -21,7 +21,7 @@ public class ConversionSpecs : SpecsBase
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -34,7 +34,9 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -59,7 +61,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -72,7 +74,9 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -97,7 +101,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -109,13 +113,15 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Foo = "" + Foo);
console.Output.WriteLine(""Bar = "" + Bar);
console.Output.WriteLine("Foo = " + Foo);
console.Output.WriteLine("Bar = " + Bar);
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -143,7 +149,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -156,7 +162,9 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -181,7 +189,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -194,7 +202,9 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -219,7 +229,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -232,7 +242,9 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -257,7 +269,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -266,11 +278,13 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(Foo.ToString(""u"", CultureInfo.InvariantCulture));
console.Output.WriteLine(Foo.ToString("u", CultureInfo.InvariantCulture));
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -295,7 +309,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -308,7 +322,9 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -333,7 +349,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public enum CustomEnum { One = 1, Two = 2, Three = 3 }
[Command]
@@ -348,7 +364,9 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -373,7 +391,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -385,13 +403,15 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Foo = "" + Foo);
console.Output.WriteLine(""Bar = "" + Bar);
console.Output.WriteLine("Foo = " + Foo);
console.Output.WriteLine("Bar = " + Bar);
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -419,7 +439,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public enum CustomEnum { One = 1, Two = 2, Three = 3 }
[Command]
@@ -433,13 +453,15 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Foo = "" + (int?) Foo);
console.Output.WriteLine(""Bar = "" + (int?) Bar);
console.Output.WriteLine("Foo = " + (int?) Foo);
console.Output.WriteLine("Bar = " + (int?) Bar);
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -467,7 +489,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public class CustomType
{
public string Value { get; }
@@ -487,7 +509,9 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -512,7 +536,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public class CustomTypeA
{
public string Value { get; }
@@ -544,13 +568,15 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Foo = "" + Foo.Value);
console.Output.WriteLine(""Bar = "" + Bar.Value);
console.Output.WriteLine("Foo = " + Foo.Value);
console.Output.WriteLine("Bar = " + Bar.Value);
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -578,7 +604,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public class CustomConverter : BindingConverter<int>
{
public override int Convert(string rawValue) =>
@@ -597,7 +623,9 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -622,7 +650,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -637,7 +665,9 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -666,7 +696,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -681,7 +711,9 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -710,7 +742,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -725,7 +757,9 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -754,7 +788,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -769,7 +803,9 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -798,7 +834,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -807,7 +843,9 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -832,7 +870,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public class CustomType {}
[Command]
@@ -843,7 +881,9 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -868,7 +908,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public class CustomType : IEnumerable<object>
{
public IEnumerator<object> GetEnumerator() => Enumerable.Empty<object>().GetEnumerator();
@@ -884,7 +924,9 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -909,7 +951,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public class ValidatorA : BindingValidator<int>
{
public override BindingValidationError Validate(int value) => Ok();
@@ -917,7 +959,7 @@ public class ValidatorA : BindingValidator<int>
public class ValidatorB : BindingValidator<int>
{
public override BindingValidationError Validate(int value) => Error(""Hello world"");
public override BindingValidationError Validate(int value) => Error("Hello world");
}
[Command]
@@ -928,7 +970,9 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
@@ -953,14 +997,14 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public class CustomType
{
public string Value { get; }
private CustomType(string value) => Value = value;
public static CustomType Parse(string value) => throw new Exception(""Hello world"");
public static CustomType Parse(string value) => throw new Exception("Hello world");
}
[Command]
@@ -971,7 +1015,9 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)

View File

@@ -75,13 +75,14 @@ public class DirectivesSpecs : SpecsBase
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
[Command(""cmd"")]
"""
[Command("cmd")]
public class Command : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)

View File

@@ -25,11 +25,11 @@ public class EnvironmentSpecs : SpecsBase
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"", IsRequired = true, EnvironmentVariable = ""ENV_FOO"")]
[CommandOption("foo", IsRequired = true, EnvironmentVariable = "ENV_FOO")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console)
@@ -38,7 +38,8 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -67,11 +68,11 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"", EnvironmentVariable = ""ENV_FOO"")]
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console)
@@ -80,7 +81,8 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -109,11 +111,11 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"", EnvironmentVariable = ""ENV_FOO"")]
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
public IReadOnlyList<string> Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console)
@@ -124,7 +126,8 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -156,11 +159,11 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"", EnvironmentVariable = ""ENV_FOO"")]
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console)
@@ -169,7 +172,8 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -198,11 +202,11 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"", EnvironmentVariable = ""ENV_FOO"")]
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console)
@@ -211,7 +215,8 @@ public class Command : ICommand
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)

View File

@@ -22,14 +22,15 @@ public class ErrorReportingSpecs : SpecsBase
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
public ValueTask ExecuteAsync(IConsole console) =>
throw new Exception(""Something went wrong"");
throw new Exception("Something went wrong");
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -60,14 +61,15 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
public ValueTask ExecuteAsync(IConsole console) =>
throw new Exception(""Something went wrong"", new Exception(""Another exception""));
throw new Exception("Something went wrong", new Exception("Another exception"));
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -99,14 +101,15 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
public ValueTask ExecuteAsync(IConsole console) =>
throw new CommandException(""Something went wrong"", 69);
throw new CommandException("Something went wrong", 69);
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -134,14 +137,15 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
public ValueTask ExecuteAsync(IConsole console) =>
throw new CommandException("""", 69);
throw new CommandException("", 69);
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -172,14 +176,15 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
public ValueTask ExecuteAsync(IConsole console) =>
throw new CommandException(""Something went wrong"", 69, true);
throw new CommandException("Something went wrong", 69, true);
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)

View File

@@ -44,13 +44,14 @@ public class HelpTextSpecs : SpecsBase
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class DefaultCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -77,19 +78,20 @@ public class DefaultCommand : ICommand
// Arrange
var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs
@"
[Command(""cmd"")]
"""
[Command("cmd")]
public class NamedCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command(""cmd child"")]
[Command("cmd child")]
public class NamedChildCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
@@ -116,25 +118,26 @@ public class NamedChildCommand : ICommand
// Arrange
var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs
@"
"""
[Command]
public class DefaultCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command(""cmd"", Description = ""Description of a named command."")]
[Command("cmd", Description = "Description of a named command.")]
public class NamedCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command(""cmd child"")]
[Command("cmd child")]
public class NamedChildCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
@@ -160,25 +163,26 @@ public class NamedChildCommand : ICommand
// Arrange
var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs
@"
"""
[Command]
public class DefaultCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command(""cmd"")]
[Command("cmd")]
public class NamedCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command(""cmd child"", Description = ""Description of a named child command."")]
[Command("cmd child", Description = "Description of a named child command.")]
public class NamedChildCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
@@ -257,13 +261,15 @@ public class NamedChildCommand : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
[Command(Description = ""Description of the default command."")]
"""
[Command(Description = "Description of the default command.")]
public class DefaultCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -292,19 +298,21 @@ public class DefaultCommand : ICommand
// Arrange
var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs
@"
"""
[Command]
public class DefaultCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command(""cmd"")]
[Command("cmd")]
public class NamedCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
@@ -333,7 +341,8 @@ public class NamedCommand : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -348,7 +357,8 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -378,7 +388,8 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
// Base members appear last in reflection order
public abstract class CommandBase : ICommand
{
@@ -399,7 +410,8 @@ public class Command : CommandBase
public override ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -428,22 +440,24 @@ public class Command : CommandBase
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"", IsRequired = true)]
[CommandOption("foo", IsRequired = true)]
public string Foo { get; set; }
[CommandOption(""bar"")]
[CommandOption("bar")]
public string Bar { get; set; }
[CommandOption(""baz"", IsRequired = true)]
[CommandOption("baz", IsRequired = true)]
public IReadOnlyList<string> Baz { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -472,19 +486,21 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandParameter(0, Name = ""foo"", Description = ""Description of foo."")]
[CommandParameter(0, Name = "foo", Description = "Description of foo.")]
public string Foo { get; set; }
[CommandOption(""bar"", Description = ""Description of bar."")]
[CommandOption("bar", Description = "Description of bar.")]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -515,13 +531,15 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -551,13 +569,15 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
[Command(""cmd"")]
"""
[Command("cmd")]
public class Command : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -589,7 +609,8 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public enum CustomEnum { One, Two, Three }
[Command]
@@ -598,12 +619,13 @@ public class Command : ICommand
[CommandParameter(0)]
public CustomEnum Foo { get; set; }
[CommandOption(""bar"")]
[CommandOption("bar")]
public CustomEnum Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -634,7 +656,8 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public enum CustomEnum { One, Two, Three }
[Command]
@@ -643,12 +666,13 @@ public class Command : ICommand
[CommandParameter(0)]
public IReadOnlyList<CustomEnum> Foo { get; set; }
[CommandOption(""bar"")]
[CommandOption("bar")]
public IReadOnlyList<CustomEnum> Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -679,7 +703,8 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public enum CustomEnum { One, Two, Three }
[Command]
@@ -688,12 +713,13 @@ public class Command : ICommand
[CommandParameter(0)]
public CustomEnum? Foo { get; set; }
[CommandOption(""bar"")]
[CommandOption("bar")]
public IReadOnlyList<CustomEnum?> Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -724,21 +750,23 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public enum CustomEnum { One, Two, Three }
[Command]
public class Command : ICommand
{
[CommandOption(""foo"", EnvironmentVariable = ""ENV_FOO"")]
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
public CustomEnum Foo { get; set; }
[CommandOption(""bar"", EnvironmentVariable = ""ENV_BAR"")]
[CommandOption("bar", EnvironmentVariable = "ENV_BAR")]
public CustomEnum Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -768,39 +796,41 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public enum CustomEnum { One, Two, Three }
[Command]
public class Command : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public object Foo { get; set; } = 42;
[CommandOption(""bar"")]
public string Bar { get; set; } = ""hello"";
[CommandOption("bar")]
public string Bar { get; set; } = "hello";
[CommandOption(""baz"")]
public IReadOnlyList<string> Baz { get; set; } = new[] {""one"", ""two"", ""three""};
[CommandOption("baz")]
public IReadOnlyList<string> Baz { get; set; } = new[] {"one", "two", "three"};
[CommandOption(""qwe"")]
[CommandOption("qwe")]
public bool Qwe { get; set; } = true;
[CommandOption(""qop"")]
[CommandOption("qop")]
public int? Qop { get; set; } = 1337;
[CommandOption(""zor"")]
[CommandOption("zor")]
public TimeSpan Zor { get; set; } = TimeSpan.FromMinutes(123);
[CommandOption(""lol"")]
[CommandOption("lol")]
public CustomEnum Lol { get; set; } = CustomEnum.Two;
[CommandOption(""hmm"", IsRequired = true)]
public string Hmm { get; set; } = ""not printed"";
[CommandOption("hmm", IsRequired = true)]
public string Hmm { get; set; } = "not printed";
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -836,25 +866,27 @@ public class Command : ICommand
// Arrange
var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs
@"
[Command(""cmd1"", Description = ""Description of one command."")]
"""
[Command("cmd1", Description = "Description of one command.")]
public class FirstCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command(""cmd2"", Description = ""Description of another command."")]
[Command("cmd2", Description = "Description of another command.")]
public class SecondCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command(""cmd2 child"", Description = ""Description of another command's child command."")]
[Command("cmd2 child", Description = "Description of another command's child command.")]
public class SecondCommandChildCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
@@ -890,37 +922,39 @@ public class SecondCommandChildCommand : ICommand
// Arrange
var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs
@"
[Command(""cmd1"")]
"""
[Command("cmd1")]
public class FirstCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command(""cmd1 child1"")]
[Command("cmd1 child1")]
public class FirstCommandFirstChildCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command(""cmd2"")]
[Command("cmd2")]
public class SecondCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command(""cmd2 child11"")]
[Command("cmd2 child11")]
public class SecondCommandFirstChildCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command(""cmd2 child2"")]
[Command("cmd2 child2")]
public class SecondCommandSecondChildCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
@@ -950,25 +984,27 @@ public class SecondCommandSecondChildCommand : ICommand
// Arrange
var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs
@"
[Command(""cmd1"")]
"""
[Command("cmd1")]
public class FirstCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command(""cmd2 child1"")]
[Command("cmd2 child1")]
public class SecondCommandFirstChildCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command(""cmd2 child2"")]
[Command("cmd2 child2")]
public class SecondCommandSecondChildCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
""");
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)

View File

@@ -22,11 +22,11 @@ public class OptionBindingSpecs : SpecsBase
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public bool Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console)
@@ -34,7 +34,9 @@ public class Command : ICommand
console.Output.WriteLine(Foo);
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -60,7 +62,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -72,7 +74,9 @@ public class Command : ICommand
console.Output.WriteLine(Foo);
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -98,24 +102,26 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
[CommandOption(""bar"")]
[CommandOption("bar")]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Foo = "" + Foo);
console.Output.WriteLine(""Bar = "" + Bar);
console.Output.WriteLine("Foo = " + Foo);
console.Output.WriteLine("Bar = " + Bar);
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -144,7 +150,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -156,12 +162,14 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Foo = "" + Foo);
console.Output.WriteLine(""Bar = "" + Bar);
console.Output.WriteLine("Foo = " + Foo);
console.Output.WriteLine("Bar = " + Bar);
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -190,7 +198,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -202,12 +210,14 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Foo = "" + Foo);
console.Output.WriteLine(""Bar = "" + Bar);
console.Output.WriteLine("Foo = " + Foo);
console.Output.WriteLine("Bar = " + Bar);
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -236,11 +246,11 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""Foo"")]
[CommandOption("Foo")]
public IReadOnlyList<string> Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console)
@@ -250,7 +260,9 @@ public class Command : ICommand
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -280,7 +292,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -294,7 +306,9 @@ public class Command : ICommand
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -324,11 +338,11 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public IReadOnlyList<string> Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console)
@@ -338,7 +352,9 @@ public class Command : ICommand
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -368,7 +384,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -382,7 +398,9 @@ public class Command : ICommand
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -412,11 +430,11 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"", 'f')]
[CommandOption("foo", 'f')]
public IReadOnlyList<string> Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console)
@@ -426,7 +444,9 @@ public class Command : ICommand
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -456,24 +476,26 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
[CommandOption(""bar"")]
public string Bar { get; set; } = ""hello"";
[CommandOption("bar")]
public string Bar { get; set; } = "hello";
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Foo = "" + Foo);
console.Output.WriteLine(""Bar = "" + Bar);
console.Output.WriteLine("Foo = " + Foo);
console.Output.WriteLine("Bar = " + Bar);
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -502,7 +524,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
public static class SharedContext
{
public static int Foo { get; set; }
@@ -512,7 +534,7 @@ public static class SharedContext
public interface IHasFoo : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public int Foo
{
get => SharedContext.Foo;
@@ -522,7 +544,7 @@ public interface IHasFoo : ICommand
public interface IHasBar : ICommand
{
[CommandOption(""bar"")]
[CommandOption("bar")]
public bool Bar
{
get => SharedContext.Bar;
@@ -538,19 +560,20 @@ public interface IHasBaz : ICommand
[Command]
public class Command : IHasFoo, IHasBar, IHasBaz
{
[CommandOption(""baz"")]
[CommandOption("baz")]
public string Baz { get; set; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Foo = "" + SharedContext.Foo);
console.Output.WriteLine(""Bar = "" + SharedContext.Bar);
console.Output.WriteLine(""Baz = "" + Baz);
console.Output.WriteLine("Foo = " + SharedContext.Foo);
console.Output.WriteLine("Bar = " + SharedContext.Bar);
console.Output.WriteLine("Baz = " + Baz);
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -579,11 +602,11 @@ public class Command : IHasFoo, IHasBar, IHasBaz
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console)
@@ -592,7 +615,9 @@ public class Command : ICommand
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -618,15 +643,17 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"", IsRequired = true)]
[CommandOption("foo", IsRequired = true)]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -652,15 +679,17 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"", IsRequired = true)]
[CommandOption("foo", IsRequired = true)]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -686,15 +715,17 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"", IsRequired = true)]
[CommandOption("foo", IsRequired = true)]
public IReadOnlyList<string> Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -720,15 +751,17 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -754,15 +787,17 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
[CommandOption(""foo"")]
[CommandOption("foo")]
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -781,4 +816,40 @@ public class Command : ICommand
exitCode.Should().NotBe(0);
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

@@ -21,7 +21,7 @@ public class ParameterBindingSpecs : SpecsBase
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -33,12 +33,14 @@ public class Command : ICommand
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Foo = "" + Foo);
console.Output.WriteLine(""Bar = "" + Bar);
console.Output.WriteLine("Foo = " + Foo);
console.Output.WriteLine("Bar = " + Bar);
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -67,7 +69,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -80,20 +82,22 @@ public class Command : ICommand
[CommandParameter(2)]
public IReadOnlyList<string> Baz { get; set; }
[CommandOption(""boo"")]
[CommandOption("boo")]
public string Boo { get; set; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Foo = "" + Foo);
console.Output.WriteLine(""Bar = "" + Bar);
console.Output.WriteLine("Foo = " + Foo);
console.Output.WriteLine("Bar = " + Bar);
foreach (var i in Baz)
console.Output.WriteLine(""Baz = "" + i);
console.Output.WriteLine("Baz = " + i);
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -125,7 +129,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -133,16 +137,18 @@ public class Command : ICommand
public string Foo { get; set; }
[CommandParameter(1, IsRequired = false)]
public string Bar { get; set; } = ""xyz"";
public string Bar { get; set; } = "xyz";
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Foo = "" + Foo);
console.Output.WriteLine(""Bar = "" + Bar);
console.Output.WriteLine("Foo = " + Foo);
console.Output.WriteLine("Bar = " + Bar);
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -171,7 +177,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -182,7 +188,9 @@ public class Command : ICommand
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -208,7 +216,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -219,7 +227,9 @@ public class Command : ICommand
public IReadOnlyList<string> Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -245,7 +255,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -256,7 +266,9 @@ public class Command : ICommand
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)

View File

@@ -21,37 +21,38 @@ public class RoutingSpecs : SpecsBase
// Arrange
var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs
@"
"""
[Command]
public class DefaultCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""default"");
console.Output.WriteLine("default");
return default;
}
}
[Command(""cmd"")]
[Command("cmd")]
public class NamedCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""cmd"");
console.Output.WriteLine("cmd");
return default;
}
}
[Command(""cmd child"")]
[Command("cmd child")]
public class NamedChildCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""cmd child"");
console.Output.WriteLine("cmd child");
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
@@ -77,37 +78,38 @@ public class NamedChildCommand : ICommand
// Arrange
var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs
@"
"""
[Command]
public class DefaultCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""default"");
console.Output.WriteLine("default");
return default;
}
}
[Command(""cmd"")]
[Command("cmd")]
public class NamedCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""cmd"");
console.Output.WriteLine("cmd");
return default;
}
}
[Command(""cmd child"")]
[Command("cmd child")]
public class NamedChildCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""cmd child"");
console.Output.WriteLine("cmd child");
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
@@ -133,37 +135,38 @@ public class NamedChildCommand : ICommand
// Arrange
var commandTypes = DynamicCommandBuilder.CompileMany(
// language=cs
@"
"""
[Command]
public class DefaultCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""default"");
console.Output.WriteLine("default");
return default;
}
}
[Command(""cmd"")]
[Command("cmd")]
public class NamedCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""cmd"");
console.Output.WriteLine("cmd");
return default;
}
}
[Command(""cmd child"")]
[Command("cmd child")]
public class NamedChildCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""cmd child"");
console.Output.WriteLine("cmd child");
return default;
}
}
");
"""
);
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)

View File

@@ -23,16 +23,18 @@ public class TypeActivationSpecs : SpecsBase
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""foo"");
console.Output.WriteLine("foo");
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -59,14 +61,16 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
public Command(string foo) {}
public ValueTask ExecuteAsync(IConsole console) => default;
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -93,7 +97,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -106,7 +110,9 @@ public class Command : ICommand
console.Output.WriteLine(_foo);
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
@@ -133,7 +139,7 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
@@ -146,7 +152,9 @@ public class Command : ICommand
console.Output.WriteLine(_foo);
return default;
}
}");
}
"""
);
var serviceProvider = new ServiceCollection()
.AddSingleton(commandType, Activator.CreateInstance(commandType, "Hello world")!)
@@ -177,16 +185,18 @@ public class Command : ICommand
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
"""
[Command]
public class Command : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""foo"");
console.Output.WriteLine("foo");
return default;
}
}");
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)

View File

@@ -61,7 +61,7 @@ internal static class DynamicCommandBuilder
var compilation = CSharpCompilation.Create(
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
new[] {ast},
ReferenceAssemblies.Net50
Net70.References.All
.Append(MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location))
.Append(MetadataReference.CreateFromFile(typeof(DynamicCommandBuilder).Assembly.Location)),
// DLL to avoid having to define the Main() method
@@ -76,9 +76,10 @@ internal static class DynamicCommandBuilder
if (compilationErrors.Any())
{
throw new InvalidOperationException(
"Failed to compile code." +
Environment.NewLine +
string.Join(Environment.NewLine, compilationErrors.Select(e => e.ToString()))
$"""
Failed to compile code.
{string.Join(Environment.NewLine, compilationErrors.Select(e => e.ToString()))}
"""
);
}
@@ -94,9 +95,10 @@ internal static class DynamicCommandBuilder
if (emitErrors.Any())
{
throw new InvalidOperationException(
"Failed to emit code." +
Environment.NewLine +
string.Join(Environment.NewLine, emitErrors.Select(e => e.ToString()))
$"""
Failed to emit code.
{string.Join(Environment.NewLine, emitErrors.Select(e => e.ToString()))}
"""
);
}

View File

@@ -32,6 +32,10 @@ public sealed class CommandOptionAttribute : Attribute
/// Whether this option is required (default: <c>false</c>).
/// If an option is required, the user will get an error if they don't set it.
/// </summary>
/// <remarks>
/// You can use the <c>required</c> keyword on the property (introduced in C# 11) to implicitly
/// set <see cref="IsRequired" /> to <c>true</c>.
/// </remarks>
public bool IsRequired { get; set; }
/// <summary>

View File

@@ -2,9 +2,12 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.1;netstandard2.0</TargetFrameworks>
<IsPackable>true</IsPackable>
</PropertyGroup>
<PropertyGroup>
<Authors>$(Company)</Authors>
<Description>Declarative framework for building command line applications</Description>
<IsPackable>true</IsPackable>
<PackageTags>command line executable interface framework parser arguments cli app application net core</PackageTags>
<PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl>
<PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes>
@@ -24,16 +27,17 @@
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="all" />
<PackageReference Include="Nullable" Version="1.3.0" PrivateAssets="all" />
<PackageReference Include="Nullable" Version="1.3.1" PrivateAssets="all" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
</ItemGroup>
<!-- Pack the analyzer assembly inside the package -->
<!-- Embed the analyzer inside the package -->
<ItemGroup>
<ProjectReference Include="../CliFx.Analyzers/CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/CliFx.Analyzers.deps.json" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/CliFx.Analyzers.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Buffers.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Collections.Immutable.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />

View File

@@ -99,11 +99,11 @@ internal class CommandBinder
}
throw CliFxException.InternalError(
$"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has an unsupported underlying property type." +
Environment.NewLine +
$"There is no known way to convert a string value into an instance of type `{targetType.FullName}`." +
Environment.NewLine +
"To fix this, either change the property to use a supported type or configure a custom converter."
$"""
{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has an unsupported underlying property type.
There is no known way to convert a string value into an instance of type `{targetType.FullName}`
To fix this, either change the property to use a supported type or configure a custom converter.
"""
);
}
@@ -133,11 +133,11 @@ internal class CommandBinder
}
throw CliFxException.InternalError(
$"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has an unsupported underlying property type." +
Environment.NewLine +
$"There is no known way to convert an array of `{targetElementType.FullName}` into an instance of type `{targetEnumerableType.FullName}`." +
Environment.NewLine +
"To fix this, change the property to use a type which can be assigned from an array or a type that has a constructor which accepts an array."
$"""
{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has an unsupported underlying property type.
There is no known way to convert an array of `{targetElementType.FullName}` into an instance of type `{targetEnumerableType.FullName}`.
To fix this, change the property to use a type which can be assigned from an array or a type that has a constructor which accepts an array.
"""
);
}
@@ -169,20 +169,21 @@ internal class CommandBinder
: ex.Message;
throw CliFxException.UserError(
$"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} cannot be set from the provided argument(s):" +
Environment.NewLine +
rawValues.Select(v => '<' + v + '>').JoinToString(" ") +
Environment.NewLine +
$"Error: {errorMessage}",
$"""
{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} cannot be set from the provided argument(s):
{rawValues.Select(v => '<' + v + '>').JoinToString(" ")}
Error: {errorMessage}
""",
ex
);
}
// Mismatch (scalar but too many values)
throw CliFxException.UserError(
$"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} expects a single argument, but provided with multiple:" +
Environment.NewLine +
rawValues.Select(v => '<' + v + '>').JoinToString(" ")
$"""
{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} expects a single argument, but provided with multiple:
{rawValues.Select(v => '<' + v + '>').JoinToString(" ")}
"""
);
}
@@ -202,11 +203,11 @@ internal class CommandBinder
if (errors.Any())
{
throw CliFxException.UserError(
$"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has been provided with an invalid value." +
Environment.NewLine +
"Error(s):" +
Environment.NewLine +
errors.Select(e => "- " + e.Message).JoinToString(Environment.NewLine)
$"""
{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has been provided with an invalid value.
Error(s):
{errors.Select(e => "- " + e.Message).JoinToString(Environment.NewLine)}
"""
);
}
}
@@ -264,22 +265,20 @@ internal class CommandBinder
if (remainingParameterInputs.Any())
{
throw CliFxException.UserError(
"Unexpected parameter(s):" +
Environment.NewLine +
remainingParameterInputs
.Select(p => p.GetFormattedIdentifier())
.JoinToString(" ")
$"""
Unexpected parameter(s):
{remainingParameterInputs.Select(p => p.GetFormattedIdentifier()).JoinToString(" ")}
"""
);
}
if (remainingRequiredParameterSchemas.Any())
{
throw CliFxException.UserError(
"Missing required parameter(s):" +
Environment.NewLine +
remainingRequiredParameterSchemas
.Select(p => p.GetFormattedIdentifier())
.JoinToString(" ")
$"""
Missing required parameter(s):
{remainingRequiredParameterSchemas.Select(p => p.GetFormattedIdentifier()).JoinToString(" ")}
"""
);
}
}
@@ -337,22 +336,20 @@ internal class CommandBinder
if (remainingOptionInputs.Any())
{
throw CliFxException.UserError(
"Unrecognized option(s):" +
Environment.NewLine +
remainingOptionInputs
.Select(o => o.GetFormattedIdentifier())
.JoinToString(", ")
$"""
Unrecognized option(s):
{remainingOptionInputs.Select(o => o.GetFormattedIdentifier()).JoinToString(", ")}
"""
);
}
if (remainingRequiredOptionSchemas.Any())
{
throw CliFxException.UserError(
"Missing required option(s):" +
Environment.NewLine +
remainingRequiredOptionSchemas
.Select(o => o.GetFormattedIdentifier())
.JoinToString(", ")
$"""
Missing required option(s):
{remainingRequiredOptionSchemas.Select(o => o.GetFormattedIdentifier()).JoinToString(", ")}
"""
);
}
}

View File

@@ -20,11 +20,11 @@ public class DefaultTypeActivator : ITypeActivator
catch (MemberAccessException ex)
{
throw CliFxException.InternalError(
$"Failed to create an instance of type `{type.FullName}`, could not access the constructor." +
Environment.NewLine +
"Default type activator is only capable of instantiating a type if it has a public parameterless constructor." +
Environment.NewLine +
"To fix this, either add a parameterless constructor to the type or configure a custom activator for the application.",
$"""
Failed to create an instance of type `{type.FullName}`, could not access the constructor.
Default type activator is only capable of instantiating a type if it has a public parameterless constructor.
To fix this, either add a parameterless constructor to the type or configure a custom activator for the application.
""",
ex
);
}

View File

@@ -24,11 +24,11 @@ public class DelegateTypeActivator : ITypeActivator
if (instance is null)
{
throw CliFxException.InternalError(
$"Failed to create an instance of type `{type.FullName}`, received <null> instead." +
Environment.NewLine +
"To fix this, ensure that the provided type activator is configured correctly, as it's not expected to return <null>." +
Environment.NewLine +
"If you are relying on a dependency container, this error may indicate that the specified type has not been registered."
$"""
Failed to create an instance of type `{type.FullName}`, received <null> instead.
To fix this, ensure that the provided type activator is configured correctly, as it's not expected to return <null>.
If you are relying on a dependency container, this error may indicate that the specified type has not been registered.
"""
);
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CliFx.Utils.Extensions;
namespace CliFx.Input;

View File

@@ -129,15 +129,13 @@ internal partial class CommandSchema
if (schema is null)
{
throw CliFxException.InternalError(
$"Type `{type.FullName}` is not a valid command type." +
Environment.NewLine +
"In order to be a valid command type, it must:" +
Environment.NewLine +
$"- Implement `{typeof(ICommand).FullName}`" +
Environment.NewLine +
$"- Be annotated with `{typeof(CommandAttribute).FullName}`" +
Environment.NewLine +
"- Not be an abstract class"
$"""
Type `{type.FullName}` is not a valid command type.
In order to be a valid command type, it must:
- Implement `{typeof(ICommand).FullName}`
- Be annotated with `{typeof(CommandAttribute).FullName}`
- Not be an abstract class
"""
);
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Reflection;
using System.Text;
using CliFx.Attributes;
using CliFx.Utils.Extensions;
namespace CliFx.Schema;
@@ -101,6 +102,7 @@ internal partial class OptionSchema
// The user may mistakenly specify dashes, thinking it's required, so trim them
var name = attribute.Name?.TrimStart('-').Trim();
var environmentVariable = attribute.EnvironmentVariable?.Trim();
var isRequired = attribute.IsRequired || property.IsRequired();
var description = attribute.Description?.Trim();
return new OptionSchema(
@@ -108,7 +110,7 @@ internal partial class OptionSchema
name,
attribute.ShortName,
environmentVariable,
attribute.IsRequired,
isRequired,
description,
attribute.Converter,
attribute.Validators

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Reflection;
using CliFx.Attributes;
using CliFx.Utils.Extensions;
namespace CliFx.Schema;
@@ -53,13 +54,14 @@ internal partial class ParameterSchema
return null;
var name = attribute.Name?.Trim() ?? property.Name.ToLowerInvariant();
var isRequired = attribute.IsRequired || property.IsRequired();
var description = attribute.Description?.Trim();
return new ParameterSchema(
new BindablePropertyDescriptor(property),
attribute.Order,
name,
attribute.IsRequired,
isRequired,
description,
attribute.Converter,
attribute.Validators

View File

@@ -1,4 +1,5 @@
using System.Collections;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
@@ -37,4 +38,14 @@ internal static class CollectionExtensions
dictionary
.Cast<DictionaryEntry>()
.ToDictionary(entry => (TKey) entry.Key, entry => (TValue) entry.Value, comparer);
public static Array ToNonGenericArray<T>(this IEnumerable<T> source, Type elementType)
{
var sourceAsCollection = source as ICollection ?? source.ToArray();
var array = Array.CreateInstance(elementType, sourceAsCollection.Count);
sourceAsCollection.CopyTo(array, 0);
return array;
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Linq;
using System.Reflection;
namespace CliFx.Utils.Extensions;
internal static class PropertyExtensions
{
public static bool IsRequired(this PropertyInfo propertyInfo) =>
// Match attribute by name to avoid depending on .NET 7.0+ and to allow polyfilling
propertyInfo.GetCustomAttributes().Any(a =>
string.Equals(
a.GetType().FullName,
"System.Runtime.CompilerServices.RequiredMemberAttribute",
StringComparison.Ordinal
)
);
}

View File

@@ -11,7 +11,8 @@ internal static class TypeExtensions
public static bool Implements(this Type type, Type interfaceType) =>
type.GetInterfaces().Contains(interfaceType);
public static Type? TryGetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type);
public static Type? TryGetNullableUnderlyingType(this Type type) =>
Nullable.GetUnderlyingType(type);
public static Type? TryGetEnumerableUnderlyingType(this Type type)
{
@@ -44,16 +45,6 @@ internal static class TypeExtensions
);
}
public static Array ToNonGenericArray<T>(this IEnumerable<T> source, Type elementType)
{
var sourceAsCollection = source as ICollection ?? source.ToArray();
var array = Array.CreateInstance(elementType, sourceAsCollection.Count);
sourceAsCollection.CopyTo(array, 0);
return array;
}
public static bool IsToStringOverriden(this Type type)
{
var toStringMethod = type.GetMethod(nameof(ToString), Type.EmptyTypes);

View File

@@ -96,9 +96,10 @@ internal partial class StackFrame
// If parsing fails, we include the original stacktrace in the
// exception so that it's shown to the user.
throw new FormatException(
"Could not parse stacktrace:" +
Environment.NewLine +
stackTrace
$"""
Could not parse stacktrace:
{stackTrace}
"""
);
}

View File

@@ -1,12 +1,13 @@
<Project>
<PropertyGroup>
<Version>2.3</Version>
<Version>2.3.1</Version>
<Company>Tyrrrz</Company>
<Copyright>Copyright (C) Oleksii Holub</Copyright>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

View File

@@ -9,7 +9,7 @@
[![Donate](https://img.shields.io/badge/donate-$$$-8a2be2.svg)](https://tyrrrz.me/donate)
[![Fuck Russia](https://img.shields.io/badge/fuck-russia-e4181c.svg?labelColor=000000)](https://twitter.com/tyrrrz/status/1495972128977571848)
> 🟢 **Project status**: active<sup>[[?]](https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md)</sup>
> 🟡 **Project status**: maintenance mode<sup>[[?]](https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md)</sup>
**CliFx** is a simple to use, yet powerful framework for building command line applications.
Its primary goal is to completely take over the user input layer, allowing you to forget about infrastructural concerns and instead focus on writing your application.
@@ -144,7 +144,7 @@ public class LogCommand : ICommand
{
// Order: 0
[CommandParameter(0, Description = "Value whose logarithm is to be found.")]
public double Value { get; init; }
public required double Value { get; init; }
// Name: --base
// Short name: -b
@@ -212,14 +212,14 @@ OPTIONS
Overall, parameters and options are both used to consume input from the command line, but they differ in a few important ways:
| | Parameter | Option |
|--------------------|--------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|
| ------------------ | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| **Identification** | Positional (by relative order). | Nominal (by name or short name). |
| **Requiredness** | Required by default. Only the last parameter can be configured to be optional. | Optional by default. Any option can be configured to be required without limitations. |
| **Arity** | Only the last parameter can be bound to a non-scalar property (i.e. an array). | Any option can be bound to a non-scalar property without limitations. |
| **Fallback** | — | Can be configured to use an environment variable as fallback if the value isn't explicitly provided. |
As a general guideline, use parameters for required inputs that the command can't function without.
Use options for all other non-required inputs or when specifying the name explicitly makes the usage clearer.
Use options for all other non-required inputs, or when specifying the name explicitly makes the usage clearer.
### Argument syntax
@@ -382,8 +382,8 @@ If the user does not provide value for such option through command line argument
[Command]
public class AuthCommand : ICommand
{
[CommandOption("token", IsRequired = true, EnvironmentVariable = "AUTH_TOKEN")]
public string AuthToken { get; init; }
[CommandOption("token", EnvironmentVariable = "AUTH_TOKEN")]
public required string AuthToken { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
@@ -403,7 +403,7 @@ test
```
Environment variables can be configured for options of non-scalar types (arrays, lists, etc.) as well.
In such case, the values of the environment variable will be split by `Path.PathSeparator` (`;` on Windows, `:` on Linux).
In such case, the values of the environment variable will be split by `Path.PathSeparator` (`;` on Windows, `:` on Unix systems).
### Multiple commands
@@ -488,7 +488,7 @@ You can run `dotnet myapp.dll cmd1 [command] --help` to show help on a specific
> **Note**:
> Defining a default (unnamed) command is not required.
> If it's absent, running the application without specifying a command will just show the root level help text.
> If it's absent, running the application without specifying a command will just show the root-level help text.
### Reporting errors
@@ -499,11 +499,11 @@ This special exception can be used to print an error message to the console, ret
[Command]
public class DivideCommand : ICommand
{
[CommandOption("dividend", IsRequired = true)]
public double Dividend { get; init; }
[CommandOption("dividend")]
public required double Dividend { get; init; }
[CommandOption("divisor", IsRequired = true)]
public double Divisor { get; init; }
[CommandOption("divisor")]
public required double Divisor { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
@@ -567,12 +567,12 @@ public class CancellableCommand : ICommand
```
> **Warning**:
> Forceful termination of a command can only be delayed once.
> If the user issues a second interrupt signal, the process will be killed immediately without waiting for graceful cancellation.
> Cancellation handler is only respected when the user sends the interrupt signal for the first time.
> If the user decides to issue the signal again, the application will terminate immediately regardless of whether the command is cancellation-aware.
### Type activation
Because **CliFx** takes responsibility for the application's entire lifecycle, it needs to be capable of instantiating various user-defined types at runtime.
Because **CliFx** takes responsibility for the application's entire lifecycle, it needs to be capable of instantiating various user-defined types at run-time.
To facilitate that, it uses an interface called `ITypeActivator` that determines how to create a new instance of a given type.
The default implementation of `ITypeActivator` only supports types that have public parameterless constructors, which is sufficient for the majority of scenarios.