This commit is contained in:
Tyrrrz
2021-12-08 23:43:35 +02:00
parent 9990387cfa
commit 2feeb21270
132 changed files with 8021 additions and 8154 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,158 +11,157 @@ using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
namespace CliFx.Analyzers.Tests.Utils
{
internal class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer, AnalyzerAssertions>
{
protected override string Identifier { get; } = "analyzer";
namespace CliFx.Analyzers.Tests.Utils;
public AnalyzerAssertions(DiagnosticAnalyzer analyzer)
: base(analyzer)
internal class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer, AnalyzerAssertions>
{
protected override string Identifier { get; } = "analyzer";
public AnalyzerAssertions(DiagnosticAnalyzer analyzer)
: base(analyzer)
{
}
private Compilation Compile(string sourceCode)
{
// Get default system namespaces
var defaultSystemNamespaces = new[]
{
"System",
"System.Collections.Generic",
"System.Threading.Tasks"
};
// Get default CliFx namespaces
var defaultCliFxNamespaces = typeof(ICommand)
.Assembly
.GetTypes()
.Where(t => t.IsPublic)
.Select(t => t.Namespace)
.Distinct()
.ToArray();
// Append default imports to the source code
var sourceCodeWithUsings =
string.Join(Environment.NewLine, defaultSystemNamespaces.Select(n => $"using {n};")) +
string.Join(Environment.NewLine, defaultCliFxNamespaces.Select(n => $"using {n};")) +
Environment.NewLine +
sourceCode;
// Parse the source code
var ast = SyntaxFactory.ParseSyntaxTree(
SourceText.From(sourceCodeWithUsings),
CSharpParseOptions.Default
);
// Compile the code to IL
var compilation = CSharpCompilation.Create(
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
new[] {ast},
ReferenceAssemblies.Net50
.Append(MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location)),
// DLL to avoid having to define the Main() method
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);
var compilationErrors = compilation
.GetDiagnostics()
.Where(d => d.Severity >= DiagnosticSeverity.Error)
.ToArray();
if (compilationErrors.Any())
{
throw new InvalidOperationException(
"Failed to compile code." +
Environment.NewLine +
string.Join(Environment.NewLine, compilationErrors.Select(e => e.ToString()))
);
}
private Compilation Compile(string sourceCode)
return compilation;
}
private IReadOnlyList<Diagnostic> GetProducedDiagnostics(string sourceCode)
{
var analyzers = ImmutableArray.Create(Subject);
var compilation = Compile(sourceCode);
return compilation
.WithAnalyzers(analyzers)
.GetAnalyzerDiagnosticsAsync(analyzers, default)
.GetAwaiter()
.GetResult();
}
public void ProduceDiagnostics(string sourceCode)
{
var expectedDiagnostics = Subject.SupportedDiagnostics;
var producedDiagnostics = GetProducedDiagnostics(sourceCode);
var expectedDiagnosticIds = expectedDiagnostics.Select(d => d.Id).Distinct().ToArray();
var producedDiagnosticIds = producedDiagnostics.Select(d => d.Id).Distinct().ToArray();
var result =
expectedDiagnosticIds.Intersect(producedDiagnosticIds).Count() ==
expectedDiagnosticIds.Length;
Execute.Assertion.ForCondition(result).FailWith(() =>
{
// Get default system namespaces
var defaultSystemNamespaces = new[]
var buffer = new StringBuilder();
buffer.AppendLine("Expected and produced diagnostics do not match.");
buffer.AppendLine();
buffer.AppendLine("Expected diagnostics:");
foreach (var expectedDiagnostic in expectedDiagnostics)
{
"System",
"System.Collections.Generic",
"System.Threading.Tasks"
};
// Get default CliFx namespaces
var defaultCliFxNamespaces = typeof(ICommand)
.Assembly
.GetTypes()
.Where(t => t.IsPublic)
.Select(t => t.Namespace)
.Distinct()
.ToArray();
// Append default imports to the source code
var sourceCodeWithUsings =
string.Join(Environment.NewLine, defaultSystemNamespaces.Select(n => $"using {n};")) +
string.Join(Environment.NewLine, defaultCliFxNamespaces.Select(n => $"using {n};")) +
Environment.NewLine +
sourceCode;
// Parse the source code
var ast = SyntaxFactory.ParseSyntaxTree(
SourceText.From(sourceCodeWithUsings),
CSharpParseOptions.Default
);
// Compile the code to IL
var compilation = CSharpCompilation.Create(
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
new[] {ast},
ReferenceAssemblies.Net50
.Append(MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location)),
// DLL to avoid having to define the Main() method
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);
var compilationErrors = compilation
.GetDiagnostics()
.Where(d => d.Severity >= DiagnosticSeverity.Error)
.ToArray();
if (compilationErrors.Any())
{
throw new InvalidOperationException(
"Failed to compile code." +
Environment.NewLine +
string.Join(Environment.NewLine, compilationErrors.Select(e => e.ToString()))
);
buffer.Append(" - ");
buffer.Append(expectedDiagnostic.Id);
buffer.AppendLine();
}
return compilation;
}
buffer.AppendLine();
private IReadOnlyList<Diagnostic> GetProducedDiagnostics(string sourceCode)
{
var analyzers = ImmutableArray.Create(Subject);
var compilation = Compile(sourceCode);
buffer.AppendLine("Produced diagnostics:");
return compilation
.WithAnalyzers(analyzers)
.GetAnalyzerDiagnosticsAsync(analyzers, default)
.GetAwaiter()
.GetResult();
}
public void ProduceDiagnostics(string sourceCode)
{
var expectedDiagnostics = Subject.SupportedDiagnostics;
var producedDiagnostics = GetProducedDiagnostics(sourceCode);
var expectedDiagnosticIds = expectedDiagnostics.Select(d => d.Id).Distinct().ToArray();
var producedDiagnosticIds = producedDiagnostics.Select(d => d.Id).Distinct().ToArray();
var result =
expectedDiagnosticIds.Intersect(producedDiagnosticIds).Count() ==
expectedDiagnosticIds.Length;
Execute.Assertion.ForCondition(result).FailWith(() =>
foreach (var producedDiagnostic in producedDiagnostics)
{
var buffer = new StringBuilder();
buffer.Append(" - ");
buffer.Append(producedDiagnostic);
}
buffer.AppendLine("Expected and produced diagnostics do not match.");
buffer.AppendLine();
buffer.AppendLine("Expected diagnostics:");
foreach (var expectedDiagnostic in expectedDiagnostics)
{
buffer.Append(" - ");
buffer.Append(expectedDiagnostic.Id);
buffer.AppendLine();
}
buffer.AppendLine();
buffer.AppendLine("Produced diagnostics:");
foreach (var producedDiagnostic in producedDiagnostics)
{
buffer.Append(" - ");
buffer.Append(producedDiagnostic);
}
return new FailReason(buffer.ToString());
});
}
public void NotProduceDiagnostics(string sourceCode)
{
var producedDiagnostics = GetProducedDiagnostics(sourceCode);
var result = !producedDiagnostics.Any();
Execute.Assertion.ForCondition(result).FailWith(() =>
{
var buffer = new StringBuilder();
buffer.AppendLine("Expected no produced diagnostics.");
buffer.AppendLine();
buffer.AppendLine("Produced diagnostics:");
foreach (var producedDiagnostic in producedDiagnostics)
{
buffer.Append(" - ");
buffer.Append(producedDiagnostic);
}
return new FailReason(buffer.ToString());
});
}
return new FailReason(buffer.ToString());
});
}
internal static class AnalyzerAssertionsExtensions
public void NotProduceDiagnostics(string sourceCode)
{
public static AnalyzerAssertions Should(this DiagnosticAnalyzer analyzer) => new(analyzer);
var producedDiagnostics = GetProducedDiagnostics(sourceCode);
var result = !producedDiagnostics.Any();
Execute.Assertion.ForCondition(result).FailWith(() =>
{
var buffer = new StringBuilder();
buffer.AppendLine("Expected no produced diagnostics.");
buffer.AppendLine();
buffer.AppendLine("Produced diagnostics:");
foreach (var producedDiagnostic in producedDiagnostics)
{
buffer.Append(" - ");
buffer.Append(producedDiagnostic);
}
return new FailReason(buffer.ToString());
});
}
}
internal static class AnalyzerAssertionsExtensions
{
public static AnalyzerAssertions Should(this DiagnosticAnalyzer analyzer) => new(analyzer);
}

View File

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

View File

@@ -5,50 +5,49 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CommandMustBeAnnotatedAnalyzer : AnalyzerBase
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CommandMustBeAnnotatedAnalyzer : AnalyzerBase
public CommandMustBeAnnotatedAnalyzer()
: base(
$"Commands must be annotated with `{SymbolNames.CliFxCommandAttribute}`",
$"This type must be annotated with `{SymbolNames.CliFxCommandAttribute}` in order to be a valid command.")
{
public CommandMustBeAnnotatedAnalyzer()
: base(
$"Commands must be annotated with `{SymbolNames.CliFxCommandAttribute}`",
$"This type must be annotated with `{SymbolNames.CliFxCommandAttribute}` in order to be a valid command.")
}
private void Analyze(
SyntaxNodeAnalysisContext context,
ClassDeclarationSyntax classDeclaration,
ITypeSymbol type)
{
// Ignore abstract classes, because they may be used to define
// base implementations for commands, in which case the command
// attribute doesn't make sense.
if (type.IsAbstract)
return;
var implementsCommandInterface = type
.AllInterfaces
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxCommandInterface));
var hasCommandAttribute = type
.GetAttributes()
.Select(a => a.AttributeClass)
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxCommandAttribute));
// If the interface is implemented, but the attribute is missing,
// then it's very likely a user error.
if (implementsCommandInterface && !hasCommandAttribute)
{
}
private void Analyze(
SyntaxNodeAnalysisContext context,
ClassDeclarationSyntax classDeclaration,
ITypeSymbol type)
{
// Ignore abstract classes, because they may be used to define
// base implementations for commands, in which case the command
// attribute doesn't make sense.
if (type.IsAbstract)
return;
var implementsCommandInterface = type
.AllInterfaces
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxCommandInterface));
var hasCommandAttribute = type
.GetAttributes()
.Select(a => a.AttributeClass)
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxCommandAttribute));
// If the interface is implemented, but the attribute is missing,
// then it's very likely a user error.
if (implementsCommandInterface && !hasCommandAttribute)
{
context.ReportDiagnostic(CreateDiagnostic(classDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandleClassDeclaration(Analyze);
context.ReportDiagnostic(CreateDiagnostic(classDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandleClassDeclaration(Analyze);
}
}

View File

@@ -5,44 +5,43 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CommandMustImplementInterfaceAnalyzer : AnalyzerBase
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CommandMustImplementInterfaceAnalyzer : AnalyzerBase
public CommandMustImplementInterfaceAnalyzer()
: base(
$"Commands must implement `{SymbolNames.CliFxCommandInterface}` interface",
$"This type must implement `{SymbolNames.CliFxCommandInterface}` interface in order to be a valid command.")
{
public CommandMustImplementInterfaceAnalyzer()
: base(
$"Commands must implement `{SymbolNames.CliFxCommandInterface}` interface",
$"This type must implement `{SymbolNames.CliFxCommandInterface}` interface in order to be a valid command.")
}
private void Analyze(
SyntaxNodeAnalysisContext context,
ClassDeclarationSyntax classDeclaration,
ITypeSymbol type)
{
var hasCommandAttribute = type
.GetAttributes()
.Select(a => a.AttributeClass)
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxCommandAttribute));
var implementsCommandInterface = type
.AllInterfaces
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxCommandInterface));
// If the attribute is present, but the interface is not implemented,
// it's very likely a user error.
if (hasCommandAttribute && !implementsCommandInterface)
{
}
private void Analyze(
SyntaxNodeAnalysisContext context,
ClassDeclarationSyntax classDeclaration,
ITypeSymbol type)
{
var hasCommandAttribute = type
.GetAttributes()
.Select(a => a.AttributeClass)
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxCommandAttribute));
var implementsCommandInterface = type
.AllInterfaces
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxCommandInterface));
// If the attribute is present, but the interface is not implemented,
// it's very likely a user error.
if (hasCommandAttribute && !implementsCommandInterface)
{
context.ReportDiagnostic(CreateDiagnostic(classDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandleClassDeclaration(Analyze);
context.ReportDiagnostic(CreateDiagnostic(classDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandleClassDeclaration(Analyze);
}
}

View File

@@ -3,81 +3,80 @@ using Microsoft.CodeAnalysis;
using System.Linq;
using CliFx.Analyzers.Utils.Extensions;
namespace CliFx.Analyzers.ObjectModel
namespace CliFx.Analyzers.ObjectModel;
internal partial class CommandOptionSymbol
{
internal partial class CommandOptionSymbol
public string? Name { get; }
public char? ShortName { get; }
public ITypeSymbol? ConverterType { get; }
public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; }
public CommandOptionSymbol(
string? name,
char? shortName,
ITypeSymbol? converterType,
IReadOnlyList<ITypeSymbol> validatorTypes)
{
public string? Name { get; }
Name = name;
ShortName = shortName;
ConverterType = converterType;
ValidatorTypes = validatorTypes;
}
}
public char? ShortName { get; }
internal partial class CommandOptionSymbol
{
private static AttributeData? TryGetOptionAttribute(IPropertySymbol property) =>
property
.GetAttributes()
.FirstOrDefault(a => a.AttributeClass.DisplayNameMatches(SymbolNames.CliFxCommandOptionAttribute));
public ITypeSymbol? ConverterType { get; }
private static CommandOptionSymbol FromAttribute(AttributeData attribute)
{
var name = attribute
.ConstructorArguments
.Where(a => a.Type.DisplayNameMatches("string") || a.Type.DisplayNameMatches("System.String"))
.Select(a => a.Value)
.FirstOrDefault() as string;
public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; }
var shortName = attribute
.ConstructorArguments
.Where(a => a.Type.DisplayNameMatches("char") || a.Type.DisplayNameMatches("System.Char"))
.Select(a => a.Value)
.FirstOrDefault() as char?;
public CommandOptionSymbol(
string? name,
char? shortName,
ITypeSymbol? converterType,
IReadOnlyList<ITypeSymbol> validatorTypes)
{
Name = name;
ShortName = shortName;
ConverterType = converterType;
ValidatorTypes = validatorTypes;
}
var converter = attribute
.NamedArguments
.Where(a => a.Key == "Converter")
.Select(a => a.Value.Value)
.Cast<ITypeSymbol?>()
.FirstOrDefault();
var validators = attribute
.NamedArguments
.Where(a => a.Key == "Validators")
.SelectMany(a => a.Value.Values)
.Select(c => c.Value)
.Cast<ITypeSymbol>()
.ToArray();
return new CommandOptionSymbol(name, shortName, converter, validators);
}
internal partial class CommandOptionSymbol
public static CommandOptionSymbol? TryResolve(IPropertySymbol property)
{
private static AttributeData? TryGetOptionAttribute(IPropertySymbol property) =>
property
.GetAttributes()
.FirstOrDefault(a => a.AttributeClass.DisplayNameMatches(SymbolNames.CliFxCommandOptionAttribute));
var attribute = TryGetOptionAttribute(property);
private static CommandOptionSymbol FromAttribute(AttributeData attribute)
{
var name = attribute
.ConstructorArguments
.Where(a => a.Type.DisplayNameMatches("string") || a.Type.DisplayNameMatches("System.String"))
.Select(a => a.Value)
.FirstOrDefault() as string;
if (attribute is null)
return null;
var shortName = attribute
.ConstructorArguments
.Where(a => a.Type.DisplayNameMatches("char") || a.Type.DisplayNameMatches("System.Char"))
.Select(a => a.Value)
.FirstOrDefault() as char?;
var converter = attribute
.NamedArguments
.Where(a => a.Key == "Converter")
.Select(a => a.Value.Value)
.Cast<ITypeSymbol?>()
.FirstOrDefault();
var validators = attribute
.NamedArguments
.Where(a => a.Key == "Validators")
.SelectMany(a => a.Value.Values)
.Select(c => c.Value)
.Cast<ITypeSymbol>()
.ToArray();
return new CommandOptionSymbol(name, shortName, converter, validators);
}
public static CommandOptionSymbol? TryResolve(IPropertySymbol property)
{
var attribute = TryGetOptionAttribute(property);
if (attribute is null)
return null;
return FromAttribute(attribute);
}
public static bool IsOptionProperty(IPropertySymbol property) =>
TryGetOptionAttribute(property) is not null;
return FromAttribute(attribute);
}
public static bool IsOptionProperty(IPropertySymbol property) =>
TryGetOptionAttribute(property) is not null;
}

View File

@@ -3,80 +3,79 @@ using System.Linq;
using CliFx.Analyzers.Utils.Extensions;
using Microsoft.CodeAnalysis;
namespace CliFx.Analyzers.ObjectModel
namespace CliFx.Analyzers.ObjectModel;
internal partial class CommandParameterSymbol
{
internal partial class CommandParameterSymbol
public int Order { get; }
public string? Name { get; }
public ITypeSymbol? ConverterType { get; }
public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; }
public CommandParameterSymbol(
int order,
string? name,
ITypeSymbol? converterType,
IReadOnlyList<ITypeSymbol> validatorTypes)
{
public int Order { get; }
Order = order;
Name = name;
ConverterType = converterType;
ValidatorTypes = validatorTypes;
}
}
public string? Name { get; }
internal partial class CommandParameterSymbol
{
private static AttributeData? TryGetParameterAttribute(IPropertySymbol property) =>
property
.GetAttributes()
.FirstOrDefault(a => a.AttributeClass.DisplayNameMatches(SymbolNames.CliFxCommandParameterAttribute));
public ITypeSymbol? ConverterType { get; }
private static CommandParameterSymbol FromAttribute(AttributeData attribute)
{
var order = (int) attribute
.ConstructorArguments
.Select(a => a.Value)
.First()!;
public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; }
var name = attribute
.NamedArguments
.Where(a => a.Key == "Name")
.Select(a => a.Value.Value)
.FirstOrDefault() as string;
public CommandParameterSymbol(
int order,
string? name,
ITypeSymbol? converterType,
IReadOnlyList<ITypeSymbol> validatorTypes)
{
Order = order;
Name = name;
ConverterType = converterType;
ValidatorTypes = validatorTypes;
}
var converter = attribute
.NamedArguments
.Where(a => a.Key == "Converter")
.Select(a => a.Value.Value)
.Cast<ITypeSymbol?>()
.FirstOrDefault();
var validators = attribute
.NamedArguments
.Where(a => a.Key == "Validators")
.SelectMany(a => a.Value.Values)
.Select(c => c.Value)
.Cast<ITypeSymbol>()
.ToArray();
return new CommandParameterSymbol(order, name, converter, validators);
}
internal partial class CommandParameterSymbol
public static CommandParameterSymbol? TryResolve(IPropertySymbol property)
{
private static AttributeData? TryGetParameterAttribute(IPropertySymbol property) =>
property
.GetAttributes()
.FirstOrDefault(a => a.AttributeClass.DisplayNameMatches(SymbolNames.CliFxCommandParameterAttribute));
var attribute = TryGetParameterAttribute(property);
private static CommandParameterSymbol FromAttribute(AttributeData attribute)
{
var order = (int) attribute
.ConstructorArguments
.Select(a => a.Value)
.First()!;
if (attribute is null)
return null;
var name = attribute
.NamedArguments
.Where(a => a.Key == "Name")
.Select(a => a.Value.Value)
.FirstOrDefault() as string;
var converter = attribute
.NamedArguments
.Where(a => a.Key == "Converter")
.Select(a => a.Value.Value)
.Cast<ITypeSymbol?>()
.FirstOrDefault();
var validators = attribute
.NamedArguments
.Where(a => a.Key == "Validators")
.SelectMany(a => a.Value.Values)
.Select(c => c.Value)
.Cast<ITypeSymbol>()
.ToArray();
return new CommandParameterSymbol(order, name, converter, validators);
}
public static CommandParameterSymbol? TryResolve(IPropertySymbol property)
{
var attribute = TryGetParameterAttribute(property);
if (attribute is null)
return null;
return FromAttribute(attribute);
}
public static bool IsParameterProperty(IPropertySymbol property) =>
TryGetParameterAttribute(property) is not null;
return FromAttribute(attribute);
}
public static bool IsParameterProperty(IPropertySymbol property) =>
TryGetParameterAttribute(property) is not null;
}

