mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
@@ -662,6 +662,44 @@ public class MyCommand : ICommand
|
|||||||
[CommandOption('o', Validators = new[] {typeof(MyValidator)})]
|
[CommandOption('o', Validators = new[] {typeof(MyValidator)})]
|
||||||
public string Option { get; set; }
|
public string Option { get; set; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}"
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new AnalyzerTestCase(
|
||||||
|
"Option with a name that doesn't start with a letter character",
|
||||||
|
DiagnosticDescriptors.CliFx0048,
|
||||||
|
|
||||||
|
// language=cs
|
||||||
|
@"
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption(""0foo"")]
|
||||||
|
public string Option { get; set; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}"
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new AnalyzerTestCase(
|
||||||
|
"Option with a short name that isn't a letter character",
|
||||||
|
DiagnosticDescriptors.CliFx0049,
|
||||||
|
|
||||||
|
// language=cs
|
||||||
|
@"
|
||||||
|
[Command]
|
||||||
|
public class MyCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption('0')]
|
||||||
|
public string Option { get; set; }
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}"
|
}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis.Diagnostics;
|
|||||||
|
|
||||||
namespace CliFx.Analyzers
|
namespace CliFx.Analyzers
|
||||||
{
|
{
|
||||||
|
// TODO: split into multiple analyzers
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
public class CommandSchemaAnalyzer : DiagnosticAnalyzer
|
public class CommandSchemaAnalyzer : DiagnosticAnalyzer
|
||||||
{
|
{
|
||||||
@@ -25,7 +26,9 @@ namespace CliFx.Analyzers
|
|||||||
DiagnosticDescriptors.CliFx0044,
|
DiagnosticDescriptors.CliFx0044,
|
||||||
DiagnosticDescriptors.CliFx0045,
|
DiagnosticDescriptors.CliFx0045,
|
||||||
DiagnosticDescriptors.CliFx0046,
|
DiagnosticDescriptors.CliFx0046,
|
||||||
DiagnosticDescriptors.CliFx0047
|
DiagnosticDescriptors.CliFx0047,
|
||||||
|
DiagnosticDescriptors.CliFx0048,
|
||||||
|
DiagnosticDescriptors.CliFx0049
|
||||||
);
|
);
|
||||||
|
|
||||||
private static bool IsScalarType(ITypeSymbol typeSymbol) =>
|
private static bool IsScalarType(ITypeSymbol typeSymbol) =>
|
||||||
@@ -307,7 +310,7 @@ namespace CliFx.Analyzers
|
|||||||
|
|
||||||
// Invalid validators
|
// Invalid validators
|
||||||
var invalidValidatorsOptions = options
|
var invalidValidatorsOptions = options
|
||||||
.Where(p => !p.Validators.All(v => v.AllInterfaces.Any(KnownSymbols.IsArgumentValueValidatorInterface)))
|
.Where(o => !o.Validators.All(v => v.AllInterfaces.Any(KnownSymbols.IsArgumentValueValidatorInterface)))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
foreach (var option in invalidValidatorsOptions)
|
foreach (var option in invalidValidatorsOptions)
|
||||||
@@ -316,6 +319,30 @@ namespace CliFx.Analyzers
|
|||||||
DiagnosticDescriptors.CliFx0047, option.Property.Locations.First()
|
DiagnosticDescriptors.CliFx0047, option.Property.Locations.First()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-letter first character in name
|
||||||
|
var nonLetterFirstCharacterInNameOptions = options
|
||||||
|
.Where(o => !string.IsNullOrWhiteSpace(o.Name) && !char.IsLetter(o.Name[0]))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var option in nonLetterFirstCharacterInNameOptions)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
|
DiagnosticDescriptors.CliFx0048, option.Property.Locations.First()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-letter short name
|
||||||
|
var nonLetterShortNameOptions = options
|
||||||
|
.Where(o => o.ShortName != null && !char.IsLetter(o.ShortName.Value))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var option in nonLetterShortNameOptions)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
|
DiagnosticDescriptors.CliFx0049, option.Property.Locations.First()
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CheckCommandType(SymbolAnalysisContext context)
|
private static void CheckCommandType(SymbolAnalysisContext context)
|
||||||
|
|||||||
@@ -109,6 +109,20 @@ namespace CliFx.Analyzers
|
|||||||
"Usage", DiagnosticSeverity.Error, true
|
"Usage", DiagnosticSeverity.Error, true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor CliFx0048 =
|
||||||
|
new DiagnosticDescriptor(nameof(CliFx0048),
|
||||||
|
"Option name must begin with a letter character.",
|
||||||
|
"Option name must begin with a letter character.",
|
||||||
|
"Usage", DiagnosticSeverity.Error, true
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor CliFx0049 =
|
||||||
|
new DiagnosticDescriptor(nameof(CliFx0049),
|
||||||
|
"Option short name must be a letter character.",
|
||||||
|
"Option short name must be a letter character.",
|
||||||
|
"Usage", DiagnosticSeverity.Error, true
|
||||||
|
);
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor CliFx0100 =
|
public static readonly DiagnosticDescriptor CliFx0100 =
|
||||||
new DiagnosticDescriptor(nameof(CliFx0100),
|
new DiagnosticDescriptor(nameof(CliFx0100),
|
||||||
"Use the provided IConsole abstraction instead of System.Console to ensure that the command can be tested in isolation",
|
"Use the provided IConsole abstraction instead of System.Console to ensure that the command can be tested in isolation",
|
||||||
|
|||||||
@@ -451,5 +451,45 @@ namespace CliFx.Tests
|
|||||||
|
|
||||||
_output.WriteLine(stdErr.GetString());
|
_output.WriteLine(stdErr.GetString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Command_options_must_have_names_that_start_with_a_letter_character()
|
||||||
|
{
|
||||||
|
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||||
|
|
||||||
|
var application = new CliApplicationBuilder()
|
||||||
|
.AddCommand<NonLetterCharacterNameCommand>()
|
||||||
|
.UseConsole(console)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().NotBe(0);
|
||||||
|
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||||
|
|
||||||
|
_output.WriteLine(stdErr.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Command_options_must_have_short_names_that_are_letter_characters()
|
||||||
|
{
|
||||||
|
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||||
|
|
||||||
|
var application = new CliApplicationBuilder()
|
||||||
|
.AddCommand<NonLetterCharacterShortNameCommand>()
|
||||||
|
.UseConsole(console)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().NotBe(0);
|
||||||
|
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||||
|
|
||||||
|
_output.WriteLine(stdErr.GetString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,6 +191,34 @@ namespace CliFx.Tests
|
|||||||
_output.WriteLine(stdErr.GetString());
|
_output.WriteLine(stdErr.GetString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Argument_that_begins_with_a_dash_is_not_parsed_as_option_name_if_it_does_not_start_with_a_letter_character()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
|
|
||||||
|
var application = new CliApplicationBuilder()
|
||||||
|
.AddCommand<SupportedArgumentTypesCommand>()
|
||||||
|
.UseConsole(console)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(new[]
|
||||||
|
{
|
||||||
|
"cmd", "--int", "-13"
|
||||||
|
});
|
||||||
|
|
||||||
|
var commandInstance = stdOut.GetString().DeserializeJson<SupportedArgumentTypesCommand>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().Be(0);
|
||||||
|
|
||||||
|
commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand
|
||||||
|
{
|
||||||
|
Int = -13
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task All_provided_option_arguments_must_be_bound_to_corresponding_properties()
|
public async Task All_provided_option_arguments_must_be_bound_to_corresponding_properties()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using CliFx.Attributes;
|
||||||
|
|
||||||
|
namespace CliFx.Tests.Commands.Invalid
|
||||||
|
{
|
||||||
|
[Command("cmd")]
|
||||||
|
public class NonLetterCharacterNameCommand : SelfSerializeCommandBase
|
||||||
|
{
|
||||||
|
[CommandOption("0foo")]
|
||||||
|
public string? Apples { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using CliFx.Attributes;
|
||||||
|
|
||||||
|
namespace CliFx.Tests.Commands.Invalid
|
||||||
|
{
|
||||||
|
[Command("cmd")]
|
||||||
|
public class NonLetterCharacterShortNameCommand : SelfSerializeCommandBase
|
||||||
|
{
|
||||||
|
[CommandOption('0')]
|
||||||
|
public string? Apples { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -161,7 +161,9 @@ namespace CliFx.Domain
|
|||||||
var argument = commandLineArguments[index];
|
var argument = commandLineArguments[index];
|
||||||
|
|
||||||
// Name
|
// Name
|
||||||
if (argument.StartsWith("--", StringComparison.Ordinal))
|
if (argument.StartsWith("--", StringComparison.Ordinal) &&
|
||||||
|
argument.Length > 2 &&
|
||||||
|
char.IsLetter(argument[2]))
|
||||||
{
|
{
|
||||||
// Flush previous
|
// Flush previous
|
||||||
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
|
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
|
||||||
@@ -171,7 +173,9 @@ namespace CliFx.Domain
|
|||||||
currentOptionValues = new List<string>();
|
currentOptionValues = new List<string>();
|
||||||
}
|
}
|
||||||
// Short name
|
// Short name
|
||||||
else if (argument.StartsWith('-'))
|
else if (argument.StartsWith('-') &&
|
||||||
|
argument.Length > 1 &&
|
||||||
|
char.IsLetter(argument[1]))
|
||||||
{
|
{
|
||||||
foreach (var alias in argument.Substring(1))
|
foreach (var alias in argument.Substring(1))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -232,6 +232,30 @@ namespace CliFx.Domain
|
|||||||
invalidValidatorOptions
|
invalidValidatorOptions
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var nonLetterFirstCharacterInNameOptions = command.Options
|
||||||
|
.Where(o => !string.IsNullOrWhiteSpace(o.Name) && !char.IsLetter(o.Name[0]))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (nonLetterFirstCharacterInNameOptions.Any())
|
||||||
|
{
|
||||||
|
throw CliFxException.OptionsWithNonLetterCharacterName(
|
||||||
|
command,
|
||||||
|
nonLetterFirstCharacterInNameOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonLetterShortNameOptions = command.Options
|
||||||
|
.Where(o => o.ShortName != null && !char.IsLetter(o.ShortName.Value))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (nonLetterShortNameOptions.Any())
|
||||||
|
{
|
||||||
|
throw CliFxException.OptionsWithNonLetterCharacterShortName(
|
||||||
|
command,
|
||||||
|
nonLetterShortNameOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ValidateCommands(IReadOnlyList<CommandSchema> commands)
|
private static void ValidateCommands(IReadOnlyList<CommandSchema> commands)
|
||||||
|
|||||||
@@ -296,6 +296,32 @@ Specified validators must inherit from {typeof(ArgumentValueValidator<>).FullNam
|
|||||||
|
|
||||||
return new CliFxException(message.Trim());
|
return new CliFxException(message.Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static CliFxException OptionsWithNonLetterCharacterName(
|
||||||
|
CommandSchema command,
|
||||||
|
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||||
|
{
|
||||||
|
var message = $@"
|
||||||
|
Command '{command.Type.FullName}' is invalid because it contains one or more options whose names don't start with a letter character:
|
||||||
|
{invalidOptions.JoinToString(Environment.NewLine)}
|
||||||
|
|
||||||
|
Option names must start with a letter character (i.e. not a digit and not a special character).";
|
||||||
|
|
||||||
|
return new CliFxException(message.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static CliFxException OptionsWithNonLetterCharacterShortName(
|
||||||
|
CommandSchema command,
|
||||||
|
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||||
|
{
|
||||||
|
var message = $@"
|
||||||
|
Command '{command.Type.FullName}' is invalid because it contains one or more options whose short names are not letter characters:
|
||||||
|
{invalidOptions.JoinToString(Environment.NewLine)}
|
||||||
|
|
||||||
|
Option short names must be letter characters (i.e. not digits and not special characters).";
|
||||||
|
|
||||||
|
return new CliFxException(message.Trim());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// End-user-facing exceptions
|
// End-user-facing exceptions
|
||||||
|
|||||||
Reference in New Issue
Block a user