mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
32 Commits
master
...
25083eff44
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25083eff44 | ||
|
|
432c8a66af | ||
|
|
0fa2ebc636 | ||
|
|
357426c536 | ||
|
|
20481d4e24 | ||
|
|
2cb9335e25 | ||
|
|
40beb283d5 | ||
|
|
71fe231f28 | ||
|
|
8546c54c23 | ||
|
|
0fc88a42ba | ||
|
|
cb8f4b122e | ||
|
|
540f307f42 | ||
|
|
a62ce71424 | ||
|
|
0532d724a1 | ||
|
|
545c7c3fbd | ||
|
|
a813436577 | ||
|
|
fcc93603a7 | ||
|
|
2d3c221b48 | ||
|
|
651146c97b | ||
|
|
82b0c6fd98 | ||
|
|
a4376c955b | ||
|
|
f7645afbdb | ||
|
|
e20672328b | ||
|
|
3e7eb08eca | ||
|
|
cfd28c133e | ||
|
|
034d3cec66 | ||
|
|
3fc7054f80 | ||
|
|
2323a57c39 | ||
|
|
24fd87b1e1 | ||
|
|
cad1c14474 | ||
|
|
57db910489 | ||
|
|
ae9c4e6d1e |
@@ -1,28 +0,0 @@
|
|||||||
<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.3" />
|
|
||||||
<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.7.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
|
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" PrivateAssets="all" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" />
|
|
||||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class CommandMustBeAnnotatedAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } = new CommandMustBeAnnotatedAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_command_is_not_annotated_with_the_command_attribute()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_command_is_annotated_with_the_command_attribute()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public abstract class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_command_is_implemented_as_an_abstract_class()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public abstract class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_class_that_is_not_a_command()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class Foo
|
|
||||||
{
|
|
||||||
public int Bar { get; init; } = 5;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class CommandMustImplementInterfaceAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new CommandMustImplementInterfaceAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_command_does_not_implement_ICommand_interface()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand
|
|
||||||
{
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_command_implements_ICommand_interface()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_class_that_is_not_a_command()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class Foo
|
|
||||||
{
|
|
||||||
public int Bar { get; init; } = 5;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using FluentAssertions;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class GeneralSpecs
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void All_analyzers_have_unique_diagnostic_IDs()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var analyzers = typeof(AnalyzerBase)
|
|
||||||
.Assembly.GetTypes()
|
|
||||||
.Where(t => !t.IsAbstract && t.IsAssignableTo(typeof(DiagnosticAnalyzer)))
|
|
||||||
.Select(t => (DiagnosticAnalyzer)Activator.CreateInstance(t)!)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var diagnosticIds = analyzers
|
|
||||||
.SelectMany(a => a.SupportedDiagnostics.Select(d => d.Id))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
diagnosticIds.Should().OnlyHaveUniqueItems();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class OptionMustBeInsideCommandAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustBeInsideCommandAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_an_option_is_inside_a_class_that_is_not_a_command()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyClass
|
|
||||||
{
|
|
||||||
[CommandOption("foo")]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_is_inside_a_command()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo")]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_is_inside_an_abstract_class()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public abstract class MyCommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo")]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class OptionMustBeRequiredIfPropertyRequiredAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new OptionMustBeRequiredIfPropertyRequiredAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_non_required_option_is_bound_to_a_required_property()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption('f', IsRequired = false)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_required_option_is_bound_to_a_required_property()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption('f')]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_non_required_option_is_bound_to_a_non_required_property()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption('f', IsRequired = false)]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_required_option_is_bound_to_a_non_required_property()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption('f')]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class OptionMustHaveNameOrShortNameAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new OptionMustHaveNameOrShortNameAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_an_option_does_not_have_a_name_or_short_name()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption(null)]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_has_a_name()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo")]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_has_a_short_name()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption('f')]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class OptionMustHaveUniqueNameAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveUniqueNameAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_an_option_has_the_same_name_as_another_option()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo")]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandOption("foo")]
|
|
||||||
public string? Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_has_a_unique_name()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo")]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandOption("bar")]
|
|
||||||
public string? Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_name()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption('f')]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class OptionMustHaveUniqueShortNameAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new OptionMustHaveUniqueShortNameAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_an_option_has_the_same_short_name_as_another_option()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption('f')]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandOption('f')]
|
|
||||||
public string? Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_has_a_unique_short_name()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption('f')]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandOption('b')]
|
|
||||||
public string? Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_has_a_short_name_which_is_unique_only_in_casing()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption('f')]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandOption('F')]
|
|
||||||
public string? Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_short_name()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo")]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class OptionMustHaveValidConverterAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new OptionMustHaveValidConverterAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_an_option_has_a_converter_that_does_not_derive_from_BindingConverter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyConverter
|
|
||||||
{
|
|
||||||
public string Convert(string? rawValue) => rawValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo", Converter = typeof(MyConverter))]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_an_option_has_a_converter_that_does_not_derive_from_a_compatible_BindingConverter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyConverter : BindingConverter<int>
|
|
||||||
{
|
|
||||||
public override int Convert(string? rawValue) => 42;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo", Converter = typeof(MyConverter))]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_has_a_converter_that_derives_from_a_compatible_BindingConverter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyConverter : BindingConverter<string>
|
|
||||||
{
|
|
||||||
public override string Convert(string? rawValue) => rawValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo", Converter = typeof(MyConverter))]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_nullable_option_has_a_converter_that_derives_from_a_compatible_BindingConverter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyConverter : BindingConverter<int>
|
|
||||||
{
|
|
||||||
public override int Convert(string? rawValue) => 42;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo", Converter = typeof(MyConverter))]
|
|
||||||
public int? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_non_scalar_option_has_a_converter_that_derives_from_a_compatible_BindingConverter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyConverter : BindingConverter<string>
|
|
||||||
{
|
|
||||||
public override string Convert(string? rawValue) => rawValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo", Converter = typeof(MyConverter))]
|
|
||||||
public IReadOnlyList<string>? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_converter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo")]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class OptionMustHaveValidNameAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidNameAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_an_option_has_a_name_which_is_too_short()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("f")]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_an_option_has_a_name_that_starts_with_a_non_letter_character()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("1foo")]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_has_a_valid_name()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo")]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_name()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption('f')]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class OptionMustHaveValidShortNameAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new OptionMustHaveValidShortNameAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_an_option_has_a_short_name_which_is_not_a_letter_character()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption('1')]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_has_a_valid_short_name()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption('f')]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_a_short_name()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo")]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class OptionMustHaveValidValidatorsAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new OptionMustHaveValidValidatorsAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_an_option_has_a_validator_that_does_not_derive_from_BindingValidator()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyValidator
|
|
||||||
{
|
|
||||||
public void Validate(string value) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo", Validators = new[] { typeof(MyValidator) })]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_an_option_has_a_validator_that_does_not_derive_from_a_compatible_BindingValidator()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyValidator : BindingValidator<int>
|
|
||||||
{
|
|
||||||
public override BindingValidationError Validate(int value) => Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo", Validators = new[] { typeof(MyValidator) })]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_has_validators_that_all_derive_from_compatible_BindingValidators()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyValidator : BindingValidator<string>
|
|
||||||
{
|
|
||||||
public override BindingValidationError Validate(string value) => Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo", Validators = new[] { typeof(MyValidator) })]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_an_option_does_not_have_validators()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo")]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_an_option()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class ParameterMustBeInsideCommandAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new ParameterMustBeInsideCommandAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_parameter_is_inside_a_class_that_is_not_a_command()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyClass
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_parameter_is_inside_a_command()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_parameter_is_inside_an_abstract_class()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public abstract class MyCommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class ParameterMustBeLastIfNonRequiredAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new ParameterMustBeLastIfNonRequiredAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_non_required_parameter_is_not_the_last_in_order()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, IsRequired = false)]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1)]
|
|
||||||
public required string Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_non_required_parameter_is_the_last_in_order()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1, IsRequired = false)]
|
|
||||||
public string? Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_no_non_required_parameters_are_defined()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1)]
|
|
||||||
public required string Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class ParameterMustBeLastIfNonScalarAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new ParameterMustBeLastIfNonScalarAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_non_scalar_parameter_is_not_the_last_in_order()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string[] Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1)]
|
|
||||||
public required string Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_is_the_last_in_order()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1)]
|
|
||||||
public required string[] Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_no_non_scalar_parameters_are_defined()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1)]
|
|
||||||
public required string Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class ParameterMustBeRequiredIfPropertyRequiredAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new ParameterMustBeRequiredIfPropertyRequiredAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_non_required_parameter_is_bound_to_a_required_property()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, IsRequired = false)]
|
|
||||||
public required string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_required_parameter_is_bound_to_a_required_property()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_non_required_parameter_is_bound_to_a_non_required_property()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, IsRequired = false)]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_required_parameter_is_bound_to_a_non_required_property()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class ParameterMustBeSingleIfNonRequiredAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new ParameterMustBeSingleIfNonRequiredAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_more_than_one_non_required_parameters_are_defined()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, IsRequired = false)]
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1, IsRequired = false)]
|
|
||||||
public string? Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_only_one_non_required_parameter_is_defined()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1, IsRequired = false)]
|
|
||||||
public string? Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_no_non_required_parameters_are_defined()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1)]
|
|
||||||
public required string Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class ParameterMustBeSingleIfNonScalarAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new ParameterMustBeSingleIfNonScalarAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_more_than_one_non_scalar_parameters_are_defined()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string[] Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1)]
|
|
||||||
public required string[] Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_only_one_non_scalar_parameter_is_defined()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1)]
|
|
||||||
public required string[] Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_no_non_scalar_parameters_are_defined()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1)]
|
|
||||||
public required string Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class ParameterMustHaveUniqueNameAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveUniqueNameAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_parameter_has_the_same_name_as_another_parameter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, Name = "foo")]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1, Name = "foo")]
|
|
||||||
public required string Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_parameter_has_unique_name()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, Name = "foo")]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1, Name = "bar")]
|
|
||||||
public required string Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class ParameterMustHaveUniqueOrderAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new ParameterMustHaveUniqueOrderAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_parameter_has_the_same_order_as_another_parameter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_parameter_has_unique_order()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
[CommandParameter(1)]
|
|
||||||
public required string Bar { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class ParameterMustHaveValidConverterAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new ParameterMustHaveValidConverterAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_parameter_has_a_converter_that_does_not_derive_from_BindingConverter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyConverter
|
|
||||||
{
|
|
||||||
public string Convert(string? rawValue) => rawValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, Converter = typeof(MyConverter))]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_parameter_has_a_converter_that_does_not_derive_from_a_compatible_BindingConverter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyConverter : BindingConverter<int>
|
|
||||||
{
|
|
||||||
public override int Convert(string? rawValue) => 42;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, Converter = typeof(MyConverter))]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyConverter : BindingConverter<string>
|
|
||||||
{
|
|
||||||
public override string Convert(string? rawValue) => rawValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, Converter = typeof(MyConverter))]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_nullable_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyConverter : BindingConverter<int>
|
|
||||||
{
|
|
||||||
public override int Convert(string? rawValue) => 42;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("foo", Converter = typeof(MyConverter))]
|
|
||||||
public int? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyConverter : BindingConverter<string>
|
|
||||||
{
|
|
||||||
public override string Convert(string? rawValue) => rawValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, Converter = typeof(MyConverter))]
|
|
||||||
public required IReadOnlyList<string> Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_parameter_does_not_have_a_converter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class ParameterMustHaveValidValidatorsAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new ParameterMustHaveValidValidatorsAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_a_parameter_has_a_validator_that_does_not_derive_from_BindingValidator()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyValidator
|
|
||||||
{
|
|
||||||
public void Validate(string value) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, Validators = new[] { typeof(MyValidator) })]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_parameter_has_a_validator_that_does_not_derive_from_a_compatible_BindingValidator()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyValidator : BindingValidator<int>
|
|
||||||
{
|
|
||||||
public override BindingValidationError Validate(int value) => Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, Validators = new[] { typeof(MyValidator) })]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_parameter_has_validators_that_all_derive_from_compatible_BindingValidators()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
public class MyValidator : BindingValidator<string>
|
|
||||||
{
|
|
||||||
public override BindingValidationError Validate(string value) => Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, Validators = new[] { typeof(MyValidator) })]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_parameter_does_not_have_validators()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0)]
|
|
||||||
public required string Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public string? Foo { get; init; }
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
using CliFx.Analyzers.Tests.Utils;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests;
|
|
||||||
|
|
||||||
public class SystemConsoleShouldBeAvoidedAnalyzerSpecs
|
|
||||||
{
|
|
||||||
private static DiagnosticAnalyzer Analyzer { get; } =
|
|
||||||
new SystemConsoleShouldBeAvoidedAnalyzer();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_command_calls_a_method_on_SystemConsole()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Hello world");
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_command_accesses_a_property_on_SystemConsole()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
Console.ForegroundColor = ConsoleColor.Black;
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_reports_an_error_if_a_command_calls_a_method_on_a_property_of_SystemConsole()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("Hello world");
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().ProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_command_interacts_with_the_console_through_IConsole()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
console.WriteLine("Hello world");
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_IConsole_is_not_available_in_the_current_method()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public void SomeOtherMethod() => Console.WriteLine("Test");
|
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Analyzer_does_not_report_an_error_if_a_command_does_not_access_SystemConsole()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// lang=csharp
|
|
||||||
const string code = """
|
|
||||||
[Command]
|
|
||||||
public class MyCommand : ICommand
|
|
||||||
{
|
|
||||||
public ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using Basic.Reference.Assemblies;
|
|
||||||
using FluentAssertions.Execution;
|
|
||||||
using FluentAssertions.Primitives;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Microsoft.CodeAnalysis.Text;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Tests.Utils;
|
|
||||||
|
|
||||||
internal class AnalyzerAssertions(DiagnosticAnalyzer analyzer, 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());
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
|
||||||
"methodDisplayOptions": "all",
|
|
||||||
"methodDisplay": "method"
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using System.Collections.Immutable;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
public abstract class AnalyzerBase : DiagnosticAnalyzer
|
|
||||||
{
|
|
||||||
public DiagnosticDescriptor SupportedDiagnostic { get; }
|
|
||||||
|
|
||||||
public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
|
|
||||||
|
|
||||||
protected AnalyzerBase(
|
|
||||||
string diagnosticTitle,
|
|
||||||
string diagnosticMessage,
|
|
||||||
DiagnosticSeverity diagnosticSeverity = DiagnosticSeverity.Error
|
|
||||||
)
|
|
||||||
{
|
|
||||||
SupportedDiagnostic = new DiagnosticDescriptor(
|
|
||||||
"CliFx_" + GetType().Name.TrimEnd("Analyzer"),
|
|
||||||
diagnosticTitle,
|
|
||||||
diagnosticMessage,
|
|
||||||
"CliFx",
|
|
||||||
diagnosticSeverity,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
SupportedDiagnostics = ImmutableArray.Create(SupportedDiagnostic);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Diagnostic CreateDiagnostic(Location location, params object?[]? messageArgs) =>
|
|
||||||
Diagnostic.Create(SupportedDiagnostic, location, messageArgs);
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
context.EnableConcurrentExecution();
|
|
||||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class CommandMustBeAnnotatedAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
$"Commands must be annotated with `{SymbolNames.CliFxCommandAttribute}`",
|
|
||||||
$"This type must be annotated with `{SymbolNames.CliFxCommandAttribute}` in order to be a valid command."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
ClassDeclarationSyntax classDeclaration,
|
|
||||||
ITypeSymbol type
|
|
||||||
)
|
|
||||||
{
|
|
||||||
// Ignore abstract classes, because they may be used to define
|
|
||||||
// base implementations for commands, in which case the command
|
|
||||||
// attribute doesn't make sense.
|
|
||||||
if (type.IsAbstract)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var implementsCommandInterface = type.AllInterfaces.Any(i =>
|
|
||||||
i.DisplayNameMatches(SymbolNames.CliFxCommandInterface)
|
|
||||||
);
|
|
||||||
|
|
||||||
var hasCommandAttribute = type.GetAttributes()
|
|
||||||
.Select(a => a.AttributeClass)
|
|
||||||
.Any(c => c.DisplayNameMatches(SymbolNames.CliFxCommandAttribute));
|
|
||||||
|
|
||||||
// If the interface is implemented, but the attribute is missing,
|
|
||||||
// then it's very likely a user error.
|
|
||||||
if (implementsCommandInterface && !hasCommandAttribute)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(CreateDiagnostic(classDeclaration.Identifier.GetLocation()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandleClassDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class CommandMustImplementInterfaceAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
$"Commands must implement `{SymbolNames.CliFxCommandInterface}` interface",
|
|
||||||
$"This type must implement `{SymbolNames.CliFxCommandInterface}` interface in order to be a valid command."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
ClassDeclarationSyntax classDeclaration,
|
|
||||||
ITypeSymbol type
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var hasCommandAttribute = type.GetAttributes()
|
|
||||||
.Select(a => a.AttributeClass)
|
|
||||||
.Any(c => c.DisplayNameMatches(SymbolNames.CliFxCommandAttribute));
|
|
||||||
|
|
||||||
var implementsCommandInterface = type.AllInterfaces.Any(i =>
|
|
||||||
i.DisplayNameMatches(SymbolNames.CliFxCommandInterface)
|
|
||||||
);
|
|
||||||
|
|
||||||
// If the attribute is present, but the interface is not implemented,
|
|
||||||
// it's very likely a user error.
|
|
||||||
if (hasCommandAttribute && !implementsCommandInterface)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(CreateDiagnostic(classDeclaration.Identifier.GetLocation()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandleClassDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.ObjectModel;
|
|
||||||
|
|
||||||
internal partial class CommandOptionSymbol(
|
|
||||||
IPropertySymbol property,
|
|
||||||
string? name,
|
|
||||||
char? shortName,
|
|
||||||
bool? isRequired,
|
|
||||||
ITypeSymbol? converterType,
|
|
||||||
IReadOnlyList<ITypeSymbol> validatorTypes
|
|
||||||
) : ICommandMemberSymbol
|
|
||||||
{
|
|
||||||
public IPropertySymbol Property { get; } = property;
|
|
||||||
|
|
||||||
public string? Name { get; } = name;
|
|
||||||
|
|
||||||
public char? ShortName { get; } = shortName;
|
|
||||||
|
|
||||||
public bool? IsRequired { get; } = isRequired;
|
|
||||||
|
|
||||||
public ITypeSymbol? ConverterType { get; } = converterType;
|
|
||||||
|
|
||||||
public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; } = validatorTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class CommandOptionSymbol
|
|
||||||
{
|
|
||||||
private static AttributeData? TryGetOptionAttribute(IPropertySymbol property) =>
|
|
||||||
property
|
|
||||||
.GetAttributes()
|
|
||||||
.FirstOrDefault(a =>
|
|
||||||
a.AttributeClass?.DisplayNameMatches(SymbolNames.CliFxCommandOptionAttribute)
|
|
||||||
== true
|
|
||||||
);
|
|
||||||
|
|
||||||
public static CommandOptionSymbol? TryResolve(IPropertySymbol property)
|
|
||||||
{
|
|
||||||
var attribute = TryGetOptionAttribute(property);
|
|
||||||
if (attribute is null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var name =
|
|
||||||
attribute
|
|
||||||
.ConstructorArguments.Where(a => a.Type?.SpecialType == SpecialType.System_String)
|
|
||||||
.Select(a => a.Value)
|
|
||||||
.FirstOrDefault() as string;
|
|
||||||
|
|
||||||
var shortName =
|
|
||||||
attribute
|
|
||||||
.ConstructorArguments.Where(a => a.Type?.SpecialType == SpecialType.System_Char)
|
|
||||||
.Select(a => a.Value)
|
|
||||||
.FirstOrDefault() as char?;
|
|
||||||
|
|
||||||
var isRequired =
|
|
||||||
attribute
|
|
||||||
.NamedArguments.Where(a => a.Key == "IsRequired")
|
|
||||||
.Select(a => a.Value.Value)
|
|
||||||
.FirstOrDefault() as bool?;
|
|
||||||
|
|
||||||
var converter = attribute
|
|
||||||
.NamedArguments.Where(a => a.Key == "Converter")
|
|
||||||
.Select(a => a.Value.Value)
|
|
||||||
.Cast<ITypeSymbol?>()
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
var validators = attribute
|
|
||||||
.NamedArguments.Where(a => a.Key == "Validators")
|
|
||||||
.SelectMany(a => a.Value.Values)
|
|
||||||
.Select(c => c.Value)
|
|
||||||
.Cast<ITypeSymbol>()
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new CommandOptionSymbol(
|
|
||||||
property,
|
|
||||||
name,
|
|
||||||
shortName,
|
|
||||||
isRequired,
|
|
||||||
converter,
|
|
||||||
validators
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsOptionProperty(IPropertySymbol property) =>
|
|
||||||
TryGetOptionAttribute(property) is not null;
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.ObjectModel;
|
|
||||||
|
|
||||||
internal partial class CommandParameterSymbol(
|
|
||||||
IPropertySymbol property,
|
|
||||||
int order,
|
|
||||||
string? name,
|
|
||||||
bool? isRequired,
|
|
||||||
ITypeSymbol? converterType,
|
|
||||||
IReadOnlyList<ITypeSymbol> validatorTypes
|
|
||||||
) : ICommandMemberSymbol
|
|
||||||
{
|
|
||||||
public IPropertySymbol Property { get; } = property;
|
|
||||||
|
|
||||||
public int Order { get; } = order;
|
|
||||||
|
|
||||||
public string? Name { get; } = name;
|
|
||||||
|
|
||||||
public bool? IsRequired { get; } = isRequired;
|
|
||||||
|
|
||||||
public ITypeSymbol? ConverterType { get; } = converterType;
|
|
||||||
|
|
||||||
public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; } = validatorTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class CommandParameterSymbol
|
|
||||||
{
|
|
||||||
private static AttributeData? TryGetParameterAttribute(IPropertySymbol property) =>
|
|
||||||
property
|
|
||||||
.GetAttributes()
|
|
||||||
.FirstOrDefault(a =>
|
|
||||||
a.AttributeClass?.DisplayNameMatches(SymbolNames.CliFxCommandParameterAttribute)
|
|
||||||
== true
|
|
||||||
);
|
|
||||||
|
|
||||||
public static CommandParameterSymbol? TryResolve(IPropertySymbol property)
|
|
||||||
{
|
|
||||||
var attribute = TryGetParameterAttribute(property);
|
|
||||||
if (attribute is null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var order = (int)attribute.ConstructorArguments.Select(a => a.Value).First()!;
|
|
||||||
|
|
||||||
var name =
|
|
||||||
attribute
|
|
||||||
.NamedArguments.Where(a => a.Key == "Name")
|
|
||||||
.Select(a => a.Value.Value)
|
|
||||||
.FirstOrDefault() as string;
|
|
||||||
|
|
||||||
var isRequired =
|
|
||||||
attribute
|
|
||||||
.NamedArguments.Where(a => a.Key == "IsRequired")
|
|
||||||
.Select(a => a.Value.Value)
|
|
||||||
.FirstOrDefault() as bool?;
|
|
||||||
|
|
||||||
var converter = attribute
|
|
||||||
.NamedArguments.Where(a => a.Key == "Converter")
|
|
||||||
.Select(a => a.Value.Value)
|
|
||||||
.Cast<ITypeSymbol?>()
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
var validators = attribute
|
|
||||||
.NamedArguments.Where(a => a.Key == "Validators")
|
|
||||||
.SelectMany(a => a.Value.Values)
|
|
||||||
.Select(c => c.Value)
|
|
||||||
.Cast<ITypeSymbol>()
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new CommandParameterSymbol(property, order, name, isRequired, converter, validators);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsParameterProperty(IPropertySymbol property) =>
|
|
||||||
TryGetParameterAttribute(property) is not null;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.ObjectModel;
|
|
||||||
|
|
||||||
internal interface ICommandMemberSymbol
|
|
||||||
{
|
|
||||||
IPropertySymbol Property { get; }
|
|
||||||
|
|
||||||
ITypeSymbol? ConverterType { get; }
|
|
||||||
|
|
||||||
IReadOnlyList<ITypeSymbol> ValidatorTypes { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static class CommandMemberSymbolExtensions
|
|
||||||
{
|
|
||||||
public static bool IsScalar(this ICommandMemberSymbol member) =>
|
|
||||||
member.Property.Type.SpecialType == SpecialType.System_String
|
|
||||||
|| member.Property.Type.TryGetEnumerableUnderlyingType() is null;
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class OptionMustBeInsideCommandAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Options must be defined inside commands",
|
|
||||||
$"This option must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (property.ContainingType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (property.ContainingType.IsAbstract)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!CommandOptionSymbol.IsOptionProperty(property))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var isInsideCommand = property.ContainingType.AllInterfaces.Any(i =>
|
|
||||||
i.DisplayNameMatches(SymbolNames.CliFxCommandInterface)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isInsideCommand)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class OptionMustBeRequiredIfPropertyRequiredAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Options bound to required properties cannot be marked as non-required",
|
|
||||||
"This option cannot be marked as non-required because it's bound to a required property."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (property.ContainingType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!property.IsRequired())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var option = CommandOptionSymbol.TryResolve(property);
|
|
||||||
if (option is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (option.IsRequired != false)
|
|
||||||
return;
|
|
||||||
|
|
||||||
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.Identifier.GetLocation()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class OptionMustHaveNameOrShortNameAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Options must have either a name or short name specified",
|
|
||||||
"This option must have either a name or short name specified."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var option = CommandOptionSymbol.TryResolve(property);
|
|
||||||
if (option is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(option.Name) && option.ShortName is null)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class OptionMustHaveUniqueNameAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Options must have unique names",
|
|
||||||
"This option's name must be unique within the command (comparison IS NOT case sensitive). "
|
|
||||||
+ "Specified name: `{0}`. "
|
|
||||||
+ "Property bound to another option with the same name: `{1}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (property.ContainingType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var option = CommandOptionSymbol.TryResolve(property);
|
|
||||||
if (option is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(option.Name))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var otherProperties = property
|
|
||||||
.ContainingType.GetMembers()
|
|
||||||
.OfType<IPropertySymbol>()
|
|
||||||
.Where(m => !m.Equals(property))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
foreach (var otherProperty in otherProperties)
|
|
||||||
{
|
|
||||||
var otherOption = CommandOptionSymbol.TryResolve(otherProperty);
|
|
||||||
if (otherOption is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(otherOption.Name))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (string.Equals(option.Name, otherOption.Name, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(
|
|
||||||
propertyDeclaration.Identifier.GetLocation(),
|
|
||||||
option.Name,
|
|
||||||
otherProperty.Name
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class OptionMustHaveUniqueShortNameAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Options must have unique short names",
|
|
||||||
"This option's short name must be unique within the command (comparison IS case sensitive). "
|
|
||||||
+ "Specified short name: `{0}` "
|
|
||||||
+ "Property bound to another option with the same short name: `{1}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (property.ContainingType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var option = CommandOptionSymbol.TryResolve(property);
|
|
||||||
if (option is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (option.ShortName is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var otherProperties = property
|
|
||||||
.ContainingType.GetMembers()
|
|
||||||
.OfType<IPropertySymbol>()
|
|
||||||
.Where(m => !m.Equals(property))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
foreach (var otherProperty in otherProperties)
|
|
||||||
{
|
|
||||||
var otherOption = CommandOptionSymbol.TryResolve(otherProperty);
|
|
||||||
if (otherOption is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (otherOption.ShortName is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (option.ShortName == otherOption.ShortName)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(
|
|
||||||
propertyDeclaration.Identifier.GetLocation(),
|
|
||||||
option.ShortName,
|
|
||||||
otherProperty.Name
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class OptionMustHaveValidConverterAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
$"Option converters must derive from `{SymbolNames.CliFxBindingConverterClass}`",
|
|
||||||
$"Converter specified for this option must derive from a compatible `{SymbolNames.CliFxBindingConverterClass}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var option = CommandOptionSymbol.TryResolve(property);
|
|
||||||
if (option is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (option.ConverterType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var converterValueType = option
|
|
||||||
.ConverterType.GetBaseTypes()
|
|
||||||
.FirstOrDefault(t =>
|
|
||||||
t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingConverterClass)
|
|
||||||
)
|
|
||||||
?.TypeArguments.FirstOrDefault();
|
|
||||||
|
|
||||||
// Value returned by the converter must be assignable to the property type
|
|
||||||
var isCompatible =
|
|
||||||
converterValueType is not null
|
|
||||||
&& (
|
|
||||||
option.IsScalar()
|
|
||||||
// Scalar
|
|
||||||
? context.Compilation.IsAssignable(converterValueType, property.Type)
|
|
||||||
// Non-scalar (assume we can handle all IEnumerable types for simplicity)
|
|
||||||
: property.Type.TryGetEnumerableUnderlyingType() is { } enumerableUnderlyingType
|
|
||||||
&& context.Compilation.IsAssignable(
|
|
||||||
converterValueType,
|
|
||||||
enumerableUnderlyingType
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isCompatible)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class OptionMustHaveValidNameAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Options must have valid names",
|
|
||||||
"This option's name must be at least 2 characters long and must start with a letter. "
|
|
||||||
+ "Specified name: `{0}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var option = CommandOptionSymbol.TryResolve(property);
|
|
||||||
if (option is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(option.Name))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (option.Name.Length < 2 || !char.IsLetter(option.Name[0]))
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation(), option.Name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class OptionMustHaveValidShortNameAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Option short names must be letter characters",
|
|
||||||
"This option's short name must be a single letter character. "
|
|
||||||
+ "Specified short name: `{0}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var option = CommandOptionSymbol.TryResolve(property);
|
|
||||||
if (option is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (option.ShortName is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!char.IsLetter(option.ShortName.Value))
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation(), option.ShortName)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class OptionMustHaveValidValidatorsAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
$"Option validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`",
|
|
||||||
$"Each validator specified for this option must derive from a compatible `{SymbolNames.CliFxBindingValidatorClass}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var option = CommandOptionSymbol.TryResolve(property);
|
|
||||||
if (option is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var validatorType in option.ValidatorTypes)
|
|
||||||
{
|
|
||||||
var validatorValueType = validatorType
|
|
||||||
.GetBaseTypes()
|
|
||||||
.FirstOrDefault(t =>
|
|
||||||
t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingValidatorClass)
|
|
||||||
)
|
|
||||||
?.TypeArguments.FirstOrDefault();
|
|
||||||
|
|
||||||
// Value passed to the validator must be assignable from the property type
|
|
||||||
var isCompatible =
|
|
||||||
validatorValueType is not null
|
|
||||||
&& context.Compilation.IsAssignable(property.Type, validatorValueType);
|
|
||||||
|
|
||||||
if (!isCompatible)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
|
||||||
);
|
|
||||||
|
|
||||||
// No need to report multiple identical diagnostics on the same node
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class ParameterMustBeInsideCommandAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Parameters must be defined inside commands",
|
|
||||||
$"This parameter must be defined inside a class that implements `{SymbolNames.CliFxCommandInterface}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (property.ContainingType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (property.ContainingType.IsAbstract)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!CommandParameterSymbol.IsParameterProperty(property))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var isInsideCommand = property.ContainingType.AllInterfaces.Any(i =>
|
|
||||||
i.DisplayNameMatches(SymbolNames.CliFxCommandInterface)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isInsideCommand)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class ParameterMustBeLastIfNonRequiredAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Parameters marked as non-required must be the last in order",
|
|
||||||
"This parameter is non-required so it must be the last in order (its order must be highest within the command). "
|
|
||||||
+ "Property bound to another non-required parameter: `{0}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (property.ContainingType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var parameter = CommandParameterSymbol.TryResolve(property);
|
|
||||||
if (parameter is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (parameter.IsRequired != false)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var otherProperties = property
|
|
||||||
.ContainingType.GetMembers()
|
|
||||||
.OfType<IPropertySymbol>()
|
|
||||||
.Where(m => !m.Equals(property))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
foreach (var otherProperty in otherProperties)
|
|
||||||
{
|
|
||||||
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
|
|
||||||
if (otherParameter is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (otherParameter.Order > parameter.Order)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(
|
|
||||||
propertyDeclaration.Identifier.GetLocation(),
|
|
||||||
otherProperty.Name
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class ParameterMustBeLastIfNonScalarAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Parameters of non-scalar types must be the last in order",
|
|
||||||
"This parameter has a non-scalar type so it must be the last in order (its order must be highest within the command). "
|
|
||||||
+ "Property bound to another non-scalar parameter: `{0}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (property.ContainingType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var parameter = CommandParameterSymbol.TryResolve(property);
|
|
||||||
if (parameter is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (parameter.IsScalar())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var otherProperties = property
|
|
||||||
.ContainingType.GetMembers()
|
|
||||||
.OfType<IPropertySymbol>()
|
|
||||||
.Where(m => !m.Equals(property))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
foreach (var otherProperty in otherProperties)
|
|
||||||
{
|
|
||||||
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
|
|
||||||
if (otherParameter is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (otherParameter.Order > parameter.Order)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(
|
|
||||||
propertyDeclaration.Identifier.GetLocation(),
|
|
||||||
otherProperty.Name
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class ParameterMustBeRequiredIfPropertyRequiredAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Parameters bound to required properties cannot be marked as non-required",
|
|
||||||
"This parameter cannot be marked as non-required because it's bound to a required property."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (property.ContainingType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!property.IsRequired())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var parameter = CommandParameterSymbol.TryResolve(property);
|
|
||||||
if (parameter is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (parameter.IsRequired != false)
|
|
||||||
return;
|
|
||||||
|
|
||||||
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.Identifier.GetLocation()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class ParameterMustBeSingleIfNonRequiredAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Parameters marked as non-required are limited to one per command",
|
|
||||||
"This parameter is non-required so it must be the only such parameter in the command. "
|
|
||||||
+ "Property bound to another non-required parameter: `{0}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (property.ContainingType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var parameter = CommandParameterSymbol.TryResolve(property);
|
|
||||||
if (parameter is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (parameter.IsRequired != false)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var otherProperties = property
|
|
||||||
.ContainingType.GetMembers()
|
|
||||||
.OfType<IPropertySymbol>()
|
|
||||||
.Where(m => !m.Equals(property))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
foreach (var otherProperty in otherProperties)
|
|
||||||
{
|
|
||||||
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
|
|
||||||
if (otherParameter is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (otherParameter.IsRequired == false)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(
|
|
||||||
propertyDeclaration.Identifier.GetLocation(),
|
|
||||||
otherProperty.Name
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class ParameterMustBeSingleIfNonScalarAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Parameters of non-scalar types are limited to one per command",
|
|
||||||
"This parameter has a non-scalar type so it must be the only such parameter in the command. "
|
|
||||||
+ "Property bound to another non-scalar parameter: `{0}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (property.ContainingType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var parameter = CommandParameterSymbol.TryResolve(property);
|
|
||||||
if (parameter is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (parameter.IsScalar())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var otherProperties = property
|
|
||||||
.ContainingType.GetMembers()
|
|
||||||
.OfType<IPropertySymbol>()
|
|
||||||
.Where(m => !m.Equals(property))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
foreach (var otherProperty in otherProperties)
|
|
||||||
{
|
|
||||||
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
|
|
||||||
if (otherParameter is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!otherParameter.IsScalar())
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(
|
|
||||||
propertyDeclaration.Identifier.GetLocation(),
|
|
||||||
otherProperty.Name
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class ParameterMustHaveUniqueNameAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Parameters must have unique names",
|
|
||||||
"This parameter's name must be unique within the command (comparison IS NOT case sensitive). "
|
|
||||||
+ "Specified name: `{0}`. "
|
|
||||||
+ "Property bound to another parameter with the same name: `{1}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (property.ContainingType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var parameter = CommandParameterSymbol.TryResolve(property);
|
|
||||||
if (parameter is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(parameter.Name))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var otherProperties = property
|
|
||||||
.ContainingType.GetMembers()
|
|
||||||
.OfType<IPropertySymbol>()
|
|
||||||
.Where(m => !m.Equals(property))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
foreach (var otherProperty in otherProperties)
|
|
||||||
{
|
|
||||||
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
|
|
||||||
if (otherParameter is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(otherParameter.Name))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (
|
|
||||||
string.Equals(
|
|
||||||
parameter.Name,
|
|
||||||
otherParameter.Name,
|
|
||||||
StringComparison.OrdinalIgnoreCase
|
|
||||||
)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(
|
|
||||||
propertyDeclaration.Identifier.GetLocation(),
|
|
||||||
parameter.Name,
|
|
||||||
otherProperty.Name
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class ParameterMustHaveUniqueOrderAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
"Parameters must have unique order",
|
|
||||||
"This parameter's order must be unique within the command. "
|
|
||||||
+ "Specified order: {0}. "
|
|
||||||
+ "Property bound to another parameter with the same order: `{1}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (property.ContainingType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var parameter = CommandParameterSymbol.TryResolve(property);
|
|
||||||
if (parameter is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var otherProperties = property
|
|
||||||
.ContainingType.GetMembers()
|
|
||||||
.OfType<IPropertySymbol>()
|
|
||||||
.Where(m => !m.Equals(property))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
foreach (var otherProperty in otherProperties)
|
|
||||||
{
|
|
||||||
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
|
|
||||||
if (otherParameter is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (parameter.Order == otherParameter.Order)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(
|
|
||||||
propertyDeclaration.Identifier.GetLocation(),
|
|
||||||
parameter.Order,
|
|
||||||
otherProperty.Name
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class ParameterMustHaveValidConverterAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
$"Parameter converters must derive from `{SymbolNames.CliFxBindingConverterClass}`",
|
|
||||||
$"Converter specified for this parameter must derive from a compatible `{SymbolNames.CliFxBindingConverterClass}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var parameter = CommandParameterSymbol.TryResolve(property);
|
|
||||||
if (parameter is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (parameter.ConverterType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var converterValueType = parameter
|
|
||||||
.ConverterType.GetBaseTypes()
|
|
||||||
.FirstOrDefault(t =>
|
|
||||||
t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingConverterClass)
|
|
||||||
)
|
|
||||||
?.TypeArguments.FirstOrDefault();
|
|
||||||
|
|
||||||
// Value returned by the converter must be assignable to the property type
|
|
||||||
var isCompatible =
|
|
||||||
converterValueType is not null
|
|
||||||
&& (
|
|
||||||
parameter.IsScalar()
|
|
||||||
// Scalar
|
|
||||||
? context.Compilation.IsAssignable(converterValueType, property.Type)
|
|
||||||
// Non-scalar (assume we can handle all IEnumerable types for simplicity)
|
|
||||||
: property.Type.TryGetEnumerableUnderlyingType() is { } enumerableUnderlyingType
|
|
||||||
&& context.Compilation.IsAssignable(
|
|
||||||
converterValueType,
|
|
||||||
enumerableUnderlyingType
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isCompatible)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class ParameterMustHaveValidValidatorsAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
$"Parameter validators must derive from `{SymbolNames.CliFxBindingValidatorClass}`",
|
|
||||||
$"Each validator specified for this parameter must derive from a compatible `{SymbolNames.CliFxBindingValidatorClass}`."
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private void Analyze(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
PropertyDeclarationSyntax propertyDeclaration,
|
|
||||||
IPropertySymbol property
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var parameter = CommandParameterSymbol.TryResolve(property);
|
|
||||||
if (parameter is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var validatorType in parameter.ValidatorTypes)
|
|
||||||
{
|
|
||||||
var validatorValueType = validatorType
|
|
||||||
.GetBaseTypes()
|
|
||||||
.FirstOrDefault(t =>
|
|
||||||
t.ConstructedFrom.DisplayNameMatches(SymbolNames.CliFxBindingValidatorClass)
|
|
||||||
)
|
|
||||||
?.TypeArguments.FirstOrDefault();
|
|
||||||
|
|
||||||
// Value passed to the validator must be assignable from the property type
|
|
||||||
var isCompatible =
|
|
||||||
validatorValueType is not null
|
|
||||||
&& context.Compilation.IsAssignable(property.Type, validatorValueType);
|
|
||||||
|
|
||||||
if (!isCompatible)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(
|
|
||||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
|
||||||
);
|
|
||||||
|
|
||||||
// No need to report multiple identical diagnostics on the same node
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.HandlePropertyDeclaration(Analyze);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using CliFx.Analyzers.ObjectModel;
|
|
||||||
using CliFx.Analyzers.Utils.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers;
|
|
||||||
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public class SystemConsoleShouldBeAvoidedAnalyzer()
|
|
||||||
: AnalyzerBase(
|
|
||||||
$"Avoid calling `System.Console` where `{SymbolNames.CliFxConsoleInterface}` is available",
|
|
||||||
$"Use the provided `{SymbolNames.CliFxConsoleInterface}` abstraction instead of `System.Console` to ensure that the command can be tested in isolation.",
|
|
||||||
DiagnosticSeverity.Warning
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private MemberAccessExpressionSyntax? TryGetSystemConsoleMemberAccess(
|
|
||||||
SyntaxNodeAnalysisContext context,
|
|
||||||
SyntaxNode node
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var currentNode = node;
|
|
||||||
|
|
||||||
while (currentNode is MemberAccessExpressionSyntax memberAccess)
|
|
||||||
{
|
|
||||||
var member = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
|
|
||||||
|
|
||||||
if (member?.ContainingType?.DisplayNameMatches("System.Console") == true)
|
|
||||||
{
|
|
||||||
return memberAccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get inner expression, which may be another member access expression.
|
|
||||||
// Example: System.Console.Error
|
|
||||||
// ~~~~~~~~~~~~~~ <- inner member access expression
|
|
||||||
// -------------------- <- outer member access expression
|
|
||||||
currentNode = memberAccess.Expression;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Analyze(SyntaxNodeAnalysisContext context)
|
|
||||||
{
|
|
||||||
// Try to get a member access on System.Console in the current expression,
|
|
||||||
// or in any of its inner expressions.
|
|
||||||
var systemConsoleMemberAccess = TryGetSystemConsoleMemberAccess(context, context.Node);
|
|
||||||
if (systemConsoleMemberAccess is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Check if IConsole is available in scope as an alternative to System.Console
|
|
||||||
var isConsoleInterfaceAvailable = context
|
|
||||||
.Node.Ancestors()
|
|
||||||
.OfType<MethodDeclarationSyntax>()
|
|
||||||
.SelectMany(m => m.ParameterList.Parameters)
|
|
||||||
.Select(p => p.Type)
|
|
||||||
.Select(t => context.SemanticModel.GetSymbolInfo(t).Symbol)
|
|
||||||
.Where(s => s is not null)
|
|
||||||
.Any(s => s.DisplayNameMatches(SymbolNames.CliFxConsoleInterface));
|
|
||||||
|
|
||||||
if (isConsoleInterfaceAvailable)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(CreateDiagnostic(systemConsoleMemberAccess.GetLocation()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
base.Initialize(context);
|
|
||||||
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.SimpleMemberAccessExpression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Utils.Extensions;
|
|
||||||
|
|
||||||
internal static class RoslynExtensions
|
|
||||||
{
|
|
||||||
public static bool DisplayNameMatches(this ISymbol symbol, string name) =>
|
|
||||||
string.Equals(
|
|
||||||
// Fully qualified name, without `global::`
|
|
||||||
symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
|
|
||||||
name,
|
|
||||||
StringComparison.Ordinal
|
|
||||||
);
|
|
||||||
|
|
||||||
public static IEnumerable<INamedTypeSymbol> GetBaseTypes(this ITypeSymbol type)
|
|
||||||
{
|
|
||||||
var current = type.BaseType;
|
|
||||||
|
|
||||||
while (current is not null)
|
|
||||||
{
|
|
||||||
yield return current;
|
|
||||||
current = current.BaseType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ITypeSymbol? TryGetEnumerableUnderlyingType(this ITypeSymbol type) =>
|
|
||||||
type
|
|
||||||
.AllInterfaces.FirstOrDefault(i =>
|
|
||||||
i.ConstructedFrom.SpecialType
|
|
||||||
== SpecialType.System_Collections_Generic_IEnumerable_T
|
|
||||||
)
|
|
||||||
?.TypeArguments[0];
|
|
||||||
|
|
||||||
// Detect if the property is required through roundabout means so as to not have to take dependency
|
|
||||||
// on higher versions of the C# compiler.
|
|
||||||
public static bool IsRequired(this IPropertySymbol property) =>
|
|
||||||
property
|
|
||||||
// Can't rely on the RequiredMemberAttribute because it's generated by the compiler, not added by the user,
|
|
||||||
// so we have to check for the presence of the `required` modifier in the syntax tree instead.
|
|
||||||
.DeclaringSyntaxReferences.Select(r => r.GetSyntax())
|
|
||||||
.OfType<PropertyDeclarationSyntax>()
|
|
||||||
.SelectMany(p => p.Modifiers)
|
|
||||||
.Any(m => m.IsKind((SyntaxKind)8447));
|
|
||||||
|
|
||||||
public static bool IsAssignable(
|
|
||||||
this Compilation compilation,
|
|
||||||
ITypeSymbol source,
|
|
||||||
ITypeSymbol destination
|
|
||||||
) => compilation.ClassifyConversion(source, destination).Exists;
|
|
||||||
|
|
||||||
public static void HandleClassDeclaration(
|
|
||||||
this AnalysisContext analysisContext,
|
|
||||||
Action<SyntaxNodeAnalysisContext, ClassDeclarationSyntax, ITypeSymbol> analyze
|
|
||||||
)
|
|
||||||
{
|
|
||||||
analysisContext.RegisterSyntaxNodeAction(
|
|
||||||
ctx =>
|
|
||||||
{
|
|
||||||
if (ctx.Node is not ClassDeclarationSyntax classDeclaration)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var type = ctx.SemanticModel.GetDeclaredSymbol(classDeclaration);
|
|
||||||
if (type is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
analyze(ctx, classDeclaration, type);
|
|
||||||
},
|
|
||||||
SyntaxKind.ClassDeclaration
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void HandlePropertyDeclaration(
|
|
||||||
this AnalysisContext analysisContext,
|
|
||||||
Action<SyntaxNodeAnalysisContext, PropertyDeclarationSyntax, IPropertySymbol> analyze
|
|
||||||
)
|
|
||||||
{
|
|
||||||
analysisContext.RegisterSyntaxNodeAction(
|
|
||||||
ctx =>
|
|
||||||
{
|
|
||||||
if (ctx.Node is not PropertyDeclarationSyntax propertyDeclaration)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var property = ctx.SemanticModel.GetDeclaredSymbol(propertyDeclaration);
|
|
||||||
if (property is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
analyze(ctx, propertyDeclaration, property);
|
|
||||||
},
|
|
||||||
SyntaxKind.PropertyDeclaration
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CliFx.Analyzers.Utils.Extensions;
|
|
||||||
|
|
||||||
internal static class StringExtensions
|
|
||||||
{
|
|
||||||
public static string TrimEnd(
|
|
||||||
this string str,
|
|
||||||
string sub,
|
|
||||||
StringComparison comparison = StringComparison.Ordinal
|
|
||||||
)
|
|
||||||
{
|
|
||||||
while (str.EndsWith(sub, comparison))
|
|
||||||
str = str[..^sub.Length];
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,14 +7,14 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.15.4" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
|
||||||
<PackageReference Include="clipr" Version="1.6.1" />
|
<PackageReference Include="clipr" Version="1.6.1" />
|
||||||
<PackageReference Include="Cocona" Version="2.2.0" />
|
<PackageReference Include="Cocona" Version="2.2.0" />
|
||||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||||
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
||||||
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
|
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
|
||||||
<PackageReference Include="PowerArgs" Version="4.0.3" />
|
<PackageReference Include="PowerArgs" Version="4.0.3" />
|
||||||
<PackageReference Include="System.CommandLine" Version="2.0.0-rc.1.25451.107" />
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta5.25306.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -4,16 +4,17 @@
|
|||||||
<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.9" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||||
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
<ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace CliFx.Demo.Domain;
|
namespace CliFx.Demo.Domain;
|
||||||
@@ -24,5 +23,5 @@ public partial record Library(IReadOnlyList<Book> Books)
|
|||||||
|
|
||||||
public partial record Library
|
public partial record Library
|
||||||
{
|
{
|
||||||
public static Library Empty { get; } = new(Array.Empty<Book>());
|
public static Library Empty { get; } = new([]);
|
||||||
}
|
}
|
||||||
|
|||||||
6
CliFx.Demo/Domain/LibraryJsonContext.cs
Normal file
6
CliFx.Demo/Domain/LibraryJsonContext.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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);
|
var data = JsonSerializer.Serialize(library, LibraryJsonContext.Default.Library);
|
||||||
File.WriteAllText(StorageFilePath, data);
|
File.WriteAllText(StorageFilePath, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +22,8 @@ public class LibraryProvider
|
|||||||
|
|
||||||
var data = File.ReadAllText(StorageFilePath);
|
var data = File.ReadAllText(StorageFilePath);
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<Library>(data) ?? Library.Empty;
|
return JsonSerializer.Deserialize(data, LibraryJsonContext.Default.Library)
|
||||||
|
?? Library.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Book? TryGetBook(string title) =>
|
public Book? TryGetBook(string title) =>
|
||||||
|
|||||||
@@ -2,11 +2,6 @@
|
|||||||
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>();
|
||||||
@@ -15,7 +10,9 @@ return await new CliApplicationBuilder()
|
|||||||
foreach (var commandType in commandTypes)
|
foreach (var commandType in commandTypes)
|
||||||
services.AddTransient(commandType);
|
services.AddTransient(commandType);
|
||||||
|
|
||||||
return services.BuildServiceProvider();
|
return await new CliApplicationBuilder()
|
||||||
})
|
.SetDescription("Demo application showcasing CliFx features.")
|
||||||
|
.AddCommandsFromThisAssembly()
|
||||||
|
.UseTypeActivator(services.BuildServiceProvider())
|
||||||
.Build()
|
.Build()
|
||||||
.RunAsync();
|
.RunAsync();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
<GenerateDependencyFile>true</GenerateDependencyFile>
|
<GenerateDependencyFile>true</GenerateDependencyFile>
|
||||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||||
<NoWarn>$(NoWarn);RS1025;RS1026</NoWarn>
|
<NoWarn>$(NoWarn);RS1035</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@@ -17,11 +17,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
<PackageReference Include="CSharpier.MsBuild" Version="0.28.2" 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="3.0.0" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.11.0" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.0.0" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
|
||||||
<PackageReference Include="PolyShim" Version="1.15.0" PrivateAssets="all" />
|
<PackageReference Include="PolyShim" Version="1.12.0" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
130
CliFx.SourceGeneration/CommandSchemaGenerator.cs
Normal file
130
CliFx.SourceGeneration/CommandSchemaGenerator.cs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
CliFx.SourceGeneration/DiagnosticDescriptors.cs
Normal file
27
CliFx.SourceGeneration/DiagnosticDescriptors.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
66
CliFx.SourceGeneration/SemanticModel/CommandInputSymbol.cs
Normal file
66
CliFx.SourceGeneration/SemanticModel/CommandInputSymbol.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
90
CliFx.SourceGeneration/SemanticModel/CommandOptionSymbol.cs
Normal file
90
CliFx.SourceGeneration/SemanticModel/CommandOptionSymbol.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
167
CliFx.SourceGeneration/SemanticModel/CommandSymbol.cs
Normal file
167
CliFx.SourceGeneration/SemanticModel/CommandSymbol.cs
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
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,13 +1,10 @@
|
|||||||
namespace CliFx.Analyzers.ObjectModel;
|
namespace CliFx.SourceGeneration.SemanticModel;
|
||||||
|
|
||||||
internal static class SymbolNames
|
internal static class KnownSymbolNames
|
||||||
{
|
{
|
||||||
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>";
|
|
||||||
}
|
}
|
||||||
44
CliFx.SourceGeneration/SemanticModel/PropertyDescriptor.cs
Normal file
44
CliFx.SourceGeneration/SemanticModel/PropertyDescriptor.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
47
CliFx.SourceGeneration/SemanticModel/TypeDescriptor.cs
Normal file
47
CliFx.SourceGeneration/SemanticModel/TypeDescriptor.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
39
CliFx.SourceGeneration/Utils/Extensions/RoslynExtensions.cs
Normal file
39
CliFx.SourceGeneration/Utils/Extensions/RoslynExtensions.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
30
CliFx.SourceGeneration/Utils/Extensions/StringExtensions.cs
Normal file
30
CliFx.SourceGeneration/Utils/Extensions/StringExtensions.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
<ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
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;
|
||||||
@@ -13,7 +12,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,
|
||||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "exe" : null
|
OperatingSystem.IsWindows() ? "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(Array.Empty<string>(), new Dictionary<string, string>());
|
var exitCode = await app.RunAsync([], 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(Array.Empty<string>(), new Dictionary<string, string>());
|
var exitCode = await app.RunAsync([], 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(Array.Empty<string>(), new Dictionary<string, string>());
|
var exitCode = await app.RunAsync([], 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>()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,18 +9,18 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Basic.Reference.Assemblies.Net80" Version="1.8.3" />
|
<PackageReference Include="Basic.Reference.Assemblies.Net80" Version="1.8.2" />
|
||||||
<PackageReference Include="CliWrap" Version="3.9.0" />
|
<PackageReference Include="CliWrap" Version="3.9.0" />
|
||||||
<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.7.0" />
|
<PackageReference Include="FluentAssertions" Version="8.4.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.9" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
<PackageReference Include="PolyShim" Version="1.15.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.1.5" PrivateAssets="all" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1" 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,5 +1,4 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
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;
|
||||||
@@ -90,7 +89,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" }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -130,7 +129,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,5 +1,4 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
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;
|
||||||
@@ -34,7 +33,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>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -73,7 +72,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>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -119,7 +118,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>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -156,7 +155,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>()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -194,7 +193,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,5 +1,4 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
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;
|
||||||
@@ -22,7 +21,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>()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
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;
|
||||||
@@ -692,7 +691,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,5 +1,4 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Tests.Utils;
|
using CliFx.Tests.Utils;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
@@ -56,7 +55,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,9 +20,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Demo", "CliFx.Demo\Cl
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{F717347D-8656-44DA-A4A2-BE515E8C4655}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{F717347D-8656-44DA-A4A2-BE515E8C4655}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Analyzers", "CliFx.Analyzers\CliFx.Analyzers.csproj", "{F8460D69-F8CF-405C-A6ED-BED02A21DB42}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.SourceGeneration", "CliFx.SourceGeneration\CliFx.SourceGeneration.csproj", "{F8460D69-F8CF-405C-A6ED-BED02A21DB42}"
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Analyzers.Tests", "CliFx.Analyzers.Tests\CliFx.Analyzers.Tests.csproj", "{49878E75-2097-4C79-9151-B98A28FBB973}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -106,18 +104,6 @@ Global
|
|||||||
{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|x64.Build.0 = Release|Any CPU
|
{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|x86.ActiveCfg = Release|Any CPU
|
{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|x86.Build.0 = Release|Any CPU
|
{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{49878E75-2097-4C79-9151-B98A28FBB973}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{49878E75-2097-4C79-9151-B98A28FBB973}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{49878E75-2097-4C79-9151-B98A28FBB973}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{49878E75-2097-4C79-9151-B98A28FBB973}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{49878E75-2097-4C79-9151-B98A28FBB973}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{49878E75-2097-4C79-9151-B98A28FBB973}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{49878E75-2097-4C79-9151-B98A28FBB973}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{49878E75-2097-4C79-9151-B98A28FBB973}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{49878E75-2097-4C79-9151-B98A28FBB973}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{49878E75-2097-4C79-9151-B98A28FBB973}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{49878E75-2097-4C79-9151-B98A28FBB973}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{49878E75-2097-4C79-9151-B98A28FBB973}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using CliFx.Schema;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CliFx;
|
namespace CliFx;
|
||||||
|
|
||||||
@@ -7,15 +6,15 @@ namespace CliFx;
|
|||||||
/// Configuration of an application.
|
/// Configuration of an application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ApplicationConfiguration(
|
public class ApplicationConfiguration(
|
||||||
IReadOnlyList<Type> commandTypes,
|
ApplicationSchema schema,
|
||||||
bool isDebugModeAllowed,
|
bool isDebugModeAllowed,
|
||||||
bool isPreviewModeAllowed
|
bool isPreviewModeAllowed
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command types defined in the application.
|
/// Application schema.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<Type> CommandTypes { get; } = commandTypes;
|
public ApplicationSchema Schema { get; } = schema;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether debug mode is allowed in the application.
|
/// Whether debug mode is allowed in the application.
|
||||||
|
|||||||
@@ -4,29 +4,22 @@ 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 sealed class CommandAttribute : Attribute
|
public class CommandAttribute(string? name = null) : 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>
|
||||||
/// <remarks>
|
public string? Name { get; } = name;
|
||||||
/// 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.
|
||||||
|
|||||||
19
CliFx/Attributes/CommandHelpOptionAttribute.cs
Normal file
19
CliFx/Attributes/CommandHelpOptionAttribute.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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.";
|
||||||
|
}
|
||||||
|
}
|
||||||
34
CliFx/Attributes/CommandInputAttribute.cs
Normal file
34
CliFx/Attributes/CommandInputAttribute.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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,13 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using CliFx.Extensibility;
|
|
||||||
|
|
||||||
namespace CliFx.Attributes;
|
namespace CliFx.Attributes;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Annotates a property that defines a command option.
|
/// Binds a property to a command option — a command-line input that is identified by a name and/or a short name.
|
||||||
/// </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 sealed class CommandOptionAttribute : Attribute
|
public class CommandOptionAttribute : CommandInputAttribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandOptionAttribute" />.
|
/// Initializes an instance of <see cref="CommandOptionAttribute" />.
|
||||||
@@ -39,25 +42,16 @@ public sealed class CommandOptionAttribute : Attribute
|
|||||||
/// <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 if they don't set it.
|
/// If an option is required, the user will get an error when 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
|
||||||
@@ -70,28 +64,4 @@ public sealed class CommandOptionAttribute : Attribute
|
|||||||
/// 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,65 +1,41 @@
|
|||||||
using System;
|
using System;
|
||||||
using CliFx.Extensibility;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace CliFx.Attributes;
|
namespace CliFx.Attributes;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Annotates a property that defines a command parameter.
|
/// Binds a property to a command parameter — a command-line input that is identified by its relative position (order).
|
||||||
|
/// 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 sealed class CommandParameterAttribute(int order) : Attribute
|
public class CommandParameterAttribute(int order) : CommandInputAttribute
|
||||||
{
|
{
|
||||||
/// <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 if they don't set it.
|
/// If a parameter is required, the user will get an error when they don't set it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Parameter marked as non-required must always be the last in order.
|
/// Parameter marked as non-required must have the highest order in the command.
|
||||||
/// Only one non-required parameter is allowed in a command.
|
/// Only one non-required parameter is allowed per command.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool IsRequired { get; set; } = true;
|
public bool IsRequired { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parameter name.
|
/// Parameter name, as shown in the help text.
|
||||||
/// 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>();
|
|
||||||
}
|
}
|
||||||
|
|||||||
19
CliFx/Attributes/CommandVersionOptionAttribute.cs
Normal file
19
CliFx/Attributes/CommandVersionOptionAttribute.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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.Input;
|
using CliFx.Parsing;
|
||||||
using CliFx.Schema;
|
using CliFx.Schema;
|
||||||
using CliFx.Utils;
|
using CliFx.Utils;
|
||||||
using CliFx.Utils.Extensions;
|
using CliFx.Utils.Extensions;
|
||||||
@@ -23,8 +23,6 @@ public class CliApplication(
|
|||||||
ITypeActivator typeActivator
|
ITypeActivator typeActivator
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
private readonly CommandBinder _commandBinder = new(typeActivator);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Application metadata.
|
/// Application metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -35,21 +33,11 @@ public class CliApplication(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ApplicationConfiguration Configuration { get; } = configuration;
|
public ApplicationConfiguration Configuration { get; } = configuration;
|
||||||
|
|
||||||
private bool IsDebugModeEnabled(CommandInput commandInput) =>
|
private bool IsDebugModeEnabled(CommandArguments commandArguments) =>
|
||||||
Configuration.IsDebugModeAllowed && commandInput.IsDebugDirectiveSpecified;
|
Configuration.IsDebugModeAllowed && commandArguments.IsDebugDirectiveSpecified;
|
||||||
|
|
||||||
private bool IsPreviewModeEnabled(CommandInput commandInput) =>
|
private bool IsPreviewModeEnabled(CommandArguments commandArguments) =>
|
||||||
Configuration.IsPreviewModeAllowed && commandInput.IsPreviewDirectiveSpecified;
|
Configuration.IsPreviewModeAllowed && commandArguments.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()
|
||||||
{
|
{
|
||||||
@@ -69,7 +57,8 @@ public class CliApplication(
|
|||||||
|
|
||||||
private async ValueTask<int> RunAsync(
|
private async ValueTask<int> RunAsync(
|
||||||
ApplicationSchema applicationSchema,
|
ApplicationSchema applicationSchema,
|
||||||
CommandInput commandInput
|
CommandArguments commandArguments,
|
||||||
|
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,
|
||||||
@@ -77,26 +66,26 @@ public class CliApplication(
|
|||||||
console.ResetColor();
|
console.ResetColor();
|
||||||
|
|
||||||
// Handle the debug directive
|
// Handle the debug directive
|
||||||
if (IsDebugModeEnabled(commandInput))
|
if (IsDebugModeEnabled(commandArguments))
|
||||||
{
|
{
|
||||||
await PromptDebuggerAsync();
|
await PromptDebuggerAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the preview directive
|
// Handle the preview directive
|
||||||
if (IsPreviewModeEnabled(commandInput))
|
if (IsPreviewModeEnabled(commandArguments))
|
||||||
{
|
{
|
||||||
console.WriteCommandInput(commandInput);
|
console.WriteCommandInput(commandArguments);
|
||||||
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 commandSchema =
|
var command =
|
||||||
(
|
(
|
||||||
!string.IsNullOrWhiteSpace(commandInput.CommandName)
|
!string.IsNullOrWhiteSpace(commandArguments.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(commandInput.CommandName)
|
? applicationSchema.TryFindCommand(commandArguments.CommandName)
|
||||||
// Otherwise, try to find the default command
|
// Otherwise, try to find the default command
|
||||||
: applicationSchema.TryFindDefaultCommand()
|
: applicationSchema.TryFindDefaultCommand()
|
||||||
)
|
)
|
||||||
@@ -107,42 +96,48 @@ public class CliApplication(
|
|||||||
|
|
||||||
// Initialize an instance of the command type
|
// Initialize an instance of the command type
|
||||||
var commandInstance =
|
var commandInstance =
|
||||||
commandSchema == FallbackDefaultCommand.Schema
|
command == FallbackDefaultCommand.Schema
|
||||||
? new FallbackDefaultCommand() // bypass the activator
|
? new FallbackDefaultCommand() // bypass the activator
|
||||||
: typeActivator.CreateInstance<ICommand>(commandSchema.Type);
|
: typeActivator.CreateInstance<ICommand>(command.Type);
|
||||||
|
|
||||||
// Assemble the help context
|
// Assemble the help context
|
||||||
var helpContext = new HelpContext(
|
var helpContext = new HelpContext(
|
||||||
Metadata,
|
Metadata,
|
||||||
applicationSchema,
|
applicationSchema,
|
||||||
commandSchema,
|
command,
|
||||||
commandSchema.GetValues(commandInstance)
|
command.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
|
||||||
{
|
{
|
||||||
// Bind and execute the command
|
// Activate the command instance with the provided user input
|
||||||
_commandBinder.Bind(commandInput, commandSchema, commandInstance);
|
command.Activate(commandInstance, commandArguments, environmentVariables);
|
||||||
await commandInstance.ExecuteAsync(console);
|
|
||||||
|
|
||||||
|
// 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);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
catch (CliFxException ex)
|
catch (CliFxException ex)
|
||||||
@@ -169,20 +164,19 @@ 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
|
||||||
{
|
{
|
||||||
var applicationSchema = ApplicationSchema.Resolve(Configuration.CommandTypes);
|
return await RunAsync(
|
||||||
|
Configuration.Schema,
|
||||||
var commandInput = CommandInput.Parse(
|
CommandArguments.Parse(
|
||||||
commandLineArguments,
|
commandLineArguments,
|
||||||
environmentVariables,
|
Configuration.Schema.GetCommandNames()
|
||||||
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.
|
||||||
@@ -210,7 +204,7 @@ public class CliApplication(
|
|||||||
commandLineArguments,
|
commandLineArguments,
|
||||||
Environment
|
Environment
|
||||||
.GetEnvironmentVariables()
|
.GetEnvironmentVariables()
|
||||||
.ToDictionary<string, string>(StringComparer.Ordinal)
|
.ToDictionary<string, string?>(StringComparer.Ordinal)
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
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;
|
||||||
@@ -16,7 +15,7 @@ namespace CliFx;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class CliApplicationBuilder
|
public partial class CliApplicationBuilder
|
||||||
{
|
{
|
||||||
private readonly HashSet<Type> _commandTypes = [];
|
private readonly HashSet<CommandSchema> _commands = [];
|
||||||
|
|
||||||
private bool _isDebugModeAllowed = true;
|
private bool _isDebugModeAllowed = true;
|
||||||
private bool _isPreviewModeAllowed = true;
|
private bool _isPreviewModeAllowed = true;
|
||||||
@@ -27,74 +26,30 @@ public partial class CliApplicationBuilder
|
|||||||
private IConsole? _console;
|
private IConsole? _console;
|
||||||
private ITypeActivator? _typeActivator;
|
private ITypeActivator? _typeActivator;
|
||||||
|
|
||||||
/// <summary>
|
// TODO:
|
||||||
/// Adds a command to the application.
|
// The source generator should generate an internal extension method for the builder called
|
||||||
/// </summary>
|
// AddCommandsFromThisAssembly() that would add all command types from the assembly where the builder is used.
|
||||||
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<TCommand>()
|
public CliApplicationBuilder AddCommand(CommandSchema command)
|
||||||
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(IEnumerable<Type> commandTypes)
|
public CliApplicationBuilder AddCommands(IReadOnlyList<CommandSchema> commands)
|
||||||
{
|
{
|
||||||
foreach (var commandType in commandTypes)
|
foreach (var command in commands)
|
||||||
AddCommand(commandType);
|
AddCommand(command);
|
||||||
|
|
||||||
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>
|
||||||
@@ -189,15 +144,6 @@ 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>
|
||||||
@@ -211,7 +157,7 @@ public partial class CliApplicationBuilder
|
|||||||
);
|
);
|
||||||
|
|
||||||
var configuration = new ApplicationConfiguration(
|
var configuration = new ApplicationConfiguration(
|
||||||
_commandTypes.ToArray(),
|
new ApplicationSchema(_commands.ToArray()),
|
||||||
_isDebugModeAllowed,
|
_isDebugModeAllowed,
|
||||||
_isPreviewModeAllowed
|
_isPreviewModeAllowed
|
||||||
);
|
);
|
||||||
@@ -241,15 +187,17 @@ 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;
|
||||||
|
|
||||||
if (
|
// Process file path should generally always be available
|
||||||
string.IsNullOrWhiteSpace(entryAssemblyFilePath)
|
if (string.IsNullOrWhiteSpace(processFilePath))
|
||||||
|| 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. "
|
||||||
@@ -257,15 +205,22 @@ public partial class CliApplicationBuilder
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the process path matches the entry assembly path, it's a legacy .NET Framework app
|
var entryAssemblyFilePath = EnvironmentEx.EntryAssembly?.Location;
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the process path has the same name and parent directory as the entry assembly path,
|
// .NET Core application launched through the native application host:
|
||||||
// but different extension, it's a framework-dependent .NET Core app launched through the apphost.
|
// entry assembly has the same file path as the process, but with a different extension.
|
||||||
if (
|
if (
|
||||||
PathEx.AreEqual(Path.ChangeExtension(entryAssemblyFilePath, "exe"), processFilePath)
|
PathEx.AreEqual(Path.ChangeExtension(entryAssemblyFilePath, "exe"), processFilePath)
|
||||||
|| PathEx.AreEqual(
|
|| PathEx.AreEqual(
|
||||||
@@ -277,7 +232,7 @@ public partial class CliApplicationBuilder
|
|||||||
return Path.GetFileNameWithoutExtension(entryAssemblyFilePath);
|
return Path.GetFileNameWithoutExtension(entryAssemblyFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, it's a framework-dependent .NET Core app launched through the .NET CLI
|
// .NET Core application launched through the .NET CLI
|
||||||
return "dotnet " + Path.GetFileName(entryAssemblyFilePath);
|
return "dotnet " + Path.GetFileName(entryAssemblyFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.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>
|
||||||
@@ -29,17 +31,7 @@
|
|||||||
|
|
||||||
<!-- Embed the analyzer inside the package -->
|
<!-- Embed the analyzer inside the package -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="../CliFx.Analyzers/CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
<ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/CliFx.Analyzers.deps.json" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/CliFx.Analyzers.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Buffers.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Collections.Immutable.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Memory.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Numerics.Vectors.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Reflection.Metadata.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Text.Encoding.CodePages.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/System.Threading.Tasks.Extensions.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,395 +0,0 @@
|
|||||||
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 when there is an error during application execution.
|
/// Exception thrown within <see cref="CliFx" />.
|
||||||
/// </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 they're meant for the end-user,
|
// User errors are typically caused by invalid input and are 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,20 +1,15 @@
|
|||||||
namespace CliFx.Extensibility;
|
using System;
|
||||||
|
|
||||||
// Used internally to simplify the usage from reflection
|
namespace CliFx.Extensibility;
|
||||||
internal interface IBindingConverter
|
|
||||||
{
|
|
||||||
object? Convert(string? rawValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base type for custom converters.
|
/// Defines custom conversion logic for activating command inputs from the corresponding raw command-line arguments.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BindingConverter<T> : IBindingConverter
|
public abstract class BindingConverter<T> : IBindingConverter
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <inheritdoc cref="IBindingConverter.Convert" />
|
||||||
/// Parses value from a raw command-line argument.
|
public abstract T? Convert(string? rawValue, IFormatProvider? formatProvider);
|
||||||
/// </summary>
|
|
||||||
public abstract T Convert(string? rawValue);
|
|
||||||
|
|
||||||
object? IBindingConverter.Convert(string? rawValue) => Convert(rawValue);
|
object? IBindingConverter.Convert(string? rawValue, IFormatProvider? formatProvider) =>
|
||||||
|
Convert(rawValue, formatProvider);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user