View File

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

View File

@@ -5,47 +5,46 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustBeInsideCommandAnalyzer : AnalyzerBase
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustBeInsideCommandAnalyzer : AnalyzerBase
public OptionMustBeInsideCommandAnalyzer()
: base(
"Options must be defined inside commands",
$"This option must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`.")
{
public OptionMustBeInsideCommandAnalyzer()
: base(
"Options must be defined inside commands",
$"This option must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`.")
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (property.ContainingType.IsAbstract)
return;
if (!CommandOptionSymbol.IsOptionProperty(property))
return;
var isInsideCommand = property
.ContainingType
.AllInterfaces
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxCommandInterface));
if (!isInsideCommand)
{
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (property.ContainingType.IsAbstract)
return;
if (!CommandOptionSymbol.IsOptionProperty(property))
return;
var isInsideCommand = property
.ContainingType
.AllInterfaces
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxCommandInterface));
if (!isInsideCommand)
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
}

View File

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

View File

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

View File

@@ -5,60 +5,59 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveUniqueShortNameAnalyzer : AnalyzerBase
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveUniqueShortNameAnalyzer : AnalyzerBase
public OptionMustHaveUniqueShortNameAnalyzer()
: base(
"Options must have unique short names",
"This option's short name must be unique within the command (comparison IS case sensitive).")
{
public OptionMustHaveUniqueShortNameAnalyzer()
: base(
"Options must have unique short names",
"This option's short name must be unique within the command (comparison IS case sensitive).")
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
var option = CommandOptionSymbol.TryResolve(property);
if (option is null)
return;
if (option.ShortName is null)
return;
var otherProperties = property
.ContainingType
.GetMembers()
.OfType<IPropertySymbol>()
.Where(m => !m.Equals(property, SymbolEqualityComparer.Default))
.ToArray();
foreach (var otherProperty in otherProperties)
{
}
var otherOption = CommandOptionSymbol.TryResolve(otherProperty);
if (otherOption is null)
continue;
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (otherOption.ShortName is null)
continue;
var option = CommandOptionSymbol.TryResolve(property);
if (option is null)
return;
if (option.ShortName is null)
return;
var otherProperties = property
.ContainingType
.GetMembers()
.OfType<IPropertySymbol>()
.Where(m => !m.Equals(property, SymbolEqualityComparer.Default))
.ToArray();
foreach (var otherProperty in otherProperties)
if (option.ShortName == otherOption.ShortName)
{
var otherOption = CommandOptionSymbol.TryResolve(otherProperty);
if (otherOption is null)
continue;
if (otherOption.ShortName is null)
continue;
if (option.ShortName == otherOption.ShortName)
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
}

View File

@@ -5,46 +5,45 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveValidConverterAnalyzer : AnalyzerBase
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveValidConverterAnalyzer : AnalyzerBase
public OptionMustHaveValidConverterAnalyzer()
: base(
$"Option converters must derive from `{SymbolNames.CliFxBindingConverterClass}`",
$"Converter specified for this option must derive from `{SymbolNames.CliFxBindingConverterClass}`.")
{
public OptionMustHaveValidConverterAnalyzer()
: base(
$"Option converters must derive from `{SymbolNames.CliFxBindingConverterClass}`",
$"Converter specified for this option must derive from `{SymbolNames.CliFxBindingConverterClass}`.")
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
var option = CommandOptionSymbol.TryResolve(property);
if (option is null)
return;
if (option.ConverterType is null)
return;
// We check against an internal interface because checking against a generic class is a pain
var converterImplementsInterface = option
.ConverterType
.AllInterfaces
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxBindingConverterInterface));
if (!converterImplementsInterface)
{
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
var option = CommandOptionSymbol.TryResolve(property);
if (option is null)
return;
if (option.ConverterType is null)
return;
// We check against an internal interface because checking against a generic class is a pain
var converterImplementsInterface = option
.ConverterType
.AllInterfaces
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxBindingConverterInterface));
if (!converterImplementsInterface)
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
}

View File

@@ -4,40 +4,39 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveValidNameAnalyzer : AnalyzerBase
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveValidNameAnalyzer : AnalyzerBase
public OptionMustHaveValidNameAnalyzer()
: base(
"Options must have valid names",
"This option's name must be at least 2 characters long and must start with a letter.")
{
public OptionMustHaveValidNameAnalyzer()
: base(
"Options must have valid names",
"This option's name must be at least 2 characters long and must start with a letter.")
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
var option = CommandOptionSymbol.TryResolve(property);
if (option is null)
return;
if (string.IsNullOrWhiteSpace(option.Name))
return;
if (option.Name.Length < 2 || !char.IsLetter(option.Name[0]))
{
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
var option = CommandOptionSymbol.TryResolve(property);
if (option is null)
return;
if (string.IsNullOrWhiteSpace(option.Name))
return;
if (option.Name.Length < 2 || !char.IsLetter(option.Name[0]))
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
}

View File

@@ -4,40 +4,39 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveValidShortNameAnalyzer : AnalyzerBase
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OptionMustHaveValidShortNameAnalyzer : AnalyzerBase
public OptionMustHaveValidShortNameAnalyzer()
: base(
"Option short names must be letter characters",
"This option's short name must be a single letter character.")
{
public OptionMustHaveValidShortNameAnalyzer()
: base(
"Option short names must be letter characters",
"This option's short name must be a single letter character.")
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
var option = CommandOptionSymbol.TryResolve(property);
if (option is null)
return;
if (option.ShortName is null)
return;
if (!char.IsLetter(option.ShortName.Value))
{
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
var option = CommandOptionSymbol.TryResolve(property);
if (option is null)
return;
if (option.ShortName is null)
return;
if (!char.IsLetter(option.ShortName.Value))
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
}

View File

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

View File

@@ -5,47 +5,46 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustBeInsideCommandAnalyzer : AnalyzerBase
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustBeInsideCommandAnalyzer : AnalyzerBase
public ParameterMustBeInsideCommandAnalyzer()
: base(
"Parameters must be defined inside commands",
$"This parameter must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`.")
{
public ParameterMustBeInsideCommandAnalyzer()
: base(
"Parameters must be defined inside commands",
$"This parameter must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`.")
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (property.ContainingType.IsAbstract)
return;
if (!CommandParameterSymbol.IsParameterProperty(property))
return;
var isInsideCommand = property
.ContainingType
.AllInterfaces
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxCommandInterface));
if (!isInsideCommand)
{
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (property.ContainingType.IsAbstract)
return;
if (!CommandParameterSymbol.IsParameterProperty(property))
return;
var isInsideCommand = property
.ContainingType
.AllInterfaces
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxCommandInterface));
if (!isInsideCommand)
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
}

View File

@@ -5,64 +5,63 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustBeLastIfNonScalarAnalyzer : AnalyzerBase
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustBeLastIfNonScalarAnalyzer : AnalyzerBase
public ParameterMustBeLastIfNonScalarAnalyzer()
: base(
"Parameters of non-scalar types must be last in order",
"This parameter has a non-scalar type so it must be last in order (its order must be highest within the command).")
{
public ParameterMustBeLastIfNonScalarAnalyzer()
: base(
"Parameters of non-scalar types must be last in order",
"This parameter has a non-scalar type so it must be last in order (its order must be highest within the command).")
}
private static bool IsScalar(ITypeSymbol type) =>
type.DisplayNameMatches("string") ||
type.DisplayNameMatches("System.String") ||
!type.AllInterfaces
.Select(i => i.ConstructedFrom)
.Any(s => s.DisplayNameMatches("System.Collections.Generic.IEnumerable<T>"));
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (IsScalar(property.Type))
return;
var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null)
return;
var otherProperties = property
.ContainingType
.GetMembers()
.OfType<IPropertySymbol>()
.Where(m => !m.Equals(property, SymbolEqualityComparer.Default))
.ToArray();
foreach (var otherProperty in otherProperties)
{
}
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
if (otherParameter is null)
continue;
private static bool IsScalar(ITypeSymbol type) =>
type.DisplayNameMatches("string") ||
type.DisplayNameMatches("System.String") ||
!type.AllInterfaces
.Select(i => i.ConstructedFrom)
.Any(s => s.DisplayNameMatches("System.Collections.Generic.IEnumerable<T>"));
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (IsScalar(property.Type))
return;
var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null)
return;
var otherProperties = property
.ContainingType
.GetMembers()
.OfType<IPropertySymbol>()
.Where(m => !m.Equals(property, SymbolEqualityComparer.Default))
.ToArray();
foreach (var otherProperty in otherProperties)
if (otherParameter.Order > parameter.Order)
{
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
if (otherParameter is null)
continue;
if (otherParameter.Order > parameter.Order)
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
}

View File

@@ -5,62 +5,61 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustBeSingleIfNonScalarAnalyzer : AnalyzerBase
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustBeSingleIfNonScalarAnalyzer : AnalyzerBase
public ParameterMustBeSingleIfNonScalarAnalyzer()
: base(
"Parameters of non-scalar types are limited to one per command",
"This parameter has a non-scalar type so it must be the only such parameter in the command.")
{
public ParameterMustBeSingleIfNonScalarAnalyzer()
: base(
"Parameters of non-scalar types are limited to one per command",
"This parameter has a non-scalar type so it must be the only such parameter in the command.")
}
private static bool IsScalar(ITypeSymbol type) =>
type.DisplayNameMatches("string") ||
type.DisplayNameMatches("System.String") ||
!type.AllInterfaces
.Select(i => i.ConstructedFrom)
.Any(s => s.DisplayNameMatches("System.Collections.Generic.IEnumerable<T>"));
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (!CommandParameterSymbol.IsParameterProperty(property))
return;
if (IsScalar(property.Type))
return;
var otherProperties = property
.ContainingType
.GetMembers()
.OfType<IPropertySymbol>()
.Where(m => !m.Equals(property, SymbolEqualityComparer.Default))
.ToArray();
foreach (var otherProperty in otherProperties)
{
}
if (!CommandParameterSymbol.IsParameterProperty(otherProperty))
continue;
private static bool IsScalar(ITypeSymbol type) =>
type.DisplayNameMatches("string") ||
type.DisplayNameMatches("System.String") ||
!type.AllInterfaces
.Select(i => i.ConstructedFrom)
.Any(s => s.DisplayNameMatches("System.Collections.Generic.IEnumerable<T>"));
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (!CommandParameterSymbol.IsParameterProperty(property))
return;
if (IsScalar(property.Type))
return;
var otherProperties = property
.ContainingType
.GetMembers()
.OfType<IPropertySymbol>()
.Where(m => !m.Equals(property, SymbolEqualityComparer.Default))
.ToArray();
foreach (var otherProperty in otherProperties)
if (!IsScalar(otherProperty.Type))
{
if (!CommandParameterSymbol.IsParameterProperty(otherProperty))
continue;
if (!IsScalar(otherProperty.Type))
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
}

View File

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

View File

@@ -5,54 +5,53 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustHaveUniqueOrderAnalyzer : AnalyzerBase
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustHaveUniqueOrderAnalyzer : AnalyzerBase
public ParameterMustHaveUniqueOrderAnalyzer()
: base(
"Parameters must have unique order",
"This parameter's order must be unique within the command.")
{
public ParameterMustHaveUniqueOrderAnalyzer()
: base(
"Parameters must have unique order",
"This parameter's order must be unique within the command.")
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null)
return;
var otherProperties = property
.ContainingType
.GetMembers()
.OfType<IPropertySymbol>()
.Where(m => !m.Equals(property, SymbolEqualityComparer.Default))
.ToArray();
foreach (var otherProperty in otherProperties)
{
}
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
if (otherParameter is null)
continue;
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null)
return;
var otherProperties = property
.ContainingType
.GetMembers()
.OfType<IPropertySymbol>()
.Where(m => !m.Equals(property, SymbolEqualityComparer.Default))
.ToArray();
foreach (var otherProperty in otherProperties)
if (parameter.Order == otherParameter.Order)
{
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
if (otherParameter is null)
continue;
if (parameter.Order == otherParameter.Order)
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
}

View File

@@ -5,46 +5,45 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustHaveValidConverterAnalyzer : AnalyzerBase
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustHaveValidConverterAnalyzer : AnalyzerBase
public ParameterMustHaveValidConverterAnalyzer()
: base(
$"Parameter converters must derive from `{SymbolNames.CliFxBindingConverterClass}`",
$"Converter specified for this parameter must derive from `{SymbolNames.CliFxBindingConverterClass}`.")
{
public ParameterMustHaveValidConverterAnalyzer()
: base(
$"Parameter converters must derive from `{SymbolNames.CliFxBindingConverterClass}`",
$"Converter specified for this parameter must derive from `{SymbolNames.CliFxBindingConverterClass}`.")
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null)
return;
if (parameter.ConverterType is null)
return;
// We check against an internal interface because checking against a generic class is a pain
var converterImplementsInterface = parameter
.ConverterType
.AllInterfaces
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxBindingConverterInterface));
if (!converterImplementsInterface)
{
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null)
return;
if (parameter.ConverterType is null)
return;
// We check against an internal interface because checking against a generic class is a pain
var converterImplementsInterface = parameter
.ConverterType
.AllInterfaces
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxBindingConverterInterface));
if (!converterImplementsInterface)
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
}

