diff --git a/CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj b/CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj deleted file mode 100644 index 5520438..0000000 --- a/CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net8.0 - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs deleted file mode 100644 index 1e90212..0000000 --- a/CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs +++ /dev/null @@ -1,75 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -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 - // lang=csharp - const string code = """ - public class MyCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // 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 - // lang=csharp - const string code = """ - [Command] - public abstract class MyCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // 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 - // lang=csharp - const string code = """ - public abstract class MyCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_class_that_is_not_a_command() - { - // Arrange - // lang=csharp - const string code = """ - public class Foo - { - public int Bar { get; init; } = 5; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/CommandMustImplementInterfaceAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/CommandMustImplementInterfaceAnalyzerSpecs.cs deleted file mode 100644 index 04d7f1c..0000000 --- a/CliFx.Analyzers.Tests/CommandMustImplementInterfaceAnalyzerSpecs.cs +++ /dev/null @@ -1,61 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand - { - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_command_implements_ICommand_interface() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_class_that_is_not_a_command() - { - // Arrange - // lang=csharp - const string code = """ - public class Foo - { - public int Bar { get; init; } = 5; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/GeneralSpecs.cs b/CliFx.Analyzers.Tests/GeneralSpecs.cs deleted file mode 100644 index 0aaa23a..0000000 --- a/CliFx.Analyzers.Tests/GeneralSpecs.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Linq; -using FluentAssertions; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -public class GeneralSpecs -{ - [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(); - - // Act - var diagnosticIds = analyzers - .SelectMany(a => a.SupportedDiagnostics.Select(d => d.Id)) - .ToArray(); - - // Assert - diagnosticIds.Should().OnlyHaveUniqueItems(); - } -} diff --git a/CliFx.Analyzers.Tests/OptionMustBeInsideCommandAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustBeInsideCommandAnalyzerSpecs.cs deleted file mode 100644 index 97a5121..0000000 --- a/CliFx.Analyzers.Tests/OptionMustBeInsideCommandAnalyzerSpecs.cs +++ /dev/null @@ -1,83 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -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 - // lang=csharp - const string code = """ - public class MyClass - { - [CommandOption("foo")] - public string? Foo { get; init; } - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_an_option_is_inside_a_command() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo")] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_an_option_is_inside_an_abstract_class() - { - // Arrange - // lang=csharp - const string code = """ - public abstract class MyCommand - { - [CommandOption("foo")] - public string? Foo { get; init; } - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/OptionMustBeRequiredIfPropertyRequiredAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustBeRequiredIfPropertyRequiredAnalyzerSpecs.cs deleted file mode 100644 index f41468c..0000000 --- a/CliFx.Analyzers.Tests/OptionMustBeRequiredIfPropertyRequiredAnalyzerSpecs.cs +++ /dev/null @@ -1,110 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -public class OptionMustBeRequiredIfPropertyRequiredAnalyzerSpecs -{ - private static DiagnosticAnalyzer Analyzer { get; } = - new OptionMustBeRequiredIfPropertyRequiredAnalyzer(); - - [Fact] - public void Analyzer_reports_an_error_if_a_non_required_option_is_bound_to_a_required_property() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption('f', IsRequired = false)] - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_required_option_is_bound_to_a_required_property() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption('f')] - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_non_required_option_is_bound_to_a_non_required_property() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption('f', IsRequired = false)] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_required_option_is_bound_to_a_non_required_property() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption('f')] - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/OptionMustHaveNameOrShortNameAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveNameOrShortNameAnalyzerSpecs.cs deleted file mode 100644 index 2eff6f5..0000000 --- a/CliFx.Analyzers.Tests/OptionMustHaveNameOrShortNameAnalyzerSpecs.cs +++ /dev/null @@ -1,90 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption(null)] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_an_option_has_a_name() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo")] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_an_option_has_a_short_name() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption('f')] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/OptionMustHaveUniqueNameAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveUniqueNameAnalyzerSpecs.cs deleted file mode 100644 index 3a70039..0000000 --- a/CliFx.Analyzers.Tests/OptionMustHaveUniqueNameAnalyzerSpecs.cs +++ /dev/null @@ -1,95 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo")] - public string? Foo { get; init; } - - [CommandOption("foo")] - public string? Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_an_option_has_a_unique_name() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo")] - public string? Foo { get; init; } - - [CommandOption("bar")] - public string? Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_name() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption('f')] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/OptionMustHaveUniqueShortNameAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveUniqueShortNameAnalyzerSpecs.cs deleted file mode 100644 index 09c2f82..0000000 --- a/CliFx.Analyzers.Tests/OptionMustHaveUniqueShortNameAnalyzerSpecs.cs +++ /dev/null @@ -1,119 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption('f')] - public string? Foo { get; init; } - - [CommandOption('f')] - public string? Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_an_option_has_a_unique_short_name() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption('f')] - public string? Foo { get; init; } - - [CommandOption('b')] - public string? Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // 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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption('f')] - public string? Foo { get; init; } - - [CommandOption('F')] - public string? Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // 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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo")] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/OptionMustHaveValidConverterAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveValidConverterAnalyzerSpecs.cs deleted file mode 100644 index dc163ab..0000000 --- a/CliFx.Analyzers.Tests/OptionMustHaveValidConverterAnalyzerSpecs.cs +++ /dev/null @@ -1,175 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -public class OptionMustHaveValidConverterAnalyzerSpecs -{ - private static DiagnosticAnalyzer Analyzer { get; } = - new OptionMustHaveValidConverterAnalyzer(); - - [Fact] - public void Analyzer_reports_an_error_if_an_option_has_a_converter_that_does_not_derive_from_BindingConverter() - { - // Arrange - // lang=csharp - const string code = """ - public class MyConverter - { - public string Convert(string? rawValue) => rawValue; - } - - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo", Converter = typeof(MyConverter))] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_reports_an_error_if_an_option_has_a_converter_that_does_not_derive_from_a_compatible_BindingConverter() - { - // Arrange - // lang=csharp - const string code = """ - public class MyConverter : BindingConverter - { - public override int Convert(string? rawValue) => 42; - } - - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo", Converter = typeof(MyConverter))] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_an_option_has_a_converter_that_derives_from_a_compatible_BindingConverter() - { - // Arrange - // lang=csharp - const string code = """ - public class MyConverter : BindingConverter - { - public override string Convert(string? rawValue) => rawValue; - } - - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo", Converter = typeof(MyConverter))] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_nullable_option_has_a_converter_that_derives_from_a_compatible_BindingConverter() - { - // Arrange - // lang=csharp - const string code = """ - public class MyConverter : BindingConverter - { - public override int Convert(string? rawValue) => 42; - } - - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo", Converter = typeof(MyConverter))] - public int? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_non_scalar_option_has_a_converter_that_derives_from_a_compatible_BindingConverter() - { - // Arrange - // lang=csharp - const string code = """ - public class MyConverter : BindingConverter - { - public override string Convert(string? rawValue) => rawValue; - } - - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo", Converter = typeof(MyConverter))] - public IReadOnlyList? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_converter() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo")] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs deleted file mode 100644 index 2f4ab73..0000000 --- a/CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs +++ /dev/null @@ -1,109 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption("f")] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // 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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption("1foo")] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_an_option_has_a_valid_name() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo")] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_name() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption('f')] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/OptionMustHaveValidShortNameAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveValidShortNameAnalyzerSpecs.cs deleted file mode 100644 index e810e79..0000000 --- a/CliFx.Analyzers.Tests/OptionMustHaveValidShortNameAnalyzerSpecs.cs +++ /dev/null @@ -1,90 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption('1')] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_an_option_has_a_valid_short_name() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption('f')] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // 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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo")] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/OptionMustHaveValidValidatorsAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveValidValidatorsAnalyzerSpecs.cs deleted file mode 100644 index ebd2546..0000000 --- a/CliFx.Analyzers.Tests/OptionMustHaveValidValidatorsAnalyzerSpecs.cs +++ /dev/null @@ -1,125 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -public class OptionMustHaveValidValidatorsAnalyzerSpecs -{ - private static DiagnosticAnalyzer Analyzer { get; } = - new OptionMustHaveValidValidatorsAnalyzer(); - - [Fact] - public void Analyzer_reports_an_error_if_an_option_has_a_validator_that_does_not_derive_from_BindingValidator() - { - // Arrange - // lang=csharp - const string code = """ - public class MyValidator - { - public void Validate(string value) {} - } - - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo", Validators = new[] { typeof(MyValidator) })] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_reports_an_error_if_an_option_has_a_validator_that_does_not_derive_from_a_compatible_BindingValidator() - { - // Arrange - // lang=csharp - const string code = """ - public class MyValidator : BindingValidator - { - public override BindingValidationError Validate(int value) => Ok(); - } - - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo", Validators = new[] { typeof(MyValidator) })] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_an_option_has_validators_that_all_derive_from_compatible_BindingValidators() - { - // Arrange - // lang=csharp - const string code = """ - public class MyValidator : BindingValidator - { - public override BindingValidationError Validate(string value) => Ok(); - } - - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo", Validators = new[] { typeof(MyValidator) })] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_validators() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo")] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/ParameterMustBeInsideCommandAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustBeInsideCommandAnalyzerSpecs.cs deleted file mode 100644 index b7b4b36..0000000 --- a/CliFx.Analyzers.Tests/ParameterMustBeInsideCommandAnalyzerSpecs.cs +++ /dev/null @@ -1,84 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -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 - // lang=csharp - const string code = """ - public class MyClass - { - [CommandParameter(0)] - public required string Foo { get; init; } - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_parameter_is_inside_a_command() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_parameter_is_inside_an_abstract_class() - { - // Arrange - // lang=csharp - const string code = """ - public abstract class MyCommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/ParameterMustBeLastIfNonRequiredAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustBeLastIfNonRequiredAnalyzerSpecs.cs deleted file mode 100644 index 3f4bb66..0000000 --- a/CliFx.Analyzers.Tests/ParameterMustBeLastIfNonRequiredAnalyzerSpecs.cs +++ /dev/null @@ -1,99 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -public class ParameterMustBeLastIfNonRequiredAnalyzerSpecs -{ - private static DiagnosticAnalyzer Analyzer { get; } = - new ParameterMustBeLastIfNonRequiredAnalyzer(); - - [Fact] - public void Analyzer_reports_an_error_if_a_non_required_parameter_is_not_the_last_in_order() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0, IsRequired = false)] - public string? Foo { get; init; } - - [CommandParameter(1)] - public required string Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_non_required_parameter_is_the_last_in_order() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - [CommandParameter(1, IsRequired = false)] - public string? Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_no_non_required_parameters_are_defined() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - [CommandParameter(1)] - public required string Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/ParameterMustBeLastIfNonScalarAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustBeLastIfNonScalarAnalyzerSpecs.cs deleted file mode 100644 index abe13db..0000000 --- a/CliFx.Analyzers.Tests/ParameterMustBeLastIfNonScalarAnalyzerSpecs.cs +++ /dev/null @@ -1,99 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -public class ParameterMustBeLastIfNonScalarAnalyzerSpecs -{ - private static DiagnosticAnalyzer Analyzer { get; } = - new ParameterMustBeLastIfNonScalarAnalyzer(); - - [Fact] - public void Analyzer_reports_an_error_if_a_non_scalar_parameter_is_not_the_last_in_order() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string[] Foo { get; init; } - - [CommandParameter(1)] - public required string Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_is_the_last_in_order() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - [CommandParameter(1)] - public required string[] Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_no_non_scalar_parameters_are_defined() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - [CommandParameter(1)] - public required string Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/ParameterMustBeRequiredIfPropertyRequiredAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustBeRequiredIfPropertyRequiredAnalyzerSpecs.cs deleted file mode 100644 index fe2162f..0000000 --- a/CliFx.Analyzers.Tests/ParameterMustBeRequiredIfPropertyRequiredAnalyzerSpecs.cs +++ /dev/null @@ -1,110 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -public class ParameterMustBeRequiredIfPropertyRequiredAnalyzerSpecs -{ - private static DiagnosticAnalyzer Analyzer { get; } = - new ParameterMustBeRequiredIfPropertyRequiredAnalyzer(); - - [Fact] - public void Analyzer_reports_an_error_if_a_non_required_parameter_is_bound_to_a_required_property() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0, IsRequired = false)] - public required string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_required_parameter_is_bound_to_a_required_property() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_non_required_parameter_is_bound_to_a_non_required_property() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0, IsRequired = false)] - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_required_parameter_is_bound_to_a_non_required_property() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/ParameterMustBeSingleIfNonRequiredAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustBeSingleIfNonRequiredAnalyzerSpecs.cs deleted file mode 100644 index 5decbc1..0000000 --- a/CliFx.Analyzers.Tests/ParameterMustBeSingleIfNonRequiredAnalyzerSpecs.cs +++ /dev/null @@ -1,99 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -public class ParameterMustBeSingleIfNonRequiredAnalyzerSpecs -{ - private static DiagnosticAnalyzer Analyzer { get; } = - new ParameterMustBeSingleIfNonRequiredAnalyzer(); - - [Fact] - public void Analyzer_reports_an_error_if_more_than_one_non_required_parameters_are_defined() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0, IsRequired = false)] - public string? Foo { get; init; } - - [CommandParameter(1, IsRequired = false)] - public string? Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_only_one_non_required_parameter_is_defined() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - [CommandParameter(1, IsRequired = false)] - public string? Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_no_non_required_parameters_are_defined() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - [CommandParameter(1)] - public required string Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/ParameterMustBeSingleIfNonScalarAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustBeSingleIfNonScalarAnalyzerSpecs.cs deleted file mode 100644 index be4bd76..0000000 --- a/CliFx.Analyzers.Tests/ParameterMustBeSingleIfNonScalarAnalyzerSpecs.cs +++ /dev/null @@ -1,99 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string[] Foo { get; init; } - - [CommandParameter(1)] - public required string[] Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_only_one_non_scalar_parameter_is_defined() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - [CommandParameter(1)] - public required string[] Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_no_non_scalar_parameters_are_defined() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - [CommandParameter(1)] - public required string Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/ParameterMustHaveUniqueNameAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustHaveUniqueNameAnalyzerSpecs.cs deleted file mode 100644 index 30fc5b9..0000000 --- a/CliFx.Analyzers.Tests/ParameterMustHaveUniqueNameAnalyzerSpecs.cs +++ /dev/null @@ -1,75 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0, Name = "foo")] - public required string Foo { get; init; } - - [CommandParameter(1, Name = "foo")] - public required string Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_parameter_has_unique_name() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0, Name = "foo")] - public required string Foo { get; init; } - - [CommandParameter(1, Name = "bar")] - public required string Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/ParameterMustHaveUniqueOrderAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustHaveUniqueOrderAnalyzerSpecs.cs deleted file mode 100644 index 1262046..0000000 --- a/CliFx.Analyzers.Tests/ParameterMustHaveUniqueOrderAnalyzerSpecs.cs +++ /dev/null @@ -1,76 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - [CommandParameter(0)] - public required string Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_parameter_has_unique_order() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - [CommandParameter(1)] - public required string Bar { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/ParameterMustHaveValidConverterAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustHaveValidConverterAnalyzerSpecs.cs deleted file mode 100644 index 60a79b7..0000000 --- a/CliFx.Analyzers.Tests/ParameterMustHaveValidConverterAnalyzerSpecs.cs +++ /dev/null @@ -1,175 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -public class ParameterMustHaveValidConverterAnalyzerSpecs -{ - private static DiagnosticAnalyzer Analyzer { get; } = - new ParameterMustHaveValidConverterAnalyzer(); - - [Fact] - public void Analyzer_reports_an_error_if_a_parameter_has_a_converter_that_does_not_derive_from_BindingConverter() - { - // Arrange - // lang=csharp - const string code = """ - public class MyConverter - { - public string Convert(string? rawValue) => rawValue; - } - - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0, Converter = typeof(MyConverter))] - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_reports_an_error_if_a_parameter_has_a_converter_that_does_not_derive_from_a_compatible_BindingConverter() - { - // Arrange - // lang=csharp - const string code = """ - public class MyConverter : BindingConverter - { - public override int Convert(string? rawValue) => 42; - } - - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0, Converter = typeof(MyConverter))] - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter() - { - // Arrange - // lang=csharp - const string code = """ - public class MyConverter : BindingConverter - { - public override string Convert(string? rawValue) => rawValue; - } - - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0, Converter = typeof(MyConverter))] - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_nullable_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter() - { - // Arrange - // lang=csharp - const string code = """ - public class MyConverter : BindingConverter - { - public override int Convert(string? rawValue) => 42; - } - - [Command] - public class MyCommand : ICommand - { - [CommandOption("foo", Converter = typeof(MyConverter))] - public int? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter() - { - // Arrange - // lang=csharp - const string code = """ - public class MyConverter : BindingConverter - { - public override string Convert(string? rawValue) => rawValue; - } - - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0, Converter = typeof(MyConverter))] - public required IReadOnlyList Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_parameter_does_not_have_a_converter() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/ParameterMustHaveValidValidatorsAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustHaveValidValidatorsAnalyzerSpecs.cs deleted file mode 100644 index d1c3bb8..0000000 --- a/CliFx.Analyzers.Tests/ParameterMustHaveValidValidatorsAnalyzerSpecs.cs +++ /dev/null @@ -1,125 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -public class ParameterMustHaveValidValidatorsAnalyzerSpecs -{ - private static DiagnosticAnalyzer Analyzer { get; } = - new ParameterMustHaveValidValidatorsAnalyzer(); - - [Fact] - public void Analyzer_reports_an_error_a_parameter_has_a_validator_that_does_not_derive_from_BindingValidator() - { - // Arrange - // lang=csharp - const string code = """ - public class MyValidator - { - public void Validate(string value) {} - } - - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0, Validators = new[] { typeof(MyValidator) })] - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_reports_an_error_if_a_parameter_has_a_validator_that_does_not_derive_from_a_compatible_BindingValidator() - { - // Arrange - // lang=csharp - const string code = """ - public class MyValidator : BindingValidator - { - public override BindingValidationError Validate(int value) => Ok(); - } - - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0, Validators = new[] { typeof(MyValidator) })] - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_parameter_has_validators_that_all_derive_from_compatible_BindingValidators() - { - // Arrange - // lang=csharp - const string code = """ - public class MyValidator : BindingValidator - { - public override BindingValidationError Validate(string value) => Ok(); - } - - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0, Validators = new[] { typeof(MyValidator) })] - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_parameter_does_not_have_validators() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - [CommandParameter(0)] - public required string Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public string? Foo { get; init; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/SystemConsoleShouldBeAvoidedAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/SystemConsoleShouldBeAvoidedAnalyzerSpecs.cs deleted file mode 100644 index a2db943..0000000 --- a/CliFx.Analyzers.Tests/SystemConsoleShouldBeAvoidedAnalyzerSpecs.cs +++ /dev/null @@ -1,134 +0,0 @@ -using CliFx.Analyzers.Tests.Utils; -using Microsoft.CodeAnalysis.Diagnostics; -using Xunit; - -namespace CliFx.Analyzers.Tests; - -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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) - { - Console.WriteLine("Hello world"); - return default; - } - } - """; - - // Act & assert - Analyzer.Should().ProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_reports_an_error_if_a_command_accesses_a_property_on_SystemConsole() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) - { - Console.ForegroundColor = ConsoleColor.Black; - return default; - } - } - """; - - // 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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) - { - Console.Error.WriteLine("Hello world"); - return default; - } - } - """; - - // 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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) - { - console.WriteLine("Hello world"); - return default; - } - } - """; - - // 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 - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public void SomeOtherMethod() => Console.WriteLine("Test"); - - public ValueTask ExecuteAsync(IConsole console) => default; - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } - - [Fact] - public void Analyzer_does_not_report_an_error_if_a_command_does_not_access_SystemConsole() - { - // Arrange - // lang=csharp - const string code = """ - [Command] - public class MyCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) - { - return default; - } - } - """; - - // Act & assert - Analyzer.Should().NotProduceDiagnostics(code); - } -} diff --git a/CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs b/CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs deleted file mode 100644 index 4475db6..0000000 --- a/CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Text; -using Basic.Reference.Assemblies; -using FluentAssertions.Execution; -using FluentAssertions.Primitives; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Text; - -namespace CliFx.Analyzers.Tests.Utils; - -internal class AnalyzerAssertions(DiagnosticAnalyzer analyzer) - : ReferenceTypeAssertions(analyzer) -{ - protected override string Identifier => "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.WithLanguageVersion(LanguageVersion.Preview) - ); - - // Compile the code to IL - var compilation = CSharpCompilation.Create( - "CliFxTests_DynamicAssembly_" + Guid.NewGuid(), - [ast], - Net80.References.All.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. - {string.Join(Environment.NewLine, compilationErrors.Select(e => e.ToString()))} - """ - ); - } - - 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 isSuccessfulAssertion = - expectedDiagnosticIds.Intersect(producedDiagnosticIds).Count() - == expectedDiagnosticIds.Length; - - Execute - .Assertion.ForCondition(isSuccessfulAssertion) - .FailWith(() => - { - var buffer = new StringBuilder(); - - 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:"); - - if (producedDiagnostics.Any()) - { - foreach (var producedDiagnostic in producedDiagnostics) - { - buffer.Append(" - "); - buffer.Append(producedDiagnostic); - } - } - else - { - buffer.AppendLine(" < none >"); - } - - return new FailReason(buffer.ToString()); - }); - } - - public void NotProduceDiagnostics(string sourceCode) - { - var producedDiagnostics = GetProducedDiagnostics(sourceCode); - var isSuccessfulAssertion = !producedDiagnostics.Any(); - - Execute - .Assertion.ForCondition(isSuccessfulAssertion) - .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); -} diff --git a/CliFx.Analyzers.Tests/xunit.runner.json b/CliFx.Analyzers.Tests/xunit.runner.json deleted file mode 100644 index 186540e..0000000 --- a/CliFx.Analyzers.Tests/xunit.runner.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", - "methodDisplayOptions": "all", - "methodDisplay": "method" -} \ No newline at end of file diff --git a/CliFx.Analyzers/AnalyzerBase.cs b/CliFx.Analyzers/AnalyzerBase.cs deleted file mode 100644 index c022ca0..0000000 --- a/CliFx.Analyzers/AnalyzerBase.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Immutable; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -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 - ) - { - SupportedDiagnostic = new DiagnosticDescriptor( - "CliFx_" + GetType().Name.TrimEnd("Analyzer"), - diagnosticTitle, - diagnosticMessage, - "CliFx", - diagnosticSeverity, - true - ); - - SupportedDiagnostics = ImmutableArray.Create(SupportedDiagnostic); - } - - protected Diagnostic CreateDiagnostic(Location location, params object?[]? messageArgs) => - Diagnostic.Create(SupportedDiagnostic, location, messageArgs); - - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - } -} diff --git a/CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs b/CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs deleted file mode 100644 index 78c98f1..0000000 --- a/CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class CommandMustBeAnnotatedAnalyzer() - : AnalyzerBase( - $"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(i => - i.DisplayNameMatches(SymbolNames.CliFxCommandInterface) - ); - - var hasCommandAttribute = type.GetAttributes() - .Select(a => a.AttributeClass) - .Any(c => c.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.Identifier.GetLocation())); - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandleClassDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs b/CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs deleted file mode 100644 index 94321b4..0000000 --- a/CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class CommandMustImplementInterfaceAnalyzer() - : AnalyzerBase( - $"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(c => c.DisplayNameMatches(SymbolNames.CliFxCommandAttribute)); - - var implementsCommandInterface = type.AllInterfaces.Any(i => - i.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.Identifier.GetLocation())); - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandleClassDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs b/CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs deleted file mode 100644 index 1307035..0000000 --- a/CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; - -namespace CliFx.Analyzers.ObjectModel; - -internal partial class CommandOptionSymbol( - IPropertySymbol property, - string? name, - char? shortName, - bool? isRequired, - ITypeSymbol? converterType, - IReadOnlyList validatorTypes -) : ICommandMemberSymbol -{ - public IPropertySymbol Property { get; } = property; - - public string? Name { get; } = name; - - public char? ShortName { get; } = shortName; - - public bool? IsRequired { get; } = isRequired; - - public ITypeSymbol? ConverterType { get; } = converterType; - - public IReadOnlyList ValidatorTypes { get; } = validatorTypes; -} - -internal partial class CommandOptionSymbol -{ - private static AttributeData? TryGetOptionAttribute(IPropertySymbol property) => - property - .GetAttributes() - .FirstOrDefault(a => - a.AttributeClass?.DisplayNameMatches(SymbolNames.CliFxCommandOptionAttribute) - == true - ); - - public static CommandOptionSymbol? TryResolve(IPropertySymbol property) - { - var attribute = TryGetOptionAttribute(property); - if (attribute is null) - return null; - - var name = - attribute - .ConstructorArguments.Where(a => a.Type?.SpecialType == SpecialType.System_String) - .Select(a => a.Value) - .FirstOrDefault() as string; - - var shortName = - attribute - .ConstructorArguments.Where(a => a.Type?.SpecialType == SpecialType.System_Char) - .Select(a => a.Value) - .FirstOrDefault() as char?; - - var isRequired = - attribute - .NamedArguments.Where(a => a.Key == "IsRequired") - .Select(a => a.Value.Value) - .FirstOrDefault() as bool?; - - var converter = attribute - .NamedArguments.Where(a => a.Key == "Converter") - .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( - property, - name, - shortName, - isRequired, - converter, - validators - ); - } - - public static bool IsOptionProperty(IPropertySymbol property) => - TryGetOptionAttribute(property) is not null; -} diff --git a/CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs b/CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs deleted file mode 100644 index ac5b6e6..0000000 --- a/CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; - -namespace CliFx.Analyzers.ObjectModel; - -internal partial class CommandParameterSymbol( - IPropertySymbol property, - int order, - string? name, - bool? isRequired, - ITypeSymbol? converterType, - IReadOnlyList validatorTypes -) : ICommandMemberSymbol -{ - public IPropertySymbol Property { get; } = property; - - public int Order { get; } = order; - - public string? Name { get; } = name; - - public bool? IsRequired { get; } = isRequired; - - public ITypeSymbol? ConverterType { get; } = converterType; - - public IReadOnlyList ValidatorTypes { get; } = validatorTypes; -} - -internal partial class CommandParameterSymbol -{ - private static AttributeData? TryGetParameterAttribute(IPropertySymbol property) => - property - .GetAttributes() - .FirstOrDefault(a => - a.AttributeClass?.DisplayNameMatches(SymbolNames.CliFxCommandParameterAttribute) - == true - ); - - public static CommandParameterSymbol? TryResolve(IPropertySymbol property) - { - var attribute = TryGetParameterAttribute(property); - if (attribute is null) - return null; - - var order = (int)attribute.ConstructorArguments.Select(a => a.Value).First()!; - - var name = - attribute - .NamedArguments.Where(a => a.Key == "Name") - .Select(a => a.Value.Value) - .FirstOrDefault() as string; - - var isRequired = - attribute - .NamedArguments.Where(a => a.Key == "IsRequired") - .Select(a => a.Value.Value) - .FirstOrDefault() as bool?; - - var converter = attribute - .NamedArguments.Where(a => a.Key == "Converter") - .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(property, order, name, isRequired, converter, validators); - } - - public static bool IsParameterProperty(IPropertySymbol property) => - TryGetParameterAttribute(property) is not null; -} diff --git a/CliFx.Analyzers/ObjectModel/ICommandMemberSymbol.cs b/CliFx.Analyzers/ObjectModel/ICommandMemberSymbol.cs deleted file mode 100644 index 377f13b..0000000 --- a/CliFx.Analyzers/ObjectModel/ICommandMemberSymbol.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; - -namespace CliFx.Analyzers.ObjectModel; - -internal interface ICommandMemberSymbol -{ - IPropertySymbol Property { get; } - - ITypeSymbol? ConverterType { get; } - - IReadOnlyList ValidatorTypes { get; } -} - -internal static class CommandMemberSymbolExtensions -{ - public static bool IsScalar(this ICommandMemberSymbol member) => - member.Property.Type.SpecialType == SpecialType.System_String - || member.Property.Type.TryGetEnumerableUnderlyingType() is null; -} diff --git a/CliFx.Analyzers/ObjectModel/SymbolNames.cs b/CliFx.Analyzers/ObjectModel/SymbolNames.cs deleted file mode 100644 index cc4b817..0000000 --- a/CliFx.Analyzers/ObjectModel/SymbolNames.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace CliFx.Analyzers.ObjectModel; - -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 CliFxBindingConverterClass = "CliFx.Extensibility.BindingConverter"; - public const string CliFxBindingValidatorClass = "CliFx.Extensibility.BindingValidator"; -} diff --git a/CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.cs b/CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.cs deleted file mode 100644 index 82f3121..0000000 --- a/CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class OptionMustBeInsideCommandAnalyzer() - : AnalyzerBase( - "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(i => - i.DisplayNameMatches(SymbolNames.CliFxCommandInterface) - ); - - if (!isInsideCommand) - { - context.ReportDiagnostic( - CreateDiagnostic(propertyDeclaration.Identifier.GetLocation()) - ); - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/OptionMustBeRequiredIfPropertyRequiredAnalyzer.cs b/CliFx.Analyzers/OptionMustBeRequiredIfPropertyRequiredAnalyzer.cs deleted file mode 100644 index 146f527..0000000 --- a/CliFx.Analyzers/OptionMustBeRequiredIfPropertyRequiredAnalyzer.cs +++ /dev/null @@ -1,43 +0,0 @@ -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class OptionMustBeRequiredIfPropertyRequiredAnalyzer() - : AnalyzerBase( - "Options bound to required properties cannot be marked as non-required", - "This option cannot be marked as non-required because it's bound to a required property." - ) -{ - private void Analyze( - SyntaxNodeAnalysisContext context, - PropertyDeclarationSyntax propertyDeclaration, - IPropertySymbol property - ) - { - if (property.ContainingType is null) - return; - - if (!property.IsRequired()) - return; - - var option = CommandOptionSymbol.TryResolve(property); - if (option is null) - return; - - if (option.IsRequired != false) - return; - - context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())); - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/OptionMustHaveNameOrShortNameAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveNameOrShortNameAnalyzer.cs deleted file mode 100644 index 9f94b7f..0000000 --- a/CliFx.Analyzers/OptionMustHaveNameOrShortNameAnalyzer.cs +++ /dev/null @@ -1,39 +0,0 @@ -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class OptionMustHaveNameOrShortNameAnalyzer() - : AnalyzerBase( - "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; - - if (string.IsNullOrWhiteSpace(option.Name) && option.ShortName is null) - { - context.ReportDiagnostic( - CreateDiagnostic(propertyDeclaration.Identifier.GetLocation()) - ); - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs deleted file mode 100644 index 3e779e8..0000000 --- a/CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class OptionMustHaveUniqueNameAnalyzer() - : AnalyzerBase( - "Options must have unique names", - "This option's name must be unique within the command (comparison IS NOT case sensitive). " - + "Specified name: `{0}`. " - + "Property bound to another option with the same name: `{1}`." - ) -{ - 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)) - .ToArray(); - - foreach (var otherProperty in otherProperties) - { - 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.Identifier.GetLocation(), - option.Name, - otherProperty.Name - ) - ); - } - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.cs deleted file mode 100644 index 6f9bb4c..0000000 --- a/CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class OptionMustHaveUniqueShortNameAnalyzer() - : AnalyzerBase( - "Options must have unique short names", - "This option's short name must be unique within the command (comparison IS case sensitive). " - + "Specified short name: `{0}` " - + "Property bound to another option with the same short name: `{1}`." - ) -{ - 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)) - .ToArray(); - - foreach (var otherProperty in otherProperties) - { - 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.Identifier.GetLocation(), - option.ShortName, - otherProperty.Name - ) - ); - } - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs deleted file mode 100644 index a8e3ef6..0000000 --- a/CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class OptionMustHaveValidConverterAnalyzer() - : AnalyzerBase( - $"Option converters must derive from `{SymbolNames.CliFxBindingConverterClass}`", - $"Converter specified for this option must derive from a compatible `{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; - - var converterValueType = option - .ConverterType.GetBaseTypes() - .FirstOrDefault(t => - t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingConverterClass) - ) - ?.TypeArguments.FirstOrDefault(); - - // Value returned by the converter must be assignable to the property type - var isCompatible = - converterValueType is not null - && ( - option.IsScalar() - // Scalar - ? context.Compilation.IsAssignable(converterValueType, property.Type) - // Non-scalar (assume we can handle all IEnumerable types for simplicity) - : property.Type.TryGetEnumerableUnderlyingType() is { } enumerableUnderlyingType - && context.Compilation.IsAssignable( - converterValueType, - enumerableUnderlyingType - ) - ); - - if (!isCompatible) - { - context.ReportDiagnostic( - CreateDiagnostic(propertyDeclaration.Identifier.GetLocation()) - ); - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs deleted file mode 100644 index 66b0cd1..0000000 --- a/CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs +++ /dev/null @@ -1,43 +0,0 @@ -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class OptionMustHaveValidNameAnalyzer() - : AnalyzerBase( - "Options must have valid names", - "This option's name must be at least 2 characters long and must start with a letter. " - + "Specified 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.Identifier.GetLocation(), option.Name) - ); - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs deleted file mode 100644 index 69e3446..0000000 --- a/CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs +++ /dev/null @@ -1,43 +0,0 @@ -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class OptionMustHaveValidShortNameAnalyzer() - : AnalyzerBase( - "Option short names must be letter characters", - "This option's short name must be a single letter character. " - + "Specified short name: `{0}`." - ) -{ - 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.Identifier.GetLocation(), option.ShortName) - ); - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs deleted file mode 100644 index e1be507..0000000 --- a/CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class OptionMustHaveValidValidatorsAnalyzer() - : AnalyzerBase( - $"Option validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`", - $"Each validator specified for this option must derive from a compatible `{SymbolNames.CliFxBindingValidatorClass}`." - ) -{ - 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) - { - var validatorValueType = validatorType - .GetBaseTypes() - .FirstOrDefault(t => - t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingValidatorClass) - ) - ?.TypeArguments.FirstOrDefault(); - - // Value passed to the validator must be assignable from the property type - var isCompatible = - validatorValueType is not null - && context.Compilation.IsAssignable(property.Type, validatorValueType); - - if (!isCompatible) - { - context.ReportDiagnostic( - CreateDiagnostic(propertyDeclaration.Identifier.GetLocation()) - ); - - // No need to report multiple identical diagnostics on the same node - break; - } - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.cs b/CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.cs deleted file mode 100644 index f7bb43f..0000000 --- a/CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class ParameterMustBeInsideCommandAnalyzer() - : AnalyzerBase( - "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(i => - i.DisplayNameMatches(SymbolNames.CliFxCommandInterface) - ); - - if (!isInsideCommand) - { - context.ReportDiagnostic( - CreateDiagnostic(propertyDeclaration.Identifier.GetLocation()) - ); - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/ParameterMustBeLastIfNonRequiredAnalyzer.cs b/CliFx.Analyzers/ParameterMustBeLastIfNonRequiredAnalyzer.cs deleted file mode 100644 index 578e815..0000000 --- a/CliFx.Analyzers/ParameterMustBeLastIfNonRequiredAnalyzer.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class ParameterMustBeLastIfNonRequiredAnalyzer() - : AnalyzerBase( - "Parameters marked as non-required must be the last in order", - "This parameter is non-required so it must be the last in order (its order must be highest within the command). " - + "Property bound to another non-required parameter: `{0}`." - ) -{ - 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 (parameter.IsRequired != false) - return; - - var otherProperties = property - .ContainingType.GetMembers() - .OfType() - .Where(m => !m.Equals(property)) - .ToArray(); - - foreach (var otherProperty in otherProperties) - { - var otherParameter = CommandParameterSymbol.TryResolve(otherProperty); - if (otherParameter is null) - continue; - - if (otherParameter.Order > parameter.Order) - { - context.ReportDiagnostic( - CreateDiagnostic( - propertyDeclaration.Identifier.GetLocation(), - otherProperty.Name - ) - ); - } - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs b/CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs deleted file mode 100644 index 3eee558..0000000 --- a/CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class ParameterMustBeLastIfNonScalarAnalyzer() - : AnalyzerBase( - "Parameters of non-scalar types must be the last in order", - "This parameter has a non-scalar type so it must be the last in order (its order must be highest within the command). " - + "Property bound to another non-scalar parameter: `{0}`." - ) -{ - 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 (parameter.IsScalar()) - return; - - var otherProperties = property - .ContainingType.GetMembers() - .OfType() - .Where(m => !m.Equals(property)) - .ToArray(); - - foreach (var otherProperty in otherProperties) - { - var otherParameter = CommandParameterSymbol.TryResolve(otherProperty); - if (otherParameter is null) - continue; - - if (otherParameter.Order > parameter.Order) - { - context.ReportDiagnostic( - CreateDiagnostic( - propertyDeclaration.Identifier.GetLocation(), - otherProperty.Name - ) - ); - } - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/ParameterMustBeRequiredIfPropertyRequiredAnalyzer.cs b/CliFx.Analyzers/ParameterMustBeRequiredIfPropertyRequiredAnalyzer.cs deleted file mode 100644 index 8d92b39..0000000 --- a/CliFx.Analyzers/ParameterMustBeRequiredIfPropertyRequiredAnalyzer.cs +++ /dev/null @@ -1,43 +0,0 @@ -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class ParameterMustBeRequiredIfPropertyRequiredAnalyzer() - : AnalyzerBase( - "Parameters bound to required properties cannot be marked as non-required", - "This parameter cannot be marked as non-required because it's bound to a required property." - ) -{ - private void Analyze( - SyntaxNodeAnalysisContext context, - PropertyDeclarationSyntax propertyDeclaration, - IPropertySymbol property - ) - { - if (property.ContainingType is null) - return; - - if (!property.IsRequired()) - return; - - var parameter = CommandParameterSymbol.TryResolve(property); - if (parameter is null) - return; - - if (parameter.IsRequired != false) - return; - - context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())); - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/ParameterMustBeSingleIfNonRequiredAnalyzer.cs b/CliFx.Analyzers/ParameterMustBeSingleIfNonRequiredAnalyzer.cs deleted file mode 100644 index 5c0e186..0000000 --- a/CliFx.Analyzers/ParameterMustBeSingleIfNonRequiredAnalyzer.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class ParameterMustBeSingleIfNonRequiredAnalyzer() - : AnalyzerBase( - "Parameters marked as non-required are limited to one per command", - "This parameter is non-required so it must be the only such parameter in the command. " - + "Property bound to another non-required parameter: `{0}`." - ) -{ - 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 (parameter.IsRequired != false) - return; - - var otherProperties = property - .ContainingType.GetMembers() - .OfType() - .Where(m => !m.Equals(property)) - .ToArray(); - - foreach (var otherProperty in otherProperties) - { - var otherParameter = CommandParameterSymbol.TryResolve(otherProperty); - if (otherParameter is null) - continue; - - if (otherParameter.IsRequired == false) - { - context.ReportDiagnostic( - CreateDiagnostic( - propertyDeclaration.Identifier.GetLocation(), - otherProperty.Name - ) - ); - } - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs b/CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs deleted file mode 100644 index d25c2aa..0000000 --- a/CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class ParameterMustBeSingleIfNonScalarAnalyzer() - : AnalyzerBase( - "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. " - + "Property bound to another non-scalar parameter: `{0}`." - ) -{ - 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 (parameter.IsScalar()) - return; - - var otherProperties = property - .ContainingType.GetMembers() - .OfType() - .Where(m => !m.Equals(property)) - .ToArray(); - - foreach (var otherProperty in otherProperties) - { - var otherParameter = CommandParameterSymbol.TryResolve(otherProperty); - if (otherParameter is null) - continue; - - if (!otherParameter.IsScalar()) - { - context.ReportDiagnostic( - CreateDiagnostic( - propertyDeclaration.Identifier.GetLocation(), - otherProperty.Name - ) - ); - } - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs b/CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs deleted file mode 100644 index 7d8d55d..0000000 --- a/CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class ParameterMustHaveUniqueNameAnalyzer() - : AnalyzerBase( - "Parameters must have unique names", - "This parameter's name must be unique within the command (comparison IS NOT case sensitive). " - + "Specified name: `{0}`. " - + "Property bound to another parameter with the same name: `{1}`." - ) -{ - 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)) - .ToArray(); - - foreach (var otherProperty in otherProperties) - { - 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.Identifier.GetLocation(), - parameter.Name, - otherProperty.Name - ) - ); - } - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs b/CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs deleted file mode 100644 index c46643b..0000000 --- a/CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class ParameterMustHaveUniqueOrderAnalyzer() - : AnalyzerBase( - "Parameters must have unique order", - "This parameter's order must be unique within the command. " - + "Specified order: {0}. " - + "Property bound to another parameter with the same order: `{1}`." - ) -{ - 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)) - .ToArray(); - - foreach (var otherProperty in otherProperties) - { - var otherParameter = CommandParameterSymbol.TryResolve(otherProperty); - if (otherParameter is null) - continue; - - if (parameter.Order == otherParameter.Order) - { - context.ReportDiagnostic( - CreateDiagnostic( - propertyDeclaration.Identifier.GetLocation(), - parameter.Order, - otherProperty.Name - ) - ); - } - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs b/CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs deleted file mode 100644 index 5e59790..0000000 --- a/CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class ParameterMustHaveValidConverterAnalyzer() - : AnalyzerBase( - $"Parameter converters must derive from `{SymbolNames.CliFxBindingConverterClass}`", - $"Converter specified for this parameter must derive from a compatible `{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; - - var converterValueType = parameter - .ConverterType.GetBaseTypes() - .FirstOrDefault(t => - t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingConverterClass) - ) - ?.TypeArguments.FirstOrDefault(); - - // Value returned by the converter must be assignable to the property type - var isCompatible = - converterValueType is not null - && ( - parameter.IsScalar() - // Scalar - ? context.Compilation.IsAssignable(converterValueType, property.Type) - // Non-scalar (assume we can handle all IEnumerable types for simplicity) - : property.Type.TryGetEnumerableUnderlyingType() is { } enumerableUnderlyingType - && context.Compilation.IsAssignable( - converterValueType, - enumerableUnderlyingType - ) - ); - - if (!isCompatible) - { - context.ReportDiagnostic( - CreateDiagnostic(propertyDeclaration.Identifier.GetLocation()) - ); - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs b/CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs deleted file mode 100644 index 4a64c02..0000000 --- a/CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class ParameterMustHaveValidValidatorsAnalyzer() - : AnalyzerBase( - $"Parameter validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`", - $"Each validator specified for this parameter must derive from a compatible `{SymbolNames.CliFxBindingValidatorClass}`." - ) -{ - 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) - { - var validatorValueType = validatorType - .GetBaseTypes() - .FirstOrDefault(t => - t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingValidatorClass) - ) - ?.TypeArguments.FirstOrDefault(); - - // Value passed to the validator must be assignable from the property type - var isCompatible = - validatorValueType is not null - && context.Compilation.IsAssignable(property.Type, validatorValueType); - - if (!isCompatible) - { - context.ReportDiagnostic( - CreateDiagnostic(propertyDeclaration.Identifier.GetLocation()) - ); - - // No need to report multiple identical diagnostics on the same node - break; - } - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.HandlePropertyDeclaration(Analyze); - } -} diff --git a/CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs b/CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs deleted file mode 100644 index d4718a5..0000000 --- a/CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Linq; -using CliFx.Analyzers.ObjectModel; -using CliFx.Analyzers.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class SystemConsoleShouldBeAvoidedAnalyzer() - : AnalyzerBase( - $"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; - - while (currentNode is MemberAccessExpressionSyntax memberAccess) - { - 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 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) - { - context.ReportDiagnostic(CreateDiagnostic(systemConsoleMemberAccess.GetLocation())); - } - } - - public override void Initialize(AnalysisContext context) - { - base.Initialize(context); - context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.SimpleMemberAccessExpression); - } -} diff --git a/CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs b/CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs deleted file mode 100644 index 1e01aff..0000000 --- a/CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace CliFx.Analyzers.Utils.Extensions; - -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 IEnumerable GetBaseTypes(this ITypeSymbol type) - { - var current = type.BaseType; - - while (current is not null) - { - yield return current; - current = current.BaseType; - } - } - - public static ITypeSymbol? TryGetEnumerableUnderlyingType(this ITypeSymbol type) => - type - .AllInterfaces.FirstOrDefault(i => - i.ConstructedFrom.SpecialType - == SpecialType.System_Collections_Generic_IEnumerable_T - ) - ?.TypeArguments[0]; - - // Detect if the property is required through roundabout means so as to not have to take dependency - // on higher versions of the C# compiler. - public static bool IsRequired(this IPropertySymbol property) => - property - // Can't rely on the RequiredMemberAttribute because it's generated by the compiler, not added by the user, - // so we have to check for the presence of the `required` modifier in the syntax tree instead. - .DeclaringSyntaxReferences.Select(r => r.GetSyntax()) - .OfType() - .SelectMany(p => p.Modifiers) - .Any(m => m.IsKind((SyntaxKind)8447)); - - public static bool IsAssignable( - this Compilation compilation, - ITypeSymbol source, - ITypeSymbol destination - ) => compilation.ClassifyConversion(source, destination).Exists; - - public static void HandleClassDeclaration( - this AnalysisContext analysisContext, - Action analyze - ) - { - analysisContext.RegisterSyntaxNodeAction( - ctx => - { - if (ctx.Node is not ClassDeclarationSyntax classDeclaration) - return; - - var type = ctx.SemanticModel.GetDeclaredSymbol(classDeclaration); - if (type is null) - return; - - analyze(ctx, classDeclaration, type); - }, - SyntaxKind.ClassDeclaration - ); - } - - public static void HandlePropertyDeclaration( - this AnalysisContext analysisContext, - Action analyze - ) - { - analysisContext.RegisterSyntaxNodeAction( - ctx => - { - if (ctx.Node is not PropertyDeclarationSyntax propertyDeclaration) - return; - - var property = ctx.SemanticModel.GetDeclaredSymbol(propertyDeclaration); - if (property is null) - return; - - analyze(ctx, propertyDeclaration, property); - }, - SyntaxKind.PropertyDeclaration - ); - } -} diff --git a/CliFx.Analyzers/Utils/Extensions/StringExtensions.cs b/CliFx.Analyzers/Utils/Extensions/StringExtensions.cs deleted file mode 100644 index 0848e56..0000000 --- a/CliFx.Analyzers/Utils/Extensions/StringExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -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[..^sub.Length]; - - return str; - } -} diff --git a/CliFx.Demo/CliFx.Demo.csproj b/CliFx.Demo/CliFx.Demo.csproj index 32de33e..baef5e8 100644 --- a/CliFx.Demo/CliFx.Demo.csproj +++ b/CliFx.Demo/CliFx.Demo.csproj @@ -14,7 +14,7 @@ - + \ No newline at end of file diff --git a/CliFx.Demo/Program.cs b/CliFx.Demo/Program.cs index 3cb8faf..67144a7 100644 --- a/CliFx.Demo/Program.cs +++ b/CliFx.Demo/Program.cs @@ -2,20 +2,17 @@ using CliFx.Demo.Domain; using Microsoft.Extensions.DependencyInjection; +// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands +var services = new ServiceCollection(); +services.AddSingleton(); + +// Register all commands as transient services +foreach (var commandType in commandTypes) + services.AddTransient(commandType); + return await new CliApplicationBuilder() .SetDescription("Demo application showcasing CliFx features.") .AddCommandsFromThisAssembly() - .UseTypeActivator(commandTypes => - { - // We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands - var services = new ServiceCollection(); - services.AddSingleton(); - - // Register all commands as transient services - foreach (var commandType in commandTypes) - services.AddTransient(commandType); - - return services.BuildServiceProvider(); - }) + .UseTypeActivator(services.BuildServiceProvider()) .Build() .RunAsync(); diff --git a/CliFx.Analyzers/CliFx.Analyzers.csproj b/CliFx.SourceGeneration/CliFx.SourceGeneration.csproj similarity index 100% rename from CliFx.Analyzers/CliFx.Analyzers.csproj rename to CliFx.SourceGeneration/CliFx.SourceGeneration.csproj diff --git a/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj b/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj index 7feda9b..c460114 100644 --- a/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj +++ b/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj @@ -12,7 +12,7 @@ - + \ No newline at end of file diff --git a/CliFx.sln b/CliFx.sln index 7587f88..f3d688a 100644 --- a/CliFx.sln +++ b/CliFx.sln @@ -20,9 +20,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Demo", "CliFx.Demo\Cl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{F717347D-8656-44DA-A4A2-BE515E8C4655}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Analyzers", "CliFx.Analyzers\CliFx.Analyzers.csproj", "{F8460D69-F8CF-405C-A6ED-BED02A21DB42}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Analyzers.Tests", "CliFx.Analyzers.Tests\CliFx.Analyzers.Tests.csproj", "{49878E75-2097-4C79-9151-B98A28FBB973}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.SourceGeneration", "CliFx.SourceGeneration\CliFx.SourceGeneration.csproj", "{F8460D69-F8CF-405C-A6ED-BED02A21DB42}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -106,18 +104,6 @@ Global {F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|x64.Build.0 = Release|Any CPU {F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|x86.ActiveCfg = Release|Any CPU {F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|x86.Build.0 = Release|Any CPU - {49878E75-2097-4C79-9151-B98A28FBB973}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {49878E75-2097-4C79-9151-B98A28FBB973}.Debug|Any CPU.Build.0 = Debug|Any CPU - {49878E75-2097-4C79-9151-B98A28FBB973}.Debug|x64.ActiveCfg = Debug|Any CPU - {49878E75-2097-4C79-9151-B98A28FBB973}.Debug|x64.Build.0 = Debug|Any CPU - {49878E75-2097-4C79-9151-B98A28FBB973}.Debug|x86.ActiveCfg = Debug|Any CPU - {49878E75-2097-4C79-9151-B98A28FBB973}.Debug|x86.Build.0 = Debug|Any CPU - {49878E75-2097-4C79-9151-B98A28FBB973}.Release|Any CPU.ActiveCfg = Release|Any CPU - {49878E75-2097-4C79-9151-B98A28FBB973}.Release|Any CPU.Build.0 = Release|Any CPU - {49878E75-2097-4C79-9151-B98A28FBB973}.Release|x64.ActiveCfg = Release|Any CPU - {49878E75-2097-4C79-9151-B98A28FBB973}.Release|x64.Build.0 = Release|Any CPU - {49878E75-2097-4C79-9151-B98A28FBB973}.Release|x86.ActiveCfg = Release|Any CPU - {49878E75-2097-4C79-9151-B98A28FBB973}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CliFx/Attributes/CommandAttribute.cs b/CliFx/Attributes/CommandAttribute.cs index 13025b6..16024fe 100644 --- a/CliFx/Attributes/CommandAttribute.cs +++ b/CliFx/Attributes/CommandAttribute.cs @@ -25,4 +25,4 @@ public class CommandAttribute(string? name = null) : Attribute /// This is shown to the user in the help text. /// public string? Description { get; set; } -} \ No newline at end of file +} diff --git a/CliFx/Attributes/CommandInputAttribute.cs b/CliFx/Attributes/CommandInputAttribute.cs index 53457ce..dd98dee 100644 --- a/CliFx/Attributes/CommandInputAttribute.cs +++ b/CliFx/Attributes/CommandInputAttribute.cs @@ -31,4 +31,4 @@ public abstract class CommandInputAttribute : Attribute /// Validators must derive from . /// public Type[] Validators { get; set; } = []; -} \ No newline at end of file +} diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs index fdd82a6..7709b7f 100644 --- a/CliFx/CliApplication.cs +++ b/CliFx/CliApplication.cs @@ -114,7 +114,7 @@ public class CliApplication( try { // Activate the command instance with the provided input - commandSchema.Activate(commandInput, commandInstance); + commandSchema.Activate(commandInstance, commandInput); // Handle the version option if (commandInstance is ICommandWithVersionOption { IsVersionRequested: true }) diff --git a/CliFx/CliApplicationBuilder.cs b/CliFx/CliApplicationBuilder.cs index 9a8dc14..c55c98c 100644 --- a/CliFx/CliApplicationBuilder.cs +++ b/CliFx/CliApplicationBuilder.cs @@ -26,6 +26,10 @@ public partial class CliApplicationBuilder private IConsole? _console; private ITypeActivator? _typeActivator; + // TODO: + // The source generator should generate an internal extension method for the builder called + // AddCommandsFromThisAssembly() that would add all command types from the assembly where the builder is used. + /// /// Adds a command to the application. /// @@ -35,6 +39,17 @@ public partial class CliApplicationBuilder return this; } + /// + /// Adds multiple commands to the application. + /// + public CliApplicationBuilder AddCommands(IReadOnlyList commandSchemas) + { + foreach (var commandSchema in commandSchemas) + AddCommand(commandSchema); + + return this; + } + /// /// Specifies whether debug mode (enabled with the [debug] directive) is allowed in the application. /// diff --git a/CliFx/CliFx.csproj b/CliFx/CliFx.csproj index d584fd7..81549f1 100644 --- a/CliFx/CliFx.csproj +++ b/CliFx/CliFx.csproj @@ -31,17 +31,7 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/CliFx/Extensibility/IBindingConverter.cs b/CliFx/Extensibility/IBindingConverter.cs index fa4fbd9..86f9fe0 100644 --- a/CliFx/Extensibility/IBindingConverter.cs +++ b/CliFx/Extensibility/IBindingConverter.cs @@ -13,5 +13,5 @@ public interface IBindingConverter /// /// Parses the value from a raw command-line argument. /// - object? Convert(string? rawValue, IFormatProvider? formatProvider); + object? Convert(string? rawArgument, IFormatProvider? formatProvider); } diff --git a/CliFx/Extensibility/NoopBindingConverter.cs b/CliFx/Extensibility/NoopBindingConverter.cs index 92537cd..fef732e 100644 --- a/CliFx/Extensibility/NoopBindingConverter.cs +++ b/CliFx/Extensibility/NoopBindingConverter.cs @@ -8,5 +8,5 @@ namespace CliFx.Extensibility; public class NoopBindingConverter : IBindingConverter { /// - public object? Convert(string? rawValue, IFormatProvider? formatProvider) => rawValue; + public object? Convert(string? rawArgument, IFormatProvider? formatProvider) => rawArgument; } diff --git a/CliFx/FallbackDefaultCommand.cs b/CliFx/FallbackDefaultCommand.cs index 6e8fd7d..3ef1406 100644 --- a/CliFx/FallbackDefaultCommand.cs +++ b/CliFx/FallbackDefaultCommand.cs @@ -9,9 +9,7 @@ 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 partial class FallbackDefaultCommand - : ICommandWithHelpOption, - ICommandWithVersionOption +internal partial class FallbackDefaultCommand : ICommandWithHelpOption, ICommandWithVersionOption { [CommandHelpOption] public bool IsHelpRequested { get; init; } @@ -26,5 +24,6 @@ internal partial class FallbackDefaultCommand internal partial class FallbackDefaultCommand { - public static CommandSchema Schema { get; } = new CommandSchema(null, null, []); + public static CommandSchema Schema { get; } = + new CommandSchema(null, null, []); } diff --git a/CliFx/Formatting/CommandInputConsoleFormatter.cs b/CliFx/Formatting/CommandInputConsoleFormatter.cs index fd9a9d6..3bac812 100644 --- a/CliFx/Formatting/CommandInputConsoleFormatter.cs +++ b/CliFx/Formatting/CommandInputConsoleFormatter.cs @@ -36,7 +36,7 @@ internal class CommandInputConsoleFormatter(ConsoleWriter consoleWriter) Write('['); // Identifier - Write(ConsoleColor.White, optionInput.GetFormattedIdentifier()); + Write(ConsoleColor.White, optionInput.FormattedIdentifier); // Value(s) foreach (var value in optionInput.Values) diff --git a/CliFx/Infrastructure/ConsoleReader.cs b/CliFx/Infrastructure/ConsoleReader.cs index ef83201..c0f0f28 100644 --- a/CliFx/Infrastructure/ConsoleReader.cs +++ b/CliFx/Infrastructure/ConsoleReader.cs @@ -7,11 +7,11 @@ using System.Threading.Tasks; namespace CliFx.Infrastructure; /// -/// Implements a for reading characters from a console stream. +/// Implements a for reading characters or binary data from a console stream. /// // Both the underlying stream AND the stream reader must be synchronized! // https://github.com/Tyrrrz/CliFx/issues/123 -public class ConsoleReader(IConsole console, Stream stream, Encoding encoding) +public sealed class ConsoleReader(IConsole console, Stream stream, Encoding encoding) : StreamReader(Stream.Synchronized(stream), encoding, false, 4096) { /// diff --git a/CliFx/Infrastructure/ConsoleWriter.cs b/CliFx/Infrastructure/ConsoleWriter.cs index 0675599..516945e 100644 --- a/CliFx/Infrastructure/ConsoleWriter.cs +++ b/CliFx/Infrastructure/ConsoleWriter.cs @@ -8,11 +8,11 @@ using CliFx.Utils; namespace CliFx.Infrastructure; /// -/// Implements a for writing characters to a console stream. +/// Implements a for writing characters or binary data to a console stream. /// // Both the underlying stream AND the stream writer must be synchronized! // https://github.com/Tyrrrz/CliFx/issues/123 -public class ConsoleWriter : StreamWriter +public sealed class ConsoleWriter : StreamWriter { /// /// Initializes an instance of . diff --git a/CliFx/Input/DirectiveInput.cs b/CliFx/Input/CommandDirectiveInput.cs similarity index 90% rename from CliFx/Input/DirectiveInput.cs rename to CliFx/Input/CommandDirectiveInput.cs index 9e934e7..2253695 100644 --- a/CliFx/Input/DirectiveInput.cs +++ b/CliFx/Input/CommandDirectiveInput.cs @@ -5,7 +5,7 @@ namespace CliFx.Input; /// /// Input provided by the means of a directive. /// -public class DirectiveInput(string name) +public class CommandDirectiveInput(string name) { /// /// Directive name. diff --git a/CliFx/Input/CommandInput.cs b/CliFx/Input/CommandInput.cs index 63a98fb..a6751f4 100644 --- a/CliFx/Input/CommandInput.cs +++ b/CliFx/Input/CommandInput.cs @@ -10,9 +10,9 @@ namespace CliFx.Input; /// public partial class CommandInput( string? commandName, - IReadOnlyList directives, - IReadOnlyList parameters, - IReadOnlyList options, + IReadOnlyList directives, + IReadOnlyList parameters, + IReadOnlyList options, IReadOnlyList environmentVariables ) { @@ -24,17 +24,17 @@ public partial class CommandInput( /// /// Provided directives. /// - public IReadOnlyList Directives { get; } = directives; + public IReadOnlyList Directives { get; } = directives; /// /// Provided parameters. /// - public IReadOnlyList Parameters { get; } = parameters; + public IReadOnlyList Parameters { get; } = parameters; /// /// Provided options. /// - public IReadOnlyList Options { get; } = options; + public IReadOnlyList Options { get; } = options; /// /// Provided environment variables. @@ -49,12 +49,12 @@ public partial class CommandInput( public partial class CommandInput { - private static IReadOnlyList ParseDirectives( + private static IReadOnlyList ParseDirectives( IReadOnlyList commandLineArguments, ref int index ) { - var result = new List(); + var result = new List(); // Consume all consecutive directive arguments for (; index < commandLineArguments.Count; index++) @@ -66,7 +66,7 @@ public partial class CommandInput break; var directiveName = argument.Substring(1, argument.Length - 2); - result.Add(new DirectiveInput(directiveName)); + result.Add(new CommandDirectiveInput(directiveName)); } return result; @@ -108,12 +108,12 @@ public partial class CommandInput return commandName; } - private static IReadOnlyList ParseParameters( + private static IReadOnlyList ParseParameters( IReadOnlyList commandLineArguments, ref int index ) { - var result = new List(); + var result = new List(); // Consume all arguments until the first option identifier for (; index < commandLineArguments.Count; index++) @@ -135,18 +135,18 @@ public partial class CommandInput if (isOptionIdentifier) break; - result.Add(new ParameterInput(index, argument)); + result.Add(new CommandParameterInput(index, argument)); } return result; } - private static IReadOnlyList ParseOptions( + private static IReadOnlyList ParseOptions( IReadOnlyList commandLineArguments, ref int index ) { - var result = new List(); + var result = new List(); var lastOptionIdentifier = default(string?); var lastOptionValues = new List(); @@ -165,7 +165,7 @@ public partial class CommandInput { // Flush previous if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) - result.Add(new OptionInput(lastOptionIdentifier, lastOptionValues)); + result.Add(new CommandOptionInput(lastOptionIdentifier, lastOptionValues)); lastOptionIdentifier = argument[2..]; lastOptionValues = []; @@ -177,7 +177,7 @@ public partial class CommandInput { // Flush previous if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) - result.Add(new OptionInput(lastOptionIdentifier, lastOptionValues)); + result.Add(new CommandOptionInput(lastOptionIdentifier, lastOptionValues)); lastOptionIdentifier = identifier.AsString(); lastOptionValues = []; @@ -192,7 +192,7 @@ public partial class CommandInput // Flush the last option if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) - result.Add(new OptionInput(lastOptionIdentifier, lastOptionValues)); + result.Add(new CommandOptionInput(lastOptionIdentifier, lastOptionValues)); return result; } diff --git a/CliFx/Input/OptionInput.cs b/CliFx/Input/CommandOptionInput.cs similarity index 64% rename from CliFx/Input/OptionInput.cs rename to CliFx/Input/CommandOptionInput.cs index 99ed920..a90e623 100644 --- a/CliFx/Input/OptionInput.cs +++ b/CliFx/Input/CommandOptionInput.cs @@ -5,22 +5,22 @@ namespace CliFx.Input; /// /// Input provided by the means of an option. /// -public class OptionInput(string identifier, IReadOnlyList values) +public class CommandOptionInput(string identifier, IReadOnlyList values) { /// /// Option identifier (either the name or the short name). /// public string Identifier { get; } = identifier; + internal string FormattedIdentifier { get; } = + identifier switch + { + { Length: >= 2 } => "--" + identifier, + _ => '-' + identifier + }; + /// /// Option value(s). /// public IReadOnlyList Values { get; } = values; - - internal string GetFormattedIdentifier() => - Identifier switch - { - { Length: >= 2 } => "--" + Identifier, - _ => '-' + Identifier - }; } diff --git a/CliFx/Input/ParameterInput.cs b/CliFx/Input/CommandParameterInput.cs similarity index 86% rename from CliFx/Input/ParameterInput.cs rename to CliFx/Input/CommandParameterInput.cs index 2a1713e..6e87a2a 100644 --- a/CliFx/Input/ParameterInput.cs +++ b/CliFx/Input/CommandParameterInput.cs @@ -3,7 +3,7 @@ /// /// Input provided by the means of a parameter. /// -public class ParameterInput(int order, string value) +public class CommandParameterInput(int order, string value) { /// /// Parameter order. diff --git a/CliFx/Schema/ApplicationSchema.cs b/CliFx/Schema/ApplicationSchema.cs index 488ce9a..8b463b5 100644 --- a/CliFx/Schema/ApplicationSchema.cs +++ b/CliFx/Schema/ApplicationSchema.cs @@ -32,7 +32,7 @@ public class ApplicationSchema(IReadOnlyList commands) foreach (var potentialDescendantCommand in potentialDescendantCommands) { - // Default commands can't be descendant of anything + // Default commands can't be descendants of anything if (string.IsNullOrWhiteSpace(potentialDescendantCommand.Name)) continue; diff --git a/CliFx/Schema/CommandInputSchema.cs b/CliFx/Schema/CommandInputSchema.cs index 962bfe8..4c08463 100644 --- a/CliFx/Schema/CommandInputSchema.cs +++ b/CliFx/Schema/CommandInputSchema.cs @@ -66,7 +66,7 @@ public abstract class CommandInputSchema( } } - internal void Activate(ICommand instance, IReadOnlyList rawInputs) + internal void Activate(ICommand instance, IReadOnlyList rawArguments) { var formatProvider = CultureInfo.InvariantCulture; @@ -75,15 +75,20 @@ public abstract class CommandInputSchema( // Multiple values expected, single or multiple values provided if (IsSequence) { - var value = rawInputs.Select(v => Converter.Convert(v, formatProvider)).ToArray(); + var value = rawArguments + .Select(v => Converter.Convert(v, formatProvider)) + .ToArray(); + + // TODO: cast array to the proper type + Validate(value); Property.SetValue(instance, value); } // Single value expected, single value provided - else if (rawInputs.Count <= 1) + else if (rawArguments.Count <= 1) { - var value = Converter.Convert(rawInputs.SingleOrDefault(), formatProvider); + var value = Converter.Convert(rawArguments.SingleOrDefault(), formatProvider); Validate(value); Property.SetValue(instance, value); @@ -93,9 +98,9 @@ public abstract class CommandInputSchema( { throw CliFxException.UserError( $""" - {Kind} {FormattedIdentifier} expects a single argument, but provided with multiple: - {rawInputs.Select(v => '<' + v + '>').JoinToString(" ")} - """ + {Kind} {FormattedIdentifier} expects a single argument, but provided with multiple: + {rawArguments.Select(v => '<' + v + '>').JoinToString(" ")} + """ ); } } @@ -103,16 +108,17 @@ public abstract class CommandInputSchema( { throw CliFxException.UserError( $""" - {Kind} {FormattedIdentifier} cannot be set from the provided argument(s): - {rawInputs.Select(v => '<' + v + '>').JoinToString(" ")} - Error: {ex.Message} - """, + {Kind} {FormattedIdentifier} cannot be set from the provided argument(s): + {rawArguments.Select(v => '<' + v + '>').JoinToString(" ")} + Error: {ex.Message} + """, ex ); } } /// + [ExcludeFromCodeCoverage] public override string ToString() => FormattedIdentifier; } diff --git a/CliFx/Schema/CommandOptionSchema.cs b/CliFx/Schema/CommandOptionSchema.cs index 598f80c..025c509 100644 --- a/CliFx/Schema/CommandOptionSchema.cs +++ b/CliFx/Schema/CommandOptionSchema.cs @@ -18,7 +18,7 @@ public class CommandOptionSchema( string? description, IBindingConverter converter, IReadOnlyList validators -) : CommandInputSchema(property,description, converter, validators) +) : CommandInputSchema(property, description, converter, validators) { internal override string Kind => "Option"; diff --git a/CliFx/Schema/CommandSchema.cs b/CliFx/Schema/CommandSchema.cs index fc027c8..310a929 100644 --- a/CliFx/Schema/CommandSchema.cs +++ b/CliFx/Schema/CommandSchema.cs @@ -53,7 +53,8 @@ public class CommandSchema( /// /// Option inputs of the command. /// - public IReadOnlyList Options { get; } = inputs.OfType().ToArray(); + public IReadOnlyList Options { get; } = + inputs.OfType().ToArray(); internal bool MatchesName(string? name) => !string.IsNullOrWhiteSpace(Name) @@ -79,7 +80,7 @@ public class CommandSchema( return result; } - private void ActivateParameters(CommandInput input, ICommand instance) + private void ActivateParameters(ICommand instance, CommandInput input) { // Ensure there are no unexpected parameters and that all parameters are provided var remainingParameterInputs = input.Parameters.ToList(); @@ -107,10 +108,7 @@ public class CommandSchema( { var parameterInputs = input.Parameters.Skip(position).ToArray(); - parameterSchema.Activate( - instance, - parameterInputs.Select(p => p.Value).ToArray() - ); + parameterSchema.Activate(instance, parameterInputs.Select(p => p.Value).ToArray()); position += parameterInputs.Length; remainingParameterInputs.RemoveRange(parameterInputs); @@ -142,12 +140,11 @@ public class CommandSchema( } } - private void ActivateOptions(CommandInput input, ICommand instance) + private void ActivateOptions(ICommand instance, CommandInput input) { // Ensure there are no unrecognized options and that all required options are set var remainingOptionInputs = input.Options.ToList(); - var remainingRequiredOptionSchemas = Options.Where(o => o.IsRequired) - .ToList(); + var remainingRequiredOptionSchemas = Options.Where(o => o.IsRequired).ToList(); foreach (var optionSchema in Options) { @@ -197,7 +194,7 @@ public class CommandSchema( throw CliFxException.UserError( $""" Unrecognized option(s): - {remainingOptionInputs.Select(o => o.GetFormattedIdentifier()).JoinToString(", ")} + {remainingOptionInputs.Select(o => o.FormattedIdentifier).JoinToString(", ")} """ ); } @@ -215,11 +212,15 @@ public class CommandSchema( } } - internal void Activate(CommandInput input, ICommand instance) + internal void Activate(ICommand instance, CommandInput input) { - ActivateParameters(input, instance); - ActivateOptions(input, instance); + ActivateParameters(instance, input); + ActivateOptions(instance, input); } + + /// + [ExcludeFromCodeCoverage] + public override string ToString() => Name ?? ""; } /// diff --git a/CliFx/Schema/PropertyBinding.cs b/CliFx/Schema/PropertyBinding.cs index 4d267d4..36130a8 100644 --- a/CliFx/Schema/PropertyBinding.cs +++ b/CliFx/Schema/PropertyBinding.cs @@ -6,7 +6,7 @@ using System.Linq; namespace CliFx.Schema; /// -/// Represents a wrapper around a CLR property that provides read and write access to its value. +/// Provides read and write access to a CLR property. /// public class PropertyBinding( [DynamicallyAccessedMembers( diff --git a/CliFx/Utils/Extensions/CollectionExtensions.cs b/CliFx/Utils/Extensions/CollectionExtensions.cs index cc1e165..f620de6 100644 --- a/CliFx/Utils/Extensions/CollectionExtensions.cs +++ b/CliFx/Utils/Extensions/CollectionExtensions.cs @@ -13,6 +13,16 @@ internal static class CollectionExtensions yield return (o, i++); } + public static IEnumerable WhereNotNull(this IEnumerable source) + where T : class + { + foreach (var i in source) + { + if (i is not null) + yield return i; + } + } + public static IEnumerable WhereNotNullOrWhiteSpace(this IEnumerable source) { foreach (var i in source) diff --git a/CliFx/Utils/Extensions/TypeExtensions.cs b/CliFx/Utils/Extensions/TypeExtensions.cs index 1d469a0..4592778 100644 --- a/CliFx/Utils/Extensions/TypeExtensions.cs +++ b/CliFx/Utils/Extensions/TypeExtensions.cs @@ -10,31 +10,29 @@ internal static class TypeExtensions { public static Type? TryGetEnumerableUnderlyingType( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type - ) - { - if (type.IsPrimitive) - return null; + ) => + type.GetInterfaces() + .Select(i => + { + if (i == typeof(IEnumerable)) + return typeof(object); - if (type == typeof(IEnumerable)) - return typeof(object); + if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + return i.GetGenericArguments().FirstOrDefault(); - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - return type.GetGenericArguments().FirstOrDefault(); - - return type.GetInterfaces() - .Select(t => TryGetEnumerableUnderlyingType(t)) - .Where(t => t is not null) + return null; + }) + .WhereNotNull() // Every IEnumerable implements IEnumerable (which is essentially IEnumerable), // so we try to get a more specific underlying type. Still, if the type only implements // IEnumerable and nothing else, then we'll just return that. .MaxBy(t => t != typeof(object)); - } public static bool IsToStringOverriden( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] this Type type ) { - var toStringMethod = type.GetMethod(nameof(ToString), Type.EmptyTypes); + var toStringMethod = type.GetMethod(nameof(ToString), []); return toStringMethod?.GetBaseDefinition().DeclaringType != toStringMethod?.DeclaringType; } }