mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
asd
This commit is contained in:
@@ -1,28 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Basic.Reference.Assemblies.Net80" Version="1.7.2" />
|
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.2" PrivateAssets="all" />
|
|
||||||
<PackageReference Include="CSharpier.MsBuild" Version="0.28.2" PrivateAssets="all" />
|
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" />
|
|
||||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
|
|
||||||
<PackageReference Include="xunit" Version="2.8.1" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1" PrivateAssets="all" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" />
|
|
||||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<int>
|
|
||||||
{
|
|
||||||
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<string>
|
|
||||||
{
|
|
||||||
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<int>
|
|
||||||
{
|
|
||||||
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<string>
|
|
||||||
{
|
|
||||||
public override string Convert(string? rawValue) => rawValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo", Converter = typeof(MyConverter))]
|
|
||||||
public IReadOnlyList<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_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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<int>
|
|
||||||
{
|
|
||||||
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<string>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<int>
|
|
||||||
{
|
|
||||||
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<string>
|
|
||||||
{
|
|
||||||
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<int>
|
|
||||||
{
|
|
||||||
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<string>
|
|
||||||
{
|
|
||||||
public override string Convert(string? rawValue) => rawValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, Converter = typeof(MyConverter))]
|
|
||||||
public required IReadOnlyList<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_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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<int>
|
|
||||||
{
|
|
||||||
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<string>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<DiagnosticAnalyzer, AnalyzerAssertions>(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<Diagnostic> GetProducedDiagnostics(string sourceCode)
|
|
||||||
{
|
|
||||||
var analyzers = ImmutableArray.Create(Subject);
|
|
||||||
var compilation = Compile(sourceCode);
|
|
||||||
|
|
||||||
return compilation
|
|
||||||
.WithAnalyzers(analyzers)
|
|
||||||
.GetAnalyzerDiagnosticsAsync(analyzers, default)
|
|
||||||
.GetAwaiter()
|
|
||||||
.GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ProduceDiagnostics(string sourceCode)
|
|
||||||
{
|
|
||||||
var expectedDiagnostics = Subject.SupportedDiagnostics;
|
|
||||||
var producedDiagnostics = GetProducedDiagnostics(sourceCode);
|
|
||||||
|
|
||||||
var expectedDiagnosticIds = expectedDiagnostics.Select(d => d.Id).Distinct().ToArray();
|
|
||||||
var producedDiagnosticIds = producedDiagnostics.Select(d => d.Id).Distinct().ToArray();
|
|
||||||
|
|
||||||
var 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);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
|
||||||
"methodDisplayOptions": "all",
|
|
||||||
"methodDisplay": "method"
|
|
||||||
}
|
|
||||||
@@ -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<DiagnosticDescriptor> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ITypeSymbol> 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<ITypeSymbol> 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<ITypeSymbol?>()
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
var validators = attribute
|
|
||||||
.NamedArguments.Where(a => a.Key == "Validators")
|
|
||||||
.SelectMany(a => a.Value.Values)
|
|
||||||
.Select(c => c.Value)
|
|
||||||
.Cast<ITypeSymbol>()
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new CommandOptionSymbol(
|
|
||||||
property,
|
|
||||||
name,
|
|
||||||
shortName,
|
|
||||||
isRequired,
|
|
||||||
converter,
|
|
||||||
validators
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsOptionProperty(IPropertySymbol property) =>
|
|
||||||
TryGetOptionAttribute(property) is not 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<ITypeSymbol> 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<ITypeSymbol> 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<ITypeSymbol?>()
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
var validators = attribute
|
|
||||||
.NamedArguments.Where(a => a.Key == "Validators")
|
|
||||||
.SelectMany(a => a.Value.Values)
|
|
||||||
.Select(c => c.Value)
|
|
||||||
.Cast<ITypeSymbol>()
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new CommandParameterSymbol(property, order, name, isRequired, converter, validators);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsParameterProperty(IPropertySymbol property) =>
|
|
||||||
TryGetParameterAttribute(property) is not 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<ITypeSymbol> 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;
|
|
||||||
}
|
|
||||||
@@ -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<T>";
|
|
||||||
public const string CliFxBindingValidatorClass = "CliFx.Extensibility.BindingValidator<T>";
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<IPropertySymbol>()
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<IPropertySymbol>()
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<IPropertySymbol>()
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<IPropertySymbol>()
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<IPropertySymbol>()
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<IPropertySymbol>()
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<IPropertySymbol>()
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<IPropertySymbol>()
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<MethodDeclarationSyntax>()
|
|
||||||
.SelectMany(m => m.ParameterList.Parameters)
|
|
||||||
.Select(p => p.Type)
|
|
||||||
.Select(t => context.SemanticModel.GetSymbolInfo(t).Symbol)
|
|
||||||
.Where(s => s is not null)
|
|
||||||
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxConsoleInterface));
|
|
||||||
|
|
||||||
if (isConsoleInterfaceAvailable)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(CreateDiagnostic(systemConsoleMemberAccess.GetLocation()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.SimpleMemberAccessExpression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<INamedTypeSymbol> 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<PropertyDeclarationSyntax>()
|
|
||||||
.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<SyntaxNodeAnalysisContext, ClassDeclarationSyntax, ITypeSymbol> 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<SyntaxNodeAnalysisContext, PropertyDeclarationSyntax, IPropertySymbol> 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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||||
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
<ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -2,20 +2,17 @@
|
|||||||
using CliFx.Demo.Domain;
|
using CliFx.Demo.Domain;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<LibraryProvider>();
|
||||||
|
|
||||||
|
// Register all commands as transient services
|
||||||
|
foreach (var commandType in commandTypes)
|
||||||
|
services.AddTransient(commandType);
|
||||||
|
|
||||||
return await new CliApplicationBuilder()
|
return await new CliApplicationBuilder()
|
||||||
.SetDescription("Demo application showcasing CliFx features.")
|
.SetDescription("Demo application showcasing CliFx features.")
|
||||||
.AddCommandsFromThisAssembly()
|
.AddCommandsFromThisAssembly()
|
||||||
.UseTypeActivator(commandTypes =>
|
.UseTypeActivator(services.BuildServiceProvider())
|
||||||
{
|
|
||||||
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
|
|
||||||
var services = new ServiceCollection();
|
|
||||||
services.AddSingleton<LibraryProvider>();
|
|
||||||
|
|
||||||
// Register all commands as transient services
|
|
||||||
foreach (var commandType in commandTypes)
|
|
||||||
services.AddTransient(commandType);
|
|
||||||
|
|
||||||
return services.BuildServiceProvider();
|
|
||||||
})
|
|
||||||
.Build()
|
.Build()
|
||||||
.RunAsync();
|
.RunAsync();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||||
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
<ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
16
CliFx.sln
16
CliFx.sln
@@ -20,9 +20,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Demo", "CliFx.Demo\Cl
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{F717347D-8656-44DA-A4A2-BE515E8C4655}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{F717347D-8656-44DA-A4A2-BE515E8C4655}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Analyzers", "CliFx.Analyzers\CliFx.Analyzers.csproj", "{F8460D69-F8CF-405C-A6ED-BED02A21DB42}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.SourceGeneration", "CliFx.SourceGeneration\CliFx.SourceGeneration.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}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
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|x64.Build.0 = Release|Any CPU
|
||||||
{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|x86.ActiveCfg = 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
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ public class CliApplication(
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Activate the command instance with the provided input
|
// Activate the command instance with the provided input
|
||||||
commandSchema.Activate(commandInput, commandInstance);
|
commandSchema.Activate(commandInstance, commandInput);
|
||||||
|
|
||||||
// Handle the version option
|
// Handle the version option
|
||||||
if (commandInstance is ICommandWithVersionOption { IsVersionRequested: true })
|
if (commandInstance is ICommandWithVersionOption { IsVersionRequested: true })
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ public partial class CliApplicationBuilder
|
|||||||
private IConsole? _console;
|
private IConsole? _console;
|
||||||
private ITypeActivator? _typeActivator;
|
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.
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a command to the application.
|
/// Adds a command to the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -35,6 +39,17 @@ public partial class CliApplicationBuilder
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds multiple commands to the application.
|
||||||
|
/// </summary>
|
||||||
|
public CliApplicationBuilder AddCommands(IReadOnlyList<CommandSchema> commandSchemas)
|
||||||
|
{
|
||||||
|
foreach (var commandSchema in commandSchemas)
|
||||||
|
AddCommand(commandSchema);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specifies whether debug mode (enabled with the [debug] directive) is allowed in the application.
|
/// Specifies whether debug mode (enabled with the [debug] directive) is allowed in the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -31,17 +31,7 @@
|
|||||||
|
|
||||||
<!-- Embed the analyzer inside the package -->
|
<!-- Embed the analyzer inside the package -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="../CliFx.Analyzers/CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
<ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/CliFx.Analyzers.deps.json" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/CliFx.Analyzers.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Buffers.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Collections.Immutable.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Memory.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Numerics.Vectors.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Reflection.Metadata.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Text.Encoding.CodePages.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Threading.Tasks.Extensions.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -13,5 +13,5 @@ public interface IBindingConverter
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses the value from a raw command-line argument.
|
/// Parses the value from a raw command-line argument.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
object? Convert(string? rawValue, IFormatProvider? formatProvider);
|
object? Convert(string? rawArgument, IFormatProvider? formatProvider);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ namespace CliFx.Extensibility;
|
|||||||
public class NoopBindingConverter : IBindingConverter
|
public class NoopBindingConverter : IBindingConverter
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public object? Convert(string? rawValue, IFormatProvider? formatProvider) => rawValue;
|
public object? Convert(string? rawArgument, IFormatProvider? formatProvider) => rawArgument;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ namespace CliFx;
|
|||||||
// Fallback command used when the application doesn't have one configured.
|
// Fallback command used when the application doesn't have one configured.
|
||||||
// This command is only used as a stub for help text.
|
// This command is only used as a stub for help text.
|
||||||
[Command]
|
[Command]
|
||||||
internal partial class FallbackDefaultCommand
|
internal partial class FallbackDefaultCommand : ICommandWithHelpOption, ICommandWithVersionOption
|
||||||
: ICommandWithHelpOption,
|
|
||||||
ICommandWithVersionOption
|
|
||||||
{
|
{
|
||||||
[CommandHelpOption]
|
[CommandHelpOption]
|
||||||
public bool IsHelpRequested { get; init; }
|
public bool IsHelpRequested { get; init; }
|
||||||
@@ -26,5 +24,6 @@ internal partial class FallbackDefaultCommand
|
|||||||
|
|
||||||
internal partial class FallbackDefaultCommand
|
internal partial class FallbackDefaultCommand
|
||||||
{
|
{
|
||||||
public static CommandSchema Schema { get; } = new CommandSchema<FallbackDefaultCommand>(null, null, []);
|
public static CommandSchema Schema { get; } =
|
||||||
|
new CommandSchema<FallbackDefaultCommand>(null, null, []);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ internal class CommandInputConsoleFormatter(ConsoleWriter consoleWriter)
|
|||||||
Write('[');
|
Write('[');
|
||||||
|
|
||||||
// Identifier
|
// Identifier
|
||||||
Write(ConsoleColor.White, optionInput.GetFormattedIdentifier());
|
Write(ConsoleColor.White, optionInput.FormattedIdentifier);
|
||||||
|
|
||||||
// Value(s)
|
// Value(s)
|
||||||
foreach (var value in optionInput.Values)
|
foreach (var value in optionInput.Values)
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ using System.Threading.Tasks;
|
|||||||
namespace CliFx.Infrastructure;
|
namespace CliFx.Infrastructure;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implements a <see cref="TextReader" /> for reading characters from a console stream.
|
/// Implements a <see cref="TextReader" /> for reading characters or binary data from a console stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
// Both the underlying stream AND the stream reader must be synchronized!
|
// Both the underlying stream AND the stream reader must be synchronized!
|
||||||
// https://github.com/Tyrrrz/CliFx/issues/123
|
// 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)
|
: StreamReader(Stream.Synchronized(stream), encoding, false, 4096)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ using CliFx.Utils;
|
|||||||
namespace CliFx.Infrastructure;
|
namespace CliFx.Infrastructure;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implements a <see cref="TextWriter" /> for writing characters to a console stream.
|
/// Implements a <see cref="TextWriter" /> for writing characters or binary data to a console stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
// Both the underlying stream AND the stream writer must be synchronized!
|
// Both the underlying stream AND the stream writer must be synchronized!
|
||||||
// https://github.com/Tyrrrz/CliFx/issues/123
|
// https://github.com/Tyrrrz/CliFx/issues/123
|
||||||
public class ConsoleWriter : StreamWriter
|
public sealed class ConsoleWriter : StreamWriter
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="ConsoleWriter" />.
|
/// Initializes an instance of <see cref="ConsoleWriter" />.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace CliFx.Input;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Input provided by the means of a directive.
|
/// Input provided by the means of a directive.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DirectiveInput(string name)
|
public class CommandDirectiveInput(string name)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Directive name.
|
/// Directive name.
|
||||||
@@ -10,9 +10,9 @@ namespace CliFx.Input;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class CommandInput(
|
public partial class CommandInput(
|
||||||
string? commandName,
|
string? commandName,
|
||||||
IReadOnlyList<DirectiveInput> directives,
|
IReadOnlyList<CommandDirectiveInput> directives,
|
||||||
IReadOnlyList<ParameterInput> parameters,
|
IReadOnlyList<CommandParameterInput> parameters,
|
||||||
IReadOnlyList<OptionInput> options,
|
IReadOnlyList<CommandOptionInput> options,
|
||||||
IReadOnlyList<EnvironmentVariableInput> environmentVariables
|
IReadOnlyList<EnvironmentVariableInput> environmentVariables
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -24,17 +24,17 @@ public partial class CommandInput(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provided directives.
|
/// Provided directives.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<DirectiveInput> Directives { get; } = directives;
|
public IReadOnlyList<CommandDirectiveInput> Directives { get; } = directives;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provided parameters.
|
/// Provided parameters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<ParameterInput> Parameters { get; } = parameters;
|
public IReadOnlyList<CommandParameterInput> Parameters { get; } = parameters;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provided options.
|
/// Provided options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<OptionInput> Options { get; } = options;
|
public IReadOnlyList<CommandOptionInput> Options { get; } = options;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provided environment variables.
|
/// Provided environment variables.
|
||||||
@@ -49,12 +49,12 @@ public partial class CommandInput(
|
|||||||
|
|
||||||
public partial class CommandInput
|
public partial class CommandInput
|
||||||
{
|
{
|
||||||
private static IReadOnlyList<DirectiveInput> ParseDirectives(
|
private static IReadOnlyList<CommandDirectiveInput> ParseDirectives(
|
||||||
IReadOnlyList<string> commandLineArguments,
|
IReadOnlyList<string> commandLineArguments,
|
||||||
ref int index
|
ref int index
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var result = new List<DirectiveInput>();
|
var result = new List<CommandDirectiveInput>();
|
||||||
|
|
||||||
// Consume all consecutive directive arguments
|
// Consume all consecutive directive arguments
|
||||||
for (; index < commandLineArguments.Count; index++)
|
for (; index < commandLineArguments.Count; index++)
|
||||||
@@ -66,7 +66,7 @@ public partial class CommandInput
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
var directiveName = argument.Substring(1, argument.Length - 2);
|
var directiveName = argument.Substring(1, argument.Length - 2);
|
||||||
result.Add(new DirectiveInput(directiveName));
|
result.Add(new CommandDirectiveInput(directiveName));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -108,12 +108,12 @@ public partial class CommandInput
|
|||||||
return commandName;
|
return commandName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<ParameterInput> ParseParameters(
|
private static IReadOnlyList<CommandParameterInput> ParseParameters(
|
||||||
IReadOnlyList<string> commandLineArguments,
|
IReadOnlyList<string> commandLineArguments,
|
||||||
ref int index
|
ref int index
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var result = new List<ParameterInput>();
|
var result = new List<CommandParameterInput>();
|
||||||
|
|
||||||
// Consume all arguments until the first option identifier
|
// Consume all arguments until the first option identifier
|
||||||
for (; index < commandLineArguments.Count; index++)
|
for (; index < commandLineArguments.Count; index++)
|
||||||
@@ -135,18 +135,18 @@ public partial class CommandInput
|
|||||||
if (isOptionIdentifier)
|
if (isOptionIdentifier)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
result.Add(new ParameterInput(index, argument));
|
result.Add(new CommandParameterInput(index, argument));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<OptionInput> ParseOptions(
|
private static IReadOnlyList<CommandOptionInput> ParseOptions(
|
||||||
IReadOnlyList<string> commandLineArguments,
|
IReadOnlyList<string> commandLineArguments,
|
||||||
ref int index
|
ref int index
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var result = new List<OptionInput>();
|
var result = new List<CommandOptionInput>();
|
||||||
|
|
||||||
var lastOptionIdentifier = default(string?);
|
var lastOptionIdentifier = default(string?);
|
||||||
var lastOptionValues = new List<string>();
|
var lastOptionValues = new List<string>();
|
||||||
@@ -165,7 +165,7 @@ public partial class CommandInput
|
|||||||
{
|
{
|
||||||
// Flush previous
|
// Flush previous
|
||||||
if (!string.IsNullOrWhiteSpace(lastOptionIdentifier))
|
if (!string.IsNullOrWhiteSpace(lastOptionIdentifier))
|
||||||
result.Add(new OptionInput(lastOptionIdentifier, lastOptionValues));
|
result.Add(new CommandOptionInput(lastOptionIdentifier, lastOptionValues));
|
||||||
|
|
||||||
lastOptionIdentifier = argument[2..];
|
lastOptionIdentifier = argument[2..];
|
||||||
lastOptionValues = [];
|
lastOptionValues = [];
|
||||||
@@ -177,7 +177,7 @@ public partial class CommandInput
|
|||||||
{
|
{
|
||||||
// Flush previous
|
// Flush previous
|
||||||
if (!string.IsNullOrWhiteSpace(lastOptionIdentifier))
|
if (!string.IsNullOrWhiteSpace(lastOptionIdentifier))
|
||||||
result.Add(new OptionInput(lastOptionIdentifier, lastOptionValues));
|
result.Add(new CommandOptionInput(lastOptionIdentifier, lastOptionValues));
|
||||||
|
|
||||||
lastOptionIdentifier = identifier.AsString();
|
lastOptionIdentifier = identifier.AsString();
|
||||||
lastOptionValues = [];
|
lastOptionValues = [];
|
||||||
@@ -192,7 +192,7 @@ public partial class CommandInput
|
|||||||
|
|
||||||
// Flush the last option
|
// Flush the last option
|
||||||
if (!string.IsNullOrWhiteSpace(lastOptionIdentifier))
|
if (!string.IsNullOrWhiteSpace(lastOptionIdentifier))
|
||||||
result.Add(new OptionInput(lastOptionIdentifier, lastOptionValues));
|
result.Add(new CommandOptionInput(lastOptionIdentifier, lastOptionValues));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,22 +5,22 @@ namespace CliFx.Input;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Input provided by the means of an option.
|
/// Input provided by the means of an option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class OptionInput(string identifier, IReadOnlyList<string> values)
|
public class CommandOptionInput(string identifier, IReadOnlyList<string> values)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Option identifier (either the name or the short name).
|
/// Option identifier (either the name or the short name).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Identifier { get; } = identifier;
|
public string Identifier { get; } = identifier;
|
||||||
|
|
||||||
|
internal string FormattedIdentifier { get; } =
|
||||||
|
identifier switch
|
||||||
|
{
|
||||||
|
{ Length: >= 2 } => "--" + identifier,
|
||||||
|
_ => '-' + identifier
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Option value(s).
|
/// Option value(s).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<string> Values { get; } = values;
|
public IReadOnlyList<string> Values { get; } = values;
|
||||||
|
|
||||||
internal string GetFormattedIdentifier() =>
|
|
||||||
Identifier switch
|
|
||||||
{
|
|
||||||
{ Length: >= 2 } => "--" + Identifier,
|
|
||||||
_ => '-' + Identifier
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Input provided by the means of a parameter.
|
/// Input provided by the means of a parameter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ParameterInput(int order, string value)
|
public class CommandParameterInput(int order, string value)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parameter order.
|
/// Parameter order.
|
||||||
@@ -32,7 +32,7 @@ public class ApplicationSchema(IReadOnlyList<CommandSchema> commands)
|
|||||||
|
|
||||||
foreach (var potentialDescendantCommand in potentialDescendantCommands)
|
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))
|
if (string.IsNullOrWhiteSpace(potentialDescendantCommand.Name))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public abstract class CommandInputSchema(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Activate(ICommand instance, IReadOnlyList<string?> rawInputs)
|
internal void Activate(ICommand instance, IReadOnlyList<string?> rawArguments)
|
||||||
{
|
{
|
||||||
var formatProvider = CultureInfo.InvariantCulture;
|
var formatProvider = CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
@@ -75,15 +75,20 @@ public abstract class CommandInputSchema(
|
|||||||
// Multiple values expected, single or multiple values provided
|
// Multiple values expected, single or multiple values provided
|
||||||
if (IsSequence)
|
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);
|
Validate(value);
|
||||||
|
|
||||||
Property.SetValue(instance, value);
|
Property.SetValue(instance, value);
|
||||||
}
|
}
|
||||||
// Single value expected, single value provided
|
// 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);
|
Validate(value);
|
||||||
|
|
||||||
Property.SetValue(instance, value);
|
Property.SetValue(instance, value);
|
||||||
@@ -94,7 +99,7 @@ public abstract class CommandInputSchema(
|
|||||||
throw CliFxException.UserError(
|
throw CliFxException.UserError(
|
||||||
$"""
|
$"""
|
||||||
{Kind} {FormattedIdentifier} expects a single argument, but provided with multiple:
|
{Kind} {FormattedIdentifier} expects a single argument, but provided with multiple:
|
||||||
{rawInputs.Select(v => '<' + v + '>').JoinToString(" ")}
|
{rawArguments.Select(v => '<' + v + '>').JoinToString(" ")}
|
||||||
"""
|
"""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -104,7 +109,7 @@ public abstract class CommandInputSchema(
|
|||||||
throw CliFxException.UserError(
|
throw CliFxException.UserError(
|
||||||
$"""
|
$"""
|
||||||
{Kind} {FormattedIdentifier} cannot be set from the provided argument(s):
|
{Kind} {FormattedIdentifier} cannot be set from the provided argument(s):
|
||||||
{rawInputs.Select(v => '<' + v + '>').JoinToString(" ")}
|
{rawArguments.Select(v => '<' + v + '>').JoinToString(" ")}
|
||||||
Error: {ex.Message}
|
Error: {ex.Message}
|
||||||
""",
|
""",
|
||||||
ex
|
ex
|
||||||
@@ -113,6 +118,7 @@ public abstract class CommandInputSchema(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
public override string ToString() => FormattedIdentifier;
|
public override string ToString() => FormattedIdentifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class CommandOptionSchema(
|
|||||||
string? description,
|
string? description,
|
||||||
IBindingConverter converter,
|
IBindingConverter converter,
|
||||||
IReadOnlyList<IBindingValidator> validators
|
IReadOnlyList<IBindingValidator> validators
|
||||||
) : CommandInputSchema(property,description, converter, validators)
|
) : CommandInputSchema(property, description, converter, validators)
|
||||||
{
|
{
|
||||||
internal override string Kind => "Option";
|
internal override string Kind => "Option";
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ public class CommandSchema(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Option inputs of the command.
|
/// Option inputs of the command.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<CommandOptionSchema> Options { get; } = inputs.OfType<CommandOptionSchema>().ToArray();
|
public IReadOnlyList<CommandOptionSchema> Options { get; } =
|
||||||
|
inputs.OfType<CommandOptionSchema>().ToArray();
|
||||||
|
|
||||||
internal bool MatchesName(string? name) =>
|
internal bool MatchesName(string? name) =>
|
||||||
!string.IsNullOrWhiteSpace(Name)
|
!string.IsNullOrWhiteSpace(Name)
|
||||||
@@ -79,7 +80,7 @@ public class CommandSchema(
|
|||||||
return result;
|
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
|
// Ensure there are no unexpected parameters and that all parameters are provided
|
||||||
var remainingParameterInputs = input.Parameters.ToList();
|
var remainingParameterInputs = input.Parameters.ToList();
|
||||||
@@ -107,10 +108,7 @@ public class CommandSchema(
|
|||||||
{
|
{
|
||||||
var parameterInputs = input.Parameters.Skip(position).ToArray();
|
var parameterInputs = input.Parameters.Skip(position).ToArray();
|
||||||
|
|
||||||
parameterSchema.Activate(
|
parameterSchema.Activate(instance, parameterInputs.Select(p => p.Value).ToArray());
|
||||||
instance,
|
|
||||||
parameterInputs.Select(p => p.Value).ToArray()
|
|
||||||
);
|
|
||||||
|
|
||||||
position += parameterInputs.Length;
|
position += parameterInputs.Length;
|
||||||
remainingParameterInputs.RemoveRange(parameterInputs);
|
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
|
// Ensure there are no unrecognized options and that all required options are set
|
||||||
var remainingOptionInputs = input.Options.ToList();
|
var remainingOptionInputs = input.Options.ToList();
|
||||||
var remainingRequiredOptionSchemas = Options.Where(o => o.IsRequired)
|
var remainingRequiredOptionSchemas = Options.Where(o => o.IsRequired).ToList();
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var optionSchema in Options)
|
foreach (var optionSchema in Options)
|
||||||
{
|
{
|
||||||
@@ -197,7 +194,7 @@ public class CommandSchema(
|
|||||||
throw CliFxException.UserError(
|
throw CliFxException.UserError(
|
||||||
$"""
|
$"""
|
||||||
Unrecognized option(s):
|
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);
|
ActivateParameters(instance, input);
|
||||||
ActivateOptions(input, instance);
|
ActivateOptions(instance, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override string ToString() => Name ?? "<default>";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="CommandSchema" />
|
/// <inheritdoc cref="CommandSchema" />
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using System.Linq;
|
|||||||
namespace CliFx.Schema;
|
namespace CliFx.Schema;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PropertyBinding(
|
public class PropertyBinding(
|
||||||
[DynamicallyAccessedMembers(
|
[DynamicallyAccessedMembers(
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ internal static class CollectionExtensions
|
|||||||
yield return (o, i++);
|
yield return (o, i++);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
foreach (var i in source)
|
||||||
|
{
|
||||||
|
if (i is not null)
|
||||||
|
yield return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static IEnumerable<string> WhereNotNullOrWhiteSpace(this IEnumerable<string?> source)
|
public static IEnumerable<string> WhereNotNullOrWhiteSpace(this IEnumerable<string?> source)
|
||||||
{
|
{
|
||||||
foreach (var i in source)
|
foreach (var i in source)
|
||||||
|
|||||||
@@ -10,31 +10,29 @@ internal static class TypeExtensions
|
|||||||
{
|
{
|
||||||
public static Type? TryGetEnumerableUnderlyingType(
|
public static Type? TryGetEnumerableUnderlyingType(
|
||||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type
|
||||||
)
|
) =>
|
||||||
|
type.GetInterfaces()
|
||||||
|
.Select(i =>
|
||||||
{
|
{
|
||||||
if (type.IsPrimitive)
|
if (i == typeof(IEnumerable))
|
||||||
return null;
|
|
||||||
|
|
||||||
if (type == typeof(IEnumerable))
|
|
||||||
return typeof(object);
|
return typeof(object);
|
||||||
|
|
||||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
||||||
return type.GetGenericArguments().FirstOrDefault();
|
return i.GetGenericArguments().FirstOrDefault();
|
||||||
|
|
||||||
return type.GetInterfaces()
|
return null;
|
||||||
.Select(t => TryGetEnumerableUnderlyingType(t))
|
})
|
||||||
.Where(t => t is not null)
|
.WhereNotNull()
|
||||||
// Every IEnumerable<T> implements IEnumerable (which is essentially IEnumerable<object>),
|
// Every IEnumerable<T> implements IEnumerable (which is essentially IEnumerable<object>),
|
||||||
// so we try to get a more specific underlying type. Still, if the type only implements
|
// so we try to get a more specific underlying type. Still, if the type only implements
|
||||||
// IEnumerable<object> and nothing else, then we'll just return that.
|
// IEnumerable<object> and nothing else, then we'll just return that.
|
||||||
.MaxBy(t => t != typeof(object));
|
.MaxBy(t => t != typeof(object));
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsToStringOverriden(
|
public static bool IsToStringOverriden(
|
||||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] this Type type
|
[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;
|
return toStringMethod?.GetBaseDefinition().DeclaringType != toStringMethod?.DeclaringType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user