View File

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

View File

@@ -6,73 +6,72 @@ using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class SystemConsoleShouldBeAvoidedAnalyzer : AnalyzerBase
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class SystemConsoleShouldBeAvoidedAnalyzer : AnalyzerBase
public SystemConsoleShouldBeAvoidedAnalyzer()
: base(
$"Avoid calling `System.Console` where `{SymbolNames.CliFxConsoleInterface}` is available",
$"Use the provided `{SymbolNames.CliFxConsoleInterface}` abstraction instead of `System.Console` to ensure that the command can be tested in isolation.",
DiagnosticSeverity.Warning)
{
public SystemConsoleShouldBeAvoidedAnalyzer()
: base(
$"Avoid calling `System.Console` where `{SymbolNames.CliFxConsoleInterface}` is available",
$"Use the provided `{SymbolNames.CliFxConsoleInterface}` abstraction instead of `System.Console` to ensure that the command can be tested in isolation.",
DiagnosticSeverity.Warning)
{
}
}
private MemberAccessExpressionSyntax? TryGetSystemConsoleMemberAccess(
SyntaxNodeAnalysisContext context,
SyntaxNode node)
{
var currentNode = node;
private MemberAccessExpressionSyntax? TryGetSystemConsoleMemberAccess(
SyntaxNodeAnalysisContext context,
SyntaxNode node)
{
var currentNode = node;
while (currentNode is MemberAccessExpressionSyntax memberAccess)
while (currentNode is MemberAccessExpressionSyntax memberAccess)
{
var member = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
if (member?.ContainingType?.DisplayNameMatches("System.Console") == true)
{
var member = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
if (member?.ContainingType?.DisplayNameMatches("System.Console") == true)
{
return memberAccess;
}
// Get inner expression, which may be another member access expression.
// Example: System.Console.Error
// ~~~~~~~~~~~~~~ <- inner member access expression
// -------------------- <- outer member access expression
currentNode = memberAccess.Expression;
return memberAccess;
}
return null;
// Get inner expression, which may be another member access expression.
// Example: System.Console.Error
// ~~~~~~~~~~~~~~ <- inner member access expression
// -------------------- <- outer member access expression
currentNode = memberAccess.Expression;
}
private void Analyze(SyntaxNodeAnalysisContext context)
return null;
}
private void Analyze(SyntaxNodeAnalysisContext context)
{
// Try to get a member access on System.Console in the current expression,
// or in any of its inner expressions.
var systemConsoleMemberAccess = TryGetSystemConsoleMemberAccess(context, context.Node);
if (systemConsoleMemberAccess is null)
return;
// Check if IConsole is available in scope as an alternative to System.Console
var isConsoleInterfaceAvailable = context
.Node
.Ancestors()
.OfType<MethodDeclarationSyntax>()
.SelectMany(m => m.ParameterList.Parameters)
.Select(p => p.Type)
.Select(t => context.SemanticModel.GetSymbolInfo(t).Symbol)
.Where(s => s is not null)
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxConsoleInterface));
if (isConsoleInterfaceAvailable)
{
// Try to get a member access on System.Console in the current expression,
// or in any of its inner expressions.
var systemConsoleMemberAccess = TryGetSystemConsoleMemberAccess(context, context.Node);
if (systemConsoleMemberAccess is null)
return;
// Check if IConsole is available in scope as an alternative to System.Console
var isConsoleInterfaceAvailable = context
.Node
.Ancestors()
.OfType<MethodDeclarationSyntax>()
.SelectMany(m => m.ParameterList.Parameters)
.Select(p => p.Type)
.Select(t => context.SemanticModel.GetSymbolInfo(t).Symbol)
.Where(s => s is not null)
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxConsoleInterface));
if (isConsoleInterfaceAvailable)
{
context.ReportDiagnostic(CreateDiagnostic(systemConsoleMemberAccess.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.SimpleMemberAccessExpression);
context.ReportDiagnostic(CreateDiagnostic(systemConsoleMemberAccess.GetLocation()));
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.SimpleMemberAccessExpression);
}
}

View File

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

View File

@@ -1,18 +1,17 @@
using System;
namespace CliFx.Analyzers.Utils.Extensions
{
internal static class StringExtensions
{
public static string TrimEnd(
this string str,
string sub,
StringComparison comparison = StringComparison.Ordinal)
{
while (str.EndsWith(sub, comparison))
str = str.Substring(0, str.Length - sub.Length);
namespace CliFx.Analyzers.Utils.Extensions;
return str;
}
internal static class StringExtensions
{
public static string TrimEnd(
this string str,
string sub,
StringComparison comparison = StringComparison.Ordinal)
{
while (str.EndsWith(sub, comparison))
str = str.Substring(0, str.Length - sub.Length);
return str;
}
}

View File

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

View File

@@ -1,27 +1,26 @@
using BenchmarkDotNet.Attributes;
using clipr;
namespace CliFx.Benchmarks
namespace CliFx.Benchmarks;
public partial class Benchmarks
{
public partial class Benchmarks
public class CliprCommand
{
public class CliprCommand
[NamedArgument('s', "str")]
public string? StrOption { get; set; }
[NamedArgument('i', "int")]
public int IntOption { get; set; }
[NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)]
public bool BoolOption { get; set; }
public void Execute()
{
[NamedArgument('s', "str")]
public string? StrOption { get; set; }
[NamedArgument('i', "int")]
public int IntOption { get; set; }
[NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)]
public bool BoolOption { get; set; }
public void Execute()
{
}
}
[Benchmark(Description = "Clipr")]
public void ExecuteWithClipr() => CliParser.Parse<CliprCommand>(Arguments).Execute();
}
[Benchmark(Description = "Clipr")]
public void ExecuteWithClipr() => CliParser.Parse<CliprCommand>(Arguments).Execute();
}

View File

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

View File

@@ -1,30 +1,29 @@
using BenchmarkDotNet.Attributes;
using CommandLine;
namespace CliFx.Benchmarks
namespace CliFx.Benchmarks;
public partial class Benchmarks
{
public partial class Benchmarks
public class CommandLineParserCommand
{
public class CommandLineParserCommand
[Option('s', "str")]
public string? StrOption { get; set; }
[Option('i', "int")]
public int IntOption { get; set; }
[Option('b', "bool")]
public bool BoolOption { get; set; }
public void Execute()
{
[Option('s', "str")]
public string? StrOption { get; set; }
[Option('i', "int")]
public int IntOption { get; set; }
[Option('b', "bool")]
public bool BoolOption { get; set; }
public void Execute()
{
}
}
[Benchmark(Description = "CommandLineParser")]
public void ExecuteWithCommandLineParser() =>
new Parser()
.ParseArguments(Arguments, typeof(CommandLineParserCommand))
.WithParsed<CommandLineParserCommand>(c => c.Execute());
}
[Benchmark(Description = "CommandLineParser")]
public void ExecuteWithCommandLineParser() =>
new Parser()
.ParseArguments(Arguments, typeof(CommandLineParserCommand))
.WithParsed<CommandLineParserCommand>(c => c.Execute());
}

View File

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

View File

