diff --git a/CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs index da89e57..8b5567b 100644 --- a/CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/CommandMustImplementInterfaceAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/CommandMustImplementInterfaceAnalyzerSpecs.cs index 9b1abc9..65b9449 100644 --- a/CliFx.Analyzers.Tests/CommandMustImplementInterfaceAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/CommandMustImplementInterfaceAnalyzerSpecs.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/GeneralSpecs.cs b/CliFx.Analyzers.Tests/GeneralSpecs.cs index 7732ee9..041b9a9 100644 --- a/CliFx.Analyzers.Tests/GeneralSpecs.cs +++ b/CliFx.Analyzers.Tests/GeneralSpecs.cs @@ -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(); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/OptionMustBeInsideCommandAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustBeInsideCommandAnalyzerSpecs.cs index e3f78ed..5db9e69 100644 --- a/CliFx.Analyzers.Tests/OptionMustBeInsideCommandAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/OptionMustBeInsideCommandAnalyzerSpecs.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/OptionMustHaveNameOrShortNameAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveNameOrShortNameAnalyzerSpecs.cs index 56a8377..d96ca66 100644 --- a/CliFx.Analyzers.Tests/OptionMustHaveNameOrShortNameAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/OptionMustHaveNameOrShortNameAnalyzerSpecs.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/OptionMustHaveUniqueNameAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveUniqueNameAnalyzerSpecs.cs index c536ef9..e238b62 100644 --- a/CliFx.Analyzers.Tests/OptionMustHaveUniqueNameAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/OptionMustHaveUniqueNameAnalyzerSpecs.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/OptionMustHaveUniqueShortNameAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveUniqueShortNameAnalyzerSpecs.cs index 018951f..d80ddd1 100644 --- a/CliFx.Analyzers.Tests/OptionMustHaveUniqueShortNameAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/OptionMustHaveUniqueShortNameAnalyzerSpecs.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/OptionMustHaveValidConverterAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveValidConverterAnalyzerSpecs.cs index d022665..2c02039 100644 --- a/CliFx.Analyzers.Tests/OptionMustHaveValidConverterAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/OptionMustHaveValidConverterAnalyzerSpecs.cs @@ -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 { 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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs index 7b56cf2..b4352ed 100644 --- a/CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/OptionMustHaveValidShortNameAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveValidShortNameAnalyzerSpecs.cs index d73487f..b077098 100644 --- a/CliFx.Analyzers.Tests/OptionMustHaveValidShortNameAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/OptionMustHaveValidShortNameAnalyzerSpecs.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/OptionMustHaveValidValidatorsAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveValidValidatorsAnalyzerSpecs.cs index 7a854a2..c97ccaf 100644 --- a/CliFx.Analyzers.Tests/OptionMustHaveValidValidatorsAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/OptionMustHaveValidValidatorsAnalyzerSpecs.cs @@ -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 { 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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/ParameterMustBeInsideCommandAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustBeInsideCommandAnalyzerSpecs.cs index 8655a34..d436230 100644 --- a/CliFx.Analyzers.Tests/ParameterMustBeInsideCommandAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/ParameterMustBeInsideCommandAnalyzerSpecs.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/ParameterMustBeLastIfNonScalarAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustBeLastIfNonScalarAnalyzerSpecs.cs index 2118678..f2cac83 100644 --- a/CliFx.Analyzers.Tests/ParameterMustBeLastIfNonScalarAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/ParameterMustBeLastIfNonScalarAnalyzerSpecs.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/ParameterMustBeSingleIfNonScalarAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustBeSingleIfNonScalarAnalyzerSpecs.cs index c17243b..a7901b7 100644 --- a/CliFx.Analyzers.Tests/ParameterMustBeSingleIfNonScalarAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/ParameterMustBeSingleIfNonScalarAnalyzerSpecs.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/ParameterMustHaveUniqueNameAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustHaveUniqueNameAnalyzerSpecs.cs index 77a5278..6fffa25 100644 --- a/CliFx.Analyzers.Tests/ParameterMustHaveUniqueNameAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/ParameterMustHaveUniqueNameAnalyzerSpecs.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/ParameterMustHaveUniqueOrderAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustHaveUniqueOrderAnalyzerSpecs.cs index 6ed24ed..4aab278 100644 --- a/CliFx.Analyzers.Tests/ParameterMustHaveUniqueOrderAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/ParameterMustHaveUniqueOrderAnalyzerSpecs.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/ParameterMustHaveValidConverterAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustHaveValidConverterAnalyzerSpecs.cs index c3635f5..e0b2c3d 100644 --- a/CliFx.Analyzers.Tests/ParameterMustHaveValidConverterAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/ParameterMustHaveValidConverterAnalyzerSpecs.cs @@ -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 { 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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/ParameterMustHaveValidValidatorsAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustHaveValidValidatorsAnalyzerSpecs.cs index 52b810f..349673b 100644 --- a/CliFx.Analyzers.Tests/ParameterMustHaveValidValidatorsAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/ParameterMustHaveValidValidatorsAnalyzerSpecs.cs @@ -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 { 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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/SystemConsoleShouldBeAvoidedAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/SystemConsoleShouldBeAvoidedAnalyzerSpecs.cs index 3687a20..b6d2647 100644 --- a/CliFx.Analyzers.Tests/SystemConsoleShouldBeAvoidedAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/SystemConsoleShouldBeAvoidedAnalyzerSpecs.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs b/CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs index c9e3375..39bc0b1 100644 --- a/CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs +++ b/CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs @@ -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 - { - protected override string Identifier { get; } = "analyzer"; +namespace CliFx.Analyzers.Tests.Utils; - public AnalyzerAssertions(DiagnosticAnalyzer analyzer) - : base(analyzer) +internal class AnalyzerAssertions : ReferenceTypeAssertions +{ + 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 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 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); } \ No newline at end of file diff --git a/CliFx.Analyzers/AnalyzerBase.cs b/CliFx.Analyzers/AnalyzerBase.cs index 3a2ee76..d87615d 100644 --- a/CliFx.Analyzers/AnalyzerBase.cs +++ b/CliFx.Analyzers/AnalyzerBase.cs @@ -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 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 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); } } \ No newline at end of file diff --git a/CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs b/CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs index bc8ebad..e02aade 100644 --- a/CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs +++ b/CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs @@ -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); + } } \ No newline at end of file diff --git a/CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs b/CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs index 9404ca8..2862a51 100644 --- a/CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs +++ b/CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs @@ -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); + } } \ No newline at end of file diff --git a/CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs b/CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs index 3321feb..d8ac629 100644 --- a/CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs +++ b/CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs @@ -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 ValidatorTypes { get; } + + public CommandOptionSymbol( + string? name, + char? shortName, + ITypeSymbol? converterType, + IReadOnlyList 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 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 validatorTypes) - { - Name = name; - ShortName = shortName; - ConverterType = converterType; - ValidatorTypes = validatorTypes; - } + var converter = attribute + .NamedArguments + .Where(a => a.Key == "Converter") + .Select(a => a.Value.Value) + .Cast() + .FirstOrDefault(); + + var validators = attribute + .NamedArguments + .Where(a => a.Key == "Validators") + .SelectMany(a => a.Value.Values) + .Select(c => c.Value) + .Cast() + .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() - .FirstOrDefault(); - - var validators = attribute - .NamedArguments - .Where(a => a.Key == "Validators") - .SelectMany(a => a.Value.Values) - .Select(c => c.Value) - .Cast() - .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; } \ No newline at end of file diff --git a/CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs b/CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs index deaf9f7..2fe4ba5 100644 --- a/CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs +++ b/CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs @@ -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 ValidatorTypes { get; } + + public CommandParameterSymbol( + int order, + string? name, + ITypeSymbol? converterType, + IReadOnlyList 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 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 validatorTypes) - { - Order = order; - Name = name; - ConverterType = converterType; - ValidatorTypes = validatorTypes; - } + var converter = attribute + .NamedArguments + .Where(a => a.Key == "Converter") + .Select(a => a.Value.Value) + .Cast() + .FirstOrDefault(); + + var validators = attribute + .NamedArguments + .Where(a => a.Key == "Validators") + .SelectMany(a => a.Value.Values) + .Select(c => c.Value) + .Cast() + .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() - .FirstOrDefault(); - - var validators = attribute - .NamedArguments - .Where(a => a.Key == "Validators") - .SelectMany(a => a.Value.Values) - .Select(c => c.Value) - .Cast() - .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; } \ No newline at end of file diff --git a/CliFx.Analyzers/ObjectModel/SymbolNames.cs b/CliFx.Analyzers/ObjectModel/SymbolNames.cs index aaa35a8..1d137e2 100644 --- a/CliFx.Analyzers/ObjectModel/SymbolNames.cs +++ b/CliFx.Analyzers/ObjectModel/SymbolNames.cs @@ -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"; - public const string CliFxBindingValidatorInterface = "CliFx.Extensibility.IBindingValidator"; - public const string CliFxBindingValidatorClass = "CliFx.Extensibility.BindingValidator"; - } + 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"; + public const string CliFxBindingValidatorInterface = "CliFx.Extensibility.IBindingValidator"; + public const string CliFxBindingValidatorClass = "CliFx.Extensibility.BindingValidator"; } \ No newline at end of file diff --git a/CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.cs b/CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.cs index fb1395f..1f666e5 100644 --- a/CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.cs +++ b/CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.cs @@ -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); + } } \ No newline at end of file diff --git a/CliFx.Analyzers/OptionMustHaveNameOrShortNameAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveNameOrShortNameAnalyzer.cs index d28387e..66c85b2 100644 --- a/CliFx.Analyzers/OptionMustHaveNameOrShortNameAnalyzer.cs +++ b/CliFx.Analyzers/OptionMustHaveNameOrShortNameAnalyzer.cs @@ -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); + } } \ No newline at end of file diff --git a/CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs index e46934c..da17bad 100644 --- a/CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs +++ b/CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs @@ -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() + .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() - .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); } } \ No newline at end of file diff --git a/CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.cs index b629216..1338a99 100644 --- a/CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.cs +++ b/CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.cs @@ -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() + .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() - .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); } } \ No newline at end of file diff --git a/CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs index 179a322..139a2fc 100644 --- a/CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs +++ b/CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs @@ -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); + } } \ No newline at end of file diff --git a/CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs index 6931a3f..ebec169 100644 --- a/CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs +++ b/CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs @@ -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); + } } \ No newline at end of file diff --git a/CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs index 7a7008d..2ee58a6 100644 --- a/CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs +++ b/CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs @@ -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); + } } \ No newline at end of file diff --git a/CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs index e463510..3297cd6 100644 --- a/CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs +++ b/CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.cs b/CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.cs index ba45b2b..0fe55ad 100644 --- a/CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.cs +++ b/CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.cs @@ -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); + } } \ No newline at end of file diff --git a/CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs b/CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs index 36186ca..0039a0a 100644 --- a/CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs +++ b/CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs @@ -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")); + + 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() + .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")); - - 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() - .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); } } \ No newline at end of file diff --git a/CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs b/CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs index 704d278..db014f6 100644 --- a/CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs +++ b/CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs @@ -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")); + + 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() + .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")); - - 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() - .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); } } \ No newline at end of file diff --git a/CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs b/CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs index 83d0bf3..f35af33 100644 --- a/CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs +++ b/CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs @@ -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() + .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() - .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); } } \ No newline at end of file diff --git a/CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs b/CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs index fa41b52..36cd106 100644 --- a/CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs +++ b/CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs @@ -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() + .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() - .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); } } \ No newline at end of file diff --git a/CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs b/CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs index 69e3df9..aead684 100644 --- a/CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs +++ b/CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs @@ -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); + } } \ No newline at end of file diff --git a/CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs b/CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs index cafec83..8bdf0cd 100644 --- a/CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs +++ b/CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs b/CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs index db63900..ca57ed0 100644 --- a/CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs +++ b/CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs @@ -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() + .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() - .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); + } } \ No newline at end of file diff --git a/CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs b/CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs index 92e93f1..d52de86 100644 --- a/CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs +++ b/CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs @@ -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 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 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 analyze) + public static void HandlePropertyDeclaration( + this AnalysisContext analysisContext, + Action 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); } } \ No newline at end of file diff --git a/CliFx.Analyzers/Utils/Extensions/StringExtensions.cs b/CliFx.Analyzers/Utils/Extensions/StringExtensions.cs index a8d9c7c..df3ed17 100644 --- a/CliFx.Analyzers/Utils/Extensions/StringExtensions.cs +++ b/CliFx.Analyzers/Utils/Extensions/StringExtensions.cs @@ -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; } } \ No newline at end of file diff --git a/CliFx.Benchmarks/Benchmarks.CliFx.cs b/CliFx.Benchmarks/Benchmarks.CliFx.cs index 62ca025..d335202 100644 --- a/CliFx.Benchmarks/Benchmarks.CliFx.cs +++ b/CliFx.Benchmarks/Benchmarks.CliFx.cs @@ -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 ExecuteWithCliFx() => - await new CliApplicationBuilder() - .AddCommand() - .Build() - .RunAsync(Arguments, new Dictionary()); + public ValueTask ExecuteAsync(IConsole console) => default; } + + [Benchmark(Description = "CliFx", Baseline = true)] + public async ValueTask ExecuteWithCliFx() => + await new CliApplicationBuilder() + .AddCommand() + .Build() + .RunAsync(Arguments, new Dictionary()); } \ No newline at end of file diff --git a/CliFx.Benchmarks/Benchmarks.Clipr.cs b/CliFx.Benchmarks/Benchmarks.Clipr.cs index 8f8eb11..7821a1d 100644 --- a/CliFx.Benchmarks/Benchmarks.Clipr.cs +++ b/CliFx.Benchmarks/Benchmarks.Clipr.cs @@ -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(Arguments).Execute(); } + + [Benchmark(Description = "Clipr")] + public void ExecuteWithClipr() => CliParser.Parse(Arguments).Execute(); } \ No newline at end of file diff --git a/CliFx.Benchmarks/Benchmarks.Cocona.cs b/CliFx.Benchmarks/Benchmarks.Cocona.cs index e9f994f..ff5e3e8 100644 --- a/CliFx.Benchmarks/Benchmarks.Cocona.cs +++ b/CliFx.Benchmarks/Benchmarks.Cocona.cs @@ -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(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(Arguments); } \ No newline at end of file diff --git a/CliFx.Benchmarks/Benchmarks.CommandLineParser.cs b/CliFx.Benchmarks/Benchmarks.CommandLineParser.cs index a5a65cb..fe89cb5 100644 --- a/CliFx.Benchmarks/Benchmarks.CommandLineParser.cs +++ b/CliFx.Benchmarks/Benchmarks.CommandLineParser.cs @@ -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(c => c.Execute()); } + + [Benchmark(Description = "CommandLineParser")] + public void ExecuteWithCommandLineParser() => + new Parser() + .ParseArguments(Arguments, typeof(CommandLineParserCommand)) + .WithParsed(c => c.Execute()); } \ No newline at end of file diff --git a/CliFx.Benchmarks/Benchmarks.McMaster.cs b/CliFx.Benchmarks/Benchmarks.McMaster.cs index ab0e827..3b599c7 100644 --- a/CliFx.Benchmarks/Benchmarks.McMaster.cs +++ b/CliFx.Benchmarks/Benchmarks.McMaster.cs @@ -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(Arguments); + public int OnExecute() => 0; } + + [Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] + public int ExecuteWithMcMaster() => CommandLineApplication.Execute(Arguments); } \ No newline at end of file diff --git a/CliFx.Benchmarks/Benchmarks.PowerArgs.cs b/CliFx.Benchmarks/Benchmarks.PowerArgs.cs index 9ce03c7..c6c9aa5 100644 --- a/CliFx.Benchmarks/Benchmarks.PowerArgs.cs +++ b/CliFx.Benchmarks/Benchmarks.PowerArgs.cs @@ -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(Arguments); } + + [Benchmark(Description = "PowerArgs")] + public void ExecuteWithPowerArgs() => Args.InvokeMain(Arguments); } \ No newline at end of file diff --git a/CliFx.Benchmarks/Benchmarks.SystemCommandLine.cs b/CliFx.Benchmarks/Benchmarks.SystemCommandLine.cs index 7475fce..37cd542 100644 --- a/CliFx.Benchmarks/Benchmarks.SystemCommandLine.cs +++ b/CliFx.Benchmarks/Benchmarks.SystemCommandLine.cs @@ -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 ExecuteAsync(string[] args) { - public static int ExecuteHandler(string s, int i, bool b) => 0; - - public Task ExecuteAsync(string[] args) + var command = new RootCommand { - var command = new RootCommand + new Option(new[] {"--str", "-s"}) { - new Option(new[] {"--str", "-s"}) - { - Argument = new Argument() - }, - new Option(new[] {"--int", "-i"}) - { - Argument = new Argument() - }, - new Option(new[] {"--bool", "-b"}) - { - Argument = new Argument() - } - }; + Argument = new Argument() + }, + new Option(new[] {"--int", "-i"}) + { + Argument = new Argument() + }, + new Option(new[] {"--bool", "-b"}) + { + Argument = new Argument() + } + }; - 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 ExecuteWithSystemCommandLine() => - await new SystemCommandLineCommand().ExecuteAsync(Arguments); } + + [Benchmark(Description = "System.CommandLine")] + public async Task ExecuteWithSystemCommandLine() => + await new SystemCommandLineCommand().ExecuteAsync(Arguments); } \ No newline at end of file diff --git a/CliFx.Benchmarks/Benchmarks.cs b/CliFx.Benchmarks/Benchmarks.cs index 0aaaac8..7f1a371 100644 --- a/CliFx.Benchmarks/Benchmarks.cs +++ b/CliFx.Benchmarks/Benchmarks.cs @@ -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( - 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( + DefaultConfig + .Instance + .WithOptions(ConfigOptions.DisableOptimizationsValidator) + ); } \ No newline at end of file diff --git a/CliFx.Demo/Commands/BookAddCommand.cs b/CliFx.Demo/Commands/BookAddCommand.cs index b552978..a8f4e5d 100644 --- a/CliFx.Demo/Commands/BookAddCommand.cs +++ b/CliFx.Demo/Commands/BookAddCommand.cs @@ -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) + ); } \ No newline at end of file diff --git a/CliFx.Demo/Commands/BookCommand.cs b/CliFx.Demo/Commands/BookCommand.cs index 59869a7..c5769aa 100644 --- a/CliFx.Demo/Commands/BookCommand.cs +++ b/CliFx.Demo/Commands/BookCommand.cs @@ -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; } } \ No newline at end of file diff --git a/CliFx.Demo/Commands/BookListCommand.cs b/CliFx.Demo/Commands/BookListCommand.cs index 61ea1db..d75b11b 100644 --- a/CliFx.Demo/Commands/BookListCommand.cs +++ b/CliFx.Demo/Commands/BookListCommand.cs @@ -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; } } \ No newline at end of file diff --git a/CliFx.Demo/Commands/BookRemoveCommand.cs b/CliFx.Demo/Commands/BookRemoveCommand.cs index 30657c5..04c1e1f 100644 --- a/CliFx.Demo/Commands/BookRemoveCommand.cs +++ b/CliFx.Demo/Commands/BookRemoveCommand.cs @@ -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; } } \ No newline at end of file diff --git a/CliFx.Demo/Domain/Book.cs b/CliFx.Demo/Domain/Book.cs index b24ec7f..24266bc 100644 --- a/CliFx.Demo/Domain/Book.cs +++ b/CliFx.Demo/Domain/Book.cs @@ -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; } } \ No newline at end of file diff --git a/CliFx.Demo/Domain/Isbn.cs b/CliFx.Demo/Domain/Isbn.cs index 5266dfe..8fdbdfb 100644 --- a/CliFx.Demo/Domain/Isbn.cs +++ b/CliFx.Demo/Domain/Isbn.cs @@ -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) + ); } } \ No newline at end of file diff --git a/CliFx.Demo/Domain/Library.cs b/CliFx.Demo/Domain/Library.cs index cebe0e8..96137a9 100644 --- a/CliFx.Demo/Domain/Library.cs +++ b/CliFx.Demo/Domain/Library.cs @@ -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 Books { get; } + + public Library(IReadOnlyList books) { - public IReadOnlyList Books { get; } - - public Library(IReadOnlyList 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()); + 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()); } \ No newline at end of file diff --git a/CliFx.Demo/Domain/LibraryProvider.cs b/CliFx.Demo/Domain/LibraryProvider.cs index 4fe2137..3ebc04c 100644 --- a/CliFx.Demo/Domain/LibraryProvider.cs +++ b/CliFx.Demo/Domain/LibraryProvider.cs @@ -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(data) ?? Library.Empty; + } - return JsonConvert.DeserializeObject(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); } } \ No newline at end of file diff --git a/CliFx.Demo/Program.cs b/CliFx.Demo/Program.cs index 9773537..e696b7c 100644 --- a/CliFx.Demo/Program.cs +++ b/CliFx.Demo/Program.cs @@ -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(); + // Register services + services.AddSingleton(); - // Register commands - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + // Register commands + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); - return services.BuildServiceProvider(); - } - - public static async Task Main() => - await new CliApplicationBuilder() - .SetDescription("Demo application showcasing CliFx features.") - .AddCommandsFromThisAssembly() - .UseTypeActivator(GetServiceProvider().GetRequiredService) - .Build() - .RunAsync(); + return services.BuildServiceProvider(); } + + public static async Task Main() => + await new CliApplicationBuilder() + .SetDescription("Demo application showcasing CliFx features.") + .AddCommandsFromThisAssembly() + .UseTypeActivator(GetServiceProvider().GetRequiredService) + .Build() + .RunAsync(); } \ No newline at end of file diff --git a/CliFx.Demo/Utils/ConsoleExtensions.cs b/CliFx.Demo/Utils/ConsoleExtensions.cs index b3ce89a..2c3f5a5 100644 --- a/CliFx.Demo/Utils/ConsoleExtensions.cs +++ b/CliFx.Demo/Utils/ConsoleExtensions.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Tests.Dummy/Commands/ConsoleTestCommand.cs b/CliFx.Tests.Dummy/Commands/ConsoleTestCommand.cs index f81f8bc..19ede61 100644 --- a/CliFx.Tests.Dummy/Commands/ConsoleTestCommand.cs +++ b/CliFx.Tests.Dummy/Commands/ConsoleTestCommand.cs @@ -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; } } \ No newline at end of file diff --git a/CliFx.Tests.Dummy/Commands/EnvironmentTestCommand.cs b/CliFx.Tests.Dummy/Commands/EnvironmentTestCommand.cs index cf5f4b4..c0a09d4 100644 --- a/CliFx.Tests.Dummy/Commands/EnvironmentTestCommand.cs +++ b/CliFx.Tests.Dummy/Commands/EnvironmentTestCommand.cs @@ -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; } } \ No newline at end of file diff --git a/CliFx.Tests.Dummy/Program.cs b/CliFx.Tests.Dummy/Program.cs index 740078e..209367a 100644 --- a/CliFx.Tests.Dummy/Program.cs +++ b/CliFx.Tests.Dummy/Program.cs @@ -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(); } \ No newline at end of file diff --git a/CliFx.Tests/ApplicationSpecs.cs b/CliFx.Tests/ApplicationSpecs.cs index ad469fc..36a7e0c 100644 --- a/CliFx.Tests/ApplicationSpecs.cs +++ b/CliFx.Tests/ApplicationSpecs.cs @@ -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(), - new Dictionary() - ); + var exitCode = await app.RunAsync( + Array.Empty(), + new Dictionary() + ); - // 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() - .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() + .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(), - new Dictionary() - ); + var exitCode = await app.RunAsync( + Array.Empty(), + new Dictionary() + ); - // 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(), - new Dictionary() - ); + var exitCode = await app.RunAsync( + Array.Empty(), + new Dictionary() + ); - 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"); } } \ No newline at end of file diff --git a/CliFx.Tests/CancellationSpecs.cs b/CliFx.Tests/CancellationSpecs.cs index 4bbc166..8abff3e 100644 --- a/CliFx.Tests/CancellationSpecs.cs +++ b/CliFx.Tests/CancellationSpecs.cs @@ -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(), - new Dictionary() - ); + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - 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"); } } \ No newline at end of file diff --git a/CliFx.Tests/ConsoleSpecs.cs b/CliFx.Tests/ConsoleSpecs.cs index 1ebe2db..6341dab 100644 --- a/CliFx.Tests/ConsoleSpecs.cs +++ b/CliFx.Tests/ConsoleSpecs.cs @@ -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(), - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - // 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(), - new Dictionary() - ); + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - 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"); } } \ No newline at end of file diff --git a/CliFx.Tests/ConversionSpecs.cs b/CliFx.Tests/ConversionSpecs.cs index 7e182bc..8761223 100644 --- a/CliFx.Tests/ConversionSpecs.cs +++ b/CliFx.Tests/ConversionSpecs.cs @@ -6,22 +6,22 @@ using FluentAssertions; using Xunit; using Xunit.Abstractions; -namespace CliFx.Tests -{ - public class ConversionSpecs : SpecsBase - { - public ConversionSpecs(ITestOutputHelper testOutput) - : base(testOutput) - { - } +namespace CliFx.Tests; - [Fact] - public async Task Parameter_or_option_value_can_be_converted_to_a_string() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" +public class ConversionSpecs : SpecsBase +{ + public ConversionSpecs(ITestOutputHelper testOutput) + : base(testOutput) + { + } + + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_a_string() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -35,31 +35,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", "xyz"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "xyz"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Trim().Should().Be("xyz"); - } + // Assert + exitCode.Should().Be(0); + stdOut.Trim().Should().Be("xyz"); + } - [Fact] - public async Task Parameter_or_option_value_can_be_converted_to_an_object() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_an_object() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -73,31 +73,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", "xyz"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "xyz"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Trim().Should().Be("xyz"); - } + // Assert + exitCode.Should().Be(0); + stdOut.Trim().Should().Be("xyz"); + } - [Fact] - public async Task Parameter_or_option_value_can_be_converted_to_a_boolean() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_a_boolean() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -116,34 +116,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", "true", "-b", "false"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "true", "-b", "false"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ConsistOfLines( - "Foo = True", - "Bar = False" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ConsistOfLines( + "Foo = True", + "Bar = False" + ); + } - [Fact] - public async Task Parameter_or_option_value_can_be_converted_to_a_boolean_with_implicit_value() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_a_boolean_with_implicit_value() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -157,31 +157,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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f"}, + new Dictionary() + ); - 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 Parameter_or_option_value_can_be_converted_to_an_integer() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_an_integer() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -195,31 +195,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", "32"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "32"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Trim().Should().Be("32"); - } + // Assert + exitCode.Should().Be(0); + stdOut.Trim().Should().Be("32"); + } - [Fact] - public async Task Parameter_or_option_value_can_be_converted_to_a_double() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_a_double() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -233,31 +233,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", "32.14"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "32.14"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Trim().Should().Be("32.14"); - } + // Assert + exitCode.Should().Be(0); + stdOut.Trim().Should().Be("32.14"); + } - [Fact] - public async Task Parameter_or_option_value_can_be_converted_to_DateTimeOffset() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_DateTimeOffset() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -271,31 +271,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", "1995-04-28Z"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "1995-04-28Z"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Trim().Should().Be("1995-04-28 00:00:00Z"); - } + // Assert + exitCode.Should().Be(0); + stdOut.Trim().Should().Be("1995-04-28 00:00:00Z"); + } - [Fact] - public async Task Parameter_or_option_value_can_be_converted_to_a_TimeSpan() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_a_TimeSpan() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -309,31 +309,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", "12:34:56"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "12:34:56"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Trim().Should().Be("12:34:56"); - } + // Assert + exitCode.Should().Be(0); + stdOut.Trim().Should().Be("12:34:56"); + } - [Fact] - public async Task Parameter_or_option_value_can_be_converted_to_an_enum() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_an_enum() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" public enum CustomEnum { One = 1, Two = 2, Three = 3 } [Command] @@ -349,31 +349,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", "two"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "two"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Trim().Should().Be("2"); - } + // Assert + exitCode.Should().Be(0); + stdOut.Trim().Should().Be("2"); + } - [Fact] - public async Task Parameter_or_option_value_can_be_converted_to_a_nullable_integer() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_a_nullable_integer() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -392,34 +392,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[] {"-b", "123"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-b", "123"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ConsistOfLines( - "Foo = ", - "Bar = 123" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ConsistOfLines( + "Foo = ", + "Bar = 123" + ); + } - [Fact] - public async Task Parameter_or_option_value_can_be_converted_to_a_nullable_enum() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_a_nullable_enum() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" public enum CustomEnum { One = 1, Two = 2, Three = 3 } [Command] @@ -440,34 +440,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[] {"-b", "two"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-b", "two"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ConsistOfLines( - "Foo = ", - "Bar = 2" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ConsistOfLines( + "Foo = ", + "Bar = 2" + ); + } - [Fact] - public async Task Parameter_or_option_value_can_be_converted_to_a_type_that_has_a_constructor_accepting_a_string() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_a_type_that_has_a_constructor_accepting_a_string() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" public class CustomType { public string Value { get; } @@ -488,31 +488,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", "xyz"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "xyz"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Trim().Should().Be("xyz"); - } + // Assert + exitCode.Should().Be(0); + stdOut.Trim().Should().Be("xyz"); + } - [Fact] - public async Task Parameter_or_option_value_can_be_converted_to_a_type_that_has_a_static_parse_method() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_a_type_that_has_a_static_parse_method() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" public class CustomTypeA { public string Value { get; } @@ -551,34 +551,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", "hello", "-b", "world"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "hello", "-b", "world"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ConsistOfLines( - "Foo = hello", - "Bar = world" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ConsistOfLines( + "Foo = hello", + "Bar = world" + ); + } - [Fact] - public async Task Parameter_or_option_value_can_be_converted_using_a_custom_converter() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_using_a_custom_converter() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" public class CustomConverter : BindingConverter { public override int Convert(string rawValue) => @@ -598,31 +598,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", "hello world"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "hello world"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Trim().Should().Be("11"); - } + // Assert + exitCode.Should().Be(0); + stdOut.Trim().Should().Be("11"); + } - [Fact] - public async Task Parameter_or_option_value_can_be_converted_to_an_array_of_strings() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_an_array_of_strings() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -638,35 +638,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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "one", "two", "three"}, + new Dictionary() + ); - 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 Parameter_or_option_value_can_be_converted_to_a_read_only_list_of_strings() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_a_read_only_list_of_strings() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -682,35 +682,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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "one", "two", "three"}, + new Dictionary() + ); - 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 Parameter_or_option_value_can_be_converted_to_a_list_of_strings() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_a_list_of_strings() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -726,35 +726,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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "one", "two", "three"}, + new Dictionary() + ); - 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 Parameter_or_option_value_can_be_converted_to_an_array_of_integers() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_can_be_converted_to_an_array_of_integers() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -770,35 +770,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", "1", "13", "27"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "1", "13", "27"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ConsistOfLines( - "1", - "13", - "27" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ConsistOfLines( + "1", + "13", + "27" + ); + } - [Fact] - public async Task Parameter_or_option_value_conversion_fails_if_the_value_cannot_be_converted_to_the_target_type() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_conversion_fails_if_the_value_cannot_be_converted_to_the_target_type() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -808,31 +808,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[] {"-f", "12.34"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "12.34"}, + new Dictionary() + ); - var stdErr = FakeConsole.ReadErrorString(); + var stdErr = FakeConsole.ReadErrorString(); - // Assert - exitCode.Should().NotBe(0); - stdErr.Should().NotBeNullOrWhiteSpace(); - } + // Assert + exitCode.Should().NotBe(0); + stdErr.Should().NotBeNullOrWhiteSpace(); + } - [Fact] - public async Task Parameter_or_option_value_conversion_fails_if_the_target_type_is_not_supported() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_conversion_fails_if_the_target_type_is_not_supported() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" public class CustomType {} [Command] @@ -844,31 +844,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[] {"-f", "xyz"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "xyz"}, + new Dictionary() + ); - var stdErr = FakeConsole.ReadErrorString(); + var stdErr = FakeConsole.ReadErrorString(); - // Assert - exitCode.Should().NotBe(0); - stdErr.Should().Contain("has an unsupported underlying property type"); - } + // Assert + exitCode.Should().NotBe(0); + stdErr.Should().Contain("has an unsupported underlying property type"); + } - [Fact] - public async Task Parameter_or_option_value_conversion_fails_if_the_target_non_scalar_type_is_not_supported() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_conversion_fails_if_the_target_non_scalar_type_is_not_supported() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" public class CustomType : IEnumerable { public IEnumerator GetEnumerator() => Enumerable.Empty().GetEnumerator(); @@ -885,31 +885,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[] {"-f", "one", "two"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "one", "two"}, + new Dictionary() + ); - var stdErr = FakeConsole.ReadErrorString(); + var stdErr = FakeConsole.ReadErrorString(); - // Assert - exitCode.Should().NotBe(0); - stdErr.Should().Contain("has an unsupported underlying property type"); - } + // Assert + exitCode.Should().NotBe(0); + stdErr.Should().Contain("has an unsupported underlying property type"); + } - [Fact] - public async Task Parameter_or_option_value_conversion_fails_if_one_of_the_validators_fail() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_conversion_fails_if_one_of_the_validators_fail() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" public class ValidatorA : BindingValidator { public override BindingValidationError Validate(int value) => Ok(); @@ -929,31 +929,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[] {"-f", "12"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "12"}, + new Dictionary() + ); - var stdErr = FakeConsole.ReadErrorString(); + var stdErr = FakeConsole.ReadErrorString(); - // Assert - exitCode.Should().NotBe(0); - stdErr.Should().Contain("Hello world"); - } + // Assert + exitCode.Should().NotBe(0); + stdErr.Should().Contain("Hello world"); + } - [Fact] - public async Task Parameter_or_option_value_conversion_fails_if_the_static_parse_method_throws() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Parameter_or_option_value_conversion_fails_if_the_static_parse_method_throws() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" public class CustomType { public string Value { get; } @@ -972,22 +972,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[] {"-f", "bar"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "bar"}, + new Dictionary() + ); - var stdErr = FakeConsole.ReadErrorString(); + var stdErr = FakeConsole.ReadErrorString(); - // Assert - exitCode.Should().NotBe(0); - stdErr.Should().Contain("Hello world"); - } + // Assert + exitCode.Should().NotBe(0); + stdErr.Should().Contain("Hello world"); } } \ No newline at end of file diff --git a/CliFx.Tests/DirectivesSpecs.cs b/CliFx.Tests/DirectivesSpecs.cs index d4707ac..25fb10f 100644 --- a/CliFx.Tests/DirectivesSpecs.cs +++ b/CliFx.Tests/DirectivesSpecs.cs @@ -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 - { - ["ENV_QOP"] = "hello", - ["ENV_KIL"] = "world" - } - ); + // Act + var exitCode = await application.RunAsync( + new[] {"[preview]", "cmd", "param", "-abc", "--option", "foo"}, + new Dictionary + { + ["ENV_QOP"] = "hello", + ["ENV_KIL"] = "world" + } + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "cmd", "", "[-a]", "[-b]", "[-c]", "[--option \"foo\"]", - "ENV_QOP", "=", "\"hello\"", - "ENV_KIL", "=", "\"world\"" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "cmd", "", "[-a]", "[-b]", "[-c]", "[--option \"foo\"]", + "ENV_QOP", "=", "\"hello\"", + "ENV_KIL", "=", "\"world\"" + ); } } \ No newline at end of file diff --git a/CliFx.Tests/EnvironmentSpecs.cs b/CliFx.Tests/EnvironmentSpecs.cs index fea1f26..1af03c8 100644 --- a/CliFx.Tests/EnvironmentSpecs.cs +++ b/CliFx.Tests/EnvironmentSpecs.cs @@ -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(), - new Dictionary - { - ["ENV_FOO"] = "bar" - } - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary + { + ["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 - { - ["ENV_FOO"] = "bar" - } - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--foo", "baz"}, + new Dictionary + { + ["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(), - new Dictionary - { - ["ENV_FOO"] = $"bar{Path.PathSeparator}baz" - } - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary + { + ["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(), - new Dictionary - { - ["ENV_FOO"] = $"bar{Path.PathSeparator}baz" - } - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary + { + ["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(), - new Dictionary - { - ["ENV_foo"] = "baz", - ["ENV_FOO"] = "bar", - ["env_FOO"] = "qop" - } - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary + { + ["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!"); } } \ No newline at end of file diff --git a/CliFx.Tests/ErrorReportingSpecs.cs b/CliFx.Tests/ErrorReportingSpecs.cs index ea34eb8..3b7251f 100644 --- a/CliFx.Tests/ErrorReportingSpecs.cs +++ b/CliFx.Tests/ErrorReportingSpecs.cs @@ -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(), - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - 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(), - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - 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(), - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - 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(), - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - 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(), - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - 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"); } } \ No newline at end of file diff --git a/CliFx.Tests/HelpTextSpecs.cs b/CliFx.Tests/HelpTextSpecs.cs index 325eb09..d9fb170 100644 --- a/CliFx.Tests/HelpTextSpecs.cs +++ b/CliFx.Tests/HelpTextSpecs.cs @@ -7,44 +7,44 @@ using FluentAssertions; using Xunit; using Xunit.Abstractions; -namespace CliFx.Tests +namespace CliFx.Tests; + +public class HelpTextSpecs : SpecsBase { - public class HelpTextSpecs : SpecsBase + public HelpTextSpecs(ITestOutputHelper testOutput) + : base(testOutput) { - public HelpTextSpecs(ITestOutputHelper testOutput) - : base(testOutput) - { - } + } - [Fact] - public async Task Help_text_is_printed_if_no_arguments_are_provided_and_the_default_command_is_not_defined() - { - // Arrange - var application = new CliApplicationBuilder() - .UseConsole(FakeConsole) - .SetDescription("This will be in help text") - .Build(); + [Fact] + public async Task Help_text_is_printed_if_no_arguments_are_provided_and_the_default_command_is_not_defined() + { + // Arrange + var application = new CliApplicationBuilder() + .UseConsole(FakeConsole) + .SetDescription("This will be in help text") + .Build(); - // Act - var exitCode = await application.RunAsync( - Array.Empty(), - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().Contain("This will be in help text"); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().Contain("This will be in help text"); + } - [Fact] - public async Task Help_text_is_printed_if_provided_arguments_contain_the_help_option() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Help_text_is_printed_if_provided_arguments_contain_the_help_option() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class DefaultCommand : ICommand { @@ -52,32 +52,32 @@ public class DefaultCommand : 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( - new[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().Contain("This will be in help text"); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().Contain("This will be in help text"); + } - [Fact] - public async Task Help_text_is_printed_if_provided_arguments_contain_the_help_option_even_if_the_default_command_is_not_defined() - { - // Arrange - var commandTypes = DynamicCommandBuilder.CompileMany( - // language=cs - @" + [Fact] + public async Task Help_text_is_printed_if_provided_arguments_contain_the_help_option_even_if_the_default_command_is_not_defined() + { + // Arrange + var commandTypes = DynamicCommandBuilder.CompileMany( + // language=cs + @" [Command(""cmd"")] public class NamedCommand : ICommand { @@ -91,32 +91,32 @@ public class NamedChildCommand : ICommand } "); - var application = new CliApplicationBuilder() - .AddCommands(commandTypes) - .UseConsole(FakeConsole) - .SetDescription("This will be in help text") - .Build(); + var application = new CliApplicationBuilder() + .AddCommands(commandTypes) + .UseConsole(FakeConsole) + .SetDescription("This will be in help text") + .Build(); - // Act - var exitCode = await application.RunAsync( - new[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().Contain("This will be in help text"); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().Contain("This will be in help text"); + } - [Fact] - public async Task Help_text_for_a_specific_named_command_is_printed_if_provided_arguments_match_its_name_and_contain_the_help_option() - { - // Arrange - var commandTypes = DynamicCommandBuilder.CompileMany( - // language=cs - @" + [Fact] + public async Task Help_text_for_a_specific_named_command_is_printed_if_provided_arguments_match_its_name_and_contain_the_help_option() + { + // Arrange + var commandTypes = DynamicCommandBuilder.CompileMany( + // language=cs + @" [Command] public class DefaultCommand : ICommand { @@ -136,31 +136,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", "--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"cmd", "--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().Contain("Description of a named command."); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().Contain("Description of a named command."); + } - [Fact] - public async Task Help_text_for_a_specific_named_child_command_is_printed_if_provided_arguments_match_its_name_and_contain_the_help_option() - { - // Arrange - var commandTypes = DynamicCommandBuilder.CompileMany( - // language=cs - @" + [Fact] + public async Task Help_text_for_a_specific_named_child_command_is_printed_if_provided_arguments_match_its_name_and_contain_the_help_option() + { + // Arrange + var commandTypes = DynamicCommandBuilder.CompileMany( + // language=cs + @" [Command] public class DefaultCommand : ICommand { @@ -180,84 +180,84 @@ 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", "sub", "--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"cmd", "sub", "--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().Contain("Description of a named child command."); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().Contain("Description of a named child command."); + } - [Fact] - public async Task Help_text_is_printed_on_invalid_user_input() - { - // Arrange - var application = new CliApplicationBuilder() - .AddCommand() - .UseConsole(FakeConsole) - .SetDescription("This will be in help text") - .Build(); + [Fact] + public async Task Help_text_is_printed_on_invalid_user_input() + { + // Arrange + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(FakeConsole) + .SetDescription("This will be in help text") + .Build(); - // Act - var exitCode = await application.RunAsync( - new[] {"invalid-command", "--invalid-option"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"invalid-command", "--invalid-option"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); - var stdErr = FakeConsole.ReadErrorString(); + var stdOut = FakeConsole.ReadOutputString(); + var stdErr = FakeConsole.ReadErrorString(); - // Assert - exitCode.Should().NotBe(0); - stdOut.Should().Contain("This will be in help text"); - stdErr.Should().NotBeNullOrWhiteSpace(); - } + // Assert + exitCode.Should().NotBe(0); + stdOut.Should().Contain("This will be in help text"); + stdErr.Should().NotBeNullOrWhiteSpace(); + } - [Fact] - public async Task Help_text_shows_application_metadata() - { - // Arrange - var application = new CliApplicationBuilder() - .UseConsole(FakeConsole) - .SetTitle("App title") - .SetDescription("App description") - .SetVersion("App version") - .Build(); + [Fact] + public async Task Help_text_shows_application_metadata() + { + // Arrange + var application = new CliApplicationBuilder() + .UseConsole(FakeConsole) + .SetTitle("App title") + .SetDescription("App description") + .SetVersion("App version") + .Build(); - // Act - var exitCode = await application.RunAsync( - new[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAll( - "App title", - "App description", - "App version" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAll( + "App title", + "App description", + "App version" + ); + } - [Fact] - public async Task Help_text_shows_command_description() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Help_text_shows_command_description() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command(Description = ""Description of the default command."")] public class DefaultCommand : ICommand { @@ -265,34 +265,34 @@ public class DefaultCommand : 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[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "DESCRIPTION", - "Description of the default command." - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "DESCRIPTION", + "Description of the default command." + ); + } - [Fact] - public async Task Help_text_shows_usage_format_which_indicates_how_to_execute_a_named_command() - { - // Arrange - var commandTypes = DynamicCommandBuilder.CompileMany( - // language=cs - @" + [Fact] + public async Task Help_text_shows_usage_format_which_indicates_how_to_execute_a_named_command() + { + // Arrange + var commandTypes = DynamicCommandBuilder.CompileMany( + // language=cs + @" [Command] public class DefaultCommand : ICommand { @@ -306,34 +306,34 @@ public class NamedCommand : 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[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "USAGE", - "[command]", "[...]" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "USAGE", + "[command]", "[...]" + ); + } - [Fact] - public async Task Help_text_shows_usage_format_which_lists_all_parameters() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Help_text_shows_usage_format_which_lists_all_parameters() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -350,34 +350,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[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "USAGE", - "", "", "" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "USAGE", + "", "", "" + ); + } - [Fact] - public async Task Help_text_shows_usage_format_which_lists_all_required_options() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Help_text_shows_usage_format_which_lists_all_required_options() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -394,34 +394,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[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "USAGE", - "--foo ", "--baz ", "[options]" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "USAGE", + "--foo ", "--baz ", "[options]" + ); + } - [Fact] - public async Task Help_text_shows_all_parameters_and_options() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Help_text_shows_all_parameters_and_options() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -435,36 +435,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( - new[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "PARAMETERS", - "foo", "Description of foo.", - "OPTIONS", - "--bar", "Description of bar." - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "PARAMETERS", + "foo", "Description of foo.", + "OPTIONS", + "--bar", "Description of bar." + ); + } - [Fact] - public async Task Help_text_shows_the_implicit_help_and_version_options_on_the_default_command() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Help_text_shows_the_implicit_help_and_version_options_on_the_default_command() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command] public class Command : ICommand { @@ -472,35 +472,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[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "OPTIONS", - "-h", "--help", "Shows help text", - "--version", "Shows version information" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "OPTIONS", + "-h", "--help", "Shows help text", + "--version", "Shows version information" + ); + } - [Fact] - public async Task Help_text_shows_the_implicit_help_option_but_not_the_version_option_on_a_named_command() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Help_text_shows_the_implicit_help_option_but_not_the_version_option_on_a_named_command() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" [Command(""cmd"")] public class Command : ICommand { @@ -508,37 +508,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[] {"cmd", "--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"cmd", "--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "OPTIONS", - "-h", "--help", "Shows help text" - ); - stdOut.Should().NotContainAny( - "--version", "Shows version information" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "OPTIONS", + "-h", "--help", "Shows help text" + ); + stdOut.Should().NotContainAny( + "--version", "Shows version information" + ); + } - [Fact] - public async Task Help_text_shows_all_valid_values_for_enum_parameters_and_options() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Help_text_shows_all_valid_values_for_enum_parameters_and_options() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" public enum CustomEnum { One, Two, Three } [Command] @@ -554,36 +554,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( - new[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "PARAMETERS", - "foo", "Choices:", "One", "Two", "Three", - "OPTIONS", - "--bar", "Choices:", "One", "Two", "Three" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "PARAMETERS", + "foo", "Choices:", "One", "Two", "Three", + "OPTIONS", + "--bar", "Choices:", "One", "Two", "Three" + ); + } - [Fact] - public async Task Help_text_shows_all_valid_values_for_non_scalar_enum_parameters_and_options() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Help_text_shows_all_valid_values_for_non_scalar_enum_parameters_and_options() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" public enum CustomEnum { One, Two, Three } [Command] @@ -599,36 +599,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( - new[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "PARAMETERS", - "foo", "Choices:", "One", "Two", "Three", - "OPTIONS", - "--bar", "Choices:", "One", "Two", "Three" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "PARAMETERS", + "foo", "Choices:", "One", "Two", "Three", + "OPTIONS", + "--bar", "Choices:", "One", "Two", "Three" + ); + } - [Fact] - public async Task Help_text_shows_all_valid_values_for_nullable_enum_parameters_and_options() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Help_text_shows_all_valid_values_for_nullable_enum_parameters_and_options() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" public enum CustomEnum { One, Two, Three } [Command] @@ -644,36 +644,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( - new[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "PARAMETERS", - "foo", "Choices:", "One", "Two", "Three", - "OPTIONS", - "--bar", "Choices:", "One", "Two", "Three" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "PARAMETERS", + "foo", "Choices:", "One", "Two", "Three", + "OPTIONS", + "--bar", "Choices:", "One", "Two", "Three" + ); + } - [Fact] - public async Task Help_text_shows_environment_variables_for_options_that_have_them_configured_as_fallback() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Help_text_shows_environment_variables_for_options_that_have_them_configured_as_fallback() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" public enum CustomEnum { One, Two, Three } [Command] @@ -689,35 +689,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[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "OPTIONS", - "--foo", "Environment variable:", "ENV_FOO", - "--bar", "Environment variable:", "ENV_BAR" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "OPTIONS", + "--foo", "Environment variable:", "ENV_FOO", + "--bar", "Environment variable:", "ENV_BAR" + ); + } - [Fact] - public async Task Help_text_shows_default_values_for_non_required_options() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" + [Fact] + public async Task Help_text_shows_default_values_for_non_required_options() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" public enum CustomEnum { One, Two, Three } [Command] @@ -751,41 +751,41 @@ 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[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "OPTIONS", - "--foo", "Default:", "42", - "--bar", "Default:", "hello", - "--baz", "Default:", "one", "two", "three", - "--qwe", "Default:", "True", - "--qop", "Default:", "1337", - "--zor", "Default:", "02:03:00", - "--lol", "Default:", "Two" - ); - stdOut.Should().NotContain("not printed"); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "OPTIONS", + "--foo", "Default:", "42", + "--bar", "Default:", "hello", + "--baz", "Default:", "one", "two", "three", + "--qwe", "Default:", "True", + "--qop", "Default:", "1337", + "--zor", "Default:", "02:03:00", + "--lol", "Default:", "Two" + ); + stdOut.Should().NotContain("not printed"); + } - [Fact] - public async Task Help_text_shows_all_immediate_child_commands() - { - // Arrange - var commandTypes = DynamicCommandBuilder.CompileMany( - // language=cs - @" + [Fact] + public async Task Help_text_shows_all_immediate_child_commands() + { + // Arrange + var commandTypes = DynamicCommandBuilder.CompileMany( + // language=cs + @" [Command(""cmd1"", Description = ""Description of one command."")] public class FirstCommand : ICommand { @@ -805,41 +805,41 @@ public class SecondCommandChildCommand : 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[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "COMMANDS", - "cmd1", "Description of one command.", - "cmd2", "Description of another command." - ); + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "COMMANDS", + "cmd1", "Description of one command.", + "cmd2", "Description of another command." + ); - // `cmd2 child` will still appear in the list of `cmd2` subcommands, - // but its description will not be seen. - stdOut.Should().NotContain( - "Description of another command's child command." - ); - } + // `cmd2 child` will still appear in the list of `cmd2` subcommands, + // but its description will not be seen. + stdOut.Should().NotContain( + "Description of another command's child command." + ); + } - [Fact] - public async Task Help_text_shows_all_immediate_child_commands_of_each_child_command() - { - // Arrange - var commandTypes = DynamicCommandBuilder.CompileMany( - // language=cs - @" + [Fact] + public async Task Help_text_shows_all_immediate_child_commands_of_each_child_command() + { + // Arrange + var commandTypes = DynamicCommandBuilder.CompileMany( + // language=cs + @" [Command(""cmd1"")] public class FirstCommand : ICommand { @@ -871,35 +871,35 @@ public class SecondCommandSecondChildCommand : 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[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "COMMANDS", - "cmd1", "Subcommands:", "cmd1 child1", - "cmd2", "Subcommands:", "cmd2 child1", "cmd2 child2" - ); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "COMMANDS", + "cmd1", "Subcommands:", "cmd1 child1", + "cmd2", "Subcommands:", "cmd2 child1", "cmd2 child2" + ); + } - [Fact] - public async Task Help_text_shows_non_immediate_child_commands_if_they_do_not_have_a_more_specific_parent() - { - // Arrange - var commandTypes = DynamicCommandBuilder.CompileMany( - // language=cs - @" + [Fact] + public async Task Help_text_shows_non_immediate_child_commands_if_they_do_not_have_a_more_specific_parent() + { + // Arrange + var commandTypes = DynamicCommandBuilder.CompileMany( + // language=cs + @" [Command(""cmd1"")] public class FirstCommand : ICommand { @@ -919,50 +919,49 @@ public class SecondCommandSecondChildCommand : 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[] {"--help"}, - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--help"}, + new Dictionary() + ); - var stdOut = FakeConsole.ReadOutputString(); + var stdOut = FakeConsole.ReadOutputString(); - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "COMMANDS", - "cmd1", - "cmd2 child1", - "cmd2 child2" - ); - } - - [Fact] - public async Task Version_text_is_printed_if_provided_arguments_contain_the_version_option() - { - // Arrange - var application = new CliApplicationBuilder() - .AddCommand() - .SetVersion("v6.9") - .UseConsole(FakeConsole) - .Build(); - - // Act - var exitCode = await application.RunAsync( - new[] {"--version"}, - new Dictionary() - ); - - var stdOut = FakeConsole.ReadOutputString(); - - // Assert - exitCode.Should().Be(0); - stdOut.Trim().Should().Be("v6.9"); - } + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "COMMANDS", + "cmd1", + "cmd2 child1", + "cmd2 child2" + ); } -} + + [Fact] + public async Task Version_text_is_printed_if_provided_arguments_contain_the_version_option() + { + // Arrange + var application = new CliApplicationBuilder() + .AddCommand() + .SetVersion("v6.9") + .UseConsole(FakeConsole) + .Build(); + + // Act + var exitCode = await application.RunAsync( + new[] {"--version"}, + new Dictionary() + ); + + var stdOut = FakeConsole.ReadOutputString(); + + // Assert + exitCode.Should().Be(0); + stdOut.Trim().Should().Be("v6.9"); + } +} \ No newline at end of file diff --git a/CliFx.Tests/OptionBindingSpecs.cs b/CliFx.Tests/OptionBindingSpecs.cs index e61a894..0a9f301 100644 --- a/CliFx.Tests/OptionBindingSpecs.cs +++ b/CliFx.Tests/OptionBindingSpecs.cs @@ -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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--foo"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--foo", "one", "--bar", "two"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "one", "-b", "two"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-fb", "value"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--foo", "one", "two", "three"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "one", "two", "three"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--foo", "one", "--foo", "two", "--foo", "three"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"-f", "one", "-f", "two", "-f", "three"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--foo", "one", "-f", "two", "--foo", "three"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--foo", "one"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--foo", "-13"}, + new Dictionary() + ); - 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(), - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--foo"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--foo"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--foo", "one", "--bar", "two"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"--foo", "one", "two", "three"}, + new Dictionary() + ); - 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"); } } \ No newline at end of file diff --git a/CliFx.Tests/ParameterBindingSpecs.cs b/CliFx.Tests/ParameterBindingSpecs.cs index 3b4a398..272237c 100644 --- a/CliFx.Tests/ParameterBindingSpecs.cs +++ b/CliFx.Tests/ParameterBindingSpecs.cs @@ -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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"one", "two"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"one", "two", "three", "four", "five", "--boo", "xxx"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"one"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"one"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"one", "two", "three"}, + new Dictionary() + ); - 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)"); } } \ No newline at end of file diff --git a/CliFx.Tests/RoutingSpecs.cs b/CliFx.Tests/RoutingSpecs.cs index b989285..4a6da1a 100644 --- a/CliFx.Tests/RoutingSpecs.cs +++ b/CliFx.Tests/RoutingSpecs.cs @@ -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(), - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"cmd"}, + new Dictionary() + ); - 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() - ); + // Act + var exitCode = await application.RunAsync( + new[] {"cmd", "child"}, + new Dictionary() + ); - 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"); } } \ No newline at end of file diff --git a/CliFx.Tests/SpecsBase.cs b/CliFx.Tests/SpecsBase.cs index 035b6b9..c5d223f 100644 --- a/CliFx.Tests/SpecsBase.cs +++ b/CliFx.Tests/SpecsBase.cs @@ -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(); } } \ No newline at end of file diff --git a/CliFx.Tests/TypeActivationSpecs.cs b/CliFx.Tests/TypeActivationSpecs.cs index 40924b7..86be4ec 100644 --- a/CliFx.Tests/TypeActivationSpecs.cs +++ b/CliFx.Tests/TypeActivationSpecs.cs @@ -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(), - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - 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(), - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - 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(), - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - 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(), - new Dictionary() - ); + // Act + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); - 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"); } } \ No newline at end of file diff --git a/CliFx.Tests/Utils/DynamicCommandBuilder.cs b/CliFx.Tests/Utils/DynamicCommandBuilder.cs index 391d64c..588f251 100644 --- a/CliFx.Tests/Utils/DynamicCommandBuilder.cs +++ b/CliFx.Tests/Utils/DynamicCommandBuilder.cs @@ -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 CompileMany(string sourceCode) { - public static IReadOnlyList 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(); } } \ No newline at end of file diff --git a/CliFx.Tests/Utils/Extensions/AssertionExtensions.cs b/CliFx.Tests/Utils/Extensions/AssertionExtensions.cs index cd27b69..8a6c0d7 100644 --- a/CliFx.Tests/Utils/Extensions/AssertionExtensions.cs +++ b/CliFx.Tests/Utils/Extensions/AssertionExtensions.cs @@ -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 lines) { - public static void ConsistOfLines( - this StringAssertions assertions, - IEnumerable 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) lines); + + public static AndConstraint ContainAllInOrder( + this StringAssertions assertions, + IEnumerable 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) lines); - - public static AndConstraint ContainAllInOrder( - this StringAssertions assertions, - IEnumerable 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 ContainAllInOrder( - this StringAssertions assertions, - params string[] values) => - assertions.ContainAllInOrder((IEnumerable) values); + return new(assertions); } + + public static AndConstraint ContainAllInOrder( + this StringAssertions assertions, + params string[] values) => + assertions.ContainAllInOrder((IEnumerable) values); } \ No newline at end of file diff --git a/CliFx.Tests/Utils/Extensions/ConsoleExtensions.cs b/CliFx.Tests/Utils/Extensions/ConsoleExtensions.cs index 793c3fa..febbdb4 100644 --- a/CliFx.Tests/Utils/Extensions/ConsoleExtensions.cs +++ b/CliFx.Tests/Utils/Extensions/ConsoleExtensions.cs @@ -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()); } } \ No newline at end of file diff --git a/CliFx.Tests/Utils/NoOpCommand.cs b/CliFx.Tests/Utils/NoOpCommand.cs index c0b77ca..034e393 100644 --- a/CliFx.Tests/Utils/NoOpCommand.cs +++ b/CliFx.Tests/Utils/NoOpCommand.cs @@ -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; } \ No newline at end of file diff --git a/CliFx/ApplicationConfiguration.cs b/CliFx/ApplicationConfiguration.cs index f03de68..956ba9b 100644 --- a/CliFx/ApplicationConfiguration.cs +++ b/CliFx/ApplicationConfiguration.cs @@ -1,39 +1,38 @@ using System; using System.Collections.Generic; -namespace CliFx +namespace CliFx; + +/// +/// Configuration of an application. +/// +public class ApplicationConfiguration { /// - /// Configuration of an application. + /// Command types defined in this application. /// - public class ApplicationConfiguration + public IReadOnlyList CommandTypes { get; } + + /// + /// Whether debug mode is allowed in this application. + /// + public bool IsDebugModeAllowed { get; } + + /// + /// Whether preview mode is allowed in this application. + /// + public bool IsPreviewModeAllowed { get; } + + /// + /// Initializes an instance of . + /// + public ApplicationConfiguration( + IReadOnlyList commandTypes, + bool isDebugModeAllowed, + bool isPreviewModeAllowed) { - /// - /// Command types defined in this application. - /// - public IReadOnlyList CommandTypes { get; } - - /// - /// Whether debug mode is allowed in this application. - /// - public bool IsDebugModeAllowed { get; } - - /// - /// Whether preview mode is allowed in this application. - /// - public bool IsPreviewModeAllowed { get; } - - /// - /// Initializes an instance of . - /// - public ApplicationConfiguration( - IReadOnlyList commandTypes, - bool isDebugModeAllowed, - bool isPreviewModeAllowed) - { - CommandTypes = commandTypes; - IsDebugModeAllowed = isDebugModeAllowed; - IsPreviewModeAllowed = isPreviewModeAllowed; - } + CommandTypes = commandTypes; + IsDebugModeAllowed = isDebugModeAllowed; + IsPreviewModeAllowed = isPreviewModeAllowed; } } \ No newline at end of file diff --git a/CliFx/ApplicationMetadata.cs b/CliFx/ApplicationMetadata.cs index 7727734..99f7dab 100644 --- a/CliFx/ApplicationMetadata.cs +++ b/CliFx/ApplicationMetadata.cs @@ -1,43 +1,42 @@ -namespace CliFx +namespace CliFx; + +/// +/// Metadata associated with an application. +/// +public class ApplicationMetadata { /// - /// Metadata associated with an application. + /// Application title. /// - public class ApplicationMetadata + public string Title { get; } + + /// + /// Application executable name. + /// + public string ExecutableName { get; } + + /// + /// Application version. + /// + public string Version { get; } + + /// + /// Application description. + /// + public string? Description { get; } + + /// + /// Initializes an instance of . + /// + public ApplicationMetadata( + string title, + string executableName, + string version, + string? description) { - /// - /// Application title. - /// - public string Title { get; } - - /// - /// Application executable name. - /// - public string ExecutableName { get; } - - /// - /// Application version. - /// - public string Version { get; } - - /// - /// Application description. - /// - public string? Description { get; } - - /// - /// Initializes an instance of . - /// - 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; } } \ No newline at end of file diff --git a/CliFx/Attributes/CommandAttribute.cs b/CliFx/Attributes/CommandAttribute.cs index 6030895..a8717d7 100644 --- a/CliFx/Attributes/CommandAttribute.cs +++ b/CliFx/Attributes/CommandAttribute.cs @@ -1,43 +1,42 @@ using System; -namespace CliFx.Attributes +namespace CliFx.Attributes; + +/// +/// Annotates a type that defines a command. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class CommandAttribute : Attribute { /// - /// Annotates a type that defines a command. + /// Command's name. /// - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class CommandAttribute : Attribute + /// + /// Command can have no name, in which case it's treated as the default command. + /// + /// All commands registered in an application must have unique names (comparison IS NOT case-sensitive). + /// Only one command without a name is allowed in an application. + /// + public string? Name { get; } + + /// + /// Command description. + /// This is shown to the user in the help text. + /// + public string? Description { get; set; } + + /// + /// Initializes an instance of . + /// + public CommandAttribute(string name) { - /// - /// Command's name. - /// - /// - /// 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. - /// - public string? Name { get; } + Name = name; + } - /// - /// Command description. - /// This is shown to the user in the help text. - /// - public string? Description { get; set; } - - /// - /// Initializes an instance of . - /// - public CommandAttribute(string name) - { - Name = name; - } - - /// - /// Initializes an instance of . - /// - public CommandAttribute() - { - } + /// + /// Initializes an instance of . + /// + public CommandAttribute() + { } } \ No newline at end of file diff --git a/CliFx/Attributes/CommandOptionAttribute.cs b/CliFx/Attributes/CommandOptionAttribute.cs index 369a96b..0a137ca 100644 --- a/CliFx/Attributes/CommandOptionAttribute.cs +++ b/CliFx/Attributes/CommandOptionAttribute.cs @@ -1,100 +1,99 @@ using System; using CliFx.Extensibility; -namespace CliFx.Attributes +namespace CliFx.Attributes; + +/// +/// Annotates a property that defines a command option. +/// +[AttributeUsage(AttributeTargets.Property)] +public sealed class CommandOptionAttribute : Attribute { /// - /// Annotates a property that defines a command option. + /// Option name. /// - [AttributeUsage(AttributeTargets.Property)] - public sealed class CommandOptionAttribute : Attribute + /// + /// Must contain at least two characters and start with a letter. + /// Either or must be set. + /// All options in a command must have unique names (comparison IS NOT case-sensitive). + /// + public string? Name { get; } + + /// + /// Option short name. + /// + /// + /// Either or must be set. + /// All options in a command must have unique short names (comparison IS case-sensitive). + /// + public char? ShortName { get; } + + /// + /// Whether this option is required. + /// If an option is required, the user will get an error if they don't set it. + /// + public bool IsRequired { get; set; } + + /// + /// Environment variable whose value will be used as a fallback if the option + /// has not been explicitly set through command line arguments. + /// + public string? EnvironmentVariable { get; set; } + + /// + /// Option description. + /// This is shown to the user in the help text. + /// + public string? Description { get; set; } + + /// + /// Custom converter used for mapping the raw command line argument into + /// a value expected by the underlying property. + /// + /// + /// Converter must derive from . + /// + public Type? Converter { get; set; } + + /// + /// Custom validators used for verifying the value of the underlying + /// property, after it has been bound. + /// + /// + /// Validators must derive from . + /// + public Type[] Validators { get; set; } = Array.Empty(); + + /// + /// Initializes an instance of . + /// + private CommandOptionAttribute(string? name, char? shortName) { - /// - /// Option name. - /// - /// - /// Must contain at least two characters and start with a letter. - /// Either or must be set. - /// All options in a command must have unique names (comparison IS NOT case-sensitive). - /// - public string? Name { get; } + Name = name; + ShortName = shortName; + } - /// - /// Option short name. - /// - /// - /// Either or must be set. - /// All options in a command must have unique short names (comparison IS case-sensitive). - /// - public char? ShortName { get; } + /// + /// Initializes an instance of . + /// + public CommandOptionAttribute(string name, char shortName) + : this(name, (char?) shortName) + { + } - /// - /// Whether this option is required. - /// If an option is required, the user will get an error if they don't set it. - /// - public bool IsRequired { get; set; } + /// + /// Initializes an instance of . + /// + public CommandOptionAttribute(string name) + : this(name, null) + { + } - /// - /// Environment variable whose value will be used as a fallback if the option - /// has not been explicitly set through command line arguments. - /// - public string? EnvironmentVariable { get; set; } - - /// - /// Option description. - /// This is shown to the user in the help text. - /// - public string? Description { get; set; } - - /// - /// Custom converter used for mapping the raw command line argument into - /// a value expected by the underlying property. - /// - /// - /// Converter must derive from . - /// - public Type? Converter { get; set; } - - /// - /// Custom validators used for verifying the value of the underlying - /// property, after it has been bound. - /// - /// - /// Validators must derive from . - /// - public Type[] Validators { get; set; } = Array.Empty(); - - /// - /// Initializes an instance of . - /// - private CommandOptionAttribute(string? name, char? shortName) - { - Name = name; - ShortName = shortName; - } - - /// - /// Initializes an instance of . - /// - public CommandOptionAttribute(string name, char shortName) - : this(name, (char?) shortName) - { - } - - /// - /// Initializes an instance of . - /// - public CommandOptionAttribute(string name) - : this(name, null) - { - } - - /// - /// Initializes an instance of . - /// - public CommandOptionAttribute(char shortName) - : this(null, (char?) shortName) - { - } + /// + /// Initializes an instance of . + /// + public CommandOptionAttribute(char shortName) + : this(null, (char?) shortName) + { } } \ No newline at end of file diff --git a/CliFx/Attributes/CommandParameterAttribute.cs b/CliFx/Attributes/CommandParameterAttribute.cs index 5012028..94e1345 100644 --- a/CliFx/Attributes/CommandParameterAttribute.cs +++ b/CliFx/Attributes/CommandParameterAttribute.cs @@ -1,67 +1,66 @@ using System; using CliFx.Extensibility; -namespace CliFx.Attributes +namespace CliFx.Attributes; + +/// +/// Annotates a property that defines a command parameter. +/// +[AttributeUsage(AttributeTargets.Property)] +public sealed class CommandParameterAttribute : Attribute { /// - /// Annotates a property that defines a command parameter. + /// Parameter order. /// - [AttributeUsage(AttributeTargets.Property)] - public sealed class CommandParameterAttribute : Attribute + /// + /// Higher order means the parameter appears later, lower order means + /// it appears earlier. + /// + /// All parameters in a command must have unique order. + /// + /// Parameter whose type is a non-scalar (e.g. array), must always be the last in order. + /// Only one non-scalar parameter is allowed in a command. + /// + public int Order { get; } + + /// + /// Parameter name. + /// This is shown to the user in the help text. + /// + /// + /// If this isn't specified, parameter name is inferred from the property name. + /// + public string? Name { get; set; } + + /// + /// Parameter description. + /// This is shown to the user in the help text. + /// + public string? Description { get; set; } + + /// + /// Custom converter used for mapping the raw command line argument into + /// a value expected by the underlying property. + /// + /// + /// Converter must derive from . + /// + public Type? Converter { get; set; } + + /// + /// Custom validators used for verifying the value of the underlying + /// property, after it has been bound. + /// + /// + /// Validators must derive from . + /// + public Type[] Validators { get; set; } = Array.Empty(); + + /// + /// Initializes an instance of . + /// + public CommandParameterAttribute(int order) { - /// - /// Parameter order. - /// - /// - /// 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. - /// - public int Order { get; } - - /// - /// Parameter name. - /// This is shown to the user in the help text. - /// - /// - /// If this isn't specified, parameter name is inferred from the property name. - /// - public string? Name { get; set; } - - /// - /// Parameter description. - /// This is shown to the user in the help text. - /// - public string? Description { get; set; } - - /// - /// Custom converter used for mapping the raw command line argument into - /// a value expected by the underlying property. - /// - /// - /// Converter must derive from . - /// - public Type? Converter { get; set; } - - /// - /// Custom validators used for verifying the value of the underlying - /// property, after it has been bound. - /// - /// - /// Validators must derive from . - /// - public Type[] Validators { get; set; } = Array.Empty(); - - /// - /// Initializes an instance of . - /// - public CommandParameterAttribute(int order) - { - Order = order; - } + Order = order; } } \ No newline at end of file diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs index d0be0b9..84d9e24 100644 --- a/CliFx/CliApplication.cs +++ b/CliFx/CliApplication.cs @@ -11,231 +11,230 @@ using CliFx.Schema; using CliFx.Utils; using CliFx.Utils.Extensions; -namespace CliFx +namespace CliFx; + +/// +/// Command line application facade. +/// +public class CliApplication { /// - /// Command line application facade. + /// Application metadata. /// - public class CliApplication + public ApplicationMetadata Metadata { get; } + + /// + /// Application configuration. + /// + public ApplicationConfiguration Configuration { get; } + + private readonly IConsole _console; + private readonly ITypeActivator _typeActivator; + + private readonly CommandBinder _commandBinder; + + /// + /// Initializes an instance of . + /// + public CliApplication( + ApplicationMetadata metadata, + ApplicationConfiguration configuration, + IConsole console, + ITypeActivator typeActivator) { - /// - /// Application metadata. - /// - public ApplicationMetadata Metadata { get; } + Metadata = metadata; + Configuration = configuration; + _console = console; + _typeActivator = typeActivator; - /// - /// Application configuration. - /// - 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; - /// - /// Initializes an instance of . - /// - 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 RunAsync(ApplicationSchema applicationSchema, CommandInput commandInput) + { + // Handle debug directive + if (IsDebugModeEnabled(commandInput)) + { + await PromptDebuggerAsync(); } - private async ValueTask 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; + /// + /// Runs the application with the specified command line arguments and environment variables. + /// Returns an exit code which indicates whether the application completed successfully. + /// + /// + /// When running WITHOUT debugger (i.e. in production), this method swallows all exceptions and + /// reports them to the console. + /// + public async ValueTask RunAsync( + IReadOnlyList commandLineArguments, + IReadOnlyDictionary 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); } - - /// - /// Runs the application with the specified command line arguments and environment variables. - /// Returns an exit code which indicates whether the application completed successfully. - /// - /// - /// When running WITHOUT debugger (i.e. in production), this method swallows all exceptions and - /// reports them to the console. - /// - public async ValueTask RunAsync( - IReadOnlyList commandLineArguments, - IReadOnlyDictionary 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; } - - /// - /// 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. - /// - /// - /// When running WITHOUT debugger (i.e. in production), this method swallows all exceptions and - /// reports them to the console. - /// - public async ValueTask RunAsync(IReadOnlyList 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(StringComparer.Ordinal) - ); - - /// - /// Runs the application. - /// Command line arguments and environment variables are resolved automatically. - /// Returns an exit code which indicates whether the application completed successfully. - /// - /// - /// When running WITHOUT debugger (i.e. in production), this method swallows all exceptions and - /// reports them to the console. - /// - public async ValueTask RunAsync() => await RunAsync( - Environment.GetCommandLineArgs() - .Skip(1) // first element is the file path - .ToArray() - ); } -} + + /// + /// 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. + /// + /// + /// When running WITHOUT debugger (i.e. in production), this method swallows all exceptions and + /// reports them to the console. + /// + public async ValueTask RunAsync(IReadOnlyList 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(StringComparer.Ordinal) + ); + + /// + /// Runs the application. + /// Command line arguments and environment variables are resolved automatically. + /// Returns an exit code which indicates whether the application completed successfully. + /// + /// + /// When running WITHOUT debugger (i.e. in production), this method swallows all exceptions and + /// reports them to the console. + /// + public async ValueTask RunAsync() => await RunAsync( + Environment.GetCommandLineArgs() + .Skip(1) // first element is the file path + .ToArray() + ); +} \ No newline at end of file diff --git a/CliFx/CliApplicationBuilder.cs b/CliFx/CliApplicationBuilder.cs index e1abb47..d53dfca 100644 --- a/CliFx/CliApplicationBuilder.cs +++ b/CliFx/CliApplicationBuilder.cs @@ -8,245 +8,244 @@ using CliFx.Infrastructure; using CliFx.Schema; using CliFx.Utils.Extensions; -namespace CliFx +namespace CliFx; + +/// +/// Builder for . +/// +public partial class CliApplicationBuilder { + private readonly HashSet _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; + /// - /// Builder for . + /// Adds a command to the application. /// - public partial class CliApplicationBuilder + public CliApplicationBuilder AddCommand(Type commandType) { - private readonly HashSet _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; - - /// - /// Adds a command to the application. - /// - public CliApplicationBuilder AddCommand(Type commandType) - { - _commandTypes.Add(commandType); - - return this; - } - - /// - /// Adds a command to the application. - /// - public CliApplicationBuilder AddCommand() where TCommand : ICommand => - AddCommand(typeof(TCommand)); - - /// - /// Adds multiple commands to the application. - /// - public CliApplicationBuilder AddCommands(IEnumerable commandTypes) - { - foreach (var commandType in commandTypes) - AddCommand(commandType); - - return this; - } - - /// - /// Adds commands from the specified assembly to the application. - /// - /// - /// This method looks for public non-abstract classes that implement - /// and are annotated by . - /// - public CliApplicationBuilder AddCommandsFrom(Assembly commandAssembly) - { - foreach (var commandType in commandAssembly.ExportedTypes.Where(CommandSchema.IsCommandType)) - AddCommand(commandType); - - return this; - } - - /// - /// Adds commands from the specified assemblies to the application. - /// - /// - /// This method looks for public non-abstract classes that implement - /// and are annotated by . - /// - public CliApplicationBuilder AddCommandsFrom(IEnumerable commandAssemblies) - { - foreach (var commandAssembly in commandAssemblies) - AddCommandsFrom(commandAssembly); - - return this; - } - - /// - /// Adds commands from the calling assembly to the application. - /// - /// - /// This method looks for public non-abstract classes that implement - /// and are annotated by . - /// - public CliApplicationBuilder AddCommandsFromThisAssembly() => AddCommandsFrom(Assembly.GetCallingAssembly()); - - /// - /// Specifies whether debug mode (enabled with the [debug] directive) is allowed in the application. - /// - public CliApplicationBuilder AllowDebugMode(bool isAllowed = true) - { - _isDebugModeAllowed = isAllowed; - return this; - } - - /// - /// Specifies whether preview mode (enabled with the [preview] directive) is allowed in the application. - /// - public CliApplicationBuilder AllowPreviewMode(bool isAllowed = true) - { - _isPreviewModeAllowed = isAllowed; - return this; - } - - /// - /// Sets application title, which is shown in the help text. - /// - /// - /// By default, application title is inferred from the assembly name. - /// - public CliApplicationBuilder SetTitle(string title) - { - _title = title; - return this; - } - - /// - /// Sets application executable name, which is shown in the help text. - /// - /// - /// 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. - /// - public CliApplicationBuilder SetExecutableName(string executableName) - { - _executableName = executableName; - return this; - } - - /// - /// Sets application version, which is shown in the help text or - /// when the user specifies the version option. - /// - /// - /// By default, application version is inferred from the assembly version. - /// - public CliApplicationBuilder SetVersion(string version) - { - _versionText = version; - return this; - } - - /// - /// Sets application description, which is shown in the help text. - /// - public CliApplicationBuilder SetDescription(string? description) - { - _description = description; - return this; - } - - /// - /// Configures the application to use the specified implementation of . - /// - public CliApplicationBuilder UseConsole(IConsole console) - { - _console = console; - return this; - } - - /// - /// Configures the application to use the specified implementation of . - /// - public CliApplicationBuilder UseTypeActivator(ITypeActivator typeActivator) - { - _typeActivator = typeActivator; - return this; - } - - /// - /// Configures the application to use the specified function for activating types. - /// - public CliApplicationBuilder UseTypeActivator(Func typeActivator) => - UseTypeActivator(new DelegateTypeActivator(typeActivator)); - - /// - /// Creates a configured instance of . - /// - 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 + /// + /// Adds a command to the application. + /// + public CliApplicationBuilder AddCommand() where TCommand : ICommand => + AddCommand(typeof(TCommand)); + + /// + /// Adds multiple commands to the application. + /// + public CliApplicationBuilder AddCommands(IEnumerable commandTypes) { - private static readonly Lazy 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; } + + /// + /// Adds commands from the specified assembly to the application. + /// + /// + /// This method looks for public non-abstract classes that implement + /// and are annotated by . + /// + public CliApplicationBuilder AddCommandsFrom(Assembly commandAssembly) + { + foreach (var commandType in commandAssembly.ExportedTypes.Where(CommandSchema.IsCommandType)) + AddCommand(commandType); + + return this; + } + + /// + /// Adds commands from the specified assemblies to the application. + /// + /// + /// This method looks for public non-abstract classes that implement + /// and are annotated by . + /// + public CliApplicationBuilder AddCommandsFrom(IEnumerable commandAssemblies) + { + foreach (var commandAssembly in commandAssemblies) + AddCommandsFrom(commandAssembly); + + return this; + } + + /// + /// Adds commands from the calling assembly to the application. + /// + /// + /// This method looks for public non-abstract classes that implement + /// and are annotated by . + /// + public CliApplicationBuilder AddCommandsFromThisAssembly() => AddCommandsFrom(Assembly.GetCallingAssembly()); + + /// + /// Specifies whether debug mode (enabled with the [debug] directive) is allowed in the application. + /// + public CliApplicationBuilder AllowDebugMode(bool isAllowed = true) + { + _isDebugModeAllowed = isAllowed; + return this; + } + + /// + /// Specifies whether preview mode (enabled with the [preview] directive) is allowed in the application. + /// + public CliApplicationBuilder AllowPreviewMode(bool isAllowed = true) + { + _isPreviewModeAllowed = isAllowed; + return this; + } + + /// + /// Sets application title, which is shown in the help text. + /// + /// + /// By default, application title is inferred from the assembly name. + /// + public CliApplicationBuilder SetTitle(string title) + { + _title = title; + return this; + } + + /// + /// Sets application executable name, which is shown in the help text. + /// + /// + /// 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. + /// + public CliApplicationBuilder SetExecutableName(string executableName) + { + _executableName = executableName; + return this; + } + + /// + /// Sets application version, which is shown in the help text or + /// when the user specifies the version option. + /// + /// + /// By default, application version is inferred from the assembly version. + /// + public CliApplicationBuilder SetVersion(string version) + { + _versionText = version; + return this; + } + + /// + /// Sets application description, which is shown in the help text. + /// + public CliApplicationBuilder SetDescription(string? description) + { + _description = description; + return this; + } + + /// + /// Configures the application to use the specified implementation of . + /// + public CliApplicationBuilder UseConsole(IConsole console) + { + _console = console; + return this; + } + + /// + /// Configures the application to use the specified implementation of . + /// + public CliApplicationBuilder UseTypeActivator(ITypeActivator typeActivator) + { + _typeActivator = typeActivator; + return this; + } + + /// + /// Configures the application to use the specified function for activating types. + /// + public CliApplicationBuilder UseTypeActivator(Func typeActivator) => + UseTypeActivator(new DelegateTypeActivator(typeActivator)); + + /// + /// Creates a configured instance of . + /// + 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 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"; } \ No newline at end of file diff --git a/CliFx/CommandBinder.cs b/CliFx/CommandBinder.cs index e5868b6..a1a6dc2 100644 --- a/CliFx/CommandBinder.cs +++ b/CliFx/CommandBinder.cs @@ -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 - 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 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, etc) - if (targetEnumerableType.IsAssignableFrom(arrayType)) - { - return array; - } - - // Array-constructible (List, HashSet, 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 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 + 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 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, etc) + if (targetEnumerableType.IsAssignableFrom(arrayType)) + { + return array; + } + + // Array-constructible (List, HashSet, 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 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(); + + foreach (var validatorType in memberSchema.ValidatorTypes) { - var errors = new List(); + 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 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 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); + } } \ No newline at end of file diff --git a/CliFx/Exceptions/CliFxException.cs b/CliFx/Exceptions/CliFxException.cs index c54e1ee..9155497 100644 --- a/CliFx/Exceptions/CliFxException.cs +++ b/CliFx/Exceptions/CliFxException.cs @@ -1,54 +1,53 @@ using System; -namespace CliFx.Exceptions +namespace CliFx.Exceptions; + +/// +/// Exception thrown when there is an error during application execution. +/// +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; } + /// - /// Exception thrown when there is an error during application execution. + /// Returned exit code. /// - public partial class CliFxException : Exception + public int ExitCode { get; } + + /// + /// Whether to show the help text before exiting. + /// + public bool ShowHelp { get; } + + /// + /// Initializes an instance of . + /// + 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; } - - /// - /// Returned exit code. - /// - public int ExitCode { get; } - - /// - /// Whether to show the help text before exiting. - /// - public bool ShowHelp { get; } - - /// - /// Initializes an instance of . - /// - 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); } \ No newline at end of file diff --git a/CliFx/Exceptions/CommandException.cs b/CliFx/Exceptions/CommandException.cs index 3afe81e..03c83bd 100644 --- a/CliFx/Exceptions/CommandException.cs +++ b/CliFx/Exceptions/CommandException.cs @@ -1,23 +1,22 @@ using System; -namespace CliFx.Exceptions +namespace CliFx.Exceptions; + +/// +/// 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. +/// +public class CommandException : CliFxException { /// - /// 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 . /// - public class CommandException : CliFxException + public CommandException( + string message, + int exitCode = DefaultExitCode, + bool showHelp = false, + Exception? innerException = null) + : base(message, exitCode, showHelp, innerException) { - /// - /// Initializes an instance of . - /// - public CommandException( - string message, - int exitCode = DefaultExitCode, - bool showHelp = false, - Exception? innerException = null) - : base(message, exitCode, showHelp, innerException) - { - } } } \ No newline at end of file diff --git a/CliFx/Extensibility/BindingConverter.cs b/CliFx/Extensibility/BindingConverter.cs index 85b73b3..48daad4 100644 --- a/CliFx/Extensibility/BindingConverter.cs +++ b/CliFx/Extensibility/BindingConverter.cs @@ -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); +} +/// +/// Base type for custom converters. +/// +public abstract class BindingConverter : IBindingConverter +{ /// - /// Base type for custom converters. + /// Parses value from a raw command line argument. /// - public abstract class BindingConverter : IBindingConverter - { - /// - /// Parses value from a raw command line argument. - /// - 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); } \ No newline at end of file diff --git a/CliFx/Extensibility/BindingValidationError.cs b/CliFx/Extensibility/BindingValidationError.cs index c37e0eb..94c6f70 100644 --- a/CliFx/Extensibility/BindingValidationError.cs +++ b/CliFx/Extensibility/BindingValidationError.cs @@ -1,18 +1,17 @@ -namespace CliFx.Extensibility +namespace CliFx.Extensibility; + +/// +/// Represents a validation error. +/// +public class BindingValidationError { /// - /// Represents a validation error. + /// Error message shown to the user. /// - public class BindingValidationError - { - /// - /// Error message shown to the user. - /// - public string Message { get; } + public string Message { get; } - /// - /// Initializes an instance of . - /// - public BindingValidationError(string message) => Message = message; - } + /// + /// Initializes an instance of . + /// + public BindingValidationError(string message) => Message = message; } \ No newline at end of file diff --git a/CliFx/Extensibility/BindingValidator.cs b/CliFx/Extensibility/BindingValidator.cs index 023666d..08897a7 100644 --- a/CliFx/Extensibility/BindingValidator.cs +++ b/CliFx/Extensibility/BindingValidator.cs @@ -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); +} + +/// +/// Base type for custom validators. +/// +public abstract class BindingValidator : IBindingValidator +{ + /// + /// Returns a successful validation result. + /// + protected BindingValidationError? Ok() => null; /// - /// Base type for custom validators. + /// Returns a non-successful validation result. /// - public abstract class BindingValidator : IBindingValidator - { - /// - /// Returns a successful validation result. - /// - protected BindingValidationError? Ok() => null; + protected BindingValidationError Error(string message) => new(message); - /// - /// Returns a non-successful validation result. - /// - protected BindingValidationError Error(string message) => new(message); + /// + /// Validates the value bound to a parameter or an option. + /// Returns null if validation is successful, or an error in case of failure. + /// + /// + /// You can use the utility methods and to + /// create an appropriate result. + /// + public abstract BindingValidationError? Validate(T value); - /// - /// Validates the value bound to a parameter or an option. - /// Returns null if validation is successful, or an error in case of failure. - /// - /// - /// You can use the utility methods and to - /// create an appropriate result. - /// - public abstract BindingValidationError? Validate(T value); - - BindingValidationError? IBindingValidator.Validate(object? value) => Validate((T) value!); - } -} + BindingValidationError? IBindingValidator.Validate(object? value) => Validate((T) value!); +} \ No newline at end of file diff --git a/CliFx/FallbackDefaultCommand.cs b/CliFx/FallbackDefaultCommand.cs index e0ffa4b..c28c2a5 100644 --- a/CliFx/FallbackDefaultCommand.cs +++ b/CliFx/FallbackDefaultCommand.cs @@ -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; } \ No newline at end of file diff --git a/CliFx/Formatting/CommandInputConsoleFormatter.cs b/CliFx/Formatting/CommandInputConsoleFormatter.cs index 07122af..d2fcd85 100644 --- a/CliFx/Formatting/CommandInputConsoleFormatter.cs +++ b/CliFx/Formatting/CommandInputConsoleFormatter.cs @@ -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); } \ No newline at end of file diff --git a/CliFx/Formatting/ConsoleFormatter.cs b/CliFx/Formatting/ConsoleFormatter.cs index 6f41f44..9f1463f 100644 --- a/CliFx/Formatting/ConsoleFormatter.cs +++ b/CliFx/Formatting/ConsoleFormatter.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx/Formatting/ExceptionConsoleFormatter.cs b/CliFx/Formatting/ExceptionConsoleFormatter.cs index cf6a571..4ec875d 100644 --- a/CliFx/Formatting/ExceptionConsoleFormatter.cs +++ b/CliFx/Formatting/ExceptionConsoleFormatter.cs @@ -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); } \ No newline at end of file diff --git a/CliFx/Formatting/HelpConsoleFormatter.cs b/CliFx/Formatting/HelpConsoleFormatter.cs index abed032..97483dc 100644 --- a/CliFx/Formatting/HelpConsoleFormatter.cs +++ b/CliFx/Formatting/HelpConsoleFormatter.cs @@ -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() - ? "" - : "" - ); - 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() + ? "" + : "" + ); + 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(); } \ No newline at end of file diff --git a/CliFx/Formatting/HelpContext.cs b/CliFx/Formatting/HelpContext.cs index ac4c125..ece079a 100644 --- a/CliFx/Formatting/HelpContext.cs +++ b/CliFx/Formatting/HelpContext.cs @@ -1,28 +1,27 @@ using System.Collections.Generic; using CliFx.Schema; -namespace CliFx.Formatting +namespace CliFx.Formatting; + +internal class HelpContext { - internal class HelpContext + public ApplicationMetadata ApplicationMetadata { get; } + + public ApplicationSchema ApplicationSchema { get; } + + public CommandSchema CommandSchema { get; } + + public IReadOnlyDictionary CommandDefaultValues { get; } + + public HelpContext( + ApplicationMetadata applicationMetadata, + ApplicationSchema applicationSchema, + CommandSchema commandSchema, + IReadOnlyDictionary commandDefaultValues) { - public ApplicationMetadata ApplicationMetadata { get; } - - public ApplicationSchema ApplicationSchema { get; } - - public CommandSchema CommandSchema { get; } - - public IReadOnlyDictionary CommandDefaultValues { get; } - - public HelpContext( - ApplicationMetadata applicationMetadata, - ApplicationSchema applicationSchema, - CommandSchema commandSchema, - IReadOnlyDictionary commandDefaultValues) - { - ApplicationMetadata = applicationMetadata; - ApplicationSchema = applicationSchema; - CommandSchema = commandSchema; - CommandDefaultValues = commandDefaultValues; - } + ApplicationMetadata = applicationMetadata; + ApplicationSchema = applicationSchema; + CommandSchema = commandSchema; + CommandDefaultValues = commandDefaultValues; } } \ No newline at end of file diff --git a/CliFx/ICommand.cs b/CliFx/ICommand.cs index 51d2eaa..33c0c01 100644 --- a/CliFx/ICommand.cs +++ b/CliFx/ICommand.cs @@ -1,20 +1,19 @@ using System.Threading.Tasks; using CliFx.Infrastructure; -namespace CliFx +namespace CliFx; + +/// +/// Entry point through which the user interacts with the command line application. +/// +public interface ICommand { /// - /// Entry point through which the user interacts with the command line application. + /// Executes the command using the specified implementation of . /// - public interface ICommand - { - /// - /// Executes the command using the specified implementation of . - /// - /// - /// If the execution of the command is not asynchronous, simply end the method with - /// return default; - /// - ValueTask ExecuteAsync(IConsole console); - } + /// + /// If the execution of the command is not asynchronous, simply end the method with + /// return default; + /// + ValueTask ExecuteAsync(IConsole console); } \ No newline at end of file diff --git a/CliFx/Infrastructure/ConsoleReader.cs b/CliFx/Infrastructure/ConsoleReader.cs index ee8f8fa..35cb92c 100644 --- a/CliFx/Infrastructure/ConsoleReader.cs +++ b/CliFx/Infrastructure/ConsoleReader.cs @@ -1,43 +1,42 @@ using System.IO; using System.Text; -namespace CliFx.Infrastructure +namespace CliFx.Infrastructure; + +/// +/// Implements a for reading characters from a console stream. +/// +public partial class ConsoleReader : StreamReader { /// - /// Implements a for reading characters from a console stream. + /// Console that owns this stream. /// - public partial class ConsoleReader : StreamReader + public IConsole Console { get; } + + /// + /// Initializes an instance of . + /// + public ConsoleReader(IConsole console, Stream stream, Encoding encoding) + : base(stream, encoding, false, 4096) { - /// - /// Console that owns this stream. - /// - public IConsole Console { get; } - - /// - /// Initializes an instance of . - /// - public ConsoleReader(IConsole console, Stream stream, Encoding encoding) - : base(stream, encoding, false, 4096) - { - Console = console; - } - - /// - /// Initializes an instance of . - /// - public ConsoleReader(IConsole console, Stream stream) - : this(console, stream, System.Console.InputEncoding) - { - } + Console = console; } - public partial class ConsoleReader + /// + /// Initializes an instance of . + /// + public ConsoleReader(IConsole console, Stream stream) + : this(console, stream, System.Console.InputEncoding) { - internal static ConsoleReader Create(IConsole console, Stream? stream) => new( - console, - stream is not null - ? Stream.Synchronized(stream) - : Stream.Null - ); } +} + +public partial class ConsoleReader +{ + internal static ConsoleReader Create(IConsole console, Stream? stream) => new( + console, + stream is not null + ? Stream.Synchronized(stream) + : Stream.Null + ); } \ No newline at end of file diff --git a/CliFx/Infrastructure/ConsoleWriter.cs b/CliFx/Infrastructure/ConsoleWriter.cs index 4b5ef0f..08fc183 100644 --- a/CliFx/Infrastructure/ConsoleWriter.cs +++ b/CliFx/Infrastructure/ConsoleWriter.cs @@ -2,43 +2,42 @@ using System.Text; using CliFx.Utils; -namespace CliFx.Infrastructure +namespace CliFx.Infrastructure; + +/// +/// Implements a for writing characters to a console stream. +/// +public partial class ConsoleWriter : StreamWriter { /// - /// Implements a for writing characters to a console stream. + /// Console that owns this stream. /// - public partial class ConsoleWriter : StreamWriter + public IConsole Console { get; } + + /// + /// Initializes an instance of . + /// + public ConsoleWriter(IConsole console, Stream stream, Encoding encoding) + : base(stream, encoding.WithoutPreamble(), 256) { - /// - /// Console that owns this stream. - /// - public IConsole Console { get; } - - /// - /// Initializes an instance of . - /// - public ConsoleWriter(IConsole console, Stream stream, Encoding encoding) - : base(stream, encoding.WithoutPreamble(), 256) - { - Console = console; - } - - /// - /// Initializes an instance of . - /// - public ConsoleWriter(IConsole console, Stream stream) - : this(console, stream, System.Console.OutputEncoding) - { - } + Console = console; } - public partial class ConsoleWriter + /// + /// Initializes an instance of . + /// + public ConsoleWriter(IConsole console, Stream stream) + : this(console, stream, System.Console.OutputEncoding) { - internal static ConsoleWriter Create(IConsole console, Stream? stream) => new( - console, - stream is not null - ? Stream.Synchronized(stream) - : Stream.Null - ) {AutoFlush = true}; } +} + +public partial class ConsoleWriter +{ + internal static ConsoleWriter Create(IConsole console, Stream? stream) => new( + console, + stream is not null + ? Stream.Synchronized(stream) + : Stream.Null + ) {AutoFlush = true}; } \ No newline at end of file diff --git a/CliFx/Infrastructure/DefaultTypeActivator.cs b/CliFx/Infrastructure/DefaultTypeActivator.cs index c2a752d..0edc320 100644 --- a/CliFx/Infrastructure/DefaultTypeActivator.cs +++ b/CliFx/Infrastructure/DefaultTypeActivator.cs @@ -1,32 +1,31 @@ using System; using CliFx.Exceptions; -namespace CliFx.Infrastructure +namespace CliFx.Infrastructure; + +/// +/// Implementation of that instantiates an object +/// by using its parameterless constructor. +/// +public class DefaultTypeActivator : ITypeActivator { - /// - /// Implementation of that instantiates an object - /// by using its parameterless constructor. - /// - public class DefaultTypeActivator : ITypeActivator + /// + public object CreateInstance(Type type) { - /// - public object CreateInstance(Type type) + try { - try - { - return Activator.CreateInstance(type); - } - catch (Exception ex) - { - throw CliFxException.InternalError( - $"Failed to create an instance of type `{type.FullName}`." + - Environment.NewLine + - "Default type activator is only capable of instantiating a type if it has a public parameterless constructor." + - Environment.NewLine + - "To fix this, either add a parameterless constructor to the type or configure a custom activator on the application.", - ex - ); - } + return Activator.CreateInstance(type); + } + catch (Exception ex) + { + throw CliFxException.InternalError( + $"Failed to create an instance of type `{type.FullName}`." + + Environment.NewLine + + "Default type activator is only capable of instantiating a type if it has a public parameterless constructor." + + Environment.NewLine + + "To fix this, either add a parameterless constructor to the type or configure a custom activator on the application.", + ex + ); } } } \ No newline at end of file diff --git a/CliFx/Infrastructure/DelegateTypeActivator.cs b/CliFx/Infrastructure/DelegateTypeActivator.cs index d8a8c66..d3be063 100644 --- a/CliFx/Infrastructure/DelegateTypeActivator.cs +++ b/CliFx/Infrastructure/DelegateTypeActivator.cs @@ -1,38 +1,37 @@ using System; using CliFx.Exceptions; -namespace CliFx.Infrastructure +namespace CliFx.Infrastructure; + +/// +/// Implementation of that instantiates an object +/// by using a predefined function. +/// +public class DelegateTypeActivator : ITypeActivator { + private readonly Func _func; + /// - /// Implementation of that instantiates an object - /// by using a predefined function. + /// Initializes an instance of . /// - public class DelegateTypeActivator : ITypeActivator + public DelegateTypeActivator(Func func) => _func = func; + + /// + public object CreateInstance(Type type) { - private readonly Func _func; + var instance = _func(type); - /// - /// Initializes an instance of . - /// - public DelegateTypeActivator(Func func) => _func = func; - - /// - public object CreateInstance(Type type) + if (instance is null) { - var instance = _func(type); - - if (instance is null) - { - throw CliFxException.InternalError( - $"Failed to create an instance of type `{type.FullName}`, received instead." + - Environment.NewLine + - "To fix this, ensure that the provided type activator is configured correctly, as it's not expected to return ." + - Environment.NewLine + - "If you are relying on a dependency container, this error may indicate that the specified type has not been registered." - ); - } - - return instance; + throw CliFxException.InternalError( + $"Failed to create an instance of type `{type.FullName}`, received instead." + + Environment.NewLine + + "To fix this, ensure that the provided type activator is configured correctly, as it's not expected to return ." + + Environment.NewLine + + "If you are relying on a dependency container, this error may indicate that the specified type has not been registered." + ); } + + return instance; } } \ No newline at end of file diff --git a/CliFx/Infrastructure/FakeConsole.cs b/CliFx/Infrastructure/FakeConsole.cs index cbe7639..9ba8289 100644 --- a/CliFx/Infrastructure/FakeConsole.cs +++ b/CliFx/Infrastructure/FakeConsole.cs @@ -2,100 +2,99 @@ using System.IO; using System.Threading; -namespace CliFx.Infrastructure +namespace CliFx.Infrastructure; + +/// +/// Implementation of that uses the provided fake +/// standard input, output, and error streams. +/// +/// +/// Use this implementation in tests to verify how a command interacts with the console. +/// +public class FakeConsole : IConsole, IDisposable { + private readonly CancellationTokenSource _cancellationTokenSource = new(); + + /// + public ConsoleReader Input { get; } + + /// + public bool IsInputRedirected => true; + + /// + public ConsoleWriter Output { get; } + + /// + public bool IsOutputRedirected => true; + + /// + public ConsoleWriter Error { get; } + + /// + public bool IsErrorRedirected => true; + + /// + public ConsoleColor ForegroundColor { get; set; } = ConsoleColor.Gray; + + /// + public ConsoleColor BackgroundColor { get; set; } = ConsoleColor.Black; + + /// + public void ResetColor() + { + ForegroundColor = ConsoleColor.Gray; + BackgroundColor = ConsoleColor.Black; + } + + /// + public int CursorLeft { get; set; } + + /// + public int CursorTop { get; set; } + /// - /// Implementation of that uses the provided fake - /// standard input, output, and error streams. + /// Initializes an instance of . + /// + public FakeConsole(Stream? input = null, Stream? output = null, Stream? error = null) + { + Input = ConsoleReader.Create(this, input); + Output = ConsoleWriter.Create(this, output); + Error = ConsoleWriter.Create(this, error); + } + + /// + public CancellationToken RegisterCancellationHandler() => _cancellationTokenSource.Token; + + /// + /// Sends a cancellation signal to the currently executing command. /// /// - /// Use this implementation in tests to verify how a command interacts with the console. + /// If the command is not cancellation-aware (i.e. it doesn't call ), + /// this method will not have any effect. /// - public class FakeConsole : IConsole, IDisposable + public void RequestCancellation(TimeSpan? delay = null) { - private readonly CancellationTokenSource _cancellationTokenSource = new(); - - /// - public ConsoleReader Input { get; } - - /// - public bool IsInputRedirected => true; - - /// - public ConsoleWriter Output { get; } - - /// - public bool IsOutputRedirected => true; - - /// - public ConsoleWriter Error { get; } - - /// - public bool IsErrorRedirected => true; - - /// - public ConsoleColor ForegroundColor { get; set; } = ConsoleColor.Gray; - - /// - public ConsoleColor BackgroundColor { get; set; } = ConsoleColor.Black; - - /// - public void ResetColor() + // Avoid unnecessary creation of a timer + if (delay is not null && delay > TimeSpan.Zero) { - ForegroundColor = ConsoleColor.Gray; - BackgroundColor = ConsoleColor.Black; + _cancellationTokenSource.CancelAfter(delay.Value); } - - /// - public int CursorLeft { get; set; } - - /// - public int CursorTop { get; set; } - - /// - /// Initializes an instance of . - /// - public FakeConsole(Stream? input = null, Stream? output = null, Stream? error = null) + else { - Input = ConsoleReader.Create(this, input); - Output = ConsoleWriter.Create(this, output); - Error = ConsoleWriter.Create(this, error); + _cancellationTokenSource.Cancel(); } - - /// - public CancellationToken RegisterCancellationHandler() => _cancellationTokenSource.Token; - - /// - /// Sends a cancellation signal to the currently executing command. - /// - /// - /// If the command is not cancellation-aware (i.e. it doesn't call ), - /// this method will not have any effect. - /// - public void RequestCancellation(TimeSpan? delay = null) - { - // Avoid unnecessary creation of a timer - if (delay is not null && delay > TimeSpan.Zero) - { - _cancellationTokenSource.CancelAfter(delay.Value); - } - else - { - _cancellationTokenSource.Cancel(); - } - } - - /// - public void Clear() - { - } - - /// - public void ReadKey(bool intercept = false) - { - } - - /// - public virtual void Dispose() => _cancellationTokenSource.Dispose(); } -} + + /// + public void Clear() + { + } + + /// + public void ReadKey(bool intercept = false) + { + } + + /// + public virtual void Dispose() => _cancellationTokenSource.Dispose(); +} \ No newline at end of file diff --git a/CliFx/Infrastructure/FakeInMemoryConsole.cs b/CliFx/Infrastructure/FakeInMemoryConsole.cs index df90a81..7a0a195 100644 --- a/CliFx/Infrastructure/FakeInMemoryConsole.cs +++ b/CliFx/Infrastructure/FakeInMemoryConsole.cs @@ -1,104 +1,103 @@ using System.IO; -namespace CliFx.Infrastructure +namespace CliFx.Infrastructure; + +/// +/// Implementation of that uses fake +/// standard input, output, and error streams backed by in-memory stores. +/// +/// +/// Use this implementation in tests to verify how a command interacts with the console. +/// +public class FakeInMemoryConsole : FakeConsole { - /// - /// Implementation of that uses fake - /// standard input, output, and error streams backed by in-memory stores. - /// - /// - /// Use this implementation in tests to verify how a command interacts with the console. - /// - public class FakeInMemoryConsole : FakeConsole + private readonly MemoryStream _input; + private readonly MemoryStream _output; + private readonly MemoryStream _error; + + private FakeInMemoryConsole(MemoryStream input, MemoryStream output, MemoryStream error) + : base(input, output, error) { - private readonly MemoryStream _input; - private readonly MemoryStream _output; - private readonly MemoryStream _error; + _input = input; + _output = output; + _error = error; + } - private FakeInMemoryConsole(MemoryStream input, MemoryStream output, MemoryStream error) - : base(input, output, error) + /// + /// Initializes an instance of . + /// + public FakeInMemoryConsole() + : this(new MemoryStream(), new MemoryStream(), new MemoryStream()) + { + } + + /// + /// Writes data to the input stream. + /// + public void WriteInput(byte[] data) + { + // We want the consumer to be able to read what we wrote + // so we need to seek the stream back to its original + // position after we finish writing. + lock (_input) { - _input = input; - _output = output; - _error = error; - } + var lastPosition = _input.Position; - /// - /// Initializes an instance of . - /// - public FakeInMemoryConsole() - : this(new MemoryStream(), new MemoryStream(), new MemoryStream()) - { - } + _input.Write(data); + _input.Flush(); - /// - /// Writes data to the input stream. - /// - public void WriteInput(byte[] data) - { - // We want the consumer to be able to read what we wrote - // so we need to seek the stream back to its original - // position after we finish writing. - lock (_input) - { - var lastPosition = _input.Position; - - _input.Write(data); - _input.Flush(); - - _input.Position = lastPosition; - } - } - - /// - /// Writes data to the input stream. - /// - public void WriteInput(string data) => WriteInput( - Input.CurrentEncoding.GetBytes(data) - ); - - /// - /// Reads the data written to the output stream. - /// - public byte[] ReadOutputBytes() - { - lock (_output) - { - _output.Flush(); - return _output.ToArray(); - } - } - - /// - /// Reads the data written to the output stream. - /// - public string ReadOutputString() => Output.Encoding.GetString(ReadOutputBytes()); - - /// - /// Reads the data written to the error stream. - /// - public byte[] ReadErrorBytes() - { - lock (_error) - { - _error.Flush(); - return _error.ToArray(); - } - } - - /// - /// Reads the data written to the error stream. - /// - public string ReadErrorString() => Error.Encoding.GetString(ReadErrorBytes()); - - /// - public override void Dispose() - { - _input.Dispose(); - _output.Dispose(); - _error.Dispose(); - - base.Dispose(); + _input.Position = lastPosition; } } + + /// + /// Writes data to the input stream. + /// + public void WriteInput(string data) => WriteInput( + Input.CurrentEncoding.GetBytes(data) + ); + + /// + /// Reads the data written to the output stream. + /// + public byte[] ReadOutputBytes() + { + lock (_output) + { + _output.Flush(); + return _output.ToArray(); + } + } + + /// + /// Reads the data written to the output stream. + /// + public string ReadOutputString() => Output.Encoding.GetString(ReadOutputBytes()); + + /// + /// Reads the data written to the error stream. + /// + public byte[] ReadErrorBytes() + { + lock (_error) + { + _error.Flush(); + return _error.ToArray(); + } + } + + /// + /// Reads the data written to the error stream. + /// + public string ReadErrorString() => Error.Encoding.GetString(ReadErrorBytes()); + + /// + public override void Dispose() + { + _input.Dispose(); + _output.Dispose(); + _error.Dispose(); + + base.Dispose(); + } } \ No newline at end of file diff --git a/CliFx/Infrastructure/IConsole.cs b/CliFx/Infrastructure/IConsole.cs index eb79f2a..c0a0bd7 100644 --- a/CliFx/Infrastructure/IConsole.cs +++ b/CliFx/Infrastructure/IConsole.cs @@ -2,138 +2,137 @@ using System.Threading; using CliFx.Utils; -namespace CliFx.Infrastructure +namespace CliFx.Infrastructure; + +/// +/// Abstraction for the console layer. +/// +public interface IConsole { /// - /// Abstraction for the console layer. + /// Input stream (stdin). /// - public interface IConsole - { - /// - /// Input stream (stdin). - /// - ConsoleReader Input { get; } + ConsoleReader Input { get; } - /// - /// Whether the input stream is redirected. - /// - bool IsInputRedirected { get; } + /// + /// Whether the input stream is redirected. + /// + bool IsInputRedirected { get; } - /// - /// Output stream (stdout). - /// - ConsoleWriter Output { get; } + /// + /// Output stream (stdout). + /// + ConsoleWriter Output { get; } - /// - /// Whether the output stream is redirected. - /// - bool IsOutputRedirected { get; } + /// + /// Whether the output stream is redirected. + /// + bool IsOutputRedirected { get; } - /// - /// Error stream (stderr). - /// - ConsoleWriter Error { get; } + /// + /// Error stream (stderr). + /// + ConsoleWriter Error { get; } - /// - /// Whether the error stream is redirected. - /// - bool IsErrorRedirected { get; } + /// + /// Whether the error stream is redirected. + /// + bool IsErrorRedirected { get; } - /// - /// Current foreground color. - /// - ConsoleColor ForegroundColor { get; set; } + /// + /// Current foreground color. + /// + ConsoleColor ForegroundColor { get; set; } - /// - /// Current background color. - /// - ConsoleColor BackgroundColor { get; set; } + /// + /// Current background color. + /// + ConsoleColor BackgroundColor { get; set; } - /// - /// Resets foreground and background colors to their default values. - /// - void ResetColor(); + /// + /// Resets foreground and background colors to their default values. + /// + void ResetColor(); - /// - /// Cursor left offset. - /// - int CursorLeft { get; set; } + /// + /// Cursor left offset. + /// + int CursorLeft { get; set; } - /// - /// Cursor top offset. - /// - int CursorTop { get; set; } + /// + /// Cursor top offset. + /// + int CursorTop { get; set; } - /// - /// Registers a handler for the interrupt signal (Ctrl+C) on the console and returns - /// a token representing the cancellation request. - /// Subsequent calls to this method have no side-effects and return the same token. - /// - /// - /// - /// Calling this method effectively makes the command cancellation-aware, which - /// means that sending the interrupt signal won't immediately terminate the application, - /// but will instead trigger a token that the command can use to exit more gracefully. - /// - /// - /// Note that the handler is only respected when the user sends the interrupt signal for the first time. - /// If the user decides to issue the signal again, the application will terminate immediately - /// regardless of whether the command is cancellation-aware. - /// - /// - CancellationToken RegisterCancellationHandler(); + /// + /// Registers a handler for the interrupt signal (Ctrl+C) on the console and returns + /// a token representing the cancellation request. + /// Subsequent calls to this method have no side-effects and return the same token. + /// + /// + /// + /// Calling this method effectively makes the command cancellation-aware, which + /// means that sending the interrupt signal won't immediately terminate the application, + /// but will instead trigger a token that the command can use to exit more gracefully. + /// + /// + /// Note that the handler is only respected when the user sends the interrupt signal for the first time. + /// If the user decides to issue the signal again, the application will terminate immediately + /// regardless of whether the command is cancellation-aware. + /// + /// + CancellationToken RegisterCancellationHandler(); - /// - /// Clears the console buffer and corresponding console window of display information. - /// - void Clear(); + /// + /// Clears the console buffer and corresponding console window of display information. + /// + void Clear(); - /// - /// Obtains the next character or function key pressed by the user. - /// - void ReadKey(bool intercept = false); + /// + /// Obtains the next character or function key pressed by the user. + /// + void ReadKey(bool intercept = false); +} + +/// +/// Extensions for . +/// +public static class ConsoleExtensions +{ + /// + /// Sets the specified foreground color and returns an + /// that will reset the color back to its previous value upon disposal. + /// + public static IDisposable WithForegroundColor(this IConsole console, ConsoleColor foregroundColor) + { + var lastColor = console.ForegroundColor; + console.ForegroundColor = foregroundColor; + + return Disposable.Create(() => console.ForegroundColor = lastColor); } /// - /// Extensions for . + /// Sets the specified background color and returns an + /// that will reset the color back to its previous value upon disposal. /// - public static class ConsoleExtensions + public static IDisposable WithBackgroundColor(this IConsole console, ConsoleColor backgroundColor) { - /// - /// Sets the specified foreground color and returns an - /// that will reset the color back to its previous value upon disposal. - /// - public static IDisposable WithForegroundColor(this IConsole console, ConsoleColor foregroundColor) - { - var lastColor = console.ForegroundColor; - console.ForegroundColor = foregroundColor; + var lastColor = console.BackgroundColor; + console.BackgroundColor = backgroundColor; - return Disposable.Create(() => console.ForegroundColor = lastColor); - } - - /// - /// Sets the specified background color and returns an - /// that will reset the color back to its previous value upon disposal. - /// - public static IDisposable WithBackgroundColor(this IConsole console, ConsoleColor backgroundColor) - { - var lastColor = console.BackgroundColor; - console.BackgroundColor = backgroundColor; - - return Disposable.Create(() => console.BackgroundColor = lastColor); - } - - /// - /// Sets the specified foreground and background colors and returns an - /// that will reset the colors back to their previous values upon disposal. - /// - public static IDisposable WithColors( - this IConsole console, - ConsoleColor foregroundColor, - ConsoleColor backgroundColor) => - Disposable.Merge( - console.WithForegroundColor(foregroundColor), - console.WithBackgroundColor(backgroundColor) - ); + return Disposable.Create(() => console.BackgroundColor = lastColor); } -} + + /// + /// Sets the specified foreground and background colors and returns an + /// that will reset the colors back to their previous values upon disposal. + /// + public static IDisposable WithColors( + this IConsole console, + ConsoleColor foregroundColor, + ConsoleColor backgroundColor) => + Disposable.Merge( + console.WithForegroundColor(foregroundColor), + console.WithBackgroundColor(backgroundColor) + ); +} \ No newline at end of file diff --git a/CliFx/Infrastructure/ITypeActivator.cs b/CliFx/Infrastructure/ITypeActivator.cs index f917b2f..d6f70c2 100644 --- a/CliFx/Infrastructure/ITypeActivator.cs +++ b/CliFx/Infrastructure/ITypeActivator.cs @@ -1,15 +1,14 @@ using System; -namespace CliFx.Infrastructure +namespace CliFx.Infrastructure; + +/// +/// Abstraction for a service that can instantiate objects at runtime. +/// +public interface ITypeActivator { /// - /// Abstraction for a service that can instantiate objects at runtime. + /// Creates an instance of the specified type. /// - public interface ITypeActivator - { - /// - /// Creates an instance of the specified type. - /// - object CreateInstance(Type type); - } + object CreateInstance(Type type); } \ No newline at end of file diff --git a/CliFx/Infrastructure/SystemConsole.cs b/CliFx/Infrastructure/SystemConsole.cs index fb04152..a1d430b 100644 --- a/CliFx/Infrastructure/SystemConsole.cs +++ b/CliFx/Infrastructure/SystemConsole.cs @@ -1,109 +1,108 @@ using System; using System.Threading; -namespace CliFx.Infrastructure +namespace CliFx.Infrastructure; + +/// +/// Implementation of that represents the real system console. +/// +public class SystemConsole : IConsole, IDisposable { - /// - /// Implementation of that represents the real system console. - /// - public class SystemConsole : IConsole, IDisposable + private CancellationTokenSource? _cancellationTokenSource; + + /// + public ConsoleReader Input { get; } + + /// + public bool IsInputRedirected => Console.IsInputRedirected; + + /// + public ConsoleWriter Output { get; } + + /// + public bool IsOutputRedirected => Console.IsOutputRedirected; + + /// + public ConsoleWriter Error { get; } + + /// + public bool IsErrorRedirected => Console.IsErrorRedirected; + + /// + public ConsoleColor ForegroundColor { - private CancellationTokenSource? _cancellationTokenSource; - - /// - public ConsoleReader Input { get; } - - /// - public bool IsInputRedirected => Console.IsInputRedirected; - - /// - public ConsoleWriter Output { get; } - - /// - public bool IsOutputRedirected => Console.IsOutputRedirected; - - /// - public ConsoleWriter Error { get; } - - /// - public bool IsErrorRedirected => Console.IsErrorRedirected; - - /// - public ConsoleColor ForegroundColor - { - get => Console.ForegroundColor; - set => Console.ForegroundColor = value; - } - - /// - public ConsoleColor BackgroundColor - { - get => Console.BackgroundColor; - set => Console.BackgroundColor = value; - } - - /// - /// Initializes an instance of . - /// - public SystemConsole() - { - Input = ConsoleReader.Create(this, Console.OpenStandardInput()); - Output = ConsoleWriter.Create(this, Console.OpenStandardOutput()); - Error = ConsoleWriter.Create(this, Console.OpenStandardError()); - } - - /// - public void ResetColor() => Console.ResetColor(); - - /// - public int CursorLeft - { - get => Console.CursorLeft; - set => Console.CursorLeft = value; - } - - /// - public int CursorTop - { - get => Console.CursorTop; - set => Console.CursorTop = value; - } - - /// - public CancellationToken RegisterCancellationHandler() - { - if (_cancellationTokenSource is not null) - return _cancellationTokenSource.Token; - - var cts = new CancellationTokenSource(); - - Console.CancelKeyPress += (_, args) => - { - // Don't delay cancellation more than once - if (!cts.IsCancellationRequested) - { - args.Cancel = true; - cts.Cancel(); - } - }; - - return (_cancellationTokenSource = cts).Token; - } - - /// - public void Clear() => Console.Clear(); - - /// - public void ReadKey(bool intercept = false) => Console.ReadKey(intercept); - - /// - public void Dispose() - { - _cancellationTokenSource?.Dispose(); - - Input.Dispose(); - Output.Dispose(); - Error.Dispose(); - } + get => Console.ForegroundColor; + set => Console.ForegroundColor = value; } -} + + /// + public ConsoleColor BackgroundColor + { + get => Console.BackgroundColor; + set => Console.BackgroundColor = value; + } + + /// + /// Initializes an instance of . + /// + public SystemConsole() + { + Input = ConsoleReader.Create(this, Console.OpenStandardInput()); + Output = ConsoleWriter.Create(this, Console.OpenStandardOutput()); + Error = ConsoleWriter.Create(this, Console.OpenStandardError()); + } + + /// + public void ResetColor() => Console.ResetColor(); + + /// + public int CursorLeft + { + get => Console.CursorLeft; + set => Console.CursorLeft = value; + } + + /// + public int CursorTop + { + get => Console.CursorTop; + set => Console.CursorTop = value; + } + + /// + public CancellationToken RegisterCancellationHandler() + { + if (_cancellationTokenSource is not null) + return _cancellationTokenSource.Token; + + var cts = new CancellationTokenSource(); + + Console.CancelKeyPress += (_, args) => + { + // Don't delay cancellation more than once + if (!cts.IsCancellationRequested) + { + args.Cancel = true; + cts.Cancel(); + } + }; + + return (_cancellationTokenSource = cts).Token; + } + + /// + public void Clear() => Console.Clear(); + + /// + public void ReadKey(bool intercept = false) => Console.ReadKey(intercept); + + /// + public void Dispose() + { + _cancellationTokenSource?.Dispose(); + + Input.Dispose(); + Output.Dispose(); + Error.Dispose(); + } +} \ No newline at end of file diff --git a/CliFx/Input/CommandInput.cs b/CliFx/Input/CommandInput.cs index 45d514d..1b14ff4 100644 --- a/CliFx/Input/CommandInput.cs +++ b/CliFx/Input/CommandInput.cs @@ -4,227 +4,226 @@ using System.Linq; using System.Text; using CliFx.Utils.Extensions; -namespace CliFx.Input +namespace CliFx.Input; + +internal partial class CommandInput { - internal partial class CommandInput + public string? CommandName { get; } + + public IReadOnlyList Directives { get; } + + public IReadOnlyList Parameters { get; } + + public IReadOnlyList Options { get; } + + public IReadOnlyList EnvironmentVariables { get; } + + public bool IsDebugDirectiveSpecified => Directives.Any(d => d.IsDebugDirective); + + public bool IsPreviewDirectiveSpecified => Directives.Any(d => d.IsPreviewDirective); + + public bool IsHelpOptionSpecified => Options.Any(o => o.IsHelpOption); + + public bool IsVersionOptionSpecified => Options.Any(o => o.IsVersionOption); + + public CommandInput( + string? commandName, + IReadOnlyList directives, + IReadOnlyList parameters, + IReadOnlyList options, + IReadOnlyList environmentVariables) { - public string? CommandName { get; } + CommandName = commandName; + Directives = directives; + Parameters = parameters; + Options = options; + EnvironmentVariables = environmentVariables; + } +} - public IReadOnlyList Directives { get; } +internal partial class CommandInput +{ + private static IReadOnlyList ParseDirectives( + IReadOnlyList commandLineArguments, + ref int index) + { + var result = new List(); - public IReadOnlyList Parameters { get; } - - public IReadOnlyList Options { get; } - - public IReadOnlyList EnvironmentVariables { get; } - - public bool IsDebugDirectiveSpecified => Directives.Any(d => d.IsDebugDirective); - - public bool IsPreviewDirectiveSpecified => Directives.Any(d => d.IsPreviewDirective); - - public bool IsHelpOptionSpecified => Options.Any(o => o.IsHelpOption); - - public bool IsVersionOptionSpecified => Options.Any(o => o.IsVersionOption); - - public CommandInput( - string? commandName, - IReadOnlyList directives, - IReadOnlyList parameters, - IReadOnlyList options, - IReadOnlyList environmentVariables) + // Consume all consecutive directive arguments + for (; index < commandLineArguments.Count; index++) { - CommandName = commandName; - Directives = directives; - Parameters = parameters; - Options = options; - EnvironmentVariables = environmentVariables; + var argument = commandLineArguments[index]; + + // Break on the first non-directive argument + if (!argument.StartsWith('[') || !argument.EndsWith(']')) + break; + + var directiveName = argument.Substring(1, argument.Length - 2); + result.Add(new DirectiveInput(directiveName)); } + + return result; } - internal partial class CommandInput + private static string? ParseCommandName( + IReadOnlyList commandLineArguments, + ISet commandNames, + ref int index) { - private static IReadOnlyList ParseDirectives( - IReadOnlyList commandLineArguments, - ref int index) + var potentialCommandNameComponents = new List(); + var commandName = default(string?); + + var lastIndex = index; + + // Append arguments to a buffer until we find the longest sequence + // that represents a valid command name. + for (var i = index; i < commandLineArguments.Count; i++) { - var result = new List(); + var argument = commandLineArguments[i]; - // Consume all consecutive directive arguments - for (; index < commandLineArguments.Count; index++) + potentialCommandNameComponents.Add(argument); + + var potentialCommandName = potentialCommandNameComponents.JoinToString(" "); + if (commandNames.Contains(potentialCommandName)) { - var argument = commandLineArguments[index]; - - // Break on the first non-directive argument - if (!argument.StartsWith('[') || !argument.EndsWith(']')) - break; - - var directiveName = argument.Substring(1, argument.Length - 2); - result.Add(new DirectiveInput(directiveName)); + // Record the position but continue the loop in case + // we find a longer (more specific) match. + commandName = potentialCommandName; + lastIndex = i; } - - return result; } - private static string? ParseCommandName( - IReadOnlyList commandLineArguments, - ISet commandNames, - ref int index) + // Move the index to the position where the command name ended + if (!string.IsNullOrWhiteSpace(commandName)) + index = lastIndex + 1; + + return commandName; + } + + private static IReadOnlyList ParseParameters( + IReadOnlyList commandLineArguments, + ref int index) + { + var result = new List(); + + // Consume all arguments until first option identifier + for (; index < commandLineArguments.Count; index++) { - var potentialCommandNameComponents = new List(); - var commandName = default(string?); - - var lastIndex = index; - - // Append arguments to a buffer until we find the longest sequence - // that represents a valid command name. - for (var i = index; i < commandLineArguments.Count; i++) - { - var argument = commandLineArguments[i]; - - potentialCommandNameComponents.Add(argument); - - var potentialCommandName = potentialCommandNameComponents.JoinToString(" "); - if (commandNames.Contains(potentialCommandName)) - { - // Record the position but continue the loop in case - // we find a longer (more specific) match. - commandName = potentialCommandName; - lastIndex = i; - } - } - - // Move the index to the position where the command name ended - if (!string.IsNullOrWhiteSpace(commandName)) - index = lastIndex + 1; - - return commandName; - } - - private static IReadOnlyList ParseParameters( - IReadOnlyList commandLineArguments, - ref int index) - { - var result = new List(); - - // Consume all arguments until first option identifier - for (; index < commandLineArguments.Count; index++) - { - var argument = commandLineArguments[index]; - - var isOptionIdentifier = - // Name - argument.StartsWith("--", StringComparison.Ordinal) && - argument.Length > 2 && - char.IsLetter(argument[2]) || - // Short name - argument.StartsWith('-') && - argument.Length > 1 && - char.IsLetter(argument[1]); - - // Break on first option identifier - if (isOptionIdentifier) - break; - - result.Add(new ParameterInput(argument)); - } - - return result; - } - - private static IReadOnlyList ParseOptions( - IReadOnlyList commandLineArguments, - ref int index) - { - var result = new List(); - - var lastOptionIdentifier = default(string?); - var lastOptionValues = new List(); - - // Consume and group all remaining arguments into options - for (; index < commandLineArguments.Count; index++) - { - var argument = commandLineArguments[index]; + var argument = commandLineArguments[index]; + var isOptionIdentifier = // Name - if (argument.StartsWith("--", StringComparison.Ordinal) && - argument.Length > 2 && - char.IsLetter(argument[2])) + argument.StartsWith("--", StringComparison.Ordinal) && + argument.Length > 2 && + char.IsLetter(argument[2]) || + // Short name + argument.StartsWith('-') && + argument.Length > 1 && + char.IsLetter(argument[1]); + + // Break on first option identifier + if (isOptionIdentifier) + break; + + result.Add(new ParameterInput(argument)); + } + + return result; + } + + private static IReadOnlyList ParseOptions( + IReadOnlyList commandLineArguments, + ref int index) + { + var result = new List(); + + var lastOptionIdentifier = default(string?); + var lastOptionValues = new List(); + + // Consume and group all remaining arguments into options + for (; index < commandLineArguments.Count; index++) + { + var argument = commandLineArguments[index]; + + // Name + if (argument.StartsWith("--", StringComparison.Ordinal) && + argument.Length > 2 && + char.IsLetter(argument[2])) + { + // Flush previous + if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) + result.Add(new OptionInput(lastOptionIdentifier, lastOptionValues)); + + lastOptionIdentifier = argument.Substring(2); + lastOptionValues = new List(); + } + // Short name + else if (argument.StartsWith('-') && + argument.Length > 1 && + char.IsLetter(argument[1])) + { + foreach (var alias in argument.Substring(1)) { // Flush previous if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) result.Add(new OptionInput(lastOptionIdentifier, lastOptionValues)); - lastOptionIdentifier = argument.Substring(2); + lastOptionIdentifier = alias.AsString(); lastOptionValues = new List(); } - // Short name - else if (argument.StartsWith('-') && - argument.Length > 1 && - char.IsLetter(argument[1])) - { - foreach (var alias in argument.Substring(1)) - { - // Flush previous - if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) - result.Add(new OptionInput(lastOptionIdentifier, lastOptionValues)); - - lastOptionIdentifier = alias.AsString(); - lastOptionValues = new List(); - } - } - // Value - else if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) - { - lastOptionValues.Add(argument); - } } - - // Flush last option - if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) - result.Add(new OptionInput(lastOptionIdentifier, lastOptionValues)); - - return result; + // Value + else if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) + { + lastOptionValues.Add(argument); + } } - public static CommandInput Parse( - IReadOnlyList commandLineArguments, - IReadOnlyDictionary environmentVariables, - IReadOnlyList availableCommandNames) - { - var index = 0; + // Flush last option + if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) + result.Add(new OptionInput(lastOptionIdentifier, lastOptionValues)); - var parsedDirectives = ParseDirectives( - commandLineArguments, - ref index - ); + return result; + } - var parsedCommandName = ParseCommandName( - commandLineArguments, - availableCommandNames.ToHashSet(StringComparer.OrdinalIgnoreCase), - ref index - ); + public static CommandInput Parse( + IReadOnlyList commandLineArguments, + IReadOnlyDictionary environmentVariables, + IReadOnlyList availableCommandNames) + { + var index = 0; - var parsedParameters = ParseParameters( - commandLineArguments, - ref index - ); + var parsedDirectives = ParseDirectives( + commandLineArguments, + ref index + ); - var parsedOptions = ParseOptions( - commandLineArguments, - ref index - ); + var parsedCommandName = ParseCommandName( + commandLineArguments, + availableCommandNames.ToHashSet(StringComparer.OrdinalIgnoreCase), + ref index + ); - var parsedEnvironmentVariables = environmentVariables - .Select(kvp => new EnvironmentVariableInput(kvp.Key, kvp.Value)) - .ToArray(); + var parsedParameters = ParseParameters( + commandLineArguments, + ref index + ); - return new CommandInput( - parsedCommandName, - parsedDirectives, - parsedParameters, - parsedOptions, - parsedEnvironmentVariables - ); - } + var parsedOptions = ParseOptions( + commandLineArguments, + ref index + ); + + var parsedEnvironmentVariables = environmentVariables + .Select(kvp => new EnvironmentVariableInput(kvp.Key, kvp.Value)) + .ToArray(); + + return new CommandInput( + parsedCommandName, + parsedDirectives, + parsedParameters, + parsedOptions, + parsedEnvironmentVariables + ); } } \ No newline at end of file diff --git a/CliFx/Input/DirectiveInput.cs b/CliFx/Input/DirectiveInput.cs index 928aa10..11dba4f 100644 --- a/CliFx/Input/DirectiveInput.cs +++ b/CliFx/Input/DirectiveInput.cs @@ -1,17 +1,16 @@ using System; -namespace CliFx.Input +namespace CliFx.Input; + +internal class DirectiveInput { - internal class DirectiveInput - { - public string Name { get; } + public string Name { get; } - public bool IsDebugDirective => - string.Equals(Name, "debug", StringComparison.OrdinalIgnoreCase); + public bool IsDebugDirective => + string.Equals(Name, "debug", StringComparison.OrdinalIgnoreCase); - public bool IsPreviewDirective => - string.Equals(Name, "preview", StringComparison.OrdinalIgnoreCase); + public bool IsPreviewDirective => + string.Equals(Name, "preview", StringComparison.OrdinalIgnoreCase); - public DirectiveInput(string name) => Name = name; - } + public DirectiveInput(string name) => Name = name; } \ No newline at end of file diff --git a/CliFx/Input/EnvironmentVariableInput.cs b/CliFx/Input/EnvironmentVariableInput.cs index 8e9c8c6..cad5ea2 100644 --- a/CliFx/Input/EnvironmentVariableInput.cs +++ b/CliFx/Input/EnvironmentVariableInput.cs @@ -1,20 +1,19 @@ using System.Collections.Generic; using System.IO; -namespace CliFx.Input +namespace CliFx.Input; + +internal class EnvironmentVariableInput { - internal class EnvironmentVariableInput + public string Name { get; } + + public string Value { get; } + + public EnvironmentVariableInput(string name, string value) { - public string Name { get; } - - public string Value { get; } - - public EnvironmentVariableInput(string name, string value) - { - Name = name; - Value = value; - } - - public IReadOnlyList SplitValues() => Value.Split(Path.PathSeparator); + Name = name; + Value = value; } + + public IReadOnlyList SplitValues() => Value.Split(Path.PathSeparator); } \ No newline at end of file diff --git a/CliFx/Input/OptionInput.cs b/CliFx/Input/OptionInput.cs index 38b1c2d..7b8af28 100644 --- a/CliFx/Input/OptionInput.cs +++ b/CliFx/Input/OptionInput.cs @@ -1,30 +1,29 @@ using System.Collections.Generic; using CliFx.Schema; -namespace CliFx.Input +namespace CliFx.Input; + +internal class OptionInput { - internal class OptionInput + public string Identifier { get; } + + public IReadOnlyList Values { get; } + + public bool IsHelpOption => + OptionSchema.HelpOption.MatchesIdentifier(Identifier); + + public bool IsVersionOption => + OptionSchema.VersionOption.MatchesIdentifier(Identifier); + + public OptionInput(string identifier, IReadOnlyList values) { - public string Identifier { get; } - - public IReadOnlyList Values { get; } - - public bool IsHelpOption => - OptionSchema.HelpOption.MatchesIdentifier(Identifier); - - public bool IsVersionOption => - OptionSchema.VersionOption.MatchesIdentifier(Identifier); - - public OptionInput(string identifier, IReadOnlyList values) - { - Identifier = identifier; - Values = values; - } - - public string GetFormattedIdentifier() => Identifier switch - { - {Length: >= 2} => "--" + Identifier, - _ => '-' + Identifier - }; + Identifier = identifier; + Values = values; } + + public string GetFormattedIdentifier() => Identifier switch + { + {Length: >= 2} => "--" + Identifier, + _ => '-' + Identifier + }; } \ No newline at end of file diff --git a/CliFx/Input/ParameterInput.cs b/CliFx/Input/ParameterInput.cs index ae3783c..bea55ff 100644 --- a/CliFx/Input/ParameterInput.cs +++ b/CliFx/Input/ParameterInput.cs @@ -1,11 +1,10 @@ -namespace CliFx.Input +namespace CliFx.Input; + +internal class ParameterInput { - internal class ParameterInput - { - public string Value { get; } + public string Value { get; } - public ParameterInput(string value) => Value = value; + public ParameterInput(string value) => Value = value; - public string GetFormattedIdentifier() => $"<{Value}>"; - } + public string GetFormattedIdentifier() => $"<{Value}>"; } \ No newline at end of file diff --git a/CliFx/Schema/ApplicationSchema.cs b/CliFx/Schema/ApplicationSchema.cs index 9b4592a..445f840 100644 --- a/CliFx/Schema/ApplicationSchema.cs +++ b/CliFx/Schema/ApplicationSchema.cs @@ -3,85 +3,84 @@ using System.Collections.Generic; using System.Linq; using CliFx.Utils.Extensions; -namespace CliFx.Schema +namespace CliFx.Schema; + +internal partial class ApplicationSchema { - internal partial class ApplicationSchema + public IReadOnlyList Commands { get; } + + public ApplicationSchema(IReadOnlyList commands) { - public IReadOnlyList Commands { get; } + Commands = commands; + } - public ApplicationSchema(IReadOnlyList commands) + public IReadOnlyList GetCommandNames() => Commands + .Select(c => c.Name) + .Where(n => !string.IsNullOrWhiteSpace(n)) + .ToArray()!; + + public CommandSchema? TryFindDefaultCommand() => + Commands.FirstOrDefault(c => c.IsDefault); + + public CommandSchema? TryFindCommand(string? commandName) => + Commands.FirstOrDefault(c => c.MatchesName(commandName)); + + private IReadOnlyList GetDescendantCommands( + IReadOnlyList potentialParentCommandSchemas, + string? parentCommandName) + { + var result = new List(); + + foreach (var potentialParentCommandSchema in potentialParentCommandSchemas) { - Commands = commands; - } + // Default commands can't be descendant of anything + if (string.IsNullOrWhiteSpace(potentialParentCommandSchema.Name)) + continue; - public IReadOnlyList GetCommandNames() => Commands - .Select(c => c.Name) - .Where(n => !string.IsNullOrWhiteSpace(n)) - .ToArray()!; + // Command can't be its own descendant + if (potentialParentCommandSchema.MatchesName(parentCommandName)) + continue; - public CommandSchema? TryFindDefaultCommand() => - Commands.FirstOrDefault(c => c.IsDefault); - - public CommandSchema? TryFindCommand(string? commandName) => - Commands.FirstOrDefault(c => c.MatchesName(commandName)); - - private IReadOnlyList GetDescendantCommands( - IReadOnlyList potentialParentCommandSchemas, - string? parentCommandName) - { - var result = new List(); - - foreach (var potentialParentCommandSchema in potentialParentCommandSchemas) - { - // Default commands can't be descendant of anything - if (string.IsNullOrWhiteSpace(potentialParentCommandSchema.Name)) - continue; - - // Command can't be its own descendant - if (potentialParentCommandSchema.MatchesName(parentCommandName)) - continue; - - var isDescendant = - // Every command is a descendant of the default command - string.IsNullOrWhiteSpace(parentCommandName) || - // Otherwise a command is a descendant if it starts with the same name segments - potentialParentCommandSchema.Name.StartsWith( - parentCommandName + ' ', - StringComparison.OrdinalIgnoreCase - ); - - if (isDescendant) - result.Add(potentialParentCommandSchema); - } - - return result; - } - - public IReadOnlyList GetDescendantCommands(string? parentCommandName) => - GetDescendantCommands(Commands, parentCommandName); - - public IReadOnlyList GetChildCommands(string? parentCommandName) - { - var descendants = GetDescendantCommands(parentCommandName); - - var result = descendants.ToList(); - - // Filter out descendants of descendants, leave only direct children - foreach (var descendant in descendants) - { - result.RemoveRange( - GetDescendantCommands(descendants, descendant.Name) + var isDescendant = + // Every command is a descendant of the default command + string.IsNullOrWhiteSpace(parentCommandName) || + // Otherwise a command is a descendant if it starts with the same name segments + potentialParentCommandSchema.Name.StartsWith( + parentCommandName + ' ', + StringComparison.OrdinalIgnoreCase ); - } - return result; + if (isDescendant) + result.Add(potentialParentCommandSchema); } + + return result; } - internal partial class ApplicationSchema + public IReadOnlyList GetDescendantCommands(string? parentCommandName) => + GetDescendantCommands(Commands, parentCommandName); + + public IReadOnlyList GetChildCommands(string? parentCommandName) { - public static ApplicationSchema Resolve(IReadOnlyList commandTypes) => new( - commandTypes.Select(CommandSchema.Resolve).ToArray() - ); + var descendants = GetDescendantCommands(parentCommandName); + + var result = descendants.ToList(); + + // Filter out descendants of descendants, leave only direct children + foreach (var descendant in descendants) + { + result.RemoveRange( + GetDescendantCommands(descendants, descendant.Name) + ); + } + + return result; } +} + +internal partial class ApplicationSchema +{ + public static ApplicationSchema Resolve(IReadOnlyList commandTypes) => new( + commandTypes.Select(CommandSchema.Resolve).ToArray() + ); } \ No newline at end of file diff --git a/CliFx/Schema/BindablePropertyDescriptor.cs b/CliFx/Schema/BindablePropertyDescriptor.cs index e620ddb..be861e0 100644 --- a/CliFx/Schema/BindablePropertyDescriptor.cs +++ b/CliFx/Schema/BindablePropertyDescriptor.cs @@ -3,44 +3,43 @@ using System.Collections.Generic; using System.Reflection; using CliFx.Utils.Extensions; -namespace CliFx.Schema +namespace CliFx.Schema; + +internal class BindablePropertyDescriptor : IPropertyDescriptor { - internal class BindablePropertyDescriptor : IPropertyDescriptor + private readonly PropertyInfo _property; + + public Type Type => _property.PropertyType; + + public BindablePropertyDescriptor(PropertyInfo property) => _property = property; + + public object? GetValue(ICommand commandInstance) => + _property.GetValue(commandInstance); + + public void SetValue(ICommand commandInstance, object? value) => + _property.SetValue(commandInstance, value); + + public IReadOnlyList GetValidValues() { - private readonly PropertyInfo _property; - - public Type Type => _property.PropertyType; - - public BindablePropertyDescriptor(PropertyInfo property) => _property = property; - - public object? GetValue(ICommand commandInstance) => - _property.GetValue(commandInstance); - - public void SetValue(ICommand commandInstance, object? value) => - _property.SetValue(commandInstance, value); - - public IReadOnlyList GetValidValues() + static Type GetUnderlyingType(Type type) { - static Type GetUnderlyingType(Type type) - { - var enumerableUnderlyingType = type.TryGetEnumerableUnderlyingType(); - if (enumerableUnderlyingType is not null) - return GetUnderlyingType(enumerableUnderlyingType); + var enumerableUnderlyingType = type.TryGetEnumerableUnderlyingType(); + if (enumerableUnderlyingType is not null) + return GetUnderlyingType(enumerableUnderlyingType); - var nullableUnderlyingType = type.TryGetNullableUnderlyingType(); - if (nullableUnderlyingType is not null) - return GetUnderlyingType(nullableUnderlyingType); + var nullableUnderlyingType = type.TryGetNullableUnderlyingType(); + if (nullableUnderlyingType is not null) + return GetUnderlyingType(nullableUnderlyingType); - return type; - } - - var underlyingType = GetUnderlyingType(Type); - - // We can only get valid values for enums - if (underlyingType.IsEnum) - return Enum.GetNames(underlyingType); - - return Array.Empty(); + return type; } + + var underlyingType = GetUnderlyingType(Type); + + // We can only get valid values for enums + if (underlyingType.IsEnum) + return Enum.GetNames(underlyingType); + + return Array.Empty(); } -} +} \ No newline at end of file diff --git a/CliFx/Schema/CommandSchema.cs b/CliFx/Schema/CommandSchema.cs index 952ffbd..61d518c 100644 --- a/CliFx/Schema/CommandSchema.cs +++ b/CliFx/Schema/CommandSchema.cs @@ -6,127 +6,126 @@ using CliFx.Attributes; using CliFx.Exceptions; using CliFx.Utils.Extensions; -namespace CliFx.Schema +namespace CliFx.Schema; + +internal partial class CommandSchema { - internal partial class CommandSchema + public Type Type { get; } + + public string? Name { get; } + + public string? Description { get; } + + public IReadOnlyList Parameters { get; } + + public IReadOnlyList Options { get; } + + public bool IsDefault => string.IsNullOrWhiteSpace(Name); + + public bool IsHelpOptionAvailable => Options.Contains(OptionSchema.HelpOption); + + public bool IsVersionOptionAvailable => Options.Contains(OptionSchema.VersionOption); + + public CommandSchema( + Type type, + string? name, + string? description, + IReadOnlyList parameters, + IReadOnlyList options) { - public Type Type { get; } - - public string? Name { get; } - - public string? Description { get; } - - public IReadOnlyList Parameters { get; } - - public IReadOnlyList Options { get; } - - public bool IsDefault => string.IsNullOrWhiteSpace(Name); - - public bool IsHelpOptionAvailable => Options.Contains(OptionSchema.HelpOption); - - public bool IsVersionOptionAvailable => Options.Contains(OptionSchema.VersionOption); - - public CommandSchema( - Type type, - string? name, - string? description, - IReadOnlyList parameters, - IReadOnlyList options) - { - Type = type; - Name = name; - Description = description; - Parameters = parameters; - Options = options; - } - - public bool MatchesName(string? name) => - !string.IsNullOrWhiteSpace(Name) - ? string.Equals(name, Name, StringComparison.OrdinalIgnoreCase) - : string.IsNullOrWhiteSpace(name); - - public IReadOnlyDictionary GetValues(ICommand instance) - { - var result = new Dictionary(); - - foreach (var parameterSchema in Parameters) - { - var value = parameterSchema.Property.GetValue(instance); - result[parameterSchema] = value; - } - - foreach (var optionSchema in Options) - { - var value = optionSchema.Property.GetValue(instance); - result[optionSchema] = value; - } - - return result; - } + Type = type; + Name = name; + Description = description; + Parameters = parameters; + Options = options; } - internal partial class CommandSchema + public bool MatchesName(string? name) => + !string.IsNullOrWhiteSpace(Name) + ? string.Equals(name, Name, StringComparison.OrdinalIgnoreCase) + : string.IsNullOrWhiteSpace(name); + + public IReadOnlyDictionary GetValues(ICommand instance) { - public static bool IsCommandType(Type type) => - type.Implements(typeof(ICommand)) && - type.IsDefined(typeof(CommandAttribute)) && - !type.IsAbstract && - !type.IsInterface; + var result = new Dictionary(); - public static CommandSchema? TryResolve(Type type) + foreach (var parameterSchema in Parameters) { - if (!IsCommandType(type)) - return null; + var value = parameterSchema.Property.GetValue(instance); + result[parameterSchema] = value; + } - var attribute = type.GetCustomAttribute(); + foreach (var optionSchema in Options) + { + var value = optionSchema.Property.GetValue(instance); + result[optionSchema] = value; + } - var name = attribute?.Name?.Trim(); - var description = attribute?.Description?.Trim(); + return result; + } +} - var implicitOptionSchemas = string.IsNullOrWhiteSpace(name) - ? new[] {OptionSchema.HelpOption, OptionSchema.VersionOption} - : new[] {OptionSchema.HelpOption}; +internal partial class CommandSchema +{ + public static bool IsCommandType(Type type) => + type.Implements(typeof(ICommand)) && + type.IsDefined(typeof(CommandAttribute)) && + !type.IsAbstract && + !type.IsInterface; - var parameterSchemas = type.GetProperties() - .Select(ParameterSchema.TryResolve) - .Where(p => p is not null) - .ToArray(); + public static CommandSchema? TryResolve(Type type) + { + if (!IsCommandType(type)) + return null; - var optionSchemas = type.GetProperties() - .Select(OptionSchema.TryResolve) - .Where(o => o is not null) - .Concat(implicitOptionSchemas) - .ToArray(); + var attribute = type.GetCustomAttribute(); - return new CommandSchema( - type, - name, - description, - parameterSchemas!, - optionSchemas! + var name = attribute?.Name?.Trim(); + var description = attribute?.Description?.Trim(); + + var implicitOptionSchemas = string.IsNullOrWhiteSpace(name) + ? new[] {OptionSchema.HelpOption, OptionSchema.VersionOption} + : new[] {OptionSchema.HelpOption}; + + var parameterSchemas = type.GetProperties() + .Select(ParameterSchema.TryResolve) + .Where(p => p is not null) + .ToArray(); + + var optionSchemas = type.GetProperties() + .Select(OptionSchema.TryResolve) + .Where(o => o is not null) + .Concat(implicitOptionSchemas) + .ToArray(); + + return new CommandSchema( + type, + name, + description, + parameterSchemas!, + optionSchemas! + ); + } + + public static CommandSchema Resolve(Type type) + { + var schema = TryResolve(type); + + if (schema is null) + { + throw CliFxException.InternalError( + $"Type `{type.FullName}` is not a valid command type." + + Environment.NewLine + + "In order to be a valid command type, it must:" + + Environment.NewLine + + $"- Implement `{typeof(ICommand).FullName}`" + + Environment.NewLine + + $"- Be annotated with `{typeof(CommandAttribute).FullName}`" + + Environment.NewLine + + "- Not be an abstract class" ); } - public static CommandSchema Resolve(Type type) - { - var schema = TryResolve(type); - - if (schema is null) - { - throw CliFxException.InternalError( - $"Type `{type.FullName}` is not a valid command type." + - Environment.NewLine + - "In order to be a valid command type, it must:" + - Environment.NewLine + - $"- Implement `{typeof(ICommand).FullName}`" + - Environment.NewLine + - $"- Be annotated with `{typeof(CommandAttribute).FullName}`" + - Environment.NewLine + - "- Not be an abstract class" - ); - } - - return schema; - } + return schema; } } \ No newline at end of file diff --git a/CliFx/Schema/IMemberSchema.cs b/CliFx/Schema/IMemberSchema.cs index 70dd3bf..32f5a66 100644 --- a/CliFx/Schema/IMemberSchema.cs +++ b/CliFx/Schema/IMemberSchema.cs @@ -1,26 +1,25 @@ using System; using System.Collections.Generic; -namespace CliFx.Schema +namespace CliFx.Schema; + +internal interface IMemberSchema { - internal interface IMemberSchema + IPropertyDescriptor Property { get; } + + Type? ConverterType { get; } + + IReadOnlyList ValidatorTypes { get; } + + string GetFormattedIdentifier(); +} + +internal static class MemberSchemaExtensions +{ + public static string GetKind(this IMemberSchema memberSchema) => memberSchema switch { - IPropertyDescriptor Property { get; } - - Type? ConverterType { get; } - - IReadOnlyList ValidatorTypes { get; } - - string GetFormattedIdentifier(); - } - - internal static class MemberSchemaExtensions - { - public static string GetKind(this IMemberSchema memberSchema) => memberSchema switch - { - ParameterSchema => "Parameter", - OptionSchema => "Option", - _ => throw new ArgumentOutOfRangeException(nameof(memberSchema)) - }; - } + ParameterSchema => "Parameter", + OptionSchema => "Option", + _ => throw new ArgumentOutOfRangeException(nameof(memberSchema)) + }; } \ No newline at end of file diff --git a/CliFx/Schema/IPropertyDescriptor.cs b/CliFx/Schema/IPropertyDescriptor.cs index 8e63e94..221c095 100644 --- a/CliFx/Schema/IPropertyDescriptor.cs +++ b/CliFx/Schema/IPropertyDescriptor.cs @@ -2,23 +2,22 @@ using System.Collections.Generic; using CliFx.Utils.Extensions; -namespace CliFx.Schema +namespace CliFx.Schema; + +internal interface IPropertyDescriptor { - internal interface IPropertyDescriptor - { - Type Type { get; } + Type Type { get; } - object? GetValue(ICommand commandInstance); + object? GetValue(ICommand commandInstance); - void SetValue(ICommand commandInstance, object? value); + void SetValue(ICommand commandInstance, object? value); - IReadOnlyList GetValidValues(); - } + IReadOnlyList GetValidValues(); +} - internal static class PropertyDescriptorExtensions - { - public static bool IsScalar(this IPropertyDescriptor propertyDescriptor) => - propertyDescriptor.Type == typeof(string) || - propertyDescriptor.Type.TryGetEnumerableUnderlyingType() is null; - } +internal static class PropertyDescriptorExtensions +{ + public static bool IsScalar(this IPropertyDescriptor propertyDescriptor) => + propertyDescriptor.Type == typeof(string) || + propertyDescriptor.Type.TryGetEnumerableUnderlyingType() is null; } \ No newline at end of file diff --git a/CliFx/Schema/NullPropertyDescriptor.cs b/CliFx/Schema/NullPropertyDescriptor.cs index c16bf33..d62a415 100644 --- a/CliFx/Schema/NullPropertyDescriptor.cs +++ b/CliFx/Schema/NullPropertyDescriptor.cs @@ -1,23 +1,22 @@ using System; using System.Collections.Generic; -namespace CliFx.Schema +namespace CliFx.Schema; + +internal partial class NullPropertyDescriptor : IPropertyDescriptor { - internal partial class NullPropertyDescriptor : IPropertyDescriptor + public Type Type { get; } = typeof(object); + + public object? GetValue(ICommand commandInstance) => null; + + public void SetValue(ICommand commandInstance, object? value) { - public Type Type { get; } = typeof(object); - - public object? GetValue(ICommand commandInstance) => null; - - public void SetValue(ICommand commandInstance, object? value) - { - } - - public IReadOnlyList GetValidValues() => Array.Empty(); } - internal partial class NullPropertyDescriptor - { - public static NullPropertyDescriptor Instance { get; } = new(); - } + public IReadOnlyList GetValidValues() => Array.Empty(); +} + +internal partial class NullPropertyDescriptor +{ + public static NullPropertyDescriptor Instance { get; } = new(); } \ No newline at end of file diff --git a/CliFx/Schema/OptionSchema.cs b/CliFx/Schema/OptionSchema.cs index af1ca2d..34e4f7b 100644 --- a/CliFx/Schema/OptionSchema.cs +++ b/CliFx/Schema/OptionSchema.cs @@ -4,140 +4,139 @@ using System.Reflection; using System.Text; using CliFx.Attributes; -namespace CliFx.Schema +namespace CliFx.Schema; + +internal partial class OptionSchema : IMemberSchema { - internal partial class OptionSchema : IMemberSchema + public IPropertyDescriptor Property { get; } + + public string? Name { get; } + + public char? ShortName { get; } + + public string? EnvironmentVariable { get; } + + public bool IsRequired { get; } + + public string? Description { get; } + + public Type? ConverterType { get; } + + public IReadOnlyList ValidatorTypes { get; } + + public OptionSchema( + IPropertyDescriptor property, + string? name, + char? shortName, + string? environmentVariable, + bool isRequired, + string? description, + Type? converterType, + IReadOnlyList validatorTypes) { - public IPropertyDescriptor Property { get; } - - public string? Name { get; } - - public char? ShortName { get; } - - public string? EnvironmentVariable { get; } - - public bool IsRequired { get; } - - public string? Description { get; } - - public Type? ConverterType { get; } - - public IReadOnlyList ValidatorTypes { get; } - - public OptionSchema( - IPropertyDescriptor property, - string? name, - char? shortName, - string? environmentVariable, - bool isRequired, - string? description, - Type? converterType, - IReadOnlyList validatorTypes) - { - Property = property; - Name = name; - ShortName = shortName; - EnvironmentVariable = environmentVariable; - IsRequired = isRequired; - Description = description; - ConverterType = converterType; - ValidatorTypes = validatorTypes; - } - - public bool MatchesName(string? name) => - !string.IsNullOrWhiteSpace(Name) && - string.Equals(Name, name, StringComparison.OrdinalIgnoreCase); - - public bool MatchesShortName(char? shortName) => - ShortName is not null && - ShortName == shortName; - - public bool MatchesIdentifier(string identifier) => - MatchesName(identifier) || - identifier.Length == 1 && MatchesShortName(identifier[0]); - - public bool MatchesEnvironmentVariable(string environmentVariableName) => - !string.IsNullOrWhiteSpace(EnvironmentVariable) && - string.Equals(EnvironmentVariable, environmentVariableName, StringComparison.Ordinal); - - public string GetFormattedIdentifier() - { - var buffer = new StringBuilder(); - - // Short name - if (ShortName is not null) - { - buffer - .Append('-') - .Append(ShortName); - } - - // Separator - if (!string.IsNullOrWhiteSpace(Name) && ShortName is not null) - { - buffer.Append('|'); - } - - // Name - if (!string.IsNullOrWhiteSpace(Name)) - { - buffer - .Append("--") - .Append(Name); - } - - return buffer.ToString(); - } + Property = property; + Name = name; + ShortName = shortName; + EnvironmentVariable = environmentVariable; + IsRequired = isRequired; + Description = description; + ConverterType = converterType; + ValidatorTypes = validatorTypes; } - internal partial class OptionSchema + public bool MatchesName(string? name) => + !string.IsNullOrWhiteSpace(Name) && + string.Equals(Name, name, StringComparison.OrdinalIgnoreCase); + + public bool MatchesShortName(char? shortName) => + ShortName is not null && + ShortName == shortName; + + public bool MatchesIdentifier(string identifier) => + MatchesName(identifier) || + identifier.Length == 1 && MatchesShortName(identifier[0]); + + public bool MatchesEnvironmentVariable(string environmentVariableName) => + !string.IsNullOrWhiteSpace(EnvironmentVariable) && + string.Equals(EnvironmentVariable, environmentVariableName, StringComparison.Ordinal); + + public string GetFormattedIdentifier() { - public static OptionSchema? TryResolve(PropertyInfo property) + var buffer = new StringBuilder(); + + // Short name + if (ShortName is not null) { - var attribute = property.GetCustomAttribute(); - if (attribute is null) - return null; - - // The user may mistakenly specify dashes, thinking it's required, so trim them - var name = attribute.Name?.TrimStart('-').Trim(); - var environmentVariable = attribute.EnvironmentVariable?.Trim(); - var description = attribute.Description?.Trim(); - - return new OptionSchema( - new BindablePropertyDescriptor(property), - name, - attribute.ShortName, - environmentVariable, - attribute.IsRequired, - description, - attribute.Converter, - attribute.Validators - ); + buffer + .Append('-') + .Append(ShortName); } + + // Separator + if (!string.IsNullOrWhiteSpace(Name) && ShortName is not null) + { + buffer.Append('|'); + } + + // Name + if (!string.IsNullOrWhiteSpace(Name)) + { + buffer + .Append("--") + .Append(Name); + } + + return buffer.ToString(); } +} - internal partial class OptionSchema +internal partial class OptionSchema +{ + public static OptionSchema? TryResolve(PropertyInfo property) { - public static OptionSchema HelpOption { get; } = new( - NullPropertyDescriptor.Instance, - "help", - 'h', - null, - false, - "Shows help text.", - null, - Array.Empty() - ); + var attribute = property.GetCustomAttribute(); + if (attribute is null) + return null; - public static OptionSchema VersionOption { get; } = new( - NullPropertyDescriptor.Instance, - "version", - null, - null, - false, - "Shows version information.", - null, - Array.Empty() + // The user may mistakenly specify dashes, thinking it's required, so trim them + var name = attribute.Name?.TrimStart('-').Trim(); + var environmentVariable = attribute.EnvironmentVariable?.Trim(); + var description = attribute.Description?.Trim(); + + return new OptionSchema( + new BindablePropertyDescriptor(property), + name, + attribute.ShortName, + environmentVariable, + attribute.IsRequired, + description, + attribute.Converter, + attribute.Validators ); } +} + +internal partial class OptionSchema +{ + public static OptionSchema HelpOption { get; } = new( + NullPropertyDescriptor.Instance, + "help", + 'h', + null, + false, + "Shows help text.", + null, + Array.Empty() + ); + + public static OptionSchema VersionOption { get; } = new( + NullPropertyDescriptor.Instance, + "version", + null, + null, + false, + "Shows version information.", + null, + Array.Empty() + ); } \ No newline at end of file diff --git a/CliFx/Schema/ParameterSchema.cs b/CliFx/Schema/ParameterSchema.cs index 3c51856..e8ec7c6 100644 --- a/CliFx/Schema/ParameterSchema.cs +++ b/CliFx/Schema/ParameterSchema.cs @@ -3,62 +3,61 @@ using System.Collections.Generic; using System.Reflection; using CliFx.Attributes; -namespace CliFx.Schema +namespace CliFx.Schema; + +internal partial class ParameterSchema : IMemberSchema { - internal partial class ParameterSchema : IMemberSchema + public IPropertyDescriptor Property { get; } + + public int Order { get; } + + public string Name { get; } + + public string? Description { get; } + + public Type? ConverterType { get; } + + public IReadOnlyList ValidatorTypes { get; } + + public ParameterSchema( + IPropertyDescriptor property, + int order, + string name, + string? description, + Type? converterType, + IReadOnlyList validatorTypes) { - public IPropertyDescriptor Property { get; } - - public int Order { get; } - - public string Name { get; } - - public string? Description { get; } - - public Type? ConverterType { get; } - - public IReadOnlyList ValidatorTypes { get; } - - public ParameterSchema( - IPropertyDescriptor property, - int order, - string name, - string? description, - Type? converterType, - IReadOnlyList validatorTypes) - { - Property = property; - Order = order; - Name = name; - Description = description; - ConverterType = converterType; - ValidatorTypes = validatorTypes; - } - - public string GetFormattedIdentifier() => Property.IsScalar() - ? $"<{Name}>" - : $"<{Name}...>"; + Property = property; + Order = order; + Name = name; + Description = description; + ConverterType = converterType; + ValidatorTypes = validatorTypes; } - internal partial class ParameterSchema + public string GetFormattedIdentifier() => Property.IsScalar() + ? $"<{Name}>" + : $"<{Name}...>"; +} + +internal partial class ParameterSchema +{ + public static ParameterSchema? TryResolve(PropertyInfo property) { - public static ParameterSchema? TryResolve(PropertyInfo property) - { - var attribute = property.GetCustomAttribute(); - if (attribute is null) - return null; + var attribute = property.GetCustomAttribute(); + if (attribute is null) + return null; - var name = attribute.Name?.Trim() ?? property.Name.ToLowerInvariant(); - var description = attribute.Description?.Trim(); + var name = attribute.Name?.Trim() ?? property.Name.ToLowerInvariant(); + var description = attribute.Description?.Trim(); - return new ParameterSchema( - new BindablePropertyDescriptor(property), - attribute.Order, - name, - description, - attribute.Converter, - attribute.Validators - ); - } + return new ParameterSchema( + new BindablePropertyDescriptor(property), + attribute.Order, + name, + description, + attribute.Converter, + attribute.Validators + ); } } \ No newline at end of file diff --git a/CliFx/Utils/Disposable.cs b/CliFx/Utils/Disposable.cs index 8f5c888..0a71e50 100644 --- a/CliFx/Utils/Disposable.cs +++ b/CliFx/Utils/Disposable.cs @@ -1,28 +1,27 @@ using System; using System.Collections.Generic; -namespace CliFx.Utils +namespace CliFx.Utils; + +internal partial class Disposable : IDisposable { - internal partial class Disposable : IDisposable + private readonly Action _dispose; + + public Disposable(Action dispose) => _dispose = dispose; + + public void Dispose() => _dispose(); +} + +internal partial class Disposable +{ + public static IDisposable Create(Action dispose) => new Disposable(dispose); + + public static IDisposable Merge(IEnumerable disposables) => Create(() => { - private readonly Action _dispose; + foreach (var disposable in disposables) + disposable.Dispose(); + }); - public Disposable(Action dispose) => _dispose = dispose; - - public void Dispose() => _dispose(); - } - - internal partial class Disposable - { - public static IDisposable Create(Action dispose) => new Disposable(dispose); - - public static IDisposable Merge(IEnumerable disposables) => Create(() => - { - foreach (var disposable in disposables) - disposable.Dispose(); - }); - - public static IDisposable Merge(params IDisposable[] disposables) => - Merge((IEnumerable) disposables); - } + public static IDisposable Merge(params IDisposable[] disposables) => + Merge((IEnumerable) disposables); } \ No newline at end of file diff --git a/CliFx/Utils/Extensions/CollectionExtensions.cs b/CliFx/Utils/Extensions/CollectionExtensions.cs index aaccfe6..a2d7375 100644 --- a/CliFx/Utils/Extensions/CollectionExtensions.cs +++ b/CliFx/Utils/Extensions/CollectionExtensions.cs @@ -2,21 +2,20 @@ using System.Collections.Generic; using System.Linq; -namespace CliFx.Utils.Extensions -{ - internal static class CollectionExtensions - { - public static void RemoveRange(this ICollection source, IEnumerable items) - { - foreach (var item in items) - source.Remove(item); - } +namespace CliFx.Utils.Extensions; - public static Dictionary ToDictionary( - this IDictionary dictionary, - IEqualityComparer comparer) => - dictionary - .Cast() - .ToDictionary(entry => (TKey) entry.Key, entry => (TValue) entry.Value, comparer)!; +internal static class CollectionExtensions +{ + public static void RemoveRange(this ICollection source, IEnumerable items) + { + foreach (var item in items) + source.Remove(item); } + + public static Dictionary ToDictionary( + this IDictionary dictionary, + IEqualityComparer comparer) => + dictionary + .Cast() + .ToDictionary(entry => (TKey) entry.Key, entry => (TValue) entry.Value, comparer)!; } \ No newline at end of file diff --git a/CliFx/Utils/Extensions/StringExtensions.cs b/CliFx/Utils/Extensions/StringExtensions.cs index 0449f47..1a9831b 100644 --- a/CliFx/Utils/Extensions/StringExtensions.cs +++ b/CliFx/Utils/Extensions/StringExtensions.cs @@ -1,28 +1,27 @@ using System; using System.Collections.Generic; -namespace CliFx.Utils.Extensions +namespace CliFx.Utils.Extensions; + +internal static class StringExtensions { - internal static class StringExtensions - { - public static string? NullIfWhiteSpace(this string str) => - !string.IsNullOrWhiteSpace(str) - ? str - : null; + public static string? NullIfWhiteSpace(this string str) => + !string.IsNullOrWhiteSpace(str) + ? str + : null; - public static string Repeat(this char c, int count) => new(c, count); + public static string Repeat(this char c, int count) => new(c, count); - public static string AsString(this char c) => c.Repeat(1); + public static string AsString(this char c) => c.Repeat(1); - public static string JoinToString(this IEnumerable source, string separator) => - string.Join(separator, source); + public static string JoinToString(this IEnumerable source, string separator) => + string.Join(separator, source); - public static string ToString( - this object obj, - IFormatProvider? formatProvider = null, - string? format = null) => - obj is IFormattable formattable - ? formattable.ToString(format, formatProvider) - : obj.ToString(); - } + public static string ToString( + this object obj, + IFormatProvider? formatProvider = null, + string? format = null) => + obj is IFormattable formattable + ? formattable.ToString(format, formatProvider) + : obj.ToString(); } \ No newline at end of file diff --git a/CliFx/Utils/Extensions/TypeExtensions.cs b/CliFx/Utils/Extensions/TypeExtensions.cs index 8916965..d8c71b1 100644 --- a/CliFx/Utils/Extensions/TypeExtensions.cs +++ b/CliFx/Utils/Extensions/TypeExtensions.cs @@ -4,83 +4,82 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -namespace CliFx.Utils.Extensions +namespace CliFx.Utils.Extensions; + +internal static class TypeExtensions { - internal static class TypeExtensions + public static bool Implements(this Type type, Type interfaceType) => + type.GetInterfaces().Contains(interfaceType); + + public static Type? TryGetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type); + + public static Type? TryGetEnumerableUnderlyingType(this Type type) { - public static bool Implements(this Type type, Type interfaceType) => - type.GetInterfaces().Contains(interfaceType); + if (type.IsPrimitive) + return null; - public static Type? TryGetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type); + if (type == typeof(IEnumerable)) + return typeof(object); - public static Type? TryGetEnumerableUnderlyingType(this Type type) - { - if (type.IsPrimitive) - return null; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + return type.GetGenericArguments().FirstOrDefault(); - if (type == typeof(IEnumerable)) - return typeof(object); - - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - return type.GetGenericArguments().FirstOrDefault(); - - return type - .GetInterfaces() - .Select(TryGetEnumerableUnderlyingType) - .Where(t => t is not null) - .OrderByDescending(t => t != typeof(object)) // prioritize more specific types - .FirstOrDefault(); - } - - public static MethodInfo? TryGetStaticParseMethod(this Type type, bool withFormatProvider = false) - { - var argumentTypes = withFormatProvider - ? new[] {typeof(string), typeof(IFormatProvider)} - : new[] {typeof(string)}; - - return type.GetMethod("Parse", - BindingFlags.Public | BindingFlags.Static, - null, argumentTypes, null - ); - } - - public static Array ToNonGenericArray(this IEnumerable source, Type elementType) - { - var sourceAsCollection = source as ICollection ?? source.ToArray(); - - var array = Array.CreateInstance(elementType, sourceAsCollection.Count); - sourceAsCollection.CopyTo(array, 0); - - return array; - } - - public static bool IsToStringOverriden(this Type type) - { - var toStringMethod = type.GetMethod(nameof(ToString), Type.EmptyTypes); - return toStringMethod?.GetBaseDefinition()?.DeclaringType != toStringMethod?.DeclaringType; - } - - // Types supported by `Convert.ChangeType(...)` - private static readonly HashSet ConvertibleTypes = new() - { - typeof(bool), - typeof(char), - typeof(sbyte), - typeof(byte), - typeof(short), - typeof(ushort), - typeof(int), - typeof(uint), - typeof(long), - typeof(ulong), - typeof(float), - typeof(double), - typeof(decimal), - typeof(DateTime), - typeof(string), - typeof(object) - }; - - public static bool IsConvertible(this Type type) => ConvertibleTypes.Contains(type); + return type + .GetInterfaces() + .Select(TryGetEnumerableUnderlyingType) + .Where(t => t is not null) + .OrderByDescending(t => t != typeof(object)) // prioritize more specific types + .FirstOrDefault(); } + + public static MethodInfo? TryGetStaticParseMethod(this Type type, bool withFormatProvider = false) + { + var argumentTypes = withFormatProvider + ? new[] {typeof(string), typeof(IFormatProvider)} + : new[] {typeof(string)}; + + return type.GetMethod("Parse", + BindingFlags.Public | BindingFlags.Static, + null, argumentTypes, null + ); + } + + public static Array ToNonGenericArray(this IEnumerable source, Type elementType) + { + var sourceAsCollection = source as ICollection ?? source.ToArray(); + + var array = Array.CreateInstance(elementType, sourceAsCollection.Count); + sourceAsCollection.CopyTo(array, 0); + + return array; + } + + public static bool IsToStringOverriden(this Type type) + { + var toStringMethod = type.GetMethod(nameof(ToString), Type.EmptyTypes); + return toStringMethod?.GetBaseDefinition()?.DeclaringType != toStringMethod?.DeclaringType; + } + + // Types supported by `Convert.ChangeType(...)` + private static readonly HashSet ConvertibleTypes = new() + { + typeof(bool), + typeof(char), + typeof(sbyte), + typeof(byte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(decimal), + typeof(DateTime), + typeof(string), + typeof(object) + }; + + public static bool IsConvertible(this Type type) => ConvertibleTypes.Contains(type); } \ No newline at end of file diff --git a/CliFx/Utils/Extensions/VersionExtensions.cs b/CliFx/Utils/Extensions/VersionExtensions.cs index 0b2cfcc..62bed50 100644 --- a/CliFx/Utils/Extensions/VersionExtensions.cs +++ b/CliFx/Utils/Extensions/VersionExtensions.cs @@ -1,10 +1,9 @@ using System; -namespace CliFx.Utils.Extensions +namespace CliFx.Utils.Extensions; + +internal static class VersionExtensions { - internal static class VersionExtensions - { - public static string ToSemanticString(this Version version) => - version.Revision <= 0 ? version.ToString(3) : version.ToString(); - } + public static string ToSemanticString(this Version version) => + version.Revision <= 0 ? version.ToString(3) : version.ToString(); } \ No newline at end of file diff --git a/CliFx/Utils/NoPreambleEncoding.cs b/CliFx/Utils/NoPreambleEncoding.cs index d69b161..9f35c19 100644 --- a/CliFx/Utils/NoPreambleEncoding.cs +++ b/CliFx/Utils/NoPreambleEncoding.cs @@ -2,141 +2,140 @@ using System.Diagnostics.CodeAnalysis; using System.Text; -namespace CliFx.Utils +namespace CliFx.Utils; + +// Adapted from: +// https://github.com/dotnet/runtime/blob/01b7e73cd378145264a7cb7a09365b41ed42b240/src/libraries/Common/src/System/Text/ConsoleEncoding.cs +// Also see: +// https://source.dot.net/#System.Console/ConsoleEncoding.cs,5eedd083a4a4f4a2 +internal class NoPreambleEncoding : Encoding { - // Adapted from: - // https://github.com/dotnet/runtime/blob/01b7e73cd378145264a7cb7a09365b41ed42b240/src/libraries/Common/src/System/Text/ConsoleEncoding.cs - // Also see: - // https://source.dot.net/#System.Console/ConsoleEncoding.cs,5eedd083a4a4f4a2 - internal class NoPreambleEncoding : Encoding + private readonly Encoding _underlyingEncoding; + + [ExcludeFromCodeCoverage] + public override string EncodingName => _underlyingEncoding.EncodingName; + + [ExcludeFromCodeCoverage] + public override string BodyName => _underlyingEncoding.BodyName; + + [ExcludeFromCodeCoverage] + public override int CodePage => _underlyingEncoding.CodePage; + + [ExcludeFromCodeCoverage] + public override int WindowsCodePage => _underlyingEncoding.WindowsCodePage; + + [ExcludeFromCodeCoverage] + public override string HeaderName => _underlyingEncoding.HeaderName; + + [ExcludeFromCodeCoverage] + public override string WebName => _underlyingEncoding.WebName; + + [ExcludeFromCodeCoverage] + public override bool IsBrowserDisplay => _underlyingEncoding.IsBrowserDisplay; + + [ExcludeFromCodeCoverage] + public override bool IsBrowserSave => _underlyingEncoding.IsBrowserSave; + + [ExcludeFromCodeCoverage] + public override bool IsSingleByte => _underlyingEncoding.IsSingleByte; + + [ExcludeFromCodeCoverage] + public override bool IsMailNewsDisplay => _underlyingEncoding.IsMailNewsDisplay; + + [ExcludeFromCodeCoverage] + public override bool IsMailNewsSave => _underlyingEncoding.IsMailNewsSave; + + public NoPreambleEncoding(Encoding underlyingEncoding) + : base( + underlyingEncoding.CodePage, + underlyingEncoding.EncoderFallback, + underlyingEncoding.DecoderFallback + ) { - private readonly Encoding _underlyingEncoding; - - [ExcludeFromCodeCoverage] - public override string EncodingName => _underlyingEncoding.EncodingName; - - [ExcludeFromCodeCoverage] - public override string BodyName => _underlyingEncoding.BodyName; - - [ExcludeFromCodeCoverage] - public override int CodePage => _underlyingEncoding.CodePage; - - [ExcludeFromCodeCoverage] - public override int WindowsCodePage => _underlyingEncoding.WindowsCodePage; - - [ExcludeFromCodeCoverage] - public override string HeaderName => _underlyingEncoding.HeaderName; - - [ExcludeFromCodeCoverage] - public override string WebName => _underlyingEncoding.WebName; - - [ExcludeFromCodeCoverage] - public override bool IsBrowserDisplay => _underlyingEncoding.IsBrowserDisplay; - - [ExcludeFromCodeCoverage] - public override bool IsBrowserSave => _underlyingEncoding.IsBrowserSave; - - [ExcludeFromCodeCoverage] - public override bool IsSingleByte => _underlyingEncoding.IsSingleByte; - - [ExcludeFromCodeCoverage] - public override bool IsMailNewsDisplay => _underlyingEncoding.IsMailNewsDisplay; - - [ExcludeFromCodeCoverage] - public override bool IsMailNewsSave => _underlyingEncoding.IsMailNewsSave; - - public NoPreambleEncoding(Encoding underlyingEncoding) - : base( - underlyingEncoding.CodePage, - underlyingEncoding.EncoderFallback, - underlyingEncoding.DecoderFallback - ) - { - _underlyingEncoding = underlyingEncoding; - } - - // This is the only part that changes - public override byte[] GetPreamble() => Array.Empty(); - - [ExcludeFromCodeCoverage] - public override int GetByteCount(char[] chars, int index, int count) => - _underlyingEncoding.GetByteCount(chars, index, count); - - [ExcludeFromCodeCoverage] - public override int GetByteCount(char[] chars) => _underlyingEncoding.GetByteCount(chars); - - [ExcludeFromCodeCoverage] - public override int GetByteCount(string s) => _underlyingEncoding.GetByteCount(s); - - [ExcludeFromCodeCoverage] - public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) => - _underlyingEncoding.GetBytes(chars, charIndex, charCount, bytes, byteIndex); - - [ExcludeFromCodeCoverage] - public override byte[] GetBytes(char[] chars) => _underlyingEncoding.GetBytes(chars); - - [ExcludeFromCodeCoverage] - public override byte[] GetBytes(char[] chars, int index, int count) => - _underlyingEncoding.GetBytes(chars, index, count); - - [ExcludeFromCodeCoverage] - public override byte[] GetBytes(string s) => _underlyingEncoding.GetBytes(s); - - [ExcludeFromCodeCoverage] - public override int GetBytes(string s, int charIndex, int charCount, byte[] bytes, int byteIndex) => - _underlyingEncoding.GetBytes(s, charIndex, charCount, bytes, byteIndex); - - [ExcludeFromCodeCoverage] - public override int GetCharCount(byte[] bytes, int index, int count) => - _underlyingEncoding.GetCharCount(bytes, index, count); - - [ExcludeFromCodeCoverage] - public override int GetCharCount(byte[] bytes) => _underlyingEncoding.GetCharCount(bytes); - - [ExcludeFromCodeCoverage] - public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) => - _underlyingEncoding.GetChars(bytes, byteIndex, byteCount, chars, charIndex); - - [ExcludeFromCodeCoverage] - public override char[] GetChars(byte[] bytes) => _underlyingEncoding.GetChars(bytes); - - [ExcludeFromCodeCoverage] - public override char[] GetChars(byte[] bytes, int index, int count) => - _underlyingEncoding.GetChars(bytes, index, count); - - [ExcludeFromCodeCoverage] - public override string GetString(byte[] bytes) => _underlyingEncoding.GetString(bytes); - - [ExcludeFromCodeCoverage] - public override string GetString(byte[] bytes, int index, int count) => - _underlyingEncoding.GetString(bytes, index, count); - - [ExcludeFromCodeCoverage] - public override int GetMaxByteCount(int charCount) => - _underlyingEncoding.GetMaxByteCount(charCount); - - [ExcludeFromCodeCoverage] - public override int GetMaxCharCount(int byteCount) => - _underlyingEncoding.GetMaxCharCount(byteCount); - - [ExcludeFromCodeCoverage] - public override bool IsAlwaysNormalized(NormalizationForm form) => _underlyingEncoding.IsAlwaysNormalized(form); - - [ExcludeFromCodeCoverage] - public override Encoder GetEncoder() => _underlyingEncoding.GetEncoder(); - - [ExcludeFromCodeCoverage] - public override Decoder GetDecoder() => _underlyingEncoding.GetDecoder(); - - [ExcludeFromCodeCoverage] - public override object Clone() => new NoPreambleEncoding((Encoding) base.Clone()); + _underlyingEncoding = underlyingEncoding; } - internal static class NoPreambleEncodingExtensions - { - public static Encoding WithoutPreamble(this Encoding encoding) => - encoding.GetPreamble().Length > 0 - ? new NoPreambleEncoding(encoding) - : encoding; - } + // This is the only part that changes + public override byte[] GetPreamble() => Array.Empty(); + + [ExcludeFromCodeCoverage] + public override int GetByteCount(char[] chars, int index, int count) => + _underlyingEncoding.GetByteCount(chars, index, count); + + [ExcludeFromCodeCoverage] + public override int GetByteCount(char[] chars) => _underlyingEncoding.GetByteCount(chars); + + [ExcludeFromCodeCoverage] + public override int GetByteCount(string s) => _underlyingEncoding.GetByteCount(s); + + [ExcludeFromCodeCoverage] + public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) => + _underlyingEncoding.GetBytes(chars, charIndex, charCount, bytes, byteIndex); + + [ExcludeFromCodeCoverage] + public override byte[] GetBytes(char[] chars) => _underlyingEncoding.GetBytes(chars); + + [ExcludeFromCodeCoverage] + public override byte[] GetBytes(char[] chars, int index, int count) => + _underlyingEncoding.GetBytes(chars, index, count); + + [ExcludeFromCodeCoverage] + public override byte[] GetBytes(string s) => _underlyingEncoding.GetBytes(s); + + [ExcludeFromCodeCoverage] + public override int GetBytes(string s, int charIndex, int charCount, byte[] bytes, int byteIndex) => + _underlyingEncoding.GetBytes(s, charIndex, charCount, bytes, byteIndex); + + [ExcludeFromCodeCoverage] + public override int GetCharCount(byte[] bytes, int index, int count) => + _underlyingEncoding.GetCharCount(bytes, index, count); + + [ExcludeFromCodeCoverage] + public override int GetCharCount(byte[] bytes) => _underlyingEncoding.GetCharCount(bytes); + + [ExcludeFromCodeCoverage] + public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) => + _underlyingEncoding.GetChars(bytes, byteIndex, byteCount, chars, charIndex); + + [ExcludeFromCodeCoverage] + public override char[] GetChars(byte[] bytes) => _underlyingEncoding.GetChars(bytes); + + [ExcludeFromCodeCoverage] + public override char[] GetChars(byte[] bytes, int index, int count) => + _underlyingEncoding.GetChars(bytes, index, count); + + [ExcludeFromCodeCoverage] + public override string GetString(byte[] bytes) => _underlyingEncoding.GetString(bytes); + + [ExcludeFromCodeCoverage] + public override string GetString(byte[] bytes, int index, int count) => + _underlyingEncoding.GetString(bytes, index, count); + + [ExcludeFromCodeCoverage] + public override int GetMaxByteCount(int charCount) => + _underlyingEncoding.GetMaxByteCount(charCount); + + [ExcludeFromCodeCoverage] + public override int GetMaxCharCount(int byteCount) => + _underlyingEncoding.GetMaxCharCount(byteCount); + + [ExcludeFromCodeCoverage] + public override bool IsAlwaysNormalized(NormalizationForm form) => _underlyingEncoding.IsAlwaysNormalized(form); + + [ExcludeFromCodeCoverage] + public override Encoder GetEncoder() => _underlyingEncoding.GetEncoder(); + + [ExcludeFromCodeCoverage] + public override Decoder GetDecoder() => _underlyingEncoding.GetDecoder(); + + [ExcludeFromCodeCoverage] + public override object Clone() => new NoPreambleEncoding((Encoding) base.Clone()); +} + +internal static class NoPreambleEncodingExtensions +{ + public static Encoding WithoutPreamble(this Encoding encoding) => + encoding.GetPreamble().Length > 0 + ? new NoPreambleEncoding(encoding) + : encoding; } \ No newline at end of file diff --git a/CliFx/Utils/ProcessEx.cs b/CliFx/Utils/ProcessEx.cs index 0eb05e4..8fcde5d 100644 --- a/CliFx/Utils/ProcessEx.cs +++ b/CliFx/Utils/ProcessEx.cs @@ -1,13 +1,12 @@ using System.Diagnostics; -namespace CliFx.Utils +namespace CliFx.Utils; + +internal static class ProcessEx { - internal static class ProcessEx + public static int GetCurrentProcessId() { - public static int GetCurrentProcessId() - { - using var process = Process.GetCurrentProcess(); - return process.Id; - } + using var process = Process.GetCurrentProcess(); + return process.Id; } } \ No newline at end of file diff --git a/CliFx/Utils/StackFrame.cs b/CliFx/Utils/StackFrame.cs index 02e1f99..e9ce448 100644 --- a/CliFx/Utils/StackFrame.cs +++ b/CliFx/Utils/StackFrame.cs @@ -4,55 +4,55 @@ using System.Linq; using System.Text.RegularExpressions; using CliFx.Utils.Extensions; -namespace CliFx.Utils +namespace CliFx.Utils; + +internal class StackFrameParameter { - internal class StackFrameParameter + public string Type { get; } + + public string? Name { get; } + + public StackFrameParameter(string type, string? name) { - public string Type { get; } - - public string? Name { get; } - - public StackFrameParameter(string type, string? name) - { - Type = type; - Name = name; - } + Type = type; + Name = name; } +} - internal partial class StackFrame +internal partial class StackFrame +{ + public string ParentType { get; } + + public string MethodName { get; } + + public IReadOnlyList Parameters { get; } + + public string? FilePath { get; } + + public string? LineNumber { get; } + + public StackFrame( + string parentType, + string methodName, + IReadOnlyList parameters, + string? filePath, + string? lineNumber) { - public string ParentType { get; } - - public string MethodName { get; } - - public IReadOnlyList Parameters { get; } - - public string? FilePath { get; } - - public string? LineNumber { get; } - - public StackFrame( - string parentType, - string methodName, - IReadOnlyList parameters, - string? filePath, - string? lineNumber) - { - ParentType = parentType; - MethodName = methodName; - Parameters = parameters; - FilePath = filePath; - LineNumber = lineNumber; - } + ParentType = parentType; + MethodName = methodName; + Parameters = parameters; + FilePath = filePath; + LineNumber = lineNumber; } +} - internal partial class StackFrame - { - private const string Space = @"[\x20\t]"; - private const string NotSpace = @"[^\x20\t]"; +internal partial class StackFrame +{ + private const string Space = @"[\x20\t]"; + private const string NotSpace = @"[^\x20\t]"; - // Taken from https://github.com/atifaziz/StackTraceParser - private static readonly Regex Pattern = new(@" + // Taken from https://github.com/atifaziz/StackTraceParser + private static readonly Regex Pattern = new(@" ^ " + Space + @"* \w+ " + Space + @"+ @@ -79,44 +79,43 @@ namespace CliFx.Utils ) \s* $", - RegexOptions.IgnoreCase | - RegexOptions.Multiline | - RegexOptions.ExplicitCapture | - RegexOptions.CultureInvariant | - RegexOptions.IgnorePatternWhitespace, - TimeSpan.FromSeconds(5) - ); + RegexOptions.IgnoreCase | + RegexOptions.Multiline | + RegexOptions.ExplicitCapture | + RegexOptions.CultureInvariant | + RegexOptions.IgnorePatternWhitespace, + TimeSpan.FromSeconds(5) + ); - public static IEnumerable ParseMany(string stackTrace) + public static IEnumerable ParseMany(string stackTrace) + { + var matches = Pattern.Matches(stackTrace).Cast().ToArray(); + + if (matches.Length <= 0 || matches.Any(m => !m.Success)) { - var matches = Pattern.Matches(stackTrace).Cast().ToArray(); - - if (matches.Length <= 0 || matches.Any(m => !m.Success)) - { - // If parsing fails, we include the original stacktrace in the - // exception so that it's shown to the user. - throw new FormatException( - "Could not parse stacktrace:" + - Environment.NewLine + - stackTrace - ); - } - - return from m in matches - select m.Groups - into groups - let pt = groups["pt"].Captures - let pn = groups["pn"].Captures - select new StackFrame( - groups["type"].Value, - groups["method"].Value, - ( - from i in Enumerable.Range(0, pt.Count) - select new StackFrameParameter(pt[i].Value, pn[i].Value.NullIfWhiteSpace()) - ).ToArray(), - groups["file"].Value.NullIfWhiteSpace(), - groups["line"].Value.NullIfWhiteSpace() - ); + // If parsing fails, we include the original stacktrace in the + // exception so that it's shown to the user. + throw new FormatException( + "Could not parse stacktrace:" + + Environment.NewLine + + stackTrace + ); } + + return from m in matches + select m.Groups + into groups + let pt = groups["pt"].Captures + let pn = groups["pn"].Captures + select new StackFrame( + groups["type"].Value, + groups["method"].Value, + ( + from i in Enumerable.Range(0, pt.Count) + select new StackFrameParameter(pt[i].Value, pn[i].Value.NullIfWhiteSpace()) + ).ToArray(), + groups["file"].Value.NullIfWhiteSpace(), + groups["line"].Value.NullIfWhiteSpace() + ); } } \ No newline at end of file