mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
4 Commits
432c8a66af
...
2.3.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a02d39dba | ||
|
|
c40b4f3501 | ||
|
|
3fb2a2319b | ||
|
|
1a5a0374c7 |
28
CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj
Normal file
28
CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Basic.Reference.Assemblies.Net80" Version="1.8.0" />
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="GitHubActionsTestLogger" Version="2.4.1" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" />
|
||||||
|
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
75
CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs
Normal file
75
CliFx.Analyzers.Tests/CommandMustBeAnnotatedAnalyzerSpecs.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
CliFx.Analyzers.Tests/GeneralSpecs.cs
Normal file
29
CliFx.Analyzers.Tests/GeneralSpecs.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs
Normal file
109
CliFx.Analyzers.Tests/OptionMustHaveValidNameAnalyzerSpecs.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
177
CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs
Normal file
177
CliFx.Analyzers.Tests/Utils/AnalyzerAssertions.cs
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
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, AssertionChain assertionChain)
|
||||||
|
: ReferenceTypeAssertions<DiagnosticAnalyzer, AnalyzerAssertions>(analyzer, assertionChain)
|
||||||
|
{
|
||||||
|
private readonly AssertionChain _assertionChain = assertionChain;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
_assertionChain
|
||||||
|
.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();
|
||||||
|
|
||||||
|
_assertionChain
|
||||||
|
.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, AssertionChain.GetOrCreate());
|
||||||
|
}
|
||||||
5
CliFx.Analyzers.Tests/xunit.runner.json
Normal file
5
CliFx.Analyzers.Tests/xunit.runner.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||||
|
"methodDisplayOptions": "all",
|
||||||
|
"methodDisplay": "method"
|
||||||
|
}
|
||||||
40
CliFx.Analyzers/AnalyzerBase.cs
Normal file
40
CliFx.Analyzers/AnalyzerBase.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
<GenerateDependencyFile>true</GenerateDependencyFile>
|
<GenerateDependencyFile>true</GenerateDependencyFile>
|
||||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||||
<NoWarn>$(NoWarn);RS1035</NoWarn>
|
<NoWarn>$(NoWarn);RS1025;RS1026</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@@ -17,11 +17,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CSharpier.MsBuild" Version="0.28.2" PrivateAssets="all" />
|
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
||||||
<!-- Make sure to target the lowest possible version of the compiler for wider support -->
|
<!-- Make sure to target the lowest possible version of the compiler for wider support -->
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.11.0" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.0.0" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.0.0" PrivateAssets="all" />
|
||||||
<PackageReference Include="PolyShim" Version="1.12.0" PrivateAssets="all" />
|
<PackageReference Include="PolyShim" Version="1.15.0" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
50
CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs
Normal file
50
CliFx.Analyzers/CommandMustBeAnnotatedAnalyzer.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs
Normal file
44
CliFx.Analyzers/CommandMustImplementInterfaceAnalyzer.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs
Normal file
89
CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
78
CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs
Normal file
78
CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
21
CliFx.Analyzers/ObjectModel/ICommandMemberSymbol.cs
Normal file
21
CliFx.Analyzers/ObjectModel/ICommandMemberSymbol.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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,10 +1,13 @@
|
|||||||
namespace CliFx.SourceGeneration.SemanticModel;
|
namespace CliFx.Analyzers.ObjectModel;
|
||||||
|
|
||||||
internal static class KnownSymbolNames
|
internal static class SymbolNames
|
||||||
{
|
{
|
||||||
public const string CliFxCommandInterface = "CliFx.ICommand";
|
public const string CliFxCommandInterface = "CliFx.ICommand";
|
||||||
public const string CliFxCommandAttribute = "CliFx.Attributes.CommandAttribute";
|
public const string CliFxCommandAttribute = "CliFx.Attributes.CommandAttribute";
|
||||||
public const string CliFxCommandParameterAttribute =
|
public const string CliFxCommandParameterAttribute =
|
||||||
"CliFx.Attributes.CommandParameterAttribute";
|
"CliFx.Attributes.CommandParameterAttribute";
|
||||||
public const string CliFxCommandOptionAttribute = "CliFx.Attributes.CommandOptionAttribute";
|
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>";
|
||||||
}
|
}
|
||||||
49
CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.cs
Normal file
49
CliFx.Analyzers/OptionMustBeInsideCommandAnalyzer.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
CliFx.Analyzers/OptionMustHaveNameOrShortNameAnalyzer.cs
Normal file
39
CliFx.Analyzers/OptionMustHaveNameOrShortNameAnalyzer.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs
Normal file
69
CliFx.Analyzers/OptionMustHaveUniqueNameAnalyzer.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.cs
Normal file
68
CliFx.Analyzers/OptionMustHaveUniqueShortNameAnalyzer.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs
Normal file
65
CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs
Normal file
43
CliFx.Analyzers/OptionMustHaveValidNameAnalyzer.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs
Normal file
43
CliFx.Analyzers/OptionMustHaveValidShortNameAnalyzer.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs
Normal file
58
CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.cs
Normal file
49
CliFx.Analyzers/ParameterMustBeInsideCommandAnalyzer.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
CliFx.Analyzers/ParameterMustBeLastIfNonRequiredAnalyzer.cs
Normal file
63
CliFx.Analyzers/ParameterMustBeLastIfNonRequiredAnalyzer.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs
Normal file
63
CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs
Normal file
63
CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs
Normal file
75
CliFx.Analyzers/ParameterMustHaveUniqueNameAnalyzer.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs
Normal file
62
CliFx.Analyzers/ParameterMustHaveUniqueOrderAnalyzer.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs
Normal file
65
CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs
Normal file
58
CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs
Normal file
74
CliFx.Analyzers/SystemConsoleShouldBeAvoidedAnalyzer.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs
Normal file
98
CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
CliFx.Analyzers/Utils/Extensions/StringExtensions.cs
Normal file
18
CliFx.Analyzers/Utils/Extensions/StringExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
||||||
<PublishAot>true</PublishAot>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||||
<ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace CliFx.Demo.Domain;
|
namespace CliFx.Demo.Domain;
|
||||||
@@ -23,5 +24,5 @@ public partial record Library(IReadOnlyList<Book> Books)
|
|||||||
|
|
||||||
public partial record Library
|
public partial record Library
|
||||||
{
|
{
|
||||||
public static Library Empty { get; } = new([]);
|
public static Library Empty { get; } = new(Array.Empty<Book>());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CliFx.Demo.Domain;
|
|
||||||
|
|
||||||
[JsonSerializable(typeof(Library))]
|
|
||||||
public partial class LibraryJsonContext : JsonSerializerContext;
|
|
||||||
@@ -11,7 +11,7 @@ public class LibraryProvider
|
|||||||
|
|
||||||
private void StoreLibrary(Library library)
|
private void StoreLibrary(Library library)
|
||||||
{
|
{
|
||||||
var data = JsonSerializer.Serialize(library, LibraryJsonContext.Default.Library);
|
var data = JsonSerializer.Serialize(library);
|
||||||
File.WriteAllText(StorageFilePath, data);
|
File.WriteAllText(StorageFilePath, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,8 +22,7 @@ public class LibraryProvider
|
|||||||
|
|
||||||
var data = File.ReadAllText(StorageFilePath);
|
var data = File.ReadAllText(StorageFilePath);
|
||||||
|
|
||||||
return JsonSerializer.Deserialize(data, LibraryJsonContext.Default.Library)
|
return JsonSerializer.Deserialize<Library>(data) ?? Library.Empty;
|
||||||
?? Library.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Book? TryGetBook(string title) =>
|
public Book? TryGetBook(string title) =>
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
using CliFx.Demo.Domain;
|
using CliFx.Demo.Domain;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
return await new CliApplicationBuilder()
|
||||||
|
.SetDescription("Demo application showcasing CliFx features.")
|
||||||
|
.AddCommandsFromThisAssembly()
|
||||||
|
.UseTypeActivator(commandTypes =>
|
||||||
|
{
|
||||||
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
|
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddSingleton<LibraryProvider>();
|
services.AddSingleton<LibraryProvider>();
|
||||||
@@ -10,9 +15,7 @@ services.AddSingleton<LibraryProvider>();
|
|||||||
foreach (var commandType in commandTypes)
|
foreach (var commandType in commandTypes)
|
||||||
services.AddTransient(commandType);
|
services.AddTransient(commandType);
|
||||||
|
|
||||||
return await new CliApplicationBuilder()
|
return services.BuildServiceProvider();
|
||||||
.SetDescription("Demo application showcasing CliFx features.")
|
})
|
||||||
.AddCommandsFromThisAssembly()
|
|
||||||
.UseTypeActivator(services.BuildServiceProvider())
|
|
||||||
.Build()
|
.Build()
|
||||||
.RunAsync();
|
.RunAsync();
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.SourceGeneration.SemanticModel;
|
|
||||||
using CliFx.SourceGeneration.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
|
|
||||||
namespace CliFx.SourceGeneration;
|
|
||||||
|
|
||||||
[Generator]
|
|
||||||
public class CommandSchemaGenerator : IIncrementalGenerator
|
|
||||||
{
|
|
||||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
||||||
{
|
|
||||||
var values = context.SyntaxProvider.ForAttributeWithMetadataName<(
|
|
||||||
CommandSymbol?,
|
|
||||||
Diagnostic?
|
|
||||||
)>(
|
|
||||||
KnownSymbolNames.CliFxCommandAttribute,
|
|
||||||
(n, _) => n is TypeDeclarationSyntax,
|
|
||||||
(x, _) =>
|
|
||||||
{
|
|
||||||
// Predicate above ensures that these casts are safe
|
|
||||||
var commandTypeSyntax = (TypeDeclarationSyntax)x.TargetNode;
|
|
||||||
var commandTypeSymbol = (INamedTypeSymbol)x.TargetSymbol;
|
|
||||||
|
|
||||||
// Check if the target type and all its containing types are partial
|
|
||||||
if (
|
|
||||||
commandTypeSyntax
|
|
||||||
.AncestorsAndSelf()
|
|
||||||
.Any(a =>
|
|
||||||
a is TypeDeclarationSyntax t
|
|
||||||
&& !t.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
null,
|
|
||||||
Diagnostic.Create(
|
|
||||||
DiagnosticDescriptors.CommandMustBePartial,
|
|
||||||
commandTypeSyntax.Identifier.GetLocation()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the target type implements ICommand
|
|
||||||
var hasCommandInterface = commandTypeSymbol.AllInterfaces.Any(i =>
|
|
||||||
i.DisplayNameMatches(KnownSymbolNames.CliFxCommandInterface)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasCommandInterface)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
null,
|
|
||||||
Diagnostic.Create(
|
|
||||||
DiagnosticDescriptors.CommandMustImplementInterface,
|
|
||||||
commandTypeSymbol.Locations.First()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the command
|
|
||||||
var commandAttribute = x.Attributes.First(a =>
|
|
||||||
a.AttributeClass?.DisplayNameMatches(KnownSymbolNames.CliFxCommandAttribute)
|
|
||||||
== true
|
|
||||||
);
|
|
||||||
|
|
||||||
var command = CommandSymbol.FromSymbol(commandTypeSymbol, commandAttribute);
|
|
||||||
|
|
||||||
// TODO: validate command
|
|
||||||
|
|
||||||
return (command, null);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Report diagnostics
|
|
||||||
var diagnostics = values.Select((v, _) => v.Item2).WhereNotNull();
|
|
||||||
context.RegisterSourceOutput(diagnostics, (x, d) => x.ReportDiagnostic(d));
|
|
||||||
|
|
||||||
// Generate command schemas
|
|
||||||
var symbols = values.Select((v, _) => v.Item1).WhereNotNull();
|
|
||||||
context.RegisterSourceOutput(
|
|
||||||
symbols,
|
|
||||||
(x, c) =>
|
|
||||||
x.AddSource(
|
|
||||||
$"{c.Type.FullyQualifiedName}.CommandSchema.Generated.cs",
|
|
||||||
// lang=csharp
|
|
||||||
$$"""
|
|
||||||
namespace {{c.Type.Namespace}};
|
|
||||||
|
|
||||||
partial class {{c.Type.Name}}
|
|
||||||
{
|
|
||||||
public static CliFx.Schema.CommandSchema<{{c.Type.FullyQualifiedName}}> Schema { get; } = {{c.GenerateSchemaInitializationCode()}};
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate extension methods
|
|
||||||
var symbolsCollected = symbols.Collect();
|
|
||||||
context.RegisterSourceOutput(
|
|
||||||
symbolsCollected,
|
|
||||||
(x, cs) =>
|
|
||||||
x.AddSource(
|
|
||||||
"CommandSchemaExtensions.Generated.cs",
|
|
||||||
// lang=csharp
|
|
||||||
$$"""
|
|
||||||
namespace CliFx;
|
|
||||||
|
|
||||||
static partial class GeneratedExtensions
|
|
||||||
{
|
|
||||||
public static CliFx.CliApplicationBuilder AddCommandsFromThisAssembly(this CliFx.CliApplicationBuilder builder)
|
|
||||||
{
|
|
||||||
{{
|
|
||||||
cs.Select(c => c.Type.FullyQualifiedName)
|
|
||||||
.Select(t =>
|
|
||||||
// lang=csharp
|
|
||||||
$"builder.AddCommand({t}.Schema);"
|
|
||||||
)
|
|
||||||
.JoinToString("\n")
|
|
||||||
}}
|
|
||||||
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using CliFx.SourceGeneration.SemanticModel;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace CliFx.SourceGeneration;
|
|
||||||
|
|
||||||
internal static class DiagnosticDescriptors
|
|
||||||
{
|
|
||||||
public static DiagnosticDescriptor CommandMustBePartial { get; } =
|
|
||||||
new(
|
|
||||||
$"{nameof(CliFx)}_{nameof(CommandMustBePartial)}",
|
|
||||||
"Command types must be declared as `partial`",
|
|
||||||
"This type (and all its containing types, if present) must be declared as `partial` in order to be a valid command.",
|
|
||||||
"CliFx",
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
public static DiagnosticDescriptor CommandMustImplementInterface { get; } =
|
|
||||||
new(
|
|
||||||
$"{nameof(CliFx)}_{nameof(CommandMustImplementInterface)}",
|
|
||||||
$"Commands must implement the `{KnownSymbolNames.CliFxCommandInterface}` interface",
|
|
||||||
$"This type must implement the `{KnownSymbolNames.CliFxCommandInterface}` interface in order to be a valid command.",
|
|
||||||
"CliFx",
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace CliFx.SourceGeneration.SemanticModel;
|
|
||||||
|
|
||||||
internal abstract partial class CommandInputSymbol(
|
|
||||||
PropertyDescriptor property,
|
|
||||||
bool isSequence,
|
|
||||||
string? description,
|
|
||||||
TypeDescriptor? converterType,
|
|
||||||
IReadOnlyList<TypeDescriptor> validatorTypes
|
|
||||||
)
|
|
||||||
{
|
|
||||||
public PropertyDescriptor Property { get; } = property;
|
|
||||||
|
|
||||||
public bool IsSequence { get; } = isSequence;
|
|
||||||
|
|
||||||
public string? Description { get; } = description;
|
|
||||||
|
|
||||||
public TypeDescriptor? ConverterType { get; } = converterType;
|
|
||||||
|
|
||||||
public IReadOnlyList<TypeDescriptor> ValidatorTypes { get; } = validatorTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class CommandInputSymbol : IEquatable<CommandInputSymbol>
|
|
||||||
{
|
|
||||||
public bool Equals(CommandInputSymbol? other)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(null, other))
|
|
||||||
return false;
|
|
||||||
if (ReferenceEquals(this, other))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return Property.Equals(other.Property)
|
|
||||||
&& IsSequence == other.IsSequence
|
|
||||||
&& Description == other.Description
|
|
||||||
&& Equals(ConverterType, other.ConverterType)
|
|
||||||
&& ValidatorTypes.SequenceEqual(other.ValidatorTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(null, obj))
|
|
||||||
return false;
|
|
||||||
if (ReferenceEquals(this, obj))
|
|
||||||
return true;
|
|
||||||
if (obj.GetType() != GetType())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return Equals((CommandInputSymbol)obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode() =>
|
|
||||||
HashCode.Combine(Property, IsSequence, Description, ConverterType, ValidatorTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class CommandInputSymbol
|
|
||||||
{
|
|
||||||
public static bool IsSequenceType(ITypeSymbol type) =>
|
|
||||||
type.AllInterfaces.Any(i =>
|
|
||||||
i.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T
|
|
||||||
)
|
|
||||||
&& type.SpecialType != SpecialType.System_String;
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using CliFx.SourceGeneration.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace CliFx.SourceGeneration.SemanticModel;
|
|
||||||
|
|
||||||
internal partial class CommandOptionSymbol(
|
|
||||||
PropertyDescriptor property,
|
|
||||||
bool isSequence,
|
|
||||||
string? name,
|
|
||||||
char? shortName,
|
|
||||||
string? environmentVariable,
|
|
||||||
bool isRequired,
|
|
||||||
string? description,
|
|
||||||
TypeDescriptor? converterType,
|
|
||||||
IReadOnlyList<TypeDescriptor> validatorTypes
|
|
||||||
) : CommandInputSymbol(property, isSequence, description, converterType, validatorTypes)
|
|
||||||
{
|
|
||||||
public string? Name { get; } = name;
|
|
||||||
|
|
||||||
public char? ShortName { get; } = shortName;
|
|
||||||
|
|
||||||
public string? EnvironmentVariable { get; } = environmentVariable;
|
|
||||||
|
|
||||||
public bool IsRequired { get; } = isRequired;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class CommandOptionSymbol : IEquatable<CommandOptionSymbol>
|
|
||||||
{
|
|
||||||
public bool Equals(CommandOptionSymbol? other)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(null, other))
|
|
||||||
return false;
|
|
||||||
if (ReferenceEquals(this, other))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return base.Equals(other)
|
|
||||||
&& Name == other.Name
|
|
||||||
&& ShortName == other.ShortName
|
|
||||||
&& EnvironmentVariable == other.EnvironmentVariable
|
|
||||||
&& IsRequired == other.IsRequired;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(null, obj))
|
|
||||||
return false;
|
|
||||||
if (ReferenceEquals(this, obj))
|
|
||||||
return true;
|
|
||||||
if (obj.GetType() != GetType())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return Equals((CommandOptionSymbol)obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode() =>
|
|
||||||
HashCode.Combine(base.GetHashCode(), Name, ShortName, EnvironmentVariable, IsRequired);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class CommandOptionSymbol
|
|
||||||
{
|
|
||||||
public static CommandOptionSymbol FromSymbol(
|
|
||||||
IPropertySymbol property,
|
|
||||||
AttributeData attribute
|
|
||||||
) =>
|
|
||||||
new(
|
|
||||||
PropertyDescriptor.FromSymbol(property),
|
|
||||||
IsSequenceType(property.Type),
|
|
||||||
attribute
|
|
||||||
.ConstructorArguments.FirstOrDefault(a =>
|
|
||||||
a.Type?.SpecialType == SpecialType.System_String
|
|
||||||
)
|
|
||||||
.Value as string,
|
|
||||||
attribute
|
|
||||||
.ConstructorArguments.FirstOrDefault(a =>
|
|
||||||
a.Type?.SpecialType == SpecialType.System_Char
|
|
||||||
)
|
|
||||||
.Value as char?,
|
|
||||||
attribute.GetNamedArgumentValue("EnvironmentVariable", default(string)),
|
|
||||||
attribute.GetNamedArgumentValue("IsRequired", property.IsRequired),
|
|
||||||
attribute.GetNamedArgumentValue("Description", default(string)),
|
|
||||||
TypeDescriptor.FromSymbol(attribute.GetNamedArgumentValue<ITypeSymbol?>("Converter")),
|
|
||||||
attribute
|
|
||||||
.GetNamedArgumentValues<ITypeSymbol>("Validators")
|
|
||||||
.Select(TypeDescriptor.FromSymbol)
|
|
||||||
.ToArray()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using CliFx.SourceGeneration.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace CliFx.SourceGeneration.SemanticModel;
|
|
||||||
|
|
||||||
internal partial class CommandParameterSymbol(
|
|
||||||
PropertyDescriptor property,
|
|
||||||
bool isSequence,
|
|
||||||
int order,
|
|
||||||
string name,
|
|
||||||
bool isRequired,
|
|
||||||
string? description,
|
|
||||||
TypeDescriptor? converterType,
|
|
||||||
IReadOnlyList<TypeDescriptor> validatorTypes
|
|
||||||
) : CommandInputSymbol(property, isSequence, description, converterType, validatorTypes)
|
|
||||||
{
|
|
||||||
public int Order { get; } = order;
|
|
||||||
|
|
||||||
public string Name { get; } = name;
|
|
||||||
|
|
||||||
public bool IsRequired { get; } = isRequired;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class CommandParameterSymbol : IEquatable<CommandParameterSymbol>
|
|
||||||
{
|
|
||||||
public bool Equals(CommandParameterSymbol? other)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(null, other))
|
|
||||||
return false;
|
|
||||||
if (ReferenceEquals(this, other))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return base.Equals(other)
|
|
||||||
&& Order == other.Order
|
|
||||||
&& Name == other.Name
|
|
||||||
&& IsRequired == other.IsRequired;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(null, obj))
|
|
||||||
return false;
|
|
||||||
if (ReferenceEquals(this, obj))
|
|
||||||
return true;
|
|
||||||
if (obj.GetType() != GetType())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return Equals((CommandParameterSymbol)obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode() =>
|
|
||||||
HashCode.Combine(base.GetHashCode(), Order, Name, IsRequired);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class CommandParameterSymbol
|
|
||||||
{
|
|
||||||
public static CommandParameterSymbol FromSymbol(
|
|
||||||
IPropertySymbol property,
|
|
||||||
AttributeData attribute
|
|
||||||
) =>
|
|
||||||
new(
|
|
||||||
PropertyDescriptor.FromSymbol(property),
|
|
||||||
IsSequenceType(property.Type),
|
|
||||||
(int)attribute.ConstructorArguments.First().Value!,
|
|
||||||
attribute.GetNamedArgumentValue("Name", default(string)),
|
|
||||||
attribute.GetNamedArgumentValue("IsRequired", true),
|
|
||||||
attribute.GetNamedArgumentValue("Description", default(string)),
|
|
||||||
TypeDescriptor.FromSymbol(attribute.GetNamedArgumentValue<ITypeSymbol>("Converter")),
|
|
||||||
attribute
|
|
||||||
.GetNamedArgumentValues<ITypeSymbol>("Validators")
|
|
||||||
.Select(TypeDescriptor.FromSymbol)
|
|
||||||
.ToArray()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using CliFx.SourceGeneration.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace CliFx.SourceGeneration.SemanticModel;
|
|
||||||
|
|
||||||
internal partial class CommandSymbol(
|
|
||||||
TypeDescriptor type,
|
|
||||||
string? name,
|
|
||||||
string? description,
|
|
||||||
IReadOnlyList<CommandInputSymbol> inputs
|
|
||||||
)
|
|
||||||
{
|
|
||||||
public TypeDescriptor Type { get; } = type;
|
|
||||||
|
|
||||||
public string? Name { get; } = name;
|
|
||||||
|
|
||||||
public string? Description { get; } = description;
|
|
||||||
|
|
||||||
public IReadOnlyList<CommandInputSymbol> Inputs { get; } = inputs;
|
|
||||||
|
|
||||||
public IReadOnlyList<CommandParameterSymbol> Parameters =>
|
|
||||||
Inputs.OfType<CommandParameterSymbol>().ToArray();
|
|
||||||
|
|
||||||
public IReadOnlyList<CommandOptionSymbol> Options =>
|
|
||||||
Inputs.OfType<CommandOptionSymbol>().ToArray();
|
|
||||||
|
|
||||||
private string GeneratePropertyBindingInitializationCode(PropertyDescriptor property) =>
|
|
||||||
// lang=csharp
|
|
||||||
$$"""
|
|
||||||
new CliFx.Schema.PropertyBinding<{{Type.FullyQualifiedName}}, {{property
|
|
||||||
.Type
|
|
||||||
.FullyQualifiedName}}>(
|
|
||||||
(obj) => obj.{{property.Name}},
|
|
||||||
(obj, value) => obj.{{property.Name}} = value
|
|
||||||
)
|
|
||||||
""";
|
|
||||||
|
|
||||||
private string GenerateSchemaInitializationCode(CommandInputSymbol input) =>
|
|
||||||
input switch
|
|
||||||
{
|
|
||||||
CommandParameterSymbol parameter
|
|
||||||
=>
|
|
||||||
// lang=csharp
|
|
||||||
$$"""
|
|
||||||
new CliFx.Schema.CommandParameterSchema<{{Type.FullyQualifiedName}}, {{parameter
|
|
||||||
.Property
|
|
||||||
.Type
|
|
||||||
.FullyQualifiedName}}>(
|
|
||||||
{{GeneratePropertyBindingInitializationCode(parameter.Property)}},
|
|
||||||
{{parameter.IsSequence}},
|
|
||||||
{{parameter.Order}},
|
|
||||||
"{{parameter.Name}}",
|
|
||||||
{{parameter.IsRequired}},
|
|
||||||
"{{parameter.Description}}",
|
|
||||||
// TODO,
|
|
||||||
// TODO
|
|
||||||
);
|
|
||||||
""",
|
|
||||||
CommandOptionSymbol option
|
|
||||||
=>
|
|
||||||
// lang=csharp
|
|
||||||
$$"""
|
|
||||||
new CliFx.Schema.CommandOptionSchema<{{Type.FullyQualifiedName}}, {{option
|
|
||||||
.Property
|
|
||||||
.Type
|
|
||||||
.FullyQualifiedName}}>(
|
|
||||||
{{GeneratePropertyBindingInitializationCode(option.Property)}},
|
|
||||||
{{option.IsSequence}},
|
|
||||||
"{{option.Name}}",
|
|
||||||
'{{option.ShortName}}',
|
|
||||||
"{{option.EnvironmentVariable}}",
|
|
||||||
{{option.IsRequired}},
|
|
||||||
"{{option.Description}}",
|
|
||||||
// TODO,
|
|
||||||
// TODO
|
|
||||||
);
|
|
||||||
""",
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(input), input, null)
|
|
||||||
};
|
|
||||||
|
|
||||||
public string GenerateSchemaInitializationCode() =>
|
|
||||||
// lang=csharp
|
|
||||||
$$"""
|
|
||||||
new CliFx.Schema.CommandSchema<{{Type.FullyQualifiedName}}>(
|
|
||||||
"{{Name}}",
|
|
||||||
"{{Description}}",
|
|
||||||
new CliFx.Schema.CommandInputSchema[]
|
|
||||||
{
|
|
||||||
{{Inputs.Select(GenerateSchemaInitializationCode).JoinToString(",\n")}}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
""";
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class CommandSymbol : IEquatable<CommandSymbol>
|
|
||||||
{
|
|
||||||
public bool Equals(CommandSymbol? other)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(null, other))
|
|
||||||
return false;
|
|
||||||
if (ReferenceEquals(this, other))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return Type.Equals(other.Type)
|
|
||||||
&& Name == other.Name
|
|
||||||
&& Description == other.Description
|
|
||||||
&& Inputs.SequenceEqual(other.Inputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(null, obj))
|
|
||||||
return false;
|
|
||||||
if (ReferenceEquals(this, obj))
|
|
||||||
return true;
|
|
||||||
if (obj.GetType() != GetType())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return Equals((CommandSymbol)obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode() => HashCode.Combine(Type, Name, Description, Inputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class CommandSymbol
|
|
||||||
{
|
|
||||||
public static CommandSymbol FromSymbol(INamedTypeSymbol symbol, AttributeData attribute)
|
|
||||||
{
|
|
||||||
var inputs = new List<CommandInputSymbol>();
|
|
||||||
foreach (var property in symbol.GetMembers().OfType<IPropertySymbol>())
|
|
||||||
{
|
|
||||||
var parameterAttribute = property
|
|
||||||
.GetAttributes()
|
|
||||||
.FirstOrDefault(a =>
|
|
||||||
a.AttributeClass?.Name == KnownSymbolNames.CliFxCommandParameterAttribute
|
|
||||||
);
|
|
||||||
|
|
||||||
if (parameterAttribute is not null)
|
|
||||||
{
|
|
||||||
inputs.Add(CommandParameterSymbol.FromSymbol(property, parameterAttribute));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var optionAttribute = property
|
|
||||||
.GetAttributes()
|
|
||||||
.FirstOrDefault(a =>
|
|
||||||
a.AttributeClass?.Name == KnownSymbolNames.CliFxCommandOptionAttribute
|
|
||||||
);
|
|
||||||
|
|
||||||
if (optionAttribute is not null)
|
|
||||||
{
|
|
||||||
inputs.Add(CommandOptionSymbol.FromSymbol(property, optionAttribute));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CommandSymbol(
|
|
||||||
TypeDescriptor.FromSymbol(symbol),
|
|
||||||
attribute.ConstructorArguments.FirstOrDefault().Value as string,
|
|
||||||
attribute.GetNamedArgumentValue("Description", default(string)),
|
|
||||||
inputs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace CliFx.SourceGeneration.SemanticModel;
|
|
||||||
|
|
||||||
internal partial class PropertyDescriptor(TypeDescriptor type, string name)
|
|
||||||
{
|
|
||||||
public TypeDescriptor Type { get; } = type;
|
|
||||||
|
|
||||||
public string Name { get; } = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class PropertyDescriptor : IEquatable<PropertyDescriptor>
|
|
||||||
{
|
|
||||||
public bool Equals(PropertyDescriptor? other)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(null, other))
|
|
||||||
return false;
|
|
||||||
if (ReferenceEquals(this, other))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return Type.Equals(other.Type) && Name == other.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(null, obj))
|
|
||||||
return false;
|
|
||||||
if (ReferenceEquals(this, obj))
|
|
||||||
return true;
|
|
||||||
if (obj.GetType() != GetType())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return Equals((PropertyDescriptor)obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode() => HashCode.Combine(Type, Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class PropertyDescriptor
|
|
||||||
{
|
|
||||||
public static PropertyDescriptor FromSymbol(IPropertySymbol symbol) =>
|
|
||||||
new(TypeDescriptor.FromSymbol(symbol.Type), symbol.Name);
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using System;
|
|
||||||
using CliFx.SourceGeneration.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace CliFx.SourceGeneration.SemanticModel;
|
|
||||||
|
|
||||||
internal partial class TypeDescriptor(string fullyQualifiedName)
|
|
||||||
{
|
|
||||||
public string FullyQualifiedName { get; } = fullyQualifiedName;
|
|
||||||
|
|
||||||
public string Namespace { get; } = fullyQualifiedName.SubstringUntilLast(".");
|
|
||||||
|
|
||||||
public string Name { get; } = fullyQualifiedName.SubstringAfterLast(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class TypeDescriptor : IEquatable<TypeDescriptor>
|
|
||||||
{
|
|
||||||
public bool Equals(TypeDescriptor? other)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(null, other))
|
|
||||||
return false;
|
|
||||||
if (ReferenceEquals(this, other))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return FullyQualifiedName == other.FullyQualifiedName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(null, obj))
|
|
||||||
return false;
|
|
||||||
if (ReferenceEquals(this, obj))
|
|
||||||
return true;
|
|
||||||
if (obj.GetType() != GetType())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return Equals((TypeDescriptor)obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode() => FullyQualifiedName.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class TypeDescriptor
|
|
||||||
{
|
|
||||||
public static TypeDescriptor FromSymbol(ITypeSymbol symbol) =>
|
|
||||||
new(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CliFx.SourceGeneration.Utils.Extensions;
|
|
||||||
|
|
||||||
internal static class CollectionExtensions
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CliFx.SourceGeneration.Utils.Extensions;
|
|
||||||
|
|
||||||
internal static class GenericExtensions
|
|
||||||
{
|
|
||||||
public static TOut Pipe<TIn, TOut>(this TIn input, Func<TIn, TOut> transform) =>
|
|
||||||
transform(input);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace CliFx.SourceGeneration.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 T GetNamedArgumentValue<T>(
|
|
||||||
this AttributeData attribute,
|
|
||||||
string name,
|
|
||||||
T defaultValue = default
|
|
||||||
) =>
|
|
||||||
attribute.NamedArguments.FirstOrDefault(i => i.Key == name).Value.Value is T valueAsT
|
|
||||||
? valueAsT
|
|
||||||
: defaultValue;
|
|
||||||
|
|
||||||
public static IReadOnlyList<T> GetNamedArgumentValues<T>(
|
|
||||||
this AttributeData attribute,
|
|
||||||
string name
|
|
||||||
)
|
|
||||||
where T : class =>
|
|
||||||
attribute.NamedArguments.FirstOrDefault(i => i.Key == name).Value.Values.CastArray<T>();
|
|
||||||
|
|
||||||
public static IncrementalValuesProvider<T> WhereNotNull<T>(
|
|
||||||
this IncrementalValuesProvider<T?> values
|
|
||||||
)
|
|
||||||
where T : class => values.Where(i => i is not null);
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CliFx.SourceGeneration.Utils.Extensions;
|
|
||||||
|
|
||||||
internal static class StringExtensions
|
|
||||||
{
|
|
||||||
public static string SubstringUntilLast(
|
|
||||||
this string str,
|
|
||||||
string sub,
|
|
||||||
StringComparison comparison = StringComparison.Ordinal
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var index = str.LastIndexOf(sub, comparison);
|
|
||||||
return index < 0 ? str : str[..index];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string SubstringAfterLast(
|
|
||||||
this string str,
|
|
||||||
string sub,
|
|
||||||
StringComparison comparison = StringComparison.Ordinal
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var index = str.LastIndexOf(sub, comparison);
|
|
||||||
return index >= 0 ? str.Substring(index + sub.Length, str.Length - index - sub.Length) : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string JoinToString<T>(this IEnumerable<T> source, string separator) =>
|
|
||||||
string.Join(separator, source);
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||||
<ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CliFx.Tests.Dummy;
|
namespace CliFx.Tests.Dummy;
|
||||||
@@ -12,7 +13,7 @@ public static class Program
|
|||||||
public static string FilePath { get; } =
|
public static string FilePath { get; } =
|
||||||
Path.ChangeExtension(
|
Path.ChangeExtension(
|
||||||
Assembly.GetExecutingAssembly().Location,
|
Assembly.GetExecutingAssembly().Location,
|
||||||
OperatingSystem.IsWindows() ? "exe" : null
|
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "exe" : null
|
||||||
);
|
);
|
||||||
|
|
||||||
public static async Task Main()
|
public static async Task Main()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public class ApplicationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutp
|
|||||||
.UseConsole(FakeConsole)
|
.UseConsole(FakeConsole)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var exitCode = await app.RunAsync([], new Dictionary<string, string>());
|
var exitCode = await app.RunAsync(Array.Empty<string>(), new Dictionary<string, string>());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
exitCode.Should().Be(0);
|
exitCode.Should().Be(0);
|
||||||
@@ -45,7 +45,7 @@ public class ApplicationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutp
|
|||||||
.UseTypeActivator(Activator.CreateInstance!)
|
.UseTypeActivator(Activator.CreateInstance!)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var exitCode = await app.RunAsync([], new Dictionary<string, string>());
|
var exitCode = await app.RunAsync(Array.Empty<string>(), new Dictionary<string, string>());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
exitCode.Should().Be(0);
|
exitCode.Should().Be(0);
|
||||||
@@ -60,7 +60,7 @@ public class ApplicationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutp
|
|||||||
.UseConsole(FakeConsole)
|
.UseConsole(FakeConsole)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var exitCode = await app.RunAsync([], new Dictionary<string, string>());
|
var exitCode = await app.RunAsync(Array.Empty<string>(), new Dictionary<string, string>());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
exitCode.Should().NotBe(0);
|
exitCode.Should().NotBe(0);
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ public class CancellationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOut
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
@@ -10,17 +10,17 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Basic.Reference.Assemblies.Net80" Version="1.8.0" />
|
<PackageReference Include="Basic.Reference.Assemblies.Net80" Version="1.8.0" />
|
||||||
<PackageReference Include="CliWrap" Version="3.7.1" />
|
<PackageReference Include="CliWrap" Version="3.8.2" />
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" PrivateAssets="all" />
|
<PackageReference Include="coverlet.collector" Version="6.0.4" PrivateAssets="all" />
|
||||||
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
||||||
<PackageReference Include="FluentAssertions" Version="8.0.1" />
|
<PackageReference Include="FluentAssertions" Version="8.2.0" />
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.4.1" PrivateAssets="all" />
|
<PackageReference Include="GitHubActionsTestLogger" Version="2.4.1" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
<PackageReference Include="PolyShim" Version="1.14.0" PrivateAssets="all" />
|
<PackageReference Include="PolyShim" Version="1.15.0" PrivateAssets="all" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" PrivateAssets="all" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ public class ConsoleSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ public class ConsoleSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
|||||||
FakeConsole.WriteInput("Hello world");
|
FakeConsole.WriteInput("Hello world");
|
||||||
|
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ public class ConsoleSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
|||||||
FakeConsole.EnqueueKey(new ConsoleKeyInfo('\0', ConsoleKey.Backspace, false, false, false));
|
FakeConsole.EnqueueKey(new ConsoleKeyInfo('\0', ConsoleKey.Backspace, false, false, false));
|
||||||
|
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Tests.Utils;
|
using CliFx.Tests.Utils;
|
||||||
@@ -89,7 +90,7 @@ public class EnvironmentSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutp
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string> { ["ENV_FOO"] = $"bar{Path.PathSeparator}baz" }
|
new Dictionary<string, string> { ["ENV_FOO"] = $"bar{Path.PathSeparator}baz" }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ public class EnvironmentSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutp
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string> { ["ENV_FOO"] = $"bar{Path.PathSeparator}baz" }
|
new Dictionary<string, string> { ["ENV_FOO"] = $"bar{Path.PathSeparator}baz" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Tests.Utils;
|
using CliFx.Tests.Utils;
|
||||||
using CliFx.Tests.Utils.Extensions;
|
using CliFx.Tests.Utils.Extensions;
|
||||||
@@ -33,7 +34,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -118,7 +119,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -193,7 +194,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Tests.Utils;
|
using CliFx.Tests.Utils;
|
||||||
using CliFx.Tests.Utils.Extensions;
|
using CliFx.Tests.Utils.Extensions;
|
||||||
@@ -21,7 +22,7 @@ public class HelpTextSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ public class HelpTextSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_request_the_help_text_by_running_the_application_with_the_help_option()
|
public async Task I_can_request_the_help_text_by_running_the_application_with_the_implicit_help_option()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var commandType = DynamicCommandBuilder.Compile(
|
var commandType = DynamicCommandBuilder.Compile(
|
||||||
@@ -64,7 +65,7 @@ public class HelpTextSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_request_the_help_text_by_running_the_application_with_the_help_option_even_if_the_default_command_is_not_defined()
|
public async Task I_can_request_the_help_text_by_running_the_application_with_the_implicit_help_option_even_if_the_default_command_is_not_defined()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var commandTypes = DynamicCommandBuilder.CompileMany(
|
var commandTypes = DynamicCommandBuilder.CompileMany(
|
||||||
@@ -101,7 +102,7 @@ public class HelpTextSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_request_the_help_text_for_a_specific_command_by_running_the_application_and_specifying_its_name_with_the_help_option()
|
public async Task I_can_request_the_help_text_for_a_specific_command_by_running_the_application_and_specifying_its_name_with_the_implicit_help_option()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var commandTypes = DynamicCommandBuilder.CompileMany(
|
var commandTypes = DynamicCommandBuilder.CompileMany(
|
||||||
@@ -146,7 +147,7 @@ public class HelpTextSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_request_the_help_text_for_a_specific_nested_command_by_running_the_application_and_specifying_its_name_with_the_help_option()
|
public async Task I_can_request_the_help_text_for_a_specific_nested_command_by_running_the_application_and_specifying_its_name_with_the_implicit_help_option()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var commandTypes = DynamicCommandBuilder.CompileMany(
|
var commandTypes = DynamicCommandBuilder.CompileMany(
|
||||||
@@ -475,7 +476,7 @@ public class HelpTextSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_request_the_help_text_to_see_the_help_and_version_options()
|
public async Task I_can_request_the_help_text_to_see_the_help_and_implicit_version_options()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var commandType = DynamicCommandBuilder.Compile(
|
var commandType = DynamicCommandBuilder.Compile(
|
||||||
@@ -514,7 +515,7 @@ public class HelpTextSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_request_the_help_text_on_a_named_command_to_see_the_help_option()
|
public async Task I_can_request_the_help_text_on_a_named_command_to_see_the_implicit_help_option()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var commandType = DynamicCommandBuilder.Compile(
|
var commandType = DynamicCommandBuilder.Compile(
|
||||||
@@ -973,7 +974,7 @@ public class HelpTextSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_request_the_version_text_by_running_the_application_with_the_version_option()
|
public async Task I_can_request_the_version_text_by_running_the_application_with_the_implicit_version_option()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var application = new CliApplicationBuilder()
|
var application = new CliApplicationBuilder()
|
||||||
@@ -991,4 +992,72 @@ public class HelpTextSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
|||||||
var stdOut = FakeConsole.ReadOutputString();
|
var stdOut = FakeConsole.ReadOutputString();
|
||||||
stdOut.Trim().Should().Be("v6.9");
|
stdOut.Trim().Should().Be("v6.9");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task I_cannot_request_the_help_text_by_running_the_application_with_the_implicit_help_option_if_there_is_an_option_with_the_same_identifier()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var commandType = DynamicCommandBuilder.Compile(
|
||||||
|
// lang=csharp
|
||||||
|
"""
|
||||||
|
[Command]
|
||||||
|
public class DefaultCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("help", 'h')]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
|
var application = new CliApplicationBuilder()
|
||||||
|
.AddCommand(commandType)
|
||||||
|
.UseConsole(FakeConsole)
|
||||||
|
.SetDescription("This will be in help text")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().Be(0);
|
||||||
|
|
||||||
|
var stdOut = FakeConsole.ReadOutputString();
|
||||||
|
stdOut.Should().NotContain("This will be in help text");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task I_cannot_request_the_version_text_by_running_the_application_with_the_implicit_version_option_if_there_is_an_option_with_the_same_identifier()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var commandType = DynamicCommandBuilder.Compile(
|
||||||
|
// lang=csharp
|
||||||
|
"""
|
||||||
|
[Command]
|
||||||
|
public class DefaultCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("version")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
|
var application = new CliApplicationBuilder()
|
||||||
|
.AddCommand(commandType)
|
||||||
|
.SetVersion("v6.9")
|
||||||
|
.UseConsole(FakeConsole)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(["--version"], new Dictionary<string, string>());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().Be(0);
|
||||||
|
|
||||||
|
var stdOut = FakeConsole.ReadOutputString();
|
||||||
|
stdOut.Trim().Should().NotBe("v6.9");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Tests.Utils;
|
using CliFx.Tests.Utils;
|
||||||
using CliFx.Tests.Utils.Extensions;
|
using CliFx.Tests.Utils.Extensions;
|
||||||
@@ -586,6 +587,86 @@ public class OptionBindingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOu
|
|||||||
stdOut.Trim().Should().Be("-13");
|
stdOut.Trim().Should().Be("-13");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task I_can_bind_an_option_to_a_property_with_the_same_identifier_as_the_implicit_help_option_and_get_the_correct_value()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var commandType = DynamicCommandBuilder.Compile(
|
||||||
|
// lang=csharp
|
||||||
|
"""
|
||||||
|
[Command]
|
||||||
|
public class Command : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("help", 'h')]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
|
{
|
||||||
|
console.WriteLine(Foo);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
|
var application = new CliApplicationBuilder()
|
||||||
|
.AddCommand(commandType)
|
||||||
|
.UseConsole(FakeConsole)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(
|
||||||
|
["--help", "me"],
|
||||||
|
new Dictionary<string, string>()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().Be(0);
|
||||||
|
|
||||||
|
var stdOut = FakeConsole.ReadOutputString();
|
||||||
|
stdOut.Trim().Should().Be("me");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task I_can_bind_an_option_to_a_property_with_the_same_identifier_as_the_implicit_version_option_and_get_the_correct_value()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var commandType = DynamicCommandBuilder.Compile(
|
||||||
|
// lang=csharp
|
||||||
|
"""
|
||||||
|
[Command]
|
||||||
|
public class Command : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("version")]
|
||||||
|
public string? Foo { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
|
{
|
||||||
|
console.WriteLine(Foo);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
|
var application = new CliApplicationBuilder()
|
||||||
|
.AddCommand(commandType)
|
||||||
|
.UseConsole(FakeConsole)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(
|
||||||
|
["--version", "1.2.0"],
|
||||||
|
new Dictionary<string, string>()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().Be(0);
|
||||||
|
|
||||||
|
var stdOut = FakeConsole.ReadOutputString();
|
||||||
|
stdOut.Trim().Should().Be("1.2.0");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_try_to_bind_a_required_option_to_a_property_and_get_an_error_if_the_user_does_not_provide_the_corresponding_argument()
|
public async Task I_can_try_to_bind_a_required_option_to_a_property_and_get_an_error_if_the_user_does_not_provide_the_corresponding_argument()
|
||||||
{
|
{
|
||||||
@@ -611,7 +692,7 @@ public class OptionBindingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOu
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Tests.Utils;
|
using CliFx.Tests.Utils;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
@@ -55,7 +56,7 @@ public class RoutingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
[],
|
Array.Empty<string>(),
|
||||||
new Dictionary<string, string>()
|
new Dictionary<string, string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
16
CliFx.sln
16
CliFx.sln
@@ -20,7 +20,9 @@ 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.SourceGeneration", "CliFx.SourceGeneration\CliFx.SourceGeneration.csproj", "{F8460D69-F8CF-405C-A6ED-BED02A21DB42}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Analyzers", "CliFx.Analyzers\CliFx.Analyzers.csproj", "{F8460D69-F8CF-405C-A6ED-BED02A21DB42}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Analyzers.Tests", "CliFx.Analyzers.Tests\CliFx.Analyzers.Tests.csproj", "{49878E75-2097-4C79-9151-B98A28FBB973}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -104,6 +106,18 @@ 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
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CliFx.Schema;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace CliFx;
|
namespace CliFx;
|
||||||
|
|
||||||
@@ -6,15 +7,15 @@ namespace CliFx;
|
|||||||
/// Configuration of an application.
|
/// Configuration of an application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ApplicationConfiguration(
|
public class ApplicationConfiguration(
|
||||||
ApplicationSchema schema,
|
IReadOnlyList<Type> commandTypes,
|
||||||
bool isDebugModeAllowed,
|
bool isDebugModeAllowed,
|
||||||
bool isPreviewModeAllowed
|
bool isPreviewModeAllowed
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Application schema.
|
/// Command types defined in the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApplicationSchema Schema { get; } = schema;
|
public IReadOnlyList<Type> CommandTypes { get; } = commandTypes;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether debug mode is allowed in the application.
|
/// Whether debug mode is allowed in the application.
|
||||||
|
|||||||
@@ -4,22 +4,29 @@ namespace CliFx.Attributes;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Annotates a type that defines a command.
|
/// Annotates a type that defines a command.
|
||||||
/// If the command is named, then the user must provide its name through the
|
|
||||||
/// command-line arguments in order to execute it.
|
|
||||||
/// If the command is not named, then it is treated as the application's
|
|
||||||
/// default command and is executed whenever the user does not provide a command name.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// Only one default command is allowed per application.
|
|
||||||
/// All commands registered in an application must have unique names (comparison IS NOT case-sensitive).
|
|
||||||
/// </remarks>
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
public class CommandAttribute(string? name = null) : Attribute
|
public sealed class CommandAttribute : Attribute
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandAttribute" />.
|
||||||
|
/// </summary>
|
||||||
|
public CommandAttribute(string name) => Name = name;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandAttribute" />.
|
||||||
|
/// </summary>
|
||||||
|
public CommandAttribute() { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command name.
|
/// Command name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Name { get; } = name;
|
/// <remarks>
|
||||||
|
/// Command can have no name, in which case it's treated as the application's default command.
|
||||||
|
/// Only one default command is allowed in an application.
|
||||||
|
/// All commands registered in an application must have unique names (comparison IS NOT case-sensitive).
|
||||||
|
/// </remarks>
|
||||||
|
public string? Name { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command description.
|
/// Command description.
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
namespace CliFx.Attributes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Binds a property to the help option of a command.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This attribute is applied automatically by the framework and should not need to be used explicitly.
|
|
||||||
/// </remarks>
|
|
||||||
public class CommandHelpOptionAttribute : CommandOptionAttribute
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes an instance of <see cref="CommandHelpOptionAttribute" />.
|
|
||||||
/// </summary>
|
|
||||||
public CommandHelpOptionAttribute()
|
|
||||||
: base("help", 'h')
|
|
||||||
{
|
|
||||||
Description = "Show help for this command.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using System;
|
|
||||||
using CliFx.Extensibility;
|
|
||||||
|
|
||||||
namespace CliFx.Attributes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Binds a property to a command-line input.
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
|
||||||
public abstract class CommandInputAttribute : Attribute
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Input description, as shown in the help text.
|
|
||||||
/// </summary>
|
|
||||||
public string? Description { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Custom converter used for mapping the raw command-line argument into
|
|
||||||
/// the type and shape expected by the underlying property.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Converter must derive from <see cref="BindingConverter{T}" />.
|
|
||||||
/// </remarks>
|
|
||||||
public Type? Converter { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Custom validators used for verifying the value of the underlying
|
|
||||||
/// property, after it has been set.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Validators must derive from <see cref="BindingValidator{T}" />.
|
|
||||||
/// </remarks>
|
|
||||||
public Type[] Validators { get; set; } = [];
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using CliFx.Extensibility;
|
||||||
|
|
||||||
namespace CliFx.Attributes;
|
namespace CliFx.Attributes;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Binds a property to a command option — a command-line input that is identified by a name and/or a short name.
|
/// Annotates a property that defines a command option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// All options in a command must have unique names (comparison IS NOT case-sensitive)
|
|
||||||
/// and short names (comparison IS case-sensitive).
|
|
||||||
/// </remarks>
|
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
public class CommandOptionAttribute : CommandInputAttribute
|
public sealed class CommandOptionAttribute : Attribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandOptionAttribute" />.
|
/// Initializes an instance of <see cref="CommandOptionAttribute" />.
|
||||||
@@ -42,16 +39,25 @@ public class CommandOptionAttribute : CommandInputAttribute
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Option name.
|
/// Option name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Must contain at least two characters and start with a letter.
|
||||||
|
/// Either <see cref="Name" /> or <see cref="ShortName" /> must be set.
|
||||||
|
/// All options in a command must have unique names (comparison IS NOT case-sensitive).
|
||||||
|
/// </remarks>
|
||||||
public string? Name { get; }
|
public string? Name { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Option short name.
|
/// Option short name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Either <see cref="Name" /> or <see cref="ShortName" /> must be set.
|
||||||
|
/// All options in a command must have unique short names (comparison IS case-sensitive).
|
||||||
|
/// </remarks>
|
||||||
public char? ShortName { get; }
|
public char? ShortName { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this option is required (default: <c>false</c>).
|
/// Whether this option is required (default: <c>false</c>).
|
||||||
/// If an option is required, the user will get an error when they don't set it.
|
/// If an option is required, the user will get an error if they don't set it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// You can use the <c>required</c> keyword on the property (introduced in C# 11) to implicitly
|
/// You can use the <c>required</c> keyword on the property (introduced in C# 11) to implicitly
|
||||||
@@ -64,4 +70,28 @@ public class CommandOptionAttribute : CommandInputAttribute
|
|||||||
/// has not been explicitly set through command-line arguments.
|
/// has not been explicitly set through command-line arguments.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? EnvironmentVariable { get; set; }
|
public string? EnvironmentVariable { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Option description.
|
||||||
|
/// This is shown to the user in the help text.
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom converter used for mapping the raw command-line argument into
|
||||||
|
/// a value expected by the underlying property.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Converter must derive from <see cref="BindingConverter{T}" />.
|
||||||
|
/// </remarks>
|
||||||
|
public Type? Converter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom validators used for verifying the value of the underlying
|
||||||
|
/// property, after it has been bound.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Validators must derive from <see cref="BindingValidator{T}" />.
|
||||||
|
/// </remarks>
|
||||||
|
public Type[] Validators { get; set; } = Array.Empty<Type>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,65 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using CliFx.Extensibility;
|
||||||
|
|
||||||
namespace CliFx.Attributes;
|
namespace CliFx.Attributes;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Binds a property to a command parameter — a command-line input that is identified by its relative position (order).
|
/// Annotates a property that defines a command parameter.
|
||||||
/// Higher order means that the parameter appears later, lower order means that it appears earlier.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// All parameters in a command must have unique order values.
|
|
||||||
/// If a parameter is bound to a property whose type is a sequence (i.e. implements <see cref="IEnumerable{T}"/>; except <see cref="string" />),
|
|
||||||
/// then it must have the highest order in the command.
|
|
||||||
/// Only one sequential parameter is allowed per command.
|
|
||||||
/// </remarks>
|
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
public class CommandParameterAttribute(int order) : CommandInputAttribute
|
public sealed class CommandParameterAttribute(int order) : Attribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parameter order.
|
/// Parameter order.
|
||||||
|
/// Higher order means the parameter appears later, lower order means it appears earlier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// All parameters in a command must have unique order.
|
||||||
|
/// Parameter whose type is a non-scalar (e.g. array), must always be the last in order.
|
||||||
|
/// Only one non-scalar parameter is allowed in a command.
|
||||||
|
/// </remarks>
|
||||||
public int Order { get; } = order;
|
public int Order { get; } = order;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this parameter is required (default: <c>true</c>).
|
/// Whether this parameter is required (default: <c>true</c>).
|
||||||
/// If a parameter is required, the user will get an error when they don't set it.
|
/// If a parameter is required, the user will get an error if they don't set it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Parameter marked as non-required must have the highest order in the command.
|
/// Parameter marked as non-required must always be the last in order.
|
||||||
/// Only one non-required parameter is allowed per command.
|
/// Only one non-required parameter is allowed in a command.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool IsRequired { get; set; } = true;
|
public bool IsRequired { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parameter name, as shown in the help text.
|
/// Parameter name.
|
||||||
|
/// This is shown to the user in the help text.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// If this isn't specified, parameter name is inferred from the property name.
|
/// If this isn't specified, parameter name is inferred from the property name.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parameter description.
|
||||||
|
/// This is shown to the user in the help text.
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom converter used for mapping the raw command-line argument into
|
||||||
|
/// a value expected by the underlying property.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Converter must derive from <see cref="BindingConverter{T}" />.
|
||||||
|
/// </remarks>
|
||||||
|
public Type? Converter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom validators used for verifying the value of the underlying
|
||||||
|
/// property, after it has been bound.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Validators must derive from <see cref="BindingValidator{T}" />.
|
||||||
|
/// </remarks>
|
||||||
|
public Type[] Validators { get; set; } = Array.Empty<Type>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
namespace CliFx.Attributes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Binds a property to the version option of a command.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This attribute is applied automatically by the framework and should not need to be used explicitly.
|
|
||||||
/// </remarks>
|
|
||||||
public class CommandVersionOptionAttribute : CommandOptionAttribute
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes an instance of <see cref="CommandVersionOptionAttribute" />.
|
|
||||||
/// </summary>
|
|
||||||
public CommandVersionOptionAttribute()
|
|
||||||
: base("version")
|
|
||||||
{
|
|
||||||
Description = "Show application version.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
|
|||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Formatting;
|
using CliFx.Formatting;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using CliFx.Parsing;
|
using CliFx.Input;
|
||||||
using CliFx.Schema;
|
using CliFx.Schema;
|
||||||
using CliFx.Utils;
|
using CliFx.Utils;
|
||||||
using CliFx.Utils.Extensions;
|
using CliFx.Utils.Extensions;
|
||||||
@@ -23,6 +23,8 @@ public class CliApplication(
|
|||||||
ITypeActivator typeActivator
|
ITypeActivator typeActivator
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
private readonly CommandBinder _commandBinder = new(typeActivator);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Application metadata.
|
/// Application metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -33,11 +35,21 @@ public class CliApplication(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ApplicationConfiguration Configuration { get; } = configuration;
|
public ApplicationConfiguration Configuration { get; } = configuration;
|
||||||
|
|
||||||
private bool IsDebugModeEnabled(CommandArguments commandArguments) =>
|
private bool IsDebugModeEnabled(CommandInput commandInput) =>
|
||||||
Configuration.IsDebugModeAllowed && commandArguments.IsDebugDirectiveSpecified;
|
Configuration.IsDebugModeAllowed && commandInput.IsDebugDirectiveSpecified;
|
||||||
|
|
||||||
private bool IsPreviewModeEnabled(CommandArguments commandArguments) =>
|
private bool IsPreviewModeEnabled(CommandInput commandInput) =>
|
||||||
Configuration.IsPreviewModeAllowed && commandArguments.IsPreviewDirectiveSpecified;
|
Configuration.IsPreviewModeAllowed && commandInput.IsPreviewDirectiveSpecified;
|
||||||
|
|
||||||
|
private bool ShouldShowHelpText(CommandSchema commandSchema, CommandInput commandInput) =>
|
||||||
|
commandSchema.IsImplicitHelpOptionAvailable && commandInput.IsHelpOptionSpecified
|
||||||
|
||
|
||||||
|
// Show help text also if the fallback default command is executed without any arguments
|
||||||
|
commandSchema == FallbackDefaultCommand.Schema
|
||||||
|
&& !commandInput.HasArguments;
|
||||||
|
|
||||||
|
private bool ShouldShowVersionText(CommandSchema commandSchema, CommandInput commandInput) =>
|
||||||
|
commandSchema.IsImplicitVersionOptionAvailable && commandInput.IsVersionOptionSpecified;
|
||||||
|
|
||||||
private async ValueTask PromptDebuggerAsync()
|
private async ValueTask PromptDebuggerAsync()
|
||||||
{
|
{
|
||||||
@@ -57,8 +69,7 @@ public class CliApplication(
|
|||||||
|
|
||||||
private async ValueTask<int> RunAsync(
|
private async ValueTask<int> RunAsync(
|
||||||
ApplicationSchema applicationSchema,
|
ApplicationSchema applicationSchema,
|
||||||
CommandArguments commandArguments,
|
CommandInput commandInput
|
||||||
IReadOnlyDictionary<string, string?> environmentVariables
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// Console colors may have already been overridden by the parent process,
|
// Console colors may have already been overridden by the parent process,
|
||||||
@@ -66,26 +77,26 @@ public class CliApplication(
|
|||||||
console.ResetColor();
|
console.ResetColor();
|
||||||
|
|
||||||
// Handle the debug directive
|
// Handle the debug directive
|
||||||
if (IsDebugModeEnabled(commandArguments))
|
if (IsDebugModeEnabled(commandInput))
|
||||||
{
|
{
|
||||||
await PromptDebuggerAsync();
|
await PromptDebuggerAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the preview directive
|
// Handle the preview directive
|
||||||
if (IsPreviewModeEnabled(commandArguments))
|
if (IsPreviewModeEnabled(commandInput))
|
||||||
{
|
{
|
||||||
console.WriteCommandInput(commandArguments);
|
console.WriteCommandInput(commandInput);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get the command schema that matches the input
|
// Try to get the command schema that matches the input
|
||||||
var command =
|
var commandSchema =
|
||||||
(
|
(
|
||||||
!string.IsNullOrWhiteSpace(commandArguments.CommandName)
|
!string.IsNullOrWhiteSpace(commandInput.CommandName)
|
||||||
// If the command name is specified, try to find the command by name.
|
// If the command name is specified, try to find the command by name.
|
||||||
// This should always succeed, because the input parsing relies on
|
// This should always succeed, because the input parsing relies on
|
||||||
// the list of available command names.
|
// the list of available command names.
|
||||||
? applicationSchema.TryFindCommand(commandArguments.CommandName)
|
? applicationSchema.TryFindCommand(commandInput.CommandName)
|
||||||
// Otherwise, try to find the default command
|
// Otherwise, try to find the default command
|
||||||
: applicationSchema.TryFindDefaultCommand()
|
: applicationSchema.TryFindDefaultCommand()
|
||||||
)
|
)
|
||||||
@@ -96,48 +107,42 @@ public class CliApplication(
|
|||||||
|
|
||||||
// Initialize an instance of the command type
|
// Initialize an instance of the command type
|
||||||
var commandInstance =
|
var commandInstance =
|
||||||
command == FallbackDefaultCommand.Schema
|
commandSchema == FallbackDefaultCommand.Schema
|
||||||
? new FallbackDefaultCommand() // bypass the activator
|
? new FallbackDefaultCommand() // bypass the activator
|
||||||
: typeActivator.CreateInstance<ICommand>(command.Type);
|
: typeActivator.CreateInstance<ICommand>(commandSchema.Type);
|
||||||
|
|
||||||
// Assemble the help context
|
// Assemble the help context
|
||||||
var helpContext = new HelpContext(
|
var helpContext = new HelpContext(
|
||||||
Metadata,
|
Metadata,
|
||||||
applicationSchema,
|
applicationSchema,
|
||||||
command,
|
commandSchema,
|
||||||
command.GetValues(commandInstance)
|
commandSchema.GetValues(commandInstance)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handle the help option
|
||||||
|
if (ShouldShowHelpText(commandSchema, commandInput))
|
||||||
|
{
|
||||||
|
console.WriteHelpText(helpContext);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the version option
|
||||||
|
if (ShouldShowVersionText(commandSchema, commandInput))
|
||||||
|
{
|
||||||
|
console.WriteLine(Metadata.Version);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Starting from this point, we may produce exceptions that are meant for the
|
// Starting from this point, we may produce exceptions that are meant for the
|
||||||
// end-user of the application (i.e. invalid input, command exception, etc).
|
// end-user of the application (i.e. invalid input, command exception, etc).
|
||||||
// Catch these exceptions here, print them to the console, and don't let them
|
// Catch these exceptions here, print them to the console, and don't let them
|
||||||
// propagate further.
|
// propagate further.
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Activate the command instance with the provided user input
|
// Bind and execute the command
|
||||||
command.Activate(commandInstance, commandArguments, environmentVariables);
|
_commandBinder.Bind(commandInput, commandSchema, commandInstance);
|
||||||
|
|
||||||
// Handle the version option
|
|
||||||
if (commandInstance is ICommandWithVersionOption { IsVersionRequested: true })
|
|
||||||
{
|
|
||||||
console.WriteLine(Metadata.Version);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the help option
|
|
||||||
if (
|
|
||||||
commandInstance
|
|
||||||
is ICommandWithHelpOption { IsHelpRequested: true }
|
|
||||||
// Fallback default command always shows help, even if the option is not specified
|
|
||||||
or FallbackDefaultCommand
|
|
||||||
)
|
|
||||||
{
|
|
||||||
console.WriteHelpText(helpContext);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the command
|
|
||||||
await commandInstance.ExecuteAsync(console);
|
await commandInstance.ExecuteAsync(console);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
catch (CliFxException ex)
|
catch (CliFxException ex)
|
||||||
@@ -164,19 +169,20 @@ public class CliApplication(
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public async ValueTask<int> RunAsync(
|
public async ValueTask<int> RunAsync(
|
||||||
IReadOnlyList<string> commandLineArguments,
|
IReadOnlyList<string> commandLineArguments,
|
||||||
IReadOnlyDictionary<string, string?> environmentVariables
|
IReadOnlyDictionary<string, string> environmentVariables
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await RunAsync(
|
var applicationSchema = ApplicationSchema.Resolve(Configuration.CommandTypes);
|
||||||
Configuration.Schema,
|
|
||||||
CommandArguments.Parse(
|
var commandInput = CommandInput.Parse(
|
||||||
commandLineArguments,
|
commandLineArguments,
|
||||||
Configuration.Schema.GetCommandNames()
|
environmentVariables,
|
||||||
),
|
applicationSchema.GetCommandNames()
|
||||||
environmentVariables
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return await RunAsync(applicationSchema, commandInput);
|
||||||
}
|
}
|
||||||
// To prevent the app from showing the annoying troubleshooting dialog on Windows,
|
// To prevent the app from showing the annoying troubleshooting dialog on Windows,
|
||||||
// we handle all exceptions ourselves and print them to the console.
|
// we handle all exceptions ourselves and print them to the console.
|
||||||
@@ -204,7 +210,7 @@ public class CliApplication(
|
|||||||
commandLineArguments,
|
commandLineArguments,
|
||||||
Environment
|
Environment
|
||||||
.GetEnvironmentVariables()
|
.GetEnvironmentVariables()
|
||||||
.ToDictionary<string, string?>(StringComparer.Ordinal)
|
.ToDictionary<string, string>(StringComparer.Ordinal)
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using CliFx.Attributes;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using CliFx.Schema;
|
using CliFx.Schema;
|
||||||
using CliFx.Utils;
|
using CliFx.Utils;
|
||||||
@@ -15,7 +16,7 @@ namespace CliFx;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class CliApplicationBuilder
|
public partial class CliApplicationBuilder
|
||||||
{
|
{
|
||||||
private readonly HashSet<CommandSchema> _commands = [];
|
private readonly HashSet<Type> _commandTypes = [];
|
||||||
|
|
||||||
private bool _isDebugModeAllowed = true;
|
private bool _isDebugModeAllowed = true;
|
||||||
private bool _isPreviewModeAllowed = true;
|
private bool _isPreviewModeAllowed = true;
|
||||||
@@ -26,30 +27,74 @@ public partial class CliApplicationBuilder
|
|||||||
private IConsole? _console;
|
private IConsole? _console;
|
||||||
private ITypeActivator? _typeActivator;
|
private ITypeActivator? _typeActivator;
|
||||||
|
|
||||||
// TODO:
|
/// <summary>
|
||||||
// The source generator should generate an internal extension method for the builder called
|
/// Adds a command to the application.
|
||||||
// AddCommandsFromThisAssembly() that would add all command types from the assembly where the builder is used.
|
/// </summary>
|
||||||
|
public CliApplicationBuilder AddCommand(Type commandType)
|
||||||
|
{
|
||||||
|
_commandTypes.Add(commandType);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a command to the application.
|
/// Adds a command to the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CliApplicationBuilder AddCommand(CommandSchema command)
|
public CliApplicationBuilder AddCommand<TCommand>()
|
||||||
{
|
where TCommand : ICommand => AddCommand(typeof(TCommand));
|
||||||
_commands.Add(command);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds multiple commands to the application.
|
/// Adds multiple commands to the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CliApplicationBuilder AddCommands(IReadOnlyList<CommandSchema> commands)
|
public CliApplicationBuilder AddCommands(IEnumerable<Type> commandTypes)
|
||||||
{
|
{
|
||||||
foreach (var command in commands)
|
foreach (var commandType in commandTypes)
|
||||||
AddCommand(command);
|
AddCommand(commandType);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds commands from the specified assembly to the application.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method looks for public non-abstract classes that implement <see cref="ICommand" />
|
||||||
|
/// and are annotated by <see cref="CommandAttribute" />.
|
||||||
|
/// </remarks>
|
||||||
|
public CliApplicationBuilder AddCommandsFrom(Assembly commandAssembly)
|
||||||
|
{
|
||||||
|
foreach (
|
||||||
|
var commandType in commandAssembly.ExportedTypes.Where(CommandSchema.IsCommandType)
|
||||||
|
)
|
||||||
|
AddCommand(commandType);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds commands from the specified assemblies to the application.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method looks for public non-abstract classes that implement <see cref="ICommand" />
|
||||||
|
/// and are annotated by <see cref="CommandAttribute" />.
|
||||||
|
/// </remarks>
|
||||||
|
public CliApplicationBuilder AddCommandsFrom(IEnumerable<Assembly> commandAssemblies)
|
||||||
|
{
|
||||||
|
foreach (var commandAssembly in commandAssemblies)
|
||||||
|
AddCommandsFrom(commandAssembly);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds commands from the calling assembly to the application.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method looks for public non-abstract classes that implement <see cref="ICommand" />
|
||||||
|
/// and are annotated by <see cref="CommandAttribute" />.
|
||||||
|
/// </remarks>
|
||||||
|
public CliApplicationBuilder AddCommandsFromThisAssembly() =>
|
||||||
|
AddCommandsFrom(Assembly.GetCallingAssembly());
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
||||||
@@ -144,6 +189,15 @@ public partial class CliApplicationBuilder
|
|||||||
// Null returns are handled by DelegateTypeActivator
|
// Null returns are handled by DelegateTypeActivator
|
||||||
UseTypeActivator(serviceProvider.GetService!);
|
UseTypeActivator(serviceProvider.GetService!);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the application to use the specified service provider for activating types.
|
||||||
|
/// This method takes a delegate that receives the list of all added command types, so that you can
|
||||||
|
/// easily register them with the service provider.
|
||||||
|
/// </summary>
|
||||||
|
public CliApplicationBuilder UseTypeActivator(
|
||||||
|
Func<IReadOnlyList<Type>, IServiceProvider> getServiceProvider
|
||||||
|
) => UseTypeActivator(getServiceProvider(_commandTypes.ToArray()));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a configured instance of <see cref="CliApplication" />.
|
/// Creates a configured instance of <see cref="CliApplication" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -157,7 +211,7 @@ public partial class CliApplicationBuilder
|
|||||||
);
|
);
|
||||||
|
|
||||||
var configuration = new ApplicationConfiguration(
|
var configuration = new ApplicationConfiguration(
|
||||||
new ApplicationSchema(_commands.ToArray()),
|
_commandTypes.ToArray(),
|
||||||
_isDebugModeAllowed,
|
_isDebugModeAllowed,
|
||||||
_isPreviewModeAllowed
|
_isPreviewModeAllowed
|
||||||
);
|
);
|
||||||
@@ -187,17 +241,15 @@ public partial class CliApplicationBuilder
|
|||||||
return entryAssemblyName;
|
return entryAssemblyName;
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnconditionalSuppressMessage(
|
|
||||||
"SingleFile",
|
|
||||||
"IL3000:Avoid accessing Assembly file path when publishing as a single file",
|
|
||||||
Justification = "The file path is checked to ensure the assembly location is available."
|
|
||||||
)]
|
|
||||||
private static string GetDefaultExecutableName()
|
private static string GetDefaultExecutableName()
|
||||||
{
|
{
|
||||||
|
var entryAssemblyFilePath = EnvironmentEx.EntryAssembly?.Location;
|
||||||
var processFilePath = EnvironmentEx.ProcessPath;
|
var processFilePath = EnvironmentEx.ProcessPath;
|
||||||
|
|
||||||
// Process file path should generally always be available
|
if (
|
||||||
if (string.IsNullOrWhiteSpace(processFilePath))
|
string.IsNullOrWhiteSpace(entryAssemblyFilePath)
|
||||||
|
|| string.IsNullOrWhiteSpace(processFilePath)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Failed to infer the default application executable name. "
|
"Failed to infer the default application executable name. "
|
||||||
@@ -205,22 +257,15 @@ public partial class CliApplicationBuilder
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var entryAssemblyFilePath = EnvironmentEx.EntryAssembly?.Location;
|
// If the process path matches the entry assembly path, it's a legacy .NET Framework app
|
||||||
|
// or a self-contained .NET Core app.
|
||||||
// Single file application: entry assembly is not on disk and doesn't have a file path
|
|
||||||
if (string.IsNullOrWhiteSpace(entryAssemblyFilePath))
|
|
||||||
{
|
|
||||||
return Path.GetFileNameWithoutExtension(processFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy .NET Framework application: entry assembly has the same file path as the process
|
|
||||||
if (PathEx.AreEqual(entryAssemblyFilePath, processFilePath))
|
if (PathEx.AreEqual(entryAssemblyFilePath, processFilePath))
|
||||||
{
|
{
|
||||||
return Path.GetFileNameWithoutExtension(entryAssemblyFilePath);
|
return Path.GetFileNameWithoutExtension(entryAssemblyFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// .NET Core application launched through the native application host:
|
// If the process path has the same name and parent directory as the entry assembly path,
|
||||||
// entry assembly has the same file path as the process, but with a different extension.
|
// but different extension, it's a framework-dependent .NET Core app launched through the apphost.
|
||||||
if (
|
if (
|
||||||
PathEx.AreEqual(Path.ChangeExtension(entryAssemblyFilePath, "exe"), processFilePath)
|
PathEx.AreEqual(Path.ChangeExtension(entryAssemblyFilePath, "exe"), processFilePath)
|
||||||
|| PathEx.AreEqual(
|
|| PathEx.AreEqual(
|
||||||
@@ -232,7 +277,7 @@ public partial class CliApplicationBuilder
|
|||||||
return Path.GetFileNameWithoutExtension(entryAssemblyFilePath);
|
return Path.GetFileNameWithoutExtension(entryAssemblyFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// .NET Core application launched through the .NET CLI
|
// Otherwise, it's a framework-dependent .NET Core app launched through the .NET CLI
|
||||||
return "dotnet " + Path.GetFileName(entryAssemblyFilePath);
|
return "dotnet " + Path.GetFileName(entryAssemblyFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0</TargetFrameworks>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
<IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">true</IsTrimmable>
|
|
||||||
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@@ -25,13 +23,23 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all" />
|
||||||
<PackageReference Include="PolyShim" Version="1.14.0" PrivateAssets="all" />
|
<PackageReference Include="PolyShim" Version="1.15.0" PrivateAssets="all" />
|
||||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.6.0" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
|
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.6.0" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Embed the analyzer inside the package -->
|
<!-- Embed the analyzer inside the package -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
<ProjectReference Include="../CliFx.Analyzers/CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||||
|
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/CliFx.Analyzers.deps.json" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||||
|
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/CliFx.Analyzers.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||||
|
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Buffers.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||||
|
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Collections.Immutable.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||||
|
<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>
|
||||||
395
CliFx/CommandBinder.cs
Normal file
395
CliFx/CommandBinder.cs
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using CliFx.Exceptions;
|
||||||
|
using CliFx.Extensibility;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
|
using CliFx.Input;
|
||||||
|
using CliFx.Schema;
|
||||||
|
using CliFx.Utils.Extensions;
|
||||||
|
|
||||||
|
namespace CliFx;
|
||||||
|
|
||||||
|
internal class CommandBinder(ITypeActivator typeActivator)
|
||||||
|
{
|
||||||
|
private readonly IFormatProvider _formatProvider = CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
|
private object? ConvertSingle(IMemberSchema memberSchema, string? rawValue, Type targetType)
|
||||||
|
{
|
||||||
|
// Custom converter
|
||||||
|
if (memberSchema.ConverterType is not null)
|
||||||
|
{
|
||||||
|
var converter = typeActivator.CreateInstance<IBindingConverter>(
|
||||||
|
memberSchema.ConverterType
|
||||||
|
);
|
||||||
|
return converter.Convert(rawValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assignable from a string (e.g. string itself, object, etc)
|
||||||
|
if (targetType.IsAssignableFrom(typeof(string)))
|
||||||
|
{
|
||||||
|
return rawValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case for bool
|
||||||
|
if (targetType == typeof(bool))
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(rawValue) || bool.Parse(rawValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case for DateTimeOffset
|
||||||
|
if (targetType == typeof(DateTimeOffset))
|
||||||
|
{
|
||||||
|
// Null reference exception will be handled upstream
|
||||||
|
return DateTimeOffset.Parse(rawValue!, _formatProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case for TimeSpan
|
||||||
|
if (targetType == typeof(TimeSpan))
|
||||||
|
{
|
||||||
|
// Null reference exception will be handled upstream
|
||||||
|
return TimeSpan.Parse(rawValue!, _formatProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enum
|
||||||
|
if (targetType.IsEnum)
|
||||||
|
{
|
||||||
|
// Null reference exception will be handled upstream
|
||||||
|
return Enum.Parse(targetType, rawValue!, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertible primitives (int, double, char, etc)
|
||||||
|
if (targetType.Implements(typeof(IConvertible)))
|
||||||
|
{
|
||||||
|
return Convert.ChangeType(rawValue, targetType, _formatProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nullable<T>
|
||||||
|
var nullableUnderlyingType = targetType.TryGetNullableUnderlyingType();
|
||||||
|
if (nullableUnderlyingType is not null)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(rawValue)
|
||||||
|
? ConvertSingle(memberSchema, rawValue, nullableUnderlyingType)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// String-constructable (FileInfo, etc)
|
||||||
|
var stringConstructor = targetType.GetConstructor([typeof(string)]);
|
||||||
|
if (stringConstructor is not null)
|
||||||
|
{
|
||||||
|
return stringConstructor.Invoke([rawValue]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// String-parseable (with IFormatProvider)
|
||||||
|
var parseMethodWithFormatProvider = targetType.TryGetStaticParseMethod(true);
|
||||||
|
if (parseMethodWithFormatProvider is not null)
|
||||||
|
{
|
||||||
|
return parseMethodWithFormatProvider.Invoke(null, [rawValue, _formatProvider]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// String-parseable (without IFormatProvider)
|
||||||
|
var parseMethod = targetType.TryGetStaticParseMethod();
|
||||||
|
if (parseMethod is not null)
|
||||||
|
{
|
||||||
|
return parseMethod.Invoke(null, [rawValue]);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw CliFxException.InternalError(
|
||||||
|
$"""
|
||||||
|
{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has an unsupported underlying property type.
|
||||||
|
There is no known way to convert a string value into an instance of type `{targetType.FullName}`.
|
||||||
|
To fix this, either change the property to use a supported type or configure a custom converter.
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private object? ConvertMultiple(
|
||||||
|
IMemberSchema memberSchema,
|
||||||
|
IReadOnlyList<string> rawValues,
|
||||||
|
Type targetEnumerableType,
|
||||||
|
Type targetElementType
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var array = rawValues
|
||||||
|
.Select(v => ConvertSingle(memberSchema, v, targetElementType))
|
||||||
|
.ToNonGenericArray(targetElementType);
|
||||||
|
|
||||||
|
var arrayType = array.GetType();
|
||||||
|
|
||||||
|
// Assignable from an array (T[], IReadOnlyList<T>, etc)
|
||||||
|
if (targetEnumerableType.IsAssignableFrom(arrayType))
|
||||||
|
{
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array-constructable (List<T>, HashSet<T>, etc)
|
||||||
|
var arrayConstructor = targetEnumerableType.GetConstructor([arrayType]);
|
||||||
|
if (arrayConstructor is not null)
|
||||||
|
{
|
||||||
|
return arrayConstructor.Invoke([array]);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw CliFxException.InternalError(
|
||||||
|
$"""
|
||||||
|
{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has an unsupported underlying property type.
|
||||||
|
There is no known way to convert an array of `{targetElementType.FullName}` into an instance of type `{targetEnumerableType.FullName}`.
|
||||||
|
To fix this, change the property to use a type which can be assigned from an array or a type which has a constructor that accepts an array.
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private object? ConvertMember(IMemberSchema memberSchema, IReadOnlyList<string> rawValues)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Non-scalar
|
||||||
|
var enumerableUnderlyingType =
|
||||||
|
memberSchema.Property.Type.TryGetEnumerableUnderlyingType();
|
||||||
|
|
||||||
|
if (
|
||||||
|
enumerableUnderlyingType is not null
|
||||||
|
&& memberSchema.Property.Type != typeof(string)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return ConvertMultiple(
|
||||||
|
memberSchema,
|
||||||
|
rawValues,
|
||||||
|
memberSchema.Property.Type,
|
||||||
|
enumerableUnderlyingType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar
|
||||||
|
if (rawValues.Count <= 1)
|
||||||
|
{
|
||||||
|
return ConvertSingle(
|
||||||
|
memberSchema,
|
||||||
|
rawValues.SingleOrDefault(),
|
||||||
|
memberSchema.Property.Type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not CliFxException) // don't wrap CliFxException
|
||||||
|
{
|
||||||
|
// We use reflection-based invocation which can throw TargetInvocationException.
|
||||||
|
// Unwrap those exceptions to provide a more user-friendly error message.
|
||||||
|
var errorMessage = ex is TargetInvocationException invokeEx
|
||||||
|
? invokeEx.InnerException?.Message ?? invokeEx.Message
|
||||||
|
: ex.Message;
|
||||||
|
|
||||||
|
throw CliFxException.UserError(
|
||||||
|
$"""
|
||||||
|
{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} cannot be set from the provided argument(s):
|
||||||
|
{rawValues.Select(v => '<' + v + '>').JoinToString(" ")}
|
||||||
|
Error: {errorMessage}
|
||||||
|
""",
|
||||||
|
ex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mismatch (scalar but too many values)
|
||||||
|
throw CliFxException.UserError(
|
||||||
|
$"""
|
||||||
|
{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} expects a single argument, but provided with multiple:
|
||||||
|
{rawValues.Select(v => '<' + v + '>').JoinToString(" ")}
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateMember(IMemberSchema memberSchema, object? convertedValue)
|
||||||
|
{
|
||||||
|
var errors = new List<BindingValidationError>();
|
||||||
|
|
||||||
|
foreach (var validatorType in memberSchema.ValidatorTypes)
|
||||||
|
{
|
||||||
|
var validator = typeActivator.CreateInstance<IBindingValidator>(validatorType);
|
||||||
|
var error = validator.Validate(convertedValue);
|
||||||
|
|
||||||
|
if (error is not null)
|
||||||
|
errors.Add(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.Any())
|
||||||
|
{
|
||||||
|
throw CliFxException.UserError(
|
||||||
|
$"""
|
||||||
|
{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has been provided with an invalid value.
|
||||||
|
Error(s):
|
||||||
|
{errors.Select(e => "- " + e.Message).JoinToString(Environment.NewLine)}
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BindMember(
|
||||||
|
IMemberSchema memberSchema,
|
||||||
|
ICommand commandInstance,
|
||||||
|
IReadOnlyList<string> rawValues
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var convertedValue = ConvertMember(memberSchema, rawValues);
|
||||||
|
ValidateMember(memberSchema, convertedValue);
|
||||||
|
|
||||||
|
memberSchema.Property.SetValue(commandInstance, convertedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BindParameters(
|
||||||
|
CommandInput commandInput,
|
||||||
|
CommandSchema commandSchema,
|
||||||
|
ICommand commandInstance
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Ensure there are no unexpected parameters and that all parameters are provided
|
||||||
|
var remainingParameterInputs = commandInput.Parameters.ToList();
|
||||||
|
var remainingRequiredParameterSchemas = commandSchema
|
||||||
|
.Parameters.Where(p => p.IsRequired)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var position = 0;
|
||||||
|
|
||||||
|
foreach (var parameterSchema in commandSchema.Parameters.OrderBy(p => p.Order))
|
||||||
|
{
|
||||||
|
// Break when there are no remaining inputs
|
||||||
|
if (position >= commandInput.Parameters.Count)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Scalar: take one input at the current position
|
||||||
|
if (parameterSchema.Property.IsScalar())
|
||||||
|
{
|
||||||
|
var parameterInput = commandInput.Parameters[position];
|
||||||
|
BindMember(parameterSchema, commandInstance, [parameterInput.Value]);
|
||||||
|
|
||||||
|
position++;
|
||||||
|
remainingParameterInputs.Remove(parameterInput);
|
||||||
|
}
|
||||||
|
// Non-scalar: take all remaining inputs starting from the current position
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var parameterInputs = commandInput.Parameters.Skip(position).ToArray();
|
||||||
|
|
||||||
|
BindMember(
|
||||||
|
parameterSchema,
|
||||||
|
commandInstance,
|
||||||
|
parameterInputs.Select(p => p.Value).ToArray()
|
||||||
|
);
|
||||||
|
|
||||||
|
position += parameterInputs.Length;
|
||||||
|
remainingParameterInputs.RemoveRange(parameterInputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingRequiredParameterSchemas.Remove(parameterSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingParameterInputs.Any())
|
||||||
|
{
|
||||||
|
throw CliFxException.UserError(
|
||||||
|
$"""
|
||||||
|
Unexpected parameter(s):
|
||||||
|
{remainingParameterInputs.Select(p => p.GetFormattedIdentifier()).JoinToString(" ")}
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingRequiredParameterSchemas.Any())
|
||||||
|
{
|
||||||
|
throw CliFxException.UserError(
|
||||||
|
$"""
|
||||||
|
Missing required parameter(s):
|
||||||
|
{remainingRequiredParameterSchemas
|
||||||
|
.Select(p => p.GetFormattedIdentifier())
|
||||||
|
.JoinToString(" ")}
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BindOptions(
|
||||||
|
CommandInput commandInput,
|
||||||
|
CommandSchema commandSchema,
|
||||||
|
ICommand commandInstance
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Ensure there are no unrecognized options and that all required options are set
|
||||||
|
var remainingOptionInputs = commandInput.Options.ToList();
|
||||||
|
var remainingRequiredOptionSchemas = commandSchema
|
||||||
|
.Options.Where(o => o.IsRequired)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var optionSchema in commandSchema.Options)
|
||||||
|
{
|
||||||
|
var optionInputs = commandInput
|
||||||
|
.Options.Where(o => optionSchema.MatchesIdentifier(o.Identifier))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var environmentVariableInput = commandInput.EnvironmentVariables.FirstOrDefault(e =>
|
||||||
|
optionSchema.MatchesEnvironmentVariable(e.Name)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Direct input
|
||||||
|
if (optionInputs.Any())
|
||||||
|
{
|
||||||
|
var rawValues = optionInputs.SelectMany(o => o.Values).ToArray();
|
||||||
|
|
||||||
|
BindMember(optionSchema, commandInstance, rawValues);
|
||||||
|
|
||||||
|
// Required options need at least one value to be set
|
||||||
|
if (rawValues.Any())
|
||||||
|
remainingRequiredOptionSchemas.Remove(optionSchema);
|
||||||
|
}
|
||||||
|
// Environment variable
|
||||||
|
else if (environmentVariableInput is not null)
|
||||||
|
{
|
||||||
|
var rawValues = optionSchema.Property.IsScalar()
|
||||||
|
? [environmentVariableInput.Value]
|
||||||
|
: environmentVariableInput.SplitValues();
|
||||||
|
|
||||||
|
BindMember(optionSchema, commandInstance, rawValues);
|
||||||
|
|
||||||
|
// Required options need at least one value to be set
|
||||||
|
if (rawValues.Any())
|
||||||
|
remainingRequiredOptionSchemas.Remove(optionSchema);
|
||||||
|
}
|
||||||
|
// No input, skip
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingOptionInputs.RemoveRange(optionInputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingOptionInputs.Any())
|
||||||
|
{
|
||||||
|
throw CliFxException.UserError(
|
||||||
|
$"""
|
||||||
|
Unrecognized option(s):
|
||||||
|
{remainingOptionInputs.Select(o => o.GetFormattedIdentifier()).JoinToString(", ")}
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingRequiredOptionSchemas.Any())
|
||||||
|
{
|
||||||
|
throw CliFxException.UserError(
|
||||||
|
$"""
|
||||||
|
Missing required option(s):
|
||||||
|
{remainingRequiredOptionSchemas
|
||||||
|
.Select(o => o.GetFormattedIdentifier())
|
||||||
|
.JoinToString(", ")}
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Bind(
|
||||||
|
CommandInput commandInput,
|
||||||
|
CommandSchema commandSchema,
|
||||||
|
ICommand commandInstance
|
||||||
|
)
|
||||||
|
{
|
||||||
|
BindParameters(commandInput, commandSchema, commandInstance);
|
||||||
|
BindOptions(commandInput, commandSchema, commandInstance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace CliFx.Exceptions;
|
namespace CliFx.Exceptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exception thrown within <see cref="CliFx" />.
|
/// Exception thrown when there is an error during application execution.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class CliFxException(
|
public partial class CliFxException(
|
||||||
string message,
|
string message,
|
||||||
@@ -40,7 +40,7 @@ public partial class CliFxException
|
|||||||
Exception? innerException = null
|
Exception? innerException = null
|
||||||
) => new(message, DefaultExitCode, false, innerException);
|
) => new(message, DefaultExitCode, false, innerException);
|
||||||
|
|
||||||
// User errors are typically caused by invalid input and are meant for the end-user,
|
// User errors are typically caused by invalid input and they're meant for the end-user,
|
||||||
// so we want to show help.
|
// so we want to show help.
|
||||||
internal static CliFxException UserError(string message, Exception? innerException = null) =>
|
internal static CliFxException UserError(string message, Exception? innerException = null) =>
|
||||||
new(message, DefaultExitCode, true, innerException);
|
new(message, DefaultExitCode, true, innerException);
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
using System;
|
namespace CliFx.Extensibility;
|
||||||
|
|
||||||
namespace CliFx.Extensibility;
|
// Used internally to simplify the usage from reflection
|
||||||
|
internal interface IBindingConverter
|
||||||
|
{
|
||||||
|
object? Convert(string? rawValue);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines custom conversion logic for activating command inputs from the corresponding raw command-line arguments.
|
/// Base type for custom converters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BindingConverter<T> : IBindingConverter
|
public abstract class BindingConverter<T> : IBindingConverter
|
||||||
{
|
{
|
||||||
/// <inheritdoc cref="IBindingConverter.Convert" />
|
/// <summary>
|
||||||
public abstract T? Convert(string? rawValue, IFormatProvider? formatProvider);
|
/// Parses value from a raw command-line argument.
|
||||||
|
/// </summary>
|
||||||
|
public abstract T Convert(string? rawValue);
|
||||||
|
|
||||||
object? IBindingConverter.Convert(string? rawValue, IFormatProvider? formatProvider) =>
|
object? IBindingConverter.Convert(string? rawValue) => Convert(rawValue);
|
||||||
Convert(rawValue, formatProvider);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
namespace CliFx.Extensibility;
|
namespace CliFx.Extensibility;
|
||||||
|
|
||||||
|
// Used internally to simplify the usage from reflection
|
||||||
|
internal interface IBindingValidator
|
||||||
|
{
|
||||||
|
BindingValidationError? Validate(object? value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines custom validation logic for activated command inputs.
|
/// Base type for custom validators.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BindingValidator<T> : IBindingValidator
|
public abstract class BindingValidator<T> : IBindingValidator
|
||||||
{
|
{
|
||||||
@@ -15,7 +21,10 @@ public abstract class BindingValidator<T> : IBindingValidator
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected BindingValidationError Error(string message) => new(message);
|
protected BindingValidationError Error(string message) => new(message);
|
||||||
|
|
||||||
/// <inheritdoc cref="IBindingValidator.Validate" />
|
/// <summary>
|
||||||
|
/// Validates the value bound to a parameter or an option.
|
||||||
|
/// Returns null if validation is successful, or an error in case of failure.
|
||||||
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// You can use the utility methods <see cref="Ok" /> and <see cref="Error" /> to
|
/// You can use the utility methods <see cref="Ok" /> and <see cref="Error" /> to
|
||||||
/// create an appropriate result.
|
/// create an appropriate result.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user