@@ -1,27 +1,26 @@
using BenchmarkDotNet.Attributes;
using PowerArgs;
namespace CliFx.Benchmarks
namespace CliFx.Benchmarks;
public partial class Benchmarks
{
public partial class Benchmarks
public class PowerArgsCommand
{
public class PowerArgsCommand
[ArgShortcut("--str"), ArgShortcut("-s")]
public string? StrOption { get; set; }
[ArgShortcut("--int"), ArgShortcut("-i")]
public int IntOption { get; set; }
[ArgShortcut("--bool"), ArgShortcut("-b")]
public bool BoolOption { get; set; }
public void Main()
{
[ArgShortcut("--str"), ArgShortcut("-s")]
public string? StrOption { get; set; }
[ArgShortcut("--int"), ArgShortcut("-i")]
public int IntOption { get; set; }
[ArgShortcut("--bool"), ArgShortcut("-b")]
public bool BoolOption { get; set; }
public void Main()
{
}
}
[Benchmark(Description = "PowerArgs")]
public void ExecuteWithPowerArgs() => Args.InvokeMain<PowerArgsCommand>(Arguments);
}
[Benchmark(Description = "PowerArgs")]
public void ExecuteWithPowerArgs() => Args.InvokeMain<PowerArgsCommand>(Arguments);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,35 +2,34 @@
using System.Collections.Generic;
using System.Linq;
namespace CliFx.Demo.Domain
namespace CliFx.Demo.Domain;
public partial class Library
{
public partial class Library
public IReadOnlyList<Book> Books { get; }
public Library(IReadOnlyList<Book> books)
{
public IReadOnlyList<Book> Books { get; }
public Library(IReadOnlyList<Book> books)
{
Books = books;
}
public Library WithBook(Book book)
{
var books = Books.ToList();
books.Add(book);
return new Library(books);
}
public Library WithoutBook(Book book)
{
var books = Books.Where(b => b != book).ToArray();
return new Library(books);
}
Books = books;
}
public partial class Library
public Library WithBook(Book book)
{
public static Library Empty { get; } = new(Array.Empty<Book>());
var books = Books.ToList();
books.Add(book);
return new Library(books);
}
public Library WithoutBook(Book book)
{
var books = Books.Where(b => b != book).ToArray();
return new Library(books);
}
}
public partial class Library
{
public static Library Empty { get; } = new(Array.Empty<Book>());
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,129 +8,128 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
namespace CliFx.Tests.Utils
namespace CliFx.Tests.Utils;
// This class uses Roslyn to compile commands dynamically.
//
// It allows us to collocate commands with tests more
// easily, which helps a lot when reasoning about them.
// Unfortunately, this comes at a cost of static typing,
// but this is still a worthwhile trade off.
//
// Maybe one day C# will allow declaring classes inside
// methods and doing this will no longer be necessary.
// Language proposal: https://github.com/dotnet/csharplang/discussions/130
internal static class DynamicCommandBuilder
{
// This class uses Roslyn to compile commands dynamically.
//
// It allows us to collocate commands with tests more
// easily, which helps a lot when reasoning about them.
// Unfortunately, this comes at a cost of static typing,
// but this is still a worthwhile trade off.
//
// Maybe one day C# will allow declaring classes inside
// methods and doing this will no longer be necessary.
// Language proposal: https://github.com/dotnet/csharplang/discussions/130
internal static class DynamicCommandBuilder
public static IReadOnlyList<Type> CompileMany(string sourceCode)
{
public static IReadOnlyList<Type> CompileMany(string sourceCode)
// Get default system namespaces
var defaultSystemNamespaces = new[]
{
// Get default system namespaces
var defaultSystemNamespaces = new[]
{
"System",
"System.Collections",
"System.Collections.Generic",
"System.Linq",
"System.Threading.Tasks",
"System.Globalization"
};
"System",
"System.Collections",
"System.Collections.Generic",
"System.Linq",
"System.Threading.Tasks",
"System.Globalization"
};
// Get default CliFx namespaces
var defaultCliFxNamespaces = typeof(ICommand)
.Assembly
.GetTypes()
.Where(t => t.IsPublic)
.Select(t => t.Namespace)
.Distinct()
.ToArray();
// Get default CliFx namespaces
var defaultCliFxNamespaces = typeof(ICommand)
.Assembly
.GetTypes()
.Where(t => t.IsPublic)
.Select(t => t.Namespace)
.Distinct()
.ToArray();
// Append default imports to the source code
var sourceCodeWithUsings =
string.Join(Environment.NewLine, defaultSystemNamespaces.Select(n => $"using {n};")) +
string.Join(Environment.NewLine, defaultCliFxNamespaces.Select(n => $"using {n};")) +
// Append default imports to the source code
var sourceCodeWithUsings =
string.Join(Environment.NewLine, defaultSystemNamespaces.Select(n => $"using {n};")) +
string.Join(Environment.NewLine, defaultCliFxNamespaces.Select(n => $"using {n};")) +
Environment.NewLine +
sourceCode;
// Parse the source code
var ast = SyntaxFactory.ParseSyntaxTree(
SourceText.From(sourceCodeWithUsings),
CSharpParseOptions.Default
);
// Compile the code to IL
var compilation = CSharpCompilation.Create(
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
new[] {ast},
ReferenceAssemblies.Net50
.Append(MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location))
.Append(MetadataReference.CreateFromFile(typeof(DynamicCommandBuilder).Assembly.Location)),
// DLL to avoid having to define the Main() method
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);
var compilationErrors = compilation
.GetDiagnostics()
.Where(d => d.Severity >= DiagnosticSeverity.Error)
.ToArray();
if (compilationErrors.Any())
{
throw new InvalidOperationException(
"Failed to compile code." +
Environment.NewLine +
sourceCode;
// Parse the source code
var ast = SyntaxFactory.ParseSyntaxTree(
SourceText.From(sourceCodeWithUsings),
CSharpParseOptions.Default
string.Join(Environment.NewLine, compilationErrors.Select(e => e.ToString()))
);
// Compile the code to IL
var compilation = CSharpCompilation.Create(
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
new[] {ast},
ReferenceAssemblies.Net50
.Append(MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location))
.Append(MetadataReference.CreateFromFile(typeof(DynamicCommandBuilder).Assembly.Location)),
// DLL to avoid having to define the Main() method
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);
var compilationErrors = compilation
.GetDiagnostics()
.Where(d => d.Severity >= DiagnosticSeverity.Error)
.ToArray();
if (compilationErrors.Any())
{
throw new InvalidOperationException(
"Failed to compile code." +
Environment.NewLine +
string.Join(Environment.NewLine, compilationErrors.Select(e => e.ToString()))
);
}
// Emit the code to an in-memory buffer
using var buffer = new MemoryStream();
var emit = compilation.Emit(buffer);
var emitErrors = emit
.Diagnostics
.Where(d => d.Severity >= DiagnosticSeverity.Error)
.ToArray();
if (emitErrors.Any())
{
throw new InvalidOperationException(
"Failed to emit code." +
Environment.NewLine +
string.Join(Environment.NewLine, emitErrors.Select(e => e.ToString()))
);
}
// Load the generated assembly
var generatedAssembly = Assembly.Load(buffer.ToArray());
// Return all defined commands
var commandTypes = generatedAssembly
.GetTypes()
.Where(t => t.IsAssignableTo(typeof(ICommand)))
.ToArray();
if (commandTypes.Length <= 0)
{
throw new InvalidOperationException(
"There are no command definitions in the provide source code."
);
}
return commandTypes;
}
public static Type Compile(string sourceCode)
// Emit the code to an in-memory buffer
using var buffer = new MemoryStream();
var emit = compilation.Emit(buffer);
var emitErrors = emit
.Diagnostics
.Where(d => d.Severity >= DiagnosticSeverity.Error)
.ToArray();
if (emitErrors.Any())
{
var commandTypes = CompileMany(sourceCode);
if (commandTypes.Count > 1)
{
throw new InvalidOperationException(
"There are more than one command definitions in the provide source code."
);
}
return commandTypes.Single();
throw new InvalidOperationException(
"Failed to emit code." +
Environment.NewLine +
string.Join(Environment.NewLine, emitErrors.Select(e => e.ToString()))
);
}
// Load the generated assembly
var generatedAssembly = Assembly.Load(buffer.ToArray());
// Return all defined commands
var commandTypes = generatedAssembly
.GetTypes()
.Where(t => t.IsAssignableTo(typeof(ICommand)))
.ToArray();
if (commandTypes.Length <= 0)
{
throw new InvalidOperationException(
"There are no command definitions in the provide source code."
);
}
return commandTypes;
}
public static Type Compile(string sourceCode)
{
var commandTypes = CompileMany(sourceCode);
if (commandTypes.Count > 1)
{
throw new InvalidOperationException(
"There are more than one command definitions in the provide source code."
);
}
return commandTypes.Single();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,231 +11,230 @@ using CliFx.Schema;
using CliFx.Utils;
using CliFx.Utils.Extensions;
namespace CliFx
namespace CliFx;
/// <summary>
/// Command line application facade.
/// </summary>
public class CliApplication
{
/// <summary>
/// Command line application facade.
/// Application metadata.
/// </summary>
public class CliApplication
public ApplicationMetadata Metadata { get; }
/// <summary>
/// Application configuration.
/// </summary>
public ApplicationConfiguration Configuration { get; }
private readonly IConsole _console;
private readonly ITypeActivator _typeActivator;
private readonly CommandBinder _commandBinder;
/// <summary>
/// Initializes an instance of <see cref="CliApplication"/>.
/// </summary>
public CliApplication(
ApplicationMetadata metadata,
ApplicationConfiguration configuration,
IConsole console,
ITypeActivator typeActivator)
{
/// <summary>
/// Application metadata.
/// </summary>
public ApplicationMetadata Metadata { get; }
Metadata = metadata;
Configuration = configuration;
_console = console;
_typeActivator = typeActivator;
/// <summary>
/// Application configuration.
/// </summary>
public ApplicationConfiguration Configuration { get; }
_commandBinder = new CommandBinder(typeActivator);
}
private readonly IConsole _console;
private readonly ITypeActivator _typeActivator;
private bool IsDebugModeEnabled(CommandInput commandInput) =>
Configuration.IsDebugModeAllowed && commandInput.IsDebugDirectiveSpecified;
private readonly CommandBinder _commandBinder;
private bool IsPreviewModeEnabled(CommandInput commandInput) =>
Configuration.IsPreviewModeAllowed && commandInput.IsPreviewDirectiveSpecified;
/// <summary>
/// Initializes an instance of <see cref="CliApplication"/>.
/// </summary>
public CliApplication(
ApplicationMetadata metadata,
ApplicationConfiguration configuration,
IConsole console,
ITypeActivator typeActivator)
private bool ShouldShowHelpText(CommandSchema commandSchema, CommandInput commandInput) =>
commandSchema.IsHelpOptionAvailable && commandInput.IsHelpOptionSpecified ||
// Show help text also in case the fallback default command is
// executed without any arguments.
commandSchema == FallbackDefaultCommand.Schema &&
string.IsNullOrWhiteSpace(commandInput.CommandName) &&
!commandInput.Parameters.Any() &&
!commandInput.Options.Any();
private bool ShouldShowVersionText(CommandSchema commandSchema, CommandInput commandInput) =>
commandSchema.IsVersionOptionAvailable && commandInput.IsVersionOptionSpecified;
private async ValueTask PromptDebuggerAsync()
{
using (_console.WithForegroundColor(ConsoleColor.Green))
{
Metadata = metadata;
Configuration = configuration;
_console = console;
_typeActivator = typeActivator;
var processId = ProcessEx.GetCurrentProcessId();
_commandBinder = new CommandBinder(typeActivator);
_console.Output.WriteLine(
$"Attach debugger to PID {processId} to continue."
);
}
private bool IsDebugModeEnabled(CommandInput commandInput) =>
Configuration.IsDebugModeAllowed && commandInput.IsDebugDirectiveSpecified;
// Try to also launch debugger ourselves (only works if VS is installed)
Debugger.Launch();
private bool IsPreviewModeEnabled(CommandInput commandInput) =>
Configuration.IsPreviewModeAllowed && commandInput.IsPreviewDirectiveSpecified;
private bool ShouldShowHelpText(CommandSchema commandSchema, CommandInput commandInput) =>
commandSchema.IsHelpOptionAvailable && commandInput.IsHelpOptionSpecified ||
// Show help text also in case the fallback default command is
// executed without any arguments.
commandSchema == FallbackDefaultCommand.Schema &&
string.IsNullOrWhiteSpace(commandInput.CommandName) &&
!commandInput.Parameters.Any() &&
!commandInput.Options.Any();
private bool ShouldShowVersionText(CommandSchema commandSchema, CommandInput commandInput) =>
commandSchema.IsVersionOptionAvailable && commandInput.IsVersionOptionSpecified;
private async ValueTask PromptDebuggerAsync()
while (!Debugger.IsAttached)
{
using (_console.WithForegroundColor(ConsoleColor.Green))
{
var processId = ProcessEx.GetCurrentProcessId();
await Task.Delay(100);
}
}
_console.Output.WriteLine(
$"Attach debugger to PID {processId} to continue."
);
}
// Try to also launch debugger ourselves (only works if VS is installed)
Debugger.Launch();
while (!Debugger.IsAttached)
{
await Task.Delay(100);
}
private async ValueTask<int> RunAsync(ApplicationSchema applicationSchema, CommandInput commandInput)
{
// Handle debug directive
if (IsDebugModeEnabled(commandInput))
{
await PromptDebuggerAsync();
}
private async ValueTask<int> RunAsync(ApplicationSchema applicationSchema, CommandInput commandInput)
// Handle preview directive
if (IsPreviewModeEnabled(commandInput))
{
// Handle debug directive
if (IsDebugModeEnabled(commandInput))
_console.Output.WriteCommandInput(commandInput);
return 0;
}
// Try to get the command schema that matches the input
var commandSchema =
applicationSchema.TryFindCommand(commandInput.CommandName) ??
applicationSchema.TryFindDefaultCommand() ??
FallbackDefaultCommand.Schema;
// Activate command instance
var commandInstance = commandSchema == FallbackDefaultCommand.Schema
? new FallbackDefaultCommand() // bypass activator
: (ICommand) _typeActivator.CreateInstance(commandSchema.Type);
// Assemble help context
var helpContext = new HelpContext(
Metadata,
applicationSchema,
commandSchema,
commandSchema.GetValues(commandInstance)
);
// Handle help option
if (ShouldShowHelpText(commandSchema, commandInput))
{
_console.Output.WriteHelpText(helpContext);
return 0;
}
// Handle version option
if (ShouldShowVersionText(commandSchema, commandInput))
{
_console.Output.WriteLine(Metadata.Version);
return 0;
}
// Starting from this point, we may produce exceptions that are meant for the
// end user of the application (i.e. invalid input, command exception, etc).
// Catch these exceptions here, print them to the console, and don't let them
// propagate further.
try
{
// Bind and execute command
_commandBinder.Bind(commandInput, commandSchema, commandInstance);
await commandInstance.ExecuteAsync(_console);
return 0;
}
catch (CliFxException ex)
{
_console.Error.WriteException(ex);
if (ex.ShowHelp)
{
await PromptDebuggerAsync();
_console.Output.WriteLine();
_console.Output.WriteHelpText(helpContext);
}
// Handle preview directive
if (IsPreviewModeEnabled(commandInput))
{
_console.Output.WriteCommandInput(commandInput);
return 0;
}
return ex.ExitCode;
}
}
// Try to get the command schema that matches the input
var commandSchema =
applicationSchema.TryFindCommand(commandInput.CommandName) ??
applicationSchema.TryFindDefaultCommand() ??
FallbackDefaultCommand.Schema;
/// <summary>
/// Runs the application with the specified command line arguments and environment variables.
/// Returns an exit code which indicates whether the application completed successfully.
/// </summary>
/// <remarks>
/// When running WITHOUT debugger (i.e. in production), this method swallows all exceptions and
/// reports them to the console.
/// </remarks>
public async ValueTask<int> RunAsync(
IReadOnlyList<string> commandLineArguments,
IReadOnlyDictionary<string, string> environmentVariables)
{
try
{
// Console colors may have already been overriden by the parent process,
// so we need to reset it to make sure that everything we write looks properly.
_console.ResetColor();
// Activate command instance
var commandInstance = commandSchema == FallbackDefaultCommand.Schema
? new FallbackDefaultCommand() // bypass activator
: (ICommand) _typeActivator.CreateInstance(commandSchema.Type);
var applicationSchema = ApplicationSchema.Resolve(Configuration.CommandTypes);
// Assemble help context
var helpContext = new HelpContext(
Metadata,
applicationSchema,
commandSchema,
commandSchema.GetValues(commandInstance)
var commandInput = CommandInput.Parse(
commandLineArguments,
environmentVariables,
applicationSchema.GetCommandNames()
);
// Handle help option
if (ShouldShowHelpText(commandSchema, commandInput))
{
_console.Output.WriteHelpText(helpContext);
return 0;
}
// Handle version option
if (ShouldShowVersionText(commandSchema, commandInput))
{
_console.Output.WriteLine(Metadata.Version);
return 0;
}
// Starting from this point, we may produce exceptions that are meant for the
// end user of the application (i.e. invalid input, command exception, etc).
// Catch these exceptions here, print them to the console, and don't let them
// propagate further.
try
{
// Bind and execute command
_commandBinder.Bind(commandInput, commandSchema, commandInstance);
await commandInstance.ExecuteAsync(_console);
return 0;
}
catch (CliFxException ex)
{
_console.Error.WriteException(ex);
if (ex.ShowHelp)
{
_console.Output.WriteLine();
_console.Output.WriteHelpText(helpContext);
}
return ex.ExitCode;
}
return await RunAsync(applicationSchema, commandInput);
}
/// <summary>
/// Runs the application with the specified command line arguments and environment variables.
/// Returns an exit code which indicates whether the application completed successfully.
/// </summary>
/// <remarks>
/// When running WITHOUT debugger (i.e. in production), this method swallows all exceptions and
/// reports them to the console.
/// </remarks>
public async ValueTask<int> RunAsync(
IReadOnlyList<string> commandLineArguments,
IReadOnlyDictionary<string, string> environmentVariables)
// To prevent the app from showing the annoying troubleshooting dialog on Windows,
// we handle all exceptions ourselves and print them to the console.
//
// We only want to do that if the app is running in production, which we infer
// based on whether a debugger is attached to the process.
//
// When not running in production, we want the IDE to show exceptions to the
// developer, so we don't swallow them in that case.
catch (Exception ex) when (!Debugger.IsAttached)
{
try
{
// Console colors may have already been overriden by the parent process,
// so we need to reset it to make sure that everything we write looks properly.
_console.ResetColor();
var applicationSchema = ApplicationSchema.Resolve(Configuration.CommandTypes);
var commandInput = CommandInput.Parse(
commandLineArguments,
environmentVariables,
applicationSchema.GetCommandNames()
);
return await RunAsync(applicationSchema, commandInput);
}
// To prevent the app from showing the annoying troubleshooting dialog on Windows,
// we handle all exceptions ourselves and print them to the console.
//
// We only want to do that if the app is running in production, which we infer
// based on whether a debugger is attached to the process.
//
// When not running in production, we want the IDE to show exceptions to the
// developer, so we don't swallow them in that case.
catch (Exception ex) when (!Debugger.IsAttached)
{
_console.Error.WriteException(ex);
return 1;
}
_console.Error.WriteException(ex);
return 1;
}
/// <summary>
/// Runs the application with the specified command line arguments.
/// Environment variables are resolved automatically.
/// Returns an exit code which indicates whether the application completed successfully.
/// </summary>
/// <remarks>
/// When running WITHOUT debugger (i.e. in production), this method swallows all exceptions and
/// reports them to the console.
/// </remarks>
public async ValueTask<int> RunAsync(IReadOnlyList<string> commandLineArguments) => await RunAsync(
commandLineArguments,
// Use case-sensitive comparison because environment variables are
// case-sensitive on Linux and macOS (but not on Windows).
Environment
.GetEnvironmentVariables()
.ToDictionary<string, string>(StringComparer.Ordinal)
);
/// <summary>
/// Runs the application.
/// Command line arguments and environment variables are resolved automatically.
/// Returns an exit code which indicates whether the application completed successfully.
/// </summary>
/// <remarks>
/// When running WITHOUT debugger (i.e. in production), this method swallows all exceptions and
/// reports them to the console.
/// </remarks>
public async ValueTask<int> RunAsync() => await RunAsync(
Environment.GetCommandLineArgs()
.Skip(1) // first element is the file path
.ToArray()
);
}
}
/// <summary>
/// Runs the application with the specified command line arguments.
/// Environment variables are resolved automatically.
/// Returns an exit code which indicates whether the application completed successfully.
/// </summary>
/// <remarks>
/// When running WITHOUT debugger (i.e. in production), this method swallows all exceptions and
/// reports them to the console.
/// </remarks>
public async ValueTask<int> RunAsync(IReadOnlyList<string> commandLineArguments) => await RunAsync(
commandLineArguments,
// Use case-sensitive comparison because environment variables are
// case-sensitive on Linux and macOS (but not on Windows).
Environment
.GetEnvironmentVariables()
.ToDictionary<string, string>(StringComparer.Ordinal)
);
/// <summary>
/// Runs the application.
/// Command line arguments and environment variables are resolved automatically.
/// Returns an exit code which indicates whether the application completed successfully.
/// </summary>
/// <remarks>
/// When running WITHOUT debugger (i.e. in production), this method swallows all exceptions and
/// reports them to the console.
/// </remarks>
public async ValueTask<int> RunAsync() => await RunAsync(
Environment.GetCommandLineArgs()
.Skip(1) // first element is the file path
.ToArray()
);
}

View File

@@ -8,245 +8,244 @@ using CliFx.Infrastructure;
using CliFx.Schema;
using CliFx.Utils.Extensions;
namespace CliFx
namespace CliFx;
/// <summary>
/// Builder for <see cref="CliApplication"/>.
/// </summary>
public partial class CliApplicationBuilder
{
private readonly HashSet<Type> _commandTypes = new();
private bool _isDebugModeAllowed = true;
private bool _isPreviewModeAllowed = true;
private string? _title;
private string? _executableName;
private string? _versionText;
private string? _description;
private IConsole? _console;
private ITypeActivator? _typeActivator;
/// <summary>
/// Builder for <see cref="CliApplication"/>.
/// Adds a command to the application.
/// </summary>
public partial class CliApplicationBuilder
public CliApplicationBuilder AddCommand(Type commandType)
{
private readonly HashSet<Type> _commandTypes = new();
_commandTypes.Add(commandType);
private bool _isDebugModeAllowed = true;
private bool _isPreviewModeAllowed = true;
private string? _title;
private string? _executableName;
private string? _versionText;
private string? _description;
private IConsole? _console;
private ITypeActivator? _typeActivator;
/// <summary>
/// Adds a command to the application.
/// </summary>
public CliApplicationBuilder AddCommand(Type commandType)
{
_commandTypes.Add(commandType);
return this;
}
/// <summary>
/// Adds a command to the application.
/// </summary>
public CliApplicationBuilder AddCommand<TCommand>() where TCommand : ICommand =>
AddCommand(typeof(TCommand));
/// <summary>
/// Adds multiple commands to the application.
/// </summary>
public CliApplicationBuilder AddCommands(IEnumerable<Type> commandTypes)
{
foreach (var commandType in commandTypes)
AddCommand(commandType);
return this;
}
/// <summary>
/// Adds commands from the specified assembly to the application.
/// </summary>
/// <remarks>
/// This method looks for public non-abstract classes that implement <see cref="ICommand"/>
/// and are annotated by <see cref="CommandAttribute"/>.
/// </remarks>
public CliApplicationBuilder AddCommandsFrom(Assembly commandAssembly)
{
foreach (var commandType in commandAssembly.ExportedTypes.Where(CommandSchema.IsCommandType))
AddCommand(commandType);
return this;
}
/// <summary>
/// Adds commands from the specified assemblies to the application.
/// </summary>
/// <remarks>
/// This method looks for public non-abstract classes that implement <see cref="ICommand"/>
/// and are annotated by <see cref="CommandAttribute"/>.
/// </remarks>
public CliApplicationBuilder AddCommandsFrom(IEnumerable<Assembly> commandAssemblies)
{
foreach (var commandAssembly in commandAssemblies)
AddCommandsFrom(commandAssembly);
return this;
}
/// <summary>
/// Adds commands from the calling assembly to the application.
/// </summary>
/// <remarks>
/// This method looks for public non-abstract classes that implement <see cref="ICommand"/>
/// and are annotated by <see cref="CommandAttribute"/>.
/// </remarks>
public CliApplicationBuilder AddCommandsFromThisAssembly() => AddCommandsFrom(Assembly.GetCallingAssembly());
/// <summary>
/// Specifies whether debug mode (enabled with the [debug] directive) is allowed in the application.
/// </summary>
public CliApplicationBuilder AllowDebugMode(bool isAllowed = true)
{
_isDebugModeAllowed = isAllowed;
return this;
}
/// <summary>
/// Specifies whether preview mode (enabled with the [preview] directive) is allowed in the application.
/// </summary>
public CliApplicationBuilder AllowPreviewMode(bool isAllowed = true)
{
_isPreviewModeAllowed = isAllowed;
return this;
}
/// <summary>
/// Sets application title, which is shown in the help text.
/// </summary>
/// <remarks>
/// By default, application title is inferred from the assembly name.
/// </remarks>
public CliApplicationBuilder SetTitle(string title)
{
_title = title;
return this;
}
/// <summary>
/// Sets application executable name, which is shown in the help text.
/// </summary>
/// <remarks>
/// By default, application executable name is inferred from the assembly file name.
/// The file name is also prefixed with `dotnet` if it's a DLL file.
/// </remarks>
public CliApplicationBuilder SetExecutableName(string executableName)
{
_executableName = executableName;
return this;
}
/// <summary>
/// Sets application version, which is shown in the help text or
/// when the user specifies the version option.
/// </summary>
/// <remarks>
/// By default, application version is inferred from the assembly version.
/// </remarks>
public CliApplicationBuilder SetVersion(string version)
{
_versionText = version;
return this;
}
/// <summary>
/// Sets application description, which is shown in the help text.
/// </summary>
public CliApplicationBuilder SetDescription(string? description)
{
_description = description;
return this;
}
/// <summary>
/// Configures the application to use the specified implementation of <see cref="IConsole"/>.
/// </summary>
public CliApplicationBuilder UseConsole(IConsole console)
{
_console = console;
return this;
}
/// <summary>
/// Configures the application to use the specified implementation of <see cref="ITypeActivator"/>.
/// </summary>
public CliApplicationBuilder UseTypeActivator(ITypeActivator typeActivator)
{
_typeActivator = typeActivator;
return this;
}
/// <summary>
/// Configures the application to use the specified function for activating types.
/// </summary>
public CliApplicationBuilder UseTypeActivator(Func<Type, object> typeActivator) =>
UseTypeActivator(new DelegateTypeActivator(typeActivator));
/// <summary>
/// Creates a configured instance of <see cref="CliApplication"/>.
/// </summary>
public CliApplication Build()
{
var metadata = new ApplicationMetadata(
_title ?? GetDefaultTitle(),
_executableName ?? GetDefaultExecutableName(),
_versionText ?? GetDefaultVersionText(),
_description
);
var configuration = new ApplicationConfiguration(
_commandTypes.ToArray(),
_isDebugModeAllowed,
_isPreviewModeAllowed
);
return new CliApplication(
metadata,
configuration,
_console ?? new SystemConsole(),
_typeActivator ?? new DefaultTypeActivator()
);
}
return this;
}
public partial class CliApplicationBuilder
/// <summary>
/// Adds a command to the application.
/// </summary>
public CliApplicationBuilder AddCommand<TCommand>() where TCommand : ICommand =>
AddCommand(typeof(TCommand));
/// <summary>
/// Adds multiple commands to the application.
/// </summary>
public CliApplicationBuilder AddCommands(IEnumerable<Type> commandTypes)
{
private static readonly Lazy<Assembly?> EntryAssemblyLazy = new(Assembly.GetEntryAssembly);
foreach (var commandType in commandTypes)
AddCommand(commandType);
// Entry assembly can be null, for example in tests
private static Assembly? EntryAssembly => EntryAssemblyLazy.Value;
private static string GetDefaultTitle()
{
var entryAssemblyName = EntryAssembly?.GetName().Name;
if (string.IsNullOrWhiteSpace(entryAssemblyName))
return "App";
return entryAssemblyName;
}
private static string GetDefaultExecutableName()
{
var entryAssemblyLocation = EntryAssembly?.Location;
if (string.IsNullOrWhiteSpace(entryAssemblyLocation))
return "app";
// The assembly can be an .exe or a .dll, depending on how it was packaged
var isDll = string.Equals(
Path.GetExtension(entryAssemblyLocation),
".dll",
StringComparison.OrdinalIgnoreCase
);
var name = isDll
? "dotnet " + Path.GetFileName(entryAssemblyLocation)
: Path.GetFileNameWithoutExtension(entryAssemblyLocation);
return name;
}
private static string GetDefaultVersionText() =>
EntryAssembly is not null
? "v" + EntryAssembly.GetName().Version.ToSemanticString()
: "v1.0";
return this;
}
/// <summary>
/// Adds commands from the specified assembly to the application.
/// </summary>
/// <remarks>
/// This method looks for public non-abstract classes that implement <see cref="ICommand"/>
/// and are annotated by <see cref="CommandAttribute"/>.
/// </remarks>
public CliApplicationBuilder AddCommandsFrom(Assembly commandAssembly)
{
foreach (var commandType in commandAssembly.ExportedTypes.Where(CommandSchema.IsCommandType))
AddCommand(commandType);
return this;
}
/// <summary>
/// Adds commands from the specified assemblies to the application.
/// </summary>
/// <remarks>
/// This method looks for public non-abstract classes that implement <see cref="ICommand"/>
/// and are annotated by <see cref="CommandAttribute"/>.
/// </remarks>
public CliApplicationBuilder AddCommandsFrom(IEnumerable<Assembly> commandAssemblies)
{
foreach (var commandAssembly in commandAssemblies)
AddCommandsFrom(commandAssembly);
return this;
}
/// <summary>
/// Adds commands from the calling assembly to the application.
/// </summary>
/// <remarks>
/// This method looks for public non-abstract classes that implement <see cref="ICommand"/>
/// and are annotated by <see cref="CommandAttribute"/>.
/// </remarks>
public CliApplicationBuilder AddCommandsFromThisAssembly() => AddCommandsFrom(Assembly.GetCallingAssembly());
/// <summary>
/// Specifies whether debug mode (enabled with the [debug] directive) is allowed in the application.
/// </summary>
public CliApplicationBuilder AllowDebugMode(bool isAllowed = true)
{
_isDebugModeAllowed = isAllowed;
return this;
}
/// <summary>
/// Specifies whether preview mode (enabled with the [preview] directive) is allowed in the application.
/// </summary>
public CliApplicationBuilder AllowPreviewMode(bool isAllowed = true)
{
_isPreviewModeAllowed = isAllowed;
return this;
}
/// <summary>
/// Sets application title, which is shown in the help text.
/// </summary>
/// <remarks>
/// By default, application title is inferred from the assembly name.
/// </remarks>
public CliApplicationBuilder SetTitle(string title)
{
_title = title;
return this;
}
/// <summary>
/// Sets application executable name, which is shown in the help text.
/// </summary>
/// <remarks>
/// By default, application executable name is inferred from the assembly file name.
/// The file name is also prefixed with `dotnet` if it's a DLL file.
/// </remarks>
public CliApplicationBuilder SetExecutableName(string executableName)
{
_executableName = executableName;
return this;
}
/// <summary>
/// Sets application version, which is shown in the help text or
/// when the user specifies the version option.
/// </summary>
/// <remarks>
/// By default, application version is inferred from the assembly version.
/// </remarks>
public CliApplicationBuilder SetVersion(string version)
{
_versionText = version;
return this;
}
/// <summary>
/// Sets application description, which is shown in the help text.
/// </summary>
public CliApplicationBuilder SetDescription(string? description)
{
_description = description;
return this;
}
/// <summary>
/// Configures the application to use the specified implementation of <see cref="IConsole"/>.
/// </summary>
public CliApplicationBuilder UseConsole(IConsole console)
{
_console = console;
return this;
}
/// <summary>
/// Configures the application to use the specified implementation of <see cref="ITypeActivator"/>.
/// </summary>
public CliApplicationBuilder UseTypeActivator(ITypeActivator typeActivator)
{
_typeActivator = typeActivator;
return this;
}
/// <summary>
/// Configures the application to use the specified function for activating types.
/// </summary>
public CliApplicationBuilder UseTypeActivator(Func<Type, object> typeActivator) =>
UseTypeActivator(new DelegateTypeActivator(typeActivator));
/// <summary>
/// Creates a configured instance of <see cref="CliApplication"/>.
/// </summary>
public CliApplication Build()
{
var metadata = new ApplicationMetadata(
_title ?? GetDefaultTitle(),
_executableName ?? GetDefaultExecutableName(),
_versionText ?? GetDefaultVersionText(),
_description
);
var configuration = new ApplicationConfiguration(
_commandTypes.ToArray(),
_isDebugModeAllowed,
_isPreviewModeAllowed
);
return new CliApplication(
metadata,
configuration,
_console ?? new SystemConsole(),
_typeActivator ?? new DefaultTypeActivator()
);
}
}
public partial class CliApplicationBuilder
{
private static readonly Lazy<Assembly?> EntryAssemblyLazy = new(Assembly.GetEntryAssembly);
// Entry assembly can be null, for example in tests
private static Assembly? EntryAssembly => EntryAssemblyLazy.Value;
private static string GetDefaultTitle()
{
var entryAssemblyName = EntryAssembly?.GetName().Name;
if (string.IsNullOrWhiteSpace(entryAssemblyName))
return "App";
return entryAssemblyName;
}
private static string GetDefaultExecutableName()
{
var entryAssemblyLocation = EntryAssembly?.Location;
if (string.IsNullOrWhiteSpace(entryAssemblyLocation))
return "app";
// The assembly can be an .exe or a .dll, depending on how it was packaged
var isDll = string.Equals(
Path.GetExtension(entryAssemblyLocation),
".dll",
StringComparison.OrdinalIgnoreCase
);
var name = isDll
? "dotnet " + Path.GetFileName(entryAssemblyLocation)
: Path.GetFileNameWithoutExtension(entryAssemblyLocation);
return name;
}
private static string GetDefaultVersionText() =>
EntryAssembly is not null
? "v" + EntryAssembly.GetName().Version.ToSemanticString()
: "v1.0";
}

View File

@@ -10,357 +10,356 @@ using CliFx.Input;
using CliFx.Schema;
using CliFx.Utils.Extensions;
namespace CliFx
namespace CliFx;
internal class CommandBinder
{
internal class CommandBinder
private readonly ITypeActivator _typeActivator;
private readonly IFormatProvider _formatProvider = CultureInfo.InvariantCulture;
public CommandBinder(ITypeActivator typeActivator)
{
private readonly ITypeActivator _typeActivator;
private readonly IFormatProvider _formatProvider = CultureInfo.InvariantCulture;
_typeActivator = typeActivator;
}
public CommandBinder(ITypeActivator typeActivator)
private object? ConvertSingle(IMemberSchema memberSchema, string? rawValue, Type targetType)
{
// Custom converter
if (memberSchema.ConverterType is not null)
{
_typeActivator = typeActivator;
var converter = (IBindingConverter) _typeActivator.CreateInstance(memberSchema.ConverterType);
return converter.Convert(rawValue);
}
private object? ConvertSingle(IMemberSchema memberSchema, string? rawValue, Type targetType)
// Assignable from string (e.g. string itself, object, etc)
if (targetType.IsAssignableFrom(typeof(string)))
{
// Custom converter
if (memberSchema.ConverterType is not null)
{
var converter = (IBindingConverter) _typeActivator.CreateInstance(memberSchema.ConverterType);
return converter.Convert(rawValue);
}
// Assignable from string (e.g. string itself, object, etc)
if (targetType.IsAssignableFrom(typeof(string)))
{
return rawValue;
}
// Special case for bool
if (targetType == typeof(bool))
{
return string.IsNullOrWhiteSpace(rawValue) || bool.Parse(rawValue);
}
// IConvertible primitives (int, double, char, etc)
if (targetType.IsConvertible())
{
return Convert.ChangeType(rawValue, targetType, _formatProvider);
}
// Special case for DateTimeOffset
if (targetType == typeof(DateTimeOffset))
{
return DateTimeOffset.Parse(rawValue, _formatProvider);
}
// Special case for TimeSpan
if (targetType == typeof(TimeSpan))
{
return TimeSpan.Parse(rawValue, _formatProvider);
}
// Enum
if (targetType.IsEnum)
{
// Null reference exception will be handled upstream
return Enum.Parse(targetType, rawValue!, true);
}
// Nullable<T>
var nullableUnderlyingType = targetType.TryGetNullableUnderlyingType();
if (nullableUnderlyingType is not null)
{
return !string.IsNullOrWhiteSpace(rawValue)
? ConvertSingle(memberSchema, rawValue, nullableUnderlyingType)
: null;
}
// String-constructible (FileInfo, etc)
var stringConstructor = targetType.GetConstructor(new[] {typeof(string)});
if (stringConstructor is not null)
{
return stringConstructor.Invoke(new object?[] {rawValue});
}
// String-parseable (with IFormatProvider)
var parseMethodWithFormatProvider = targetType.TryGetStaticParseMethod(true);
if (parseMethodWithFormatProvider is not null)
{
return parseMethodWithFormatProvider.Invoke(null, new object?[] {rawValue, _formatProvider});
}
// String-parseable (without IFormatProvider)
var parseMethod = targetType.TryGetStaticParseMethod();
if (parseMethod is not null)
{
return parseMethod.Invoke(null, new object?[] {rawValue});
}
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."
);
return rawValue;
}
private object? ConvertMultiple(
IMemberSchema memberSchema,
IReadOnlyList<string> rawValues,
Type targetEnumerableType,
Type targetElementType)
// Special case for bool
if (targetType == typeof(bool))
{
var array = rawValues
.Select(v => ConvertSingle(memberSchema, v, targetElementType))
.ToNonGenericArray(targetElementType);
var arrayType = array.GetType();
// Assignable from an array (T[], IReadOnlyList<T>, etc)
if (targetEnumerableType.IsAssignableFrom(arrayType))
{
return array;
}
// Array-constructible (List<T>, HashSet<T>, etc)
var arrayConstructor = targetEnumerableType.GetConstructor(new[] {arrayType});
if (arrayConstructor is not null)
{
return arrayConstructor.Invoke(new object?[] {array});
}
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."
);
return string.IsNullOrWhiteSpace(rawValue) || bool.Parse(rawValue);
}
private object? ConvertMember(IMemberSchema memberSchema, IReadOnlyList<string> rawValues)
// IConvertible primitives (int, double, char, etc)
if (targetType.IsConvertible())
{
var targetType = memberSchema.Property.Type;
return Convert.ChangeType(rawValue, targetType, _formatProvider);
}
try
// Special case for DateTimeOffset
if (targetType == typeof(DateTimeOffset))
{
return DateTimeOffset.Parse(rawValue, _formatProvider);
}
// Special case for TimeSpan
if (targetType == typeof(TimeSpan))
{
return TimeSpan.Parse(rawValue, _formatProvider);
}
// Enum
if (targetType.IsEnum)
{
// Null reference exception will be handled upstream
return Enum.Parse(targetType, rawValue!, true);
}
// Nullable<T>
var nullableUnderlyingType = targetType.TryGetNullableUnderlyingType();
if (nullableUnderlyingType is not null)
{
return !string.IsNullOrWhiteSpace(rawValue)
? ConvertSingle(memberSchema, rawValue, nullableUnderlyingType)
: null;
}
// String-constructible (FileInfo, etc)
var stringConstructor = targetType.GetConstructor(new[] {typeof(string)});
if (stringConstructor is not null)
{
return stringConstructor.Invoke(new object?[] {rawValue});
}
// String-parseable (with IFormatProvider)
var parseMethodWithFormatProvider = targetType.TryGetStaticParseMethod(true);
if (parseMethodWithFormatProvider is not null)
{
return parseMethodWithFormatProvider.Invoke(null, new object?[] {rawValue, _formatProvider});
}
// String-parseable (without IFormatProvider)
var parseMethod = targetType.TryGetStaticParseMethod();
if (parseMethod is not null)
{
return parseMethod.Invoke(null, new object?[] {rawValue});
}
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."
);
}
private object? ConvertMultiple(
IMemberSchema memberSchema,
IReadOnlyList<string> rawValues,
Type targetEnumerableType,
Type targetElementType)
{
var array = rawValues
.Select(v => ConvertSingle(memberSchema, v, targetElementType))
.ToNonGenericArray(targetElementType);
var arrayType = array.GetType();
// Assignable from an array (T[], IReadOnlyList<T>, etc)
if (targetEnumerableType.IsAssignableFrom(arrayType))
{
return array;
}
// Array-constructible (List<T>, HashSet<T>, etc)
var arrayConstructor = targetEnumerableType.GetConstructor(new[] {arrayType});
if (arrayConstructor is not null)
{
return arrayConstructor.Invoke(new object?[] {array});
}
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."
);
}
private object? ConvertMember(IMemberSchema memberSchema, IReadOnlyList<string> rawValues)
{
var targetType = memberSchema.Property.Type;
try
{
// Non-scalar
var enumerableUnderlyingType = targetType.TryGetEnumerableUnderlyingType();
if (targetType != typeof(string) && enumerableUnderlyingType is not null)
{
// Non-scalar
var enumerableUnderlyingType = targetType.TryGetEnumerableUnderlyingType();
if (targetType != typeof(string) && enumerableUnderlyingType is not null)
{
return ConvertMultiple(memberSchema, rawValues, targetType, enumerableUnderlyingType);
}
// Scalar
if (rawValues.Count <= 1)
{
return ConvertSingle(memberSchema, rawValues.SingleOrDefault(), targetType);
}
}
catch (Exception ex) when (ex is not CliFxException) // don't wrap CliFxException
{
// We use reflection-based invocation which can throw TargetInvocationException.
// Unwrap these exceptions to provide a more user-friendly error message.
var errorMessage = ex is TargetInvocationException invokeEx
? invokeEx.InnerException?.Message ?? invokeEx.Message
: ex.Message;
throw CliFxException.UserError(
$"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} cannot be set from provided argument(s):" +
Environment.NewLine +
rawValues.Select(v => '<' + v + '>').JoinToString(" ") +
Environment.NewLine +
$"Error: {errorMessage}",
ex
);
return ConvertMultiple(memberSchema, rawValues, targetType, enumerableUnderlyingType);
}
// Mismatch (scalar but too many values)
// Scalar
if (rawValues.Count <= 1)
{
return ConvertSingle(memberSchema, rawValues.SingleOrDefault(), targetType);
}
}
catch (Exception ex) when (ex is not CliFxException) // don't wrap CliFxException
{
// We use reflection-based invocation which can throw TargetInvocationException.
// Unwrap these exceptions to provide a more user-friendly error message.
var errorMessage = ex is TargetInvocationException invokeEx
? invokeEx.InnerException?.Message ?? invokeEx.Message
: ex.Message;
throw CliFxException.UserError(
$"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} expects a single argument, but provided with multiple:" +
$"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} cannot be set from provided argument(s):" +
Environment.NewLine +
rawValues.Select(v => '<' + v + '>').JoinToString(" ")
rawValues.Select(v => '<' + v + '>').JoinToString(" ") +
Environment.NewLine +
$"Error: {errorMessage}",
ex
);
}
private void ValidateMember(IMemberSchema memberSchema, object? convertedValue)
// 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(" ")
);
}
private void ValidateMember(IMemberSchema memberSchema, object? convertedValue)
{
var errors = new List<BindingValidationError>();
foreach (var validatorType in memberSchema.ValidatorTypes)
{
var errors = new List<BindingValidationError>();
var validator = (IBindingValidator) _typeActivator.CreateInstance(validatorType);
var error = validator.Validate(convertedValue);
foreach (var validatorType in memberSchema.ValidatorTypes)
{
var validator = (IBindingValidator) _typeActivator.CreateInstance(validatorType);
var error = validator.Validate(convertedValue);
if (error is not null)
errors.Add(error);
}
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)
);
}
if (error is not null)
errors.Add(error);
}
private void BindMember(IMemberSchema memberSchema, ICommand commandInstance, IReadOnlyList<string> rawValues)
if (errors.Any())
{
var convertedValue = ConvertMember(memberSchema, rawValues);
ValidateMember(memberSchema, convertedValue);
memberSchema.Property.SetValue(commandInstance, convertedValue);
}
private void BindParameters(CommandInput commandInput, CommandSchema commandSchema, ICommand commandInstance)
{
// Ensure there are no unexpected parameters and that all parameters are provided
var remainingParameterInputs = commandInput.Parameters.ToList();
var remainingParameterSchemas = commandSchema.Parameters.ToList();
var position = 0;
foreach (var parameterSchema in commandSchema.Parameters.OrderBy(p => p.Order))
{
// Break when there are no remaining inputs
if (position >= commandInput.Parameters.Count)
break;
// Scalar - take one input at the current position
if (parameterSchema.Property.IsScalar())
{
var parameterInput = commandInput.Parameters[position];
var rawValues = new[] {parameterInput.Value};
BindMember(parameterSchema, commandInstance, rawValues);
position++;
remainingParameterInputs.Remove(parameterInput);
}
// Non-scalar - take all remaining inputs starting from the current position
else
{
var parameterInputs = commandInput.Parameters.Skip(position).ToArray();
var rawValues = parameterInputs.Select(p => p.Value).ToArray();
BindMember(parameterSchema, commandInstance, rawValues);
position += parameterInputs.Length;
remainingParameterInputs.RemoveRange(parameterInputs);
}
remainingParameterSchemas.Remove(parameterSchema);
}
if (remainingParameterInputs.Any())
{
throw CliFxException.UserError(
"Unexpected parameter(s):" +
Environment.NewLine +
remainingParameterInputs
.Select(p => p.GetFormattedIdentifier())
.JoinToString(" ")
);
}
if (remainingParameterSchemas.Any())
{
throw CliFxException.UserError(
"Missing parameter(s):" +
Environment.NewLine +
remainingParameterSchemas
.Select(o => o.GetFormattedIdentifier())
.JoinToString(" ")
);
}
}
private void BindOptions(CommandInput commandInput, CommandSchema commandSchema, ICommand commandInstance)
{
// Ensure there are no unrecognized options and that all required options are set
var remainingOptionInputs = commandInput.Options.ToList();
var remainingRequiredOptionSchemas = commandSchema.Options.Where(o => o.IsRequired).ToList();
foreach (var optionSchema in commandSchema.Options)
{
var optionInputs = commandInput
.Options
.Where(o => optionSchema.MatchesIdentifier(o.Identifier))
.ToArray();
var environmentVariableInput = commandInput
.EnvironmentVariables
.FirstOrDefault(e => optionSchema.MatchesEnvironmentVariable(e.Name));
// Direct input
if (optionInputs.Any())
{
var rawValues = optionInputs.SelectMany(o => o.Values).ToArray();
BindMember(optionSchema, commandInstance, rawValues);
// Required options require at least one value to be set
if (rawValues.Any())
remainingRequiredOptionSchemas.Remove(optionSchema);
}
// Environment variable
else if (environmentVariableInput is not null)
{
var rawValues = optionSchema.Property.IsScalar()
? new[] {environmentVariableInput.Value}
: environmentVariableInput.SplitValues();
BindMember(optionSchema, commandInstance, rawValues);
// Required options require at least one value to be set
if (rawValues.Any())
remainingRequiredOptionSchemas.Remove(optionSchema);
}
// No input - skip
else
{
continue;
}
remainingOptionInputs.RemoveRange(optionInputs);
}
if (remainingOptionInputs.Any())
{
throw CliFxException.UserError(
"Unrecognized option(s):" +
Environment.NewLine +
remainingOptionInputs
.Select(o => o.GetFormattedIdentifier())
.JoinToString(", ")
);
}
if (remainingRequiredOptionSchemas.Any())
{
throw CliFxException.UserError(
"Missing required option(s):" +
Environment.NewLine +
remainingRequiredOptionSchemas
.Select(o => o.GetFormattedIdentifier())
.JoinToString(", ")
);
}
}
public void Bind(CommandInput commandInput, CommandSchema commandSchema, ICommand commandInstance)
{
BindParameters(commandInput, commandSchema, commandInstance);
BindOptions(commandInput, commandSchema, commandInstance);
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)
);
}
}
private void BindMember(IMemberSchema memberSchema, ICommand commandInstance, IReadOnlyList<string> rawValues)
{
var convertedValue = ConvertMember(memberSchema, rawValues);
ValidateMember(memberSchema, convertedValue);
memberSchema.Property.SetValue(commandInstance, convertedValue);
}
private void BindParameters(CommandInput commandInput, CommandSchema commandSchema, ICommand commandInstance)
{
// Ensure there are no unexpected parameters and that all parameters are provided
var remainingParameterInputs = commandInput.Parameters.ToList();
var remainingParameterSchemas = commandSchema.Parameters.ToList();
var position = 0;
foreach (var parameterSchema in commandSchema.Parameters.OrderBy(p => p.Order))
{
// Break when there are no remaining inputs
if (position >= commandInput.Parameters.Count)
break;
// Scalar - take one input at the current position
if (parameterSchema.Property.IsScalar())
{
var parameterInput = commandInput.Parameters[position];
var rawValues = new[] {parameterInput.Value};
BindMember(parameterSchema, commandInstance, rawValues);
position++;
remainingParameterInputs.Remove(parameterInput);
}
// Non-scalar - take all remaining inputs starting from the current position
else
{
var parameterInputs = commandInput.Parameters.Skip(position).ToArray();
var rawValues = parameterInputs.Select(p => p.Value).ToArray();
BindMember(parameterSchema, commandInstance, rawValues);
position += parameterInputs.Length;
remainingParameterInputs.RemoveRange(parameterInputs);
}
remainingParameterSchemas.Remove(parameterSchema);
}
if (remainingParameterInputs.Any())
{
throw CliFxException.UserError(
"Unexpected parameter(s):" +
Environment.NewLine +
remainingParameterInputs
.Select(p => p.GetFormattedIdentifier())
.JoinToString(" ")
);
}
if (remainingParameterSchemas.Any())
{
throw CliFxException.UserError(
"Missing parameter(s):" +
Environment.NewLine +
remainingParameterSchemas
.Select(o => o.GetFormattedIdentifier())
.JoinToString(" ")
);
}
}
private void BindOptions(CommandInput commandInput, CommandSchema commandSchema, ICommand commandInstance)
{
// Ensure there are no unrecognized options and that all required options are set
var remainingOptionInputs = commandInput.Options.ToList();
var remainingRequiredOptionSchemas = commandSchema.Options.Where(o => o.IsRequired).ToList();
foreach (var optionSchema in commandSchema.Options)
{
var optionInputs = commandInput
.Options
.Where(o => optionSchema.MatchesIdentifier(o.Identifier))
.ToArray();
var environmentVariableInput = commandInput
.EnvironmentVariables
.FirstOrDefault(e => optionSchema.MatchesEnvironmentVariable(e.Name));
// Direct input
if (optionInputs.Any())
{
var rawValues = optionInputs.SelectMany(o => o.Values).ToArray();
BindMember(optionSchema, commandInstance, rawValues);
// Required options require at least one value to be set
if (rawValues.Any())
remainingRequiredOptionSchemas.Remove(optionSchema);
}
// Environment variable
else if (environmentVariableInput is not null)
{
var rawValues = optionSchema.Property.IsScalar()
? new[] {environmentVariableInput.Value}
: environmentVariableInput.SplitValues();
BindMember(optionSchema, commandInstance, rawValues);
// Required options require at least one value to be set
if (rawValues.Any())
remainingRequiredOptionSchemas.Remove(optionSchema);
}
// No input - skip
else
{
continue;
}
remainingOptionInputs.RemoveRange(optionInputs);
}
if (remainingOptionInputs.Any())
{
throw CliFxException.UserError(
"Unrecognized option(s):" +
Environment.NewLine +
remainingOptionInputs
.Select(o => o.GetFormattedIdentifier())
.JoinToString(", ")
);
}
if (remainingRequiredOptionSchemas.Any())
{
throw CliFxException.UserError(
"Missing required option(s):" +
Environment.NewLine +
remainingRequiredOptionSchemas
.Select(o => o.GetFormattedIdentifier())
.JoinToString(", ")
);
}
}
public void Bind(CommandInput commandInput, CommandSchema commandSchema, ICommand commandInstance)
{
BindParameters(commandInput, commandSchema, commandInstance);
BindOptions(commandInput, commandSchema, commandInstance);
}
}

View File

@@ -1,54 +1,53 @@
using System;
namespace CliFx.Exceptions
namespace CliFx.Exceptions;
/// <summary>
/// Exception thrown when there is an error during application execution.
/// </summary>
public partial class CliFxException : Exception
{
internal const int DefaultExitCode = 1;
// Regular `exception.Message` never returns null, even if
// it hasn't been set.
internal string? ActualMessage { get; }
/// <summary>
/// Exception thrown when there is an error during application execution.
/// Returned exit code.
/// </summary>
public partial class CliFxException : Exception
public int ExitCode { get; }
/// <summary>
/// Whether to show the help text before exiting.
/// </summary>
public bool ShowHelp { get; }
/// <summary>
/// Initializes an instance of <see cref="CliFxException"/>.
/// </summary>
public CliFxException(
string message,
int exitCode = DefaultExitCode,
bool showHelp = false,
Exception? innerException = null)
: base(message, innerException)
{
internal const int DefaultExitCode = 1;
// Regular `exception.Message` never returns null, even if
// it hasn't been set.
internal string? ActualMessage { get; }
/// <summary>
/// Returned exit code.
/// </summary>
public int ExitCode { get; }
/// <summary>
/// Whether to show the help text before exiting.
/// </summary>
public bool ShowHelp { get; }
/// <summary>
/// Initializes an instance of <see cref="CliFxException"/>.
/// </summary>
public CliFxException(
string message,
int exitCode = DefaultExitCode,
bool showHelp = false,
Exception? innerException = null)
: base(message, innerException)
{
ActualMessage = message;
ExitCode = exitCode;
ShowHelp = showHelp;
}
ActualMessage = message;
ExitCode = exitCode;
ShowHelp = showHelp;
}
}
public partial class CliFxException
{
// Internal errors don't show help because they're meant for the developer
// and not the end-user of the application.
internal static CliFxException InternalError(string message, Exception? innerException = null) =>
new(message, DefaultExitCode, false, innerException);
public partial class CliFxException
{
// Internal errors don't show help because they're meant for the developer
// and not the end-user of the application.
internal static CliFxException InternalError(string message, Exception? innerException = null) =>
new(message, DefaultExitCode, false, innerException);
// User errors are typically caused by invalid input and they're meant for
// the end-user, so we want to show help.
internal static CliFxException UserError(string message, Exception? innerException = null) =>
new(message, DefaultExitCode, true, innerException);
}
// User errors are typically caused by invalid input and they're meant for
// the end-user, so we want to show help.
internal static CliFxException UserError(string message, Exception? innerException = null) =>
new(message, DefaultExitCode, true, innerException);
}

View File

@@ -1,23 +1,22 @@
using System;
namespace CliFx.Exceptions
namespace CliFx.Exceptions;
/// <summary>
/// Exception thrown when a command cannot proceed with its normal execution due to an error.
/// Use this exception to report an error to the console and return a specific exit code.
/// </summary>
public class CommandException : CliFxException
{
/// <summary>
/// Exception thrown when a command cannot proceed with its normal execution due to an error.
/// Use this exception to report an error to the console and return a specific exit code.
/// Initializes an instance of <see cref="CommandException"/>.
/// </summary>
public class CommandException : CliFxException
public CommandException(
string message,
int exitCode = DefaultExitCode,
bool showHelp = false,
Exception? innerException = null)
: base(message, exitCode, showHelp, innerException)
{
/// <summary>
/// Initializes an instance of <see cref="CommandException"/>.
/// </summary>
public CommandException(
string message,
int exitCode = DefaultExitCode,
bool showHelp = false,
Exception? innerException = null)
: base(message, exitCode, showHelp, innerException)
{
}
}
}

View File

@@ -1,21 +1,20 @@
namespace CliFx.Extensibility
namespace CliFx.Extensibility;
// Used internally to simplify usage from reflection
internal interface IBindingConverter
{
// Used internally to simplify usage from reflection
internal interface IBindingConverter
{
object? Convert(string? rawValue);
}
object? Convert(string? rawValue);
}
/// <summary>
/// Base type for custom converters.
/// </summary>
public abstract class BindingConverter<T> : IBindingConverter
{
/// <summary>
/// Base type for custom converters.
/// Parses value from a raw command line argument.
/// </summary>
public abstract class BindingConverter<T> : IBindingConverter
{
/// <summary>
/// Parses value from a raw command line argument.
/// </summary>
public abstract T Convert(string? rawValue);
public abstract T Convert(string? rawValue);
object? IBindingConverter.Convert(string? rawValue) => Convert(rawValue);
}
object? IBindingConverter.Convert(string? rawValue) => Convert(rawValue);
}

View File

@@ -1,18 +1,17 @@
namespace CliFx.Extensibility
namespace CliFx.Extensibility;
/// <summary>
/// Represents a validation error.
/// </summary>
public class BindingValidationError
{
/// <summary>
/// Represents a validation error.
/// Error message shown to the user.
/// </summary>
public class BindingValidationError
{
/// <summary>
/// Error message shown to the user.
/// </summary>
public string Message { get; }
public string Message { get; }
/// <summary>
/// Initializes an instance of <see cref="BindingValidationError"/>.
/// </summary>
public BindingValidationError(string message) => Message = message;
}
/// <summary>
/// Initializes an instance of <see cref="BindingValidationError"/>.
/// </summary>
public BindingValidationError(string message) => Message = message;
}

View File

@@ -1,36 +1,35 @@
namespace CliFx.Extensibility
namespace CliFx.Extensibility;
// Used internally to simplify usage from reflection
internal interface IBindingValidator
{
// Used internally to simplify usage from reflection
internal interface IBindingValidator
{
BindingValidationError? Validate(object? value);
}
BindingValidationError? Validate(object? value);
}
/// <summary>
/// Base type for custom validators.
/// </summary>
public abstract class BindingValidator<T> : IBindingValidator
{
/// <summary>
/// Returns a successful validation result.
/// </summary>
protected BindingValidationError? Ok() => null;
/// <summary>
/// Base type for custom validators.
/// Returns a non-successful validation result.
/// </summary>
public abstract class BindingValidator<T> : IBindingValidator
{
/// <summary>
/// Returns a successful validation result.
/// </summary>
protected BindingValidationError? Ok() => null;
protected BindingValidationError Error(string message) => new(message);
/// <summary>
/// Returns a non-successful validation result.
/// </summary>
protected BindingValidationError Error(string message) => new(message);
/// <summary>
/// Validates the value bound to a parameter or an option.
/// Returns <code>null</code> if validation is successful, or an error in case of failure.
/// </summary>
/// <remarks>
/// You can use the utility methods <see cref="Ok"/> and <see cref="Error"/> to
/// create an appropriate result.
/// </remarks>
public abstract BindingValidationError? Validate(T value);
/// <summary>
/// Validates the value bound to a parameter or an option.
/// Returns <code>null</code> if validation is successful, or an error in case of failure.
/// </summary>
/// <remarks>
/// You can use the utility methods <see cref="Ok"/> and <see cref="Error"/> to
/// create an appropriate result.
/// </remarks>
public abstract BindingValidationError? Validate(T value);
BindingValidationError? IBindingValidator.Validate(object? value) => Validate((T) value!);
}
}
BindingValidationError? IBindingValidator.Validate(object? value) => Validate((T) value!);
}

View File

@@ -4,18 +4,17 @@ using CliFx.Attributes;
using CliFx.Infrastructure;
using CliFx.Schema;
namespace CliFx
{
// Fallback command used when the application doesn't have one configured.
// This command is only used as a stub for help text.
[Command]
internal class FallbackDefaultCommand : ICommand
{
public static CommandSchema Schema { get; } =
CommandSchema.Resolve(typeof(FallbackDefaultCommand));
namespace CliFx;
// Never actually executed
[ExcludeFromCodeCoverage]
public ValueTask ExecuteAsync(IConsole console) => default;
}
// Fallback command used when the application doesn't have one configured.
// This command is only used as a stub for help text.
[Command]
internal class FallbackDefaultCommand : ICommand
{
public static CommandSchema Schema { get; } =
CommandSchema.Resolve(typeof(FallbackDefaultCommand));
// Never actually executed
[ExcludeFromCodeCoverage]
public ValueTask ExecuteAsync(IConsole console) => default;
}

View File

@@ -2,97 +2,96 @@
using CliFx.Infrastructure;
using CliFx.Input;
namespace CliFx.Formatting
namespace CliFx.Formatting;
internal class CommandInputConsoleFormatter : ConsoleFormatter
{
internal class CommandInputConsoleFormatter : ConsoleFormatter
public CommandInputConsoleFormatter(ConsoleWriter consoleWriter)
: base(consoleWriter)
{
public CommandInputConsoleFormatter(ConsoleWriter consoleWriter)
: base(consoleWriter)
}
private void WriteCommandLineArguments(CommandInput commandInput)
{
Write("Command line:");
WriteLine();
WriteHorizontalMargin();
// Command name
if (!string.IsNullOrWhiteSpace(commandInput.CommandName))
{
Write(ConsoleColor.Cyan, commandInput.CommandName);
Write(' ');
}
private void WriteCommandLineArguments(CommandInput commandInput)
// Parameters
foreach (var parameterInput in commandInput.Parameters)
{
Write("Command line:");
WriteLine();
Write('<');
Write(ConsoleColor.White, parameterInput.Value);
Write('>');
Write(' ');
}
// Options
foreach (var optionInput in commandInput.Options)
{
Write('[');
// Identifier
Write(ConsoleColor.White, optionInput.GetFormattedIdentifier());
// Value(s)
foreach (var value in optionInput.Values)
{
Write(' ');
Write('"');
Write(value);
Write('"');
}
Write(']');
Write(' ');
}
WriteLine();
}
private void WriteEnvironmentVariables(CommandInput commandInput)
{
Write("Environment:");
WriteLine();
// Environment variables
foreach (var environmentVariableInput in commandInput.EnvironmentVariables)
{
WriteHorizontalMargin();
// Command name
if (!string.IsNullOrWhiteSpace(commandInput.CommandName))
{
Write(ConsoleColor.Cyan, commandInput.CommandName);
Write(' ');
}
// Name
Write(ConsoleColor.White, environmentVariableInput.Name);
// Parameters
foreach (var parameterInput in commandInput.Parameters)
{
Write('<');
Write(ConsoleColor.White, parameterInput.Value);
Write('>');
Write(' ');
}
Write('=');
// Options
foreach (var optionInput in commandInput.Options)
{
Write('[');
// Identifier
Write(ConsoleColor.White, optionInput.GetFormattedIdentifier());
// Value(s)
foreach (var value in optionInput.Values)
{
Write(' ');
Write('"');
Write(value);
Write('"');
}
Write(']');
Write(' ');
}
// Value
Write('"');
Write(environmentVariableInput.Value);
Write('"');
WriteLine();
}
private void WriteEnvironmentVariables(CommandInput commandInput)
{
Write("Environment:");
WriteLine();
// Environment variables
foreach (var environmentVariableInput in commandInput.EnvironmentVariables)
{
WriteHorizontalMargin();
// Name
Write(ConsoleColor.White, environmentVariableInput.Name);
Write('=');
// Value
Write('"');
Write(environmentVariableInput.Value);
Write('"');
WriteLine();
}
}
public void WriteCommandInput(CommandInput commandInput)
{
WriteCommandLineArguments(commandInput);
WriteLine();
WriteEnvironmentVariables(commandInput);
}
}
internal static class CommandInputConsoleFormatterExtensions
public void WriteCommandInput(CommandInput commandInput)
{
public static void WriteCommandInput(this ConsoleWriter consoleWriter, CommandInput commandInput) =>
new CommandInputConsoleFormatter(consoleWriter).WriteCommandInput(commandInput);
WriteCommandLineArguments(commandInput);
WriteLine();
WriteEnvironmentVariables(commandInput);
}
}
internal static class CommandInputConsoleFormatterExtensions
{
public static void WriteCommandInput(this ConsoleWriter consoleWriter, CommandInput commandInput) =>
new CommandInputConsoleFormatter(consoleWriter).WriteCommandInput(commandInput);
}

View File

@@ -1,75 +1,74 @@
using System;
using CliFx.Infrastructure;
namespace CliFx.Formatting
namespace CliFx.Formatting;
internal class ConsoleFormatter
{
internal class ConsoleFormatter
private readonly ConsoleWriter _consoleWriter;
private int _column;
private int _row;
public bool IsEmpty => _column == 0 && _row == 0;
public ConsoleFormatter(ConsoleWriter consoleWriter) =>
_consoleWriter = consoleWriter;
public void Write(string? value)
{
private readonly ConsoleWriter _consoleWriter;
_consoleWriter.Write(value);
_column += value?.Length ?? 0;
}
private int _column;
private int _row;
public void Write(char value)
{
_consoleWriter.Write(value);
_column++;
}
public bool IsEmpty => _column == 0 && _row == 0;
public void Write(ConsoleColor foregroundColor, string? value)
{
using (_consoleWriter.Console.WithForegroundColor(foregroundColor))
Write(value);
}
public ConsoleFormatter(ConsoleWriter consoleWriter) =>
_consoleWriter = consoleWriter;
public void Write(ConsoleColor foregroundColor, char value)
{
using (_consoleWriter.Console.WithForegroundColor(foregroundColor))
Write(value);
}
public void Write(string? value)
{
_consoleWriter.Write(value);
_column += value?.Length ?? 0;
}
public void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string? value)
{
using (_consoleWriter.Console.WithColors(foregroundColor, backgroundColor))
Write(value);
}
public void Write(char value)
{
_consoleWriter.Write(value);
_column++;
}
public void WriteLine()
{
_consoleWriter.WriteLine();
_column = 0;
_row++;
}
public void Write(ConsoleColor foregroundColor, string? value)
{
using (_consoleWriter.Console.WithForegroundColor(foregroundColor))
Write(value);
}
public void WriteVerticalMargin(int size = 1)
{
for (var i = 0; i < size; i++)
WriteLine();
}
public void Write(ConsoleColor foregroundColor, char value)
{
using (_consoleWriter.Console.WithForegroundColor(foregroundColor))
Write(value);
}
public void WriteHorizontalMargin(int size = 2)
{
for (var i = 0; i < size; i++)
Write(' ');
}
public void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string? value)
{
using (_consoleWriter.Console.WithColors(foregroundColor, backgroundColor))
Write(value);
}
public void WriteLine()
{
_consoleWriter.WriteLine();
_column = 0;
_row++;
}
public void WriteVerticalMargin(int size = 1)
{
for (var i = 0; i < size; i++)
WriteLine();
}
public void WriteHorizontalMargin(int size = 2)
{
for (var i = 0; i < size; i++)
Write(' ');
}
public void WriteColumnMargin(int columnSize = 20, int offsetSize = 2)
{
if (_column + offsetSize < columnSize)
WriteHorizontalMargin(columnSize - _column);
else
WriteHorizontalMargin(offsetSize);
}
public void WriteColumnMargin(int columnSize = 20, int offsetSize = 2)
{
if (_column + offsetSize < columnSize)
WriteHorizontalMargin(columnSize - _column);
else
WriteHorizontalMargin(offsetSize);
}
}

View File

@@ -4,132 +4,131 @@ using CliFx.Exceptions;
using CliFx.Infrastructure;
using CliFx.Utils;
namespace CliFx.Formatting
namespace CliFx.Formatting;
internal class ExceptionConsoleFormatter : ConsoleFormatter
{
internal class ExceptionConsoleFormatter : ConsoleFormatter
public ExceptionConsoleFormatter(ConsoleWriter consoleWriter)
: base(consoleWriter)
{
public ExceptionConsoleFormatter(ConsoleWriter consoleWriter)
: base(consoleWriter)
}
private void WriteStackFrame(StackFrame stackFrame, int indentLevel)
{
WriteHorizontalMargin(2 + 4 * indentLevel);
Write("at ");
// Fully qualified method name
Write(stackFrame.ParentType + '.');
Write(ConsoleColor.Yellow, stackFrame.MethodName);
// Method parameters
Write('(');
for (var i = 0; i < stackFrame.Parameters.Count; i++)
{
}
var parameter = stackFrame.Parameters[i];
private void WriteStackFrame(StackFrame stackFrame, int indentLevel)
{
WriteHorizontalMargin(2 + 4 * indentLevel);
Write("at ");
// Fully qualified method name
Write(stackFrame.ParentType + '.');
Write(ConsoleColor.Yellow, stackFrame.MethodName);
// Method parameters
Write('(');
for (var i = 0; i < stackFrame.Parameters.Count; i++)
// Separator
if (i > 0)
{
var parameter = stackFrame.Parameters[i];
// Separator
if (i > 0)
{
Write(", ");
}
// Parameter type
Write(ConsoleColor.Blue, parameter.Type);
// Parameter name (can be null for dynamically generated methods)
if (!string.IsNullOrWhiteSpace(parameter.Name))
{
Write(' ');
Write(ConsoleColor.White, parameter.Name);
}
Write(", ");
}
Write(") ");
// Parameter type
Write(ConsoleColor.Blue, parameter.Type);
// Location
if (!string.IsNullOrWhiteSpace(stackFrame.FilePath))
// Parameter name (can be null for dynamically generated methods)
if (!string.IsNullOrWhiteSpace(parameter.Name))
{
var stackFrameDirectoryPath =
Path.GetDirectoryName(stackFrame.FilePath) + Path.DirectorySeparatorChar;
var stackFrameFileName = Path.GetFileName(stackFrame.FilePath);
Write("in ");
// File path
Write(stackFrameDirectoryPath);
Write(ConsoleColor.Yellow, stackFrameFileName);
// Source position
if (!string.IsNullOrWhiteSpace(stackFrame.LineNumber))
{
Write(':');
Write(ConsoleColor.Blue, stackFrame.LineNumber);
}
}
WriteLine();
}
private void WriteException(Exception exception, int indentLevel)
{
WriteHorizontalMargin(4 * indentLevel);
// Fully qualified exception type
var exceptionType = exception.GetType();
Write(exceptionType.Namespace + '.');
Write(ConsoleColor.White, exceptionType.Name);
Write(": ");
// Exception message
Write(ConsoleColor.Red, exception.Message);
WriteLine();
// Recurse into inner exceptions
if (exception.InnerException is not null)
{
WriteException(exception.InnerException, indentLevel + 1);
}
// Non-thrown exceptions (e.g. inner exceptions) have no stacktrace
if (!string.IsNullOrWhiteSpace(exception.StackTrace))
{
// Parse and pretty-print the stacktrace
foreach (var stackFrame in StackFrame.ParseMany(exception.StackTrace))
{
WriteStackFrame(stackFrame, indentLevel);
}
Write(' ');
Write(ConsoleColor.White, parameter.Name);
}
}
public void WriteException(Exception exception)
Write(") ");
// Location
if (!string.IsNullOrWhiteSpace(stackFrame.FilePath))
{
// Domain exceptions should be printed with minimal information
// because they are meant for the user of the application,
// not the user of the library.
if (exception is CliFxException cliFxException &&
!string.IsNullOrWhiteSpace(cliFxException.ActualMessage))
var stackFrameDirectoryPath =
Path.GetDirectoryName(stackFrame.FilePath) + Path.DirectorySeparatorChar;
var stackFrameFileName = Path.GetFileName(stackFrame.FilePath);
Write("in ");
// File path
Write(stackFrameDirectoryPath);
Write(ConsoleColor.Yellow, stackFrameFileName);
// Source position
if (!string.IsNullOrWhiteSpace(stackFrame.LineNumber))
{
Write(ConsoleColor.Red, cliFxException.ActualMessage);
WriteLine();
Write(':');
Write(ConsoleColor.Blue, stackFrame.LineNumber);
}
// All other exceptions most likely indicate an actual bug
// and should include stacktrace and other detailed information.
else
}
WriteLine();
}
private void WriteException(Exception exception, int indentLevel)
{
WriteHorizontalMargin(4 * indentLevel);
// Fully qualified exception type
var exceptionType = exception.GetType();
Write(exceptionType.Namespace + '.');
Write(ConsoleColor.White, exceptionType.Name);
Write(": ");
// Exception message
Write(ConsoleColor.Red, exception.Message);
WriteLine();
// Recurse into inner exceptions
if (exception.InnerException is not null)
{
WriteException(exception.InnerException, indentLevel + 1);
}
// Non-thrown exceptions (e.g. inner exceptions) have no stacktrace
if (!string.IsNullOrWhiteSpace(exception.StackTrace))
{
// Parse and pretty-print the stacktrace
foreach (var stackFrame in StackFrame.ParseMany(exception.StackTrace))
{
Write(ConsoleColor.White, ConsoleColor.DarkRed, "ERROR");
WriteLine();
WriteException(exception, 0);
WriteStackFrame(stackFrame, indentLevel);
}
}
}
internal static class ExceptionConsoleFormatterExtensions
public void WriteException(Exception exception)
{
public static void WriteException(this ConsoleWriter consoleWriter, Exception exception) =>
new ExceptionConsoleFormatter(consoleWriter).WriteException(exception);
// Domain exceptions should be printed with minimal information
// because they are meant for the user of the application,
// not the user of the library.
if (exception is CliFxException cliFxException &&
!string.IsNullOrWhiteSpace(cliFxException.ActualMessage))
{
Write(ConsoleColor.Red, cliFxException.ActualMessage);
WriteLine();
}
// All other exceptions most likely indicate an actual bug
// and should include stacktrace and other detailed information.
else
{
Write(ConsoleColor.White, ConsoleColor.DarkRed, "ERROR");
WriteLine();
WriteException(exception, 0);
}
}
}
internal static class ExceptionConsoleFormatterExtensions
{
public static void WriteException(this ConsoleWriter consoleWriter, Exception exception) =>
new ExceptionConsoleFormatter(consoleWriter).WriteException(exception);
}

View File

@@ -7,444 +7,443 @@ using CliFx.Infrastructure;
using CliFx.Schema;
using CliFx.Utils.Extensions;
namespace CliFx.Formatting
namespace CliFx.Formatting;
internal class HelpConsoleFormatter : ConsoleFormatter
{
internal class HelpConsoleFormatter : ConsoleFormatter
private readonly HelpContext _context;
public HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext context)
: base(consoleWriter)
{
private readonly HelpContext _context;
_context = context;
}
public HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext context)
: base(consoleWriter)
private void WriteHeader(string text)
{
Write(ConsoleColor.White, text.ToUpperInvariant());
WriteLine();
}
private void WriteCommandInvocation()
{
Write(_context.ApplicationMetadata.ExecutableName);
// Command name
if (!string.IsNullOrWhiteSpace(_context.CommandSchema.Name))
{
_context = context;
}
private void WriteHeader(string text)
{
Write(ConsoleColor.White, text.ToUpperInvariant());
WriteLine();
}
private void WriteCommandInvocation()
{
Write(_context.ApplicationMetadata.ExecutableName);
// Command name
if (!string.IsNullOrWhiteSpace(_context.CommandSchema.Name))
{
Write(' ');
Write(ConsoleColor.Cyan, _context.CommandSchema.Name);
}
}
private void WriteApplicationInfo()
{
if (!IsEmpty)
WriteVerticalMargin();
// Title and version
Write(ConsoleColor.White, _context.ApplicationMetadata.Title);
Write(' ');
Write(ConsoleColor.Yellow, _context.ApplicationMetadata.Version);
Write(ConsoleColor.Cyan, _context.CommandSchema.Name);
}
}
private void WriteApplicationInfo()
{
if (!IsEmpty)
WriteVerticalMargin();
// Title and version
Write(ConsoleColor.White, _context.ApplicationMetadata.Title);
Write(' ');
Write(ConsoleColor.Yellow, _context.ApplicationMetadata.Version);
WriteLine();
// Description
if (!string.IsNullOrWhiteSpace(_context.ApplicationMetadata.Description))
{
WriteHorizontalMargin();
Write(_context.ApplicationMetadata.Description);
WriteLine();
// Description
if (!string.IsNullOrWhiteSpace(_context.ApplicationMetadata.Description))
{
WriteHorizontalMargin();
Write(_context.ApplicationMetadata.Description);
WriteLine();
}
}
}
private void WriteCommandUsage()
private void WriteCommandUsage()
{
if (!IsEmpty)
WriteVerticalMargin();
WriteHeader("Usage");
// Current command usage
{
if (!IsEmpty)
WriteVerticalMargin();
WriteHeader("Usage");
// Current command usage
{
WriteHorizontalMargin();
WriteCommandInvocation();
Write(' ');
// Parameters
foreach (var parameter in _context.CommandSchema.Parameters)
{
Write(ConsoleColor.DarkCyan, parameter.Property.IsScalar()
? $"<{parameter.Name}>"
: $"<{parameter.Name}...>"
);
Write(' ');
}
// Required options
foreach (var option in _context.CommandSchema.Options.Where(o => o.IsRequired))
{
Write(ConsoleColor.Yellow, !string.IsNullOrWhiteSpace(option.Name)
? $"--{option.Name}"
: $"-{option.ShortName}"
);
Write(' ');
Write(ConsoleColor.White, option.Property.IsScalar()
? "<value>"
: "<values...>"
);
Write(' ');
}
// Placeholder for non-required options
if (_context.CommandSchema.Options.Any(o => !o.IsRequired))
{
Write(ConsoleColor.Yellow, "[options]");
}
WriteLine();
}
// Child command usage
var childCommandSchemas = _context
.ApplicationSchema
.GetChildCommands(_context.CommandSchema.Name);
if (childCommandSchemas.Any())
{
WriteHorizontalMargin();
WriteCommandInvocation();
Write(' ');
// Placeholder for child command
Write(ConsoleColor.Cyan, "[command]");
Write(' ');
// Placeholder for other arguments
Write("[...]");
WriteLine();
}
}
private void WriteCommandDescription()
{
if (string.IsNullOrWhiteSpace(_context.CommandSchema.Description))
return;
if (!IsEmpty)
WriteVerticalMargin();
WriteHeader("Description");
WriteHorizontalMargin();
Write(_context.CommandSchema.Description);
WriteLine();
}
private void WriteCommandParameters()
{
if (!_context.CommandSchema.Parameters.Any())
return;
if (!IsEmpty)
WriteVerticalMargin();
WriteHeader("Parameters");
foreach (var parameterSchema in _context.CommandSchema.Parameters.OrderBy(p => p.Order))
{
Write(ConsoleColor.Red, "* ");
Write(ConsoleColor.DarkCyan, $"{parameterSchema.Name}");
WriteColumnMargin();
// Description
if (!string.IsNullOrWhiteSpace(parameterSchema.Description))
{
Write(parameterSchema.Description);
Write(' ');
}
// Valid values
var validValues = parameterSchema.Property.GetValidValues();
if (validValues.Any())
{
Write(ConsoleColor.White, "Choices: ");
var isFirst = true;
foreach (var validValue in validValues)
{
if (validValue is null)
continue;
if (isFirst)
{
isFirst = false;
}
else
{
Write(", ");
}
Write('"');
Write(validValue.ToString());
Write('"');
}
Write('.');
Write(' ');
}
WriteLine();
}
}
private void WriteCommandOptions()
{
if (!IsEmpty)
WriteVerticalMargin();
WriteHeader("Options");
foreach (var optionSchema in _context.CommandSchema.Options.OrderByDescending(o => o.IsRequired))
{
if (optionSchema.IsRequired)
{
Write(ConsoleColor.Red, "* ");
}
else
{
WriteHorizontalMargin();
}
// Short name
if (optionSchema.ShortName is not null)
{
Write(ConsoleColor.Yellow, $"-{optionSchema.ShortName}");
}
// Separator
if (!string.IsNullOrWhiteSpace(optionSchema.Name) && optionSchema.ShortName is not null)
{
Write('|');
}
// Name
if (!string.IsNullOrWhiteSpace(optionSchema.Name))
{
Write(ConsoleColor.Yellow, $"--{optionSchema.Name}");
}
WriteColumnMargin();
// Description
if (!string.IsNullOrWhiteSpace(optionSchema.Description))
{
Write(optionSchema.Description);
Write(' ');
}
// Valid values
var validValues = optionSchema.Property.GetValidValues();
if (validValues.Any())
{
Write(ConsoleColor.White, "Choices: ");
var isFirst = true;
foreach (var validValue in validValues)
{
if (validValue is null)
continue;
if (isFirst)
{
isFirst = false;
}
else
{
Write(", ");
}
Write('"');
Write(validValue.ToString());
Write('"');
}
Write('.');
Write(' ');
}
// Environment variable
if (!string.IsNullOrWhiteSpace(optionSchema.EnvironmentVariable))
{
Write(ConsoleColor.White, "Environment variable: ");
Write(optionSchema.EnvironmentVariable);
Write('.');
Write(' ');
}
// Default value
if (!optionSchema.IsRequired)
{
var defaultValue = _context.CommandDefaultValues.GetValueOrDefault(optionSchema);
if (defaultValue is not null)
{
// Non-Scalar
if (defaultValue is not string && defaultValue is IEnumerable defaultValues)
{
var elementType =
defaultValues.GetType().TryGetEnumerableUnderlyingType() ??
typeof(object);
if (elementType.IsToStringOverriden())
{
Write(ConsoleColor.White, "Default: ");
var isFirst = true;
foreach (var element in defaultValues)
{
if (isFirst)
{
isFirst = false;
}
else
{
Write(", ");
}
Write('"');
Write(element.ToString(CultureInfo.InvariantCulture));
Write('"');
}
Write('.');
}
}
else
{
if (defaultValue.GetType().IsToStringOverriden())
{
Write(ConsoleColor.White, "Default: ");
Write('"');
Write(defaultValue.ToString(CultureInfo.InvariantCulture));
Write('"');
Write('.');
}
}
}
}
WriteLine();
}
}
private void WriteCommandChildren()
{
var childCommandSchemas = _context
.ApplicationSchema
.GetChildCommands(_context.CommandSchema.Name);
if (!childCommandSchemas.Any())
return;
if (!IsEmpty)
WriteVerticalMargin();
WriteHeader("Commands");
foreach (var childCommandSchema in childCommandSchemas)
{
// Name
WriteHorizontalMargin();
Write(
ConsoleColor.Cyan,
// Relative to current command
childCommandSchema
.Name?
.Substring(_context.CommandSchema.Name?.Length ?? 0)
.Trim()
);
WriteColumnMargin();
// Description
if (!string.IsNullOrWhiteSpace(childCommandSchema.Description))
{
Write(childCommandSchema.Description);
Write(' ');
}
// Child commands of child command
var grandChildCommandSchemas = _context
.ApplicationSchema
.GetChildCommands(childCommandSchema.Name);
if (grandChildCommandSchemas.Any())
{
Write(ConsoleColor.White, "Subcommands: ");
var isFirst = true;
foreach (var grandChildCommandSchema in grandChildCommandSchemas)
{
if (isFirst)
{
isFirst = false;
}
else
{
Write(", ");
}
Write(
ConsoleColor.Cyan,
// Relative to current command (not the parent)
grandChildCommandSchema
.Name?
.Substring(_context.CommandSchema.Name?.Length ?? 0)
.Trim()
);
}
Write('.');
}
WriteLine();
}
// Child command help tip
WriteVerticalMargin();
Write("You can run `");
WriteCommandInvocation();
Write(' ');
Write(ConsoleColor.Cyan, "[command]");
Write(' ');
Write(ConsoleColor.White, "--help");
Write("` to show help on a specific command.");
// Parameters
foreach (var parameter in _context.CommandSchema.Parameters)
{
Write(ConsoleColor.DarkCyan, parameter.Property.IsScalar()
? $"<{parameter.Name}>"
: $"<{parameter.Name}...>"
);
Write(' ');
}
// Required options
foreach (var option in _context.CommandSchema.Options.Where(o => o.IsRequired))
{
Write(ConsoleColor.Yellow, !string.IsNullOrWhiteSpace(option.Name)
? $"--{option.Name}"
: $"-{option.ShortName}"
);
Write(' ');
Write(ConsoleColor.White, option.Property.IsScalar()
? "<value>"
: "<values...>"
);
Write(' ');
}
// Placeholder for non-required options
if (_context.CommandSchema.Options.Any(o => !o.IsRequired))
{
Write(ConsoleColor.Yellow, "[options]");
}
WriteLine();
}
public void WriteHelpText()
// Child command usage
var childCommandSchemas = _context
.ApplicationSchema
.GetChildCommands(_context.CommandSchema.Name);
if (childCommandSchemas.Any())
{
WriteApplicationInfo();
WriteCommandUsage();
WriteCommandDescription();
WriteCommandParameters();
WriteCommandOptions();
WriteCommandChildren();
WriteHorizontalMargin();
WriteCommandInvocation();
Write(' ');
// Placeholder for child command
Write(ConsoleColor.Cyan, "[command]");
Write(' ');
// Placeholder for other arguments
Write("[...]");
WriteLine();
}
}
internal static class HelpConsoleFormatterExtensions
private void WriteCommandDescription()
{
public static void WriteHelpText(this ConsoleWriter consoleWriter, HelpContext context) =>
new HelpConsoleFormatter(consoleWriter, context).WriteHelpText();
if (string.IsNullOrWhiteSpace(_context.CommandSchema.Description))
return;
if (!IsEmpty)
WriteVerticalMargin();
WriteHeader("Description");
WriteHorizontalMargin();
Write(_context.CommandSchema.Description);
WriteLine();
}
private void WriteCommandParameters()
{
if (!_context.CommandSchema.Parameters.Any())
return;
if (!IsEmpty)
WriteVerticalMargin();
WriteHeader("Parameters");
foreach (var parameterSchema in _context.CommandSchema.Parameters.OrderBy(p => p.Order))
{
Write(ConsoleColor.Red, "* ");
Write(ConsoleColor.DarkCyan, $"{parameterSchema.Name}");
WriteColumnMargin();
// Description
if (!string.IsNullOrWhiteSpace(parameterSchema.Description))
{
Write(parameterSchema.Description);
Write(' ');
}
// Valid values
var validValues = parameterSchema.Property.GetValidValues();
if (validValues.Any())
{
Write(ConsoleColor.White, "Choices: ");
var isFirst = true;
foreach (var validValue in validValues)
{
if (validValue is null)
continue;
if (isFirst)
{
isFirst = false;
}
else
{
Write(", ");
}
Write('"');
Write(validValue.ToString());
Write('"');
}
Write('.');
Write(' ');
}
WriteLine();
}
}
private void WriteCommandOptions()
{
if (!IsEmpty)
WriteVerticalMargin();
WriteHeader("Options");
foreach (var optionSchema in _context.CommandSchema.Options.OrderByDescending(o => o.IsRequired))
{
if (optionSchema.IsRequired)
{
Write(ConsoleColor.Red, "* ");
}
else
{
WriteHorizontalMargin();
}
// Short name
if (optionSchema.ShortName is not null)
{
Write(ConsoleColor.Yellow, $"-{optionSchema.ShortName}");
}
// Separator
if (!string.IsNullOrWhiteSpace(optionSchema.Name) && optionSchema.ShortName is not null)
{
Write('|');
}
// Name
if (!string.IsNullOrWhiteSpace(optionSchema.Name))
{
Write(ConsoleColor.Yellow, $"--{optionSchema.Name}");
}
WriteColumnMargin();
// Description
if (!string.IsNullOrWhiteSpace(optionSchema.Description))
{
Write(optionSchema.Description);
Write(' ');
}
// Valid values
var validValues = optionSchema.Property.GetValidValues();
if (validValues.Any())
{
Write(ConsoleColor.White, "Choices: ");
var isFirst = true;
foreach (var validValue in validValues)
{
if (validValue is null)
continue;
if (isFirst)
{
isFirst = false;
}
else
{
Write(", ");
}
Write('"');
Write(validValue.ToString());
Write('"');
}
Write('.');
Write(' ');
}
// Environment variable
if (!string.IsNullOrWhiteSpace(optionSchema.EnvironmentVariable))
{
Write(ConsoleColor.White, "Environment variable: ");
Write(optionSchema.EnvironmentVariable);
Write('.');
Write(' ');
}
// Default value
if (!optionSchema.IsRequired)
{
var defaultValue = _context.CommandDefaultValues.GetValueOrDefault(optionSchema);
if (defaultValue is not null)
{
// Non-Scalar
if (defaultValue is not string && defaultValue is IEnumerable defaultValues)
{
var elementType =
defaultValues.GetType().TryGetEnumerableUnderlyingType() ??
typeof(object);
if (elementType.IsToStringOverriden())
{
Write(ConsoleColor.White, "Default: ");
var isFirst = true;
foreach (var element in defaultValues)
{
if (isFirst)
{
isFirst = false;
}
else
{
Write(", ");
}
Write('"');
Write(element.ToString(CultureInfo.InvariantCulture));
Write('"');
}
Write('.');
}
}
else
{
if (defaultValue.GetType().IsToStringOverriden())
{
Write(ConsoleColor.White, "Default: ");
Write('"');
Write(defaultValue.ToString(CultureInfo.InvariantCulture));
Write('"');
Write('.');
}
}
}
}
WriteLine();
}
}
private void WriteCommandChildren()
{
var childCommandSchemas = _context
.ApplicationSchema
.GetChildCommands(_context.CommandSchema.Name);
if (!childCommandSchemas.Any())
return;
if (!IsEmpty)
WriteVerticalMargin();
WriteHeader("Commands");
foreach (var childCommandSchema in childCommandSchemas)
{
// Name
WriteHorizontalMargin();
Write(
ConsoleColor.Cyan,
// Relative to current command
childCommandSchema
.Name?
.Substring(_context.CommandSchema.Name?.Length ?? 0)
.Trim()
);
WriteColumnMargin();
// Description
if (!string.IsNullOrWhiteSpace(childCommandSchema.Description))
{
Write(childCommandSchema.Description);
Write(' ');
}
// Child commands of child command
var grandChildCommandSchemas = _context
.ApplicationSchema
.GetChildCommands(childCommandSchema.Name);
if (grandChildCommandSchemas.Any())
{
Write(ConsoleColor.White, "Subcommands: ");
var isFirst = true;
foreach (var grandChildCommandSchema in grandChildCommandSchemas)
{
if (isFirst)
{
isFirst = false;
}
else
{
Write(", ");
}
Write(
ConsoleColor.Cyan,
// Relative to current command (not the parent)
grandChildCommandSchema
.Name?
.Substring(_context.CommandSchema.Name?.Length ?? 0)
.Trim()
);
}
Write('.');
}
WriteLine();
}
// Child command help tip
WriteVerticalMargin();
Write("You can run `");
WriteCommandInvocation();
Write(' ');
Write(ConsoleColor.Cyan, "[command]");
Write(' ');
Write(ConsoleColor.White, "--help");
Write("` to show help on a specific command.");
WriteLine();
}
public void WriteHelpText()
{
WriteApplicationInfo();
WriteCommandUsage();
WriteCommandDescription();
WriteCommandParameters();
WriteCommandOptions();
WriteCommandChildren();
}
}
internal static class HelpConsoleFormatterExtensions
{
public static void WriteHelpText(this ConsoleWriter consoleWriter, HelpContext context) =>
new HelpConsoleFormatter(consoleWriter, context).WriteHelpText();
}

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