mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d33c5cdad | ||
|
|
e4c899c6c2 | ||
|
|
35b3ad0d63 | ||
|
|
4e70557b47 | ||
|
|
0a8d58255a | ||
|
|
d3fbc9c643 | ||
|
|
1cbf8776be | ||
|
|
16e33f7b8f | ||
|
|
5c848056c5 | ||
|
|
864efd3179 | ||
|
|
7f206a0c77 | ||
|
|
22c15f8ec6 |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -4,7 +4,9 @@ on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
main:
|
||||
uses: Tyrrrz/.github/.github/workflows/NuGet.yml@master
|
||||
uses: Tyrrrz/.github/.github/workflows/nuget.yml@master
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
### v2.2.4 (22-Apr-2022)
|
||||
|
||||
- Added more contextual information to analyzer diagnostics.
|
||||
- Fixed an issue where the analyzer incorrectly reported an error on converters that didn't directly match the target type but were compatible through known built-in conversions.
|
||||
- Fixed an issue where MSBuild produced a lot of analyzer-related warnings in certain circumstances.
|
||||
|
||||
### v2.2.3 (17-Apr-2022)
|
||||
|
||||
- Changed method signature of `IConsole.ReadKey()` to return `ConsoleKeyInfo` instead of `void`. The return type was originally defined as `void` by mistake. This change is source-backwards-compatible but may break on binary level if you were previously calling this method indirectly (i.e. through a library).
|
||||
@@ -140,4 +146,4 @@
|
||||
|
||||
- Changed `IConsole` interface (and as a result, `SystemConsole` and `VirtualConsole`) to support writing binary data. Instead of `TextReader`/`TextWriter` instances, the streams are now exposed as `StreamReader`/`StreamWriter` which provide the `BaseStream` property that allows raw access. Existing usages inside commands should remain the same because `StreamReader`/`StreamWriter` are compatible with their base classes `TextReader`/`TextWriter`, but if you were using `VirtualConsole` in tests, you may have to update it to the new API. Refer to the readme for more info.
|
||||
- Changed argument binding behavior so that an error is produced if the user provides an argument that doesn't match with any parameter or option. This is done in order to improve user experience, as otherwise the user may make a typo without knowing that their input wasn't taken into account.
|
||||
- Changed argument binding behavior so that options can be set to multiple argument values while specifying them with mixed naming. For example, `--option value1 -o value2 --option value3` would result in the option being set to corresponding three values, assuming `--option` and `-o` match with the same option.
|
||||
- Changed argument binding behavior so that options can be set to multiple argument values while specifying them with mixed naming. For example, `--option value1 -o value2 --option value3` would result in the option being set to corresponding three values, assuming `--option` and `-o` match with the same option.
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Basic.Reference.Assemblies" Version="1.2.4" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.3.0" PrivateAssets="all" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.5.1" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.4.1" PrivateAssets="all" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.6.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
|
||||
@@ -9,7 +9,7 @@ public class OptionMustHaveValidConverterAnalyzerSpecs
|
||||
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidConverterAnalyzer();
|
||||
|
||||
[Fact]
|
||||
public void Analyzer_reports_an_error_if_the_specified_option_converter_does_not_derive_from_BindingConverter()
|
||||
public void Analyzer_reports_an_error_if_an_option_has_a_converter_that_does_not_derive_from_BindingConverter()
|
||||
{
|
||||
// Arrange
|
||||
// language=cs
|
||||
@@ -33,7 +33,7 @@ public class MyCommand : ICommand
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyzer_reports_an_error_if_the_specified_option_converter_does_not_derive_from_a_compatible_BindingConverter()
|
||||
public void Analyzer_reports_an_error_if_an_option_has_a_converter_that_does_not_derive_from_a_compatible_BindingConverter()
|
||||
{
|
||||
// Arrange
|
||||
// language=cs
|
||||
@@ -57,7 +57,7 @@ public class MyCommand : ICommand
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyzer_does_not_report_an_error_if_the_specified_option_converter_derives_from_a_compatible_BindingConverter()
|
||||
public void Analyzer_does_not_report_an_error_if_an_option_has_a_converter_that_derives_from_a_compatible_BindingConverter()
|
||||
{
|
||||
// Arrange
|
||||
// language=cs
|
||||
@@ -80,6 +80,54 @@ public class MyCommand : ICommand
|
||||
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
|
||||
// language=cs
|
||||
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; set; }
|
||||
|
||||
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
|
||||
// language=cs
|
||||
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; set; }
|
||||
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ public class OptionMustHaveValidValidatorsAnalyzerSpecs
|
||||
private static DiagnosticAnalyzer Analyzer { get; } = new OptionMustHaveValidValidatorsAnalyzer();
|
||||
|
||||
[Fact]
|
||||
public void Analyzer_reports_an_error_if_one_of_the_specified_option_validators_does_not_derive_from_BindingValidator()
|
||||
public void Analyzer_reports_an_error_if_an_option_has_a_validator_that_does_not_derive_from_BindingValidator()
|
||||
{
|
||||
// Arrange
|
||||
// language=cs
|
||||
@@ -33,7 +33,7 @@ public class MyCommand : ICommand
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyzer_reports_an_error_if_one_of_the_specified_option_validators_does_not_derive_from_a_compatible_BindingValidator()
|
||||
public void Analyzer_reports_an_error_if_an_option_has_a_validator_that_does_not_derive_from_a_compatible_BindingValidator()
|
||||
{
|
||||
// Arrange
|
||||
// language=cs
|
||||
@@ -57,7 +57,7 @@ public class MyCommand : ICommand
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyzer_does_not_report_an_error_if_each_specified_option_validator_derives_from_a_compatible_BindingValidator()
|
||||
public void Analyzer_does_not_report_an_error_if_an_option_has_validators_that_all_derive_from_compatible_BindingValidators()
|
||||
{
|
||||
// Arrange
|
||||
// language=cs
|
||||
|
||||
@@ -9,7 +9,7 @@ public class ParameterMustHaveValidConverterAnalyzerSpecs
|
||||
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveValidConverterAnalyzer();
|
||||
|
||||
[Fact]
|
||||
public void Analyzer_reports_an_error_if_the_specified_parameter_converter_does_not_derive_from_BindingConverter()
|
||||
public void Analyzer_reports_an_error_if_a_parameter_has_a_converter_that_does_not_derive_from_BindingConverter()
|
||||
{
|
||||
// Arrange
|
||||
// language=cs
|
||||
@@ -33,7 +33,7 @@ public class MyCommand : ICommand
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyzer_reports_an_error_if_the_specified_parameter_converter_does_not_derive_from_a_compatible_BindingConverter()
|
||||
public void Analyzer_reports_an_error_if_a_parameter_has_a_converter_that_does_not_derive_from_a_compatible_BindingConverter()
|
||||
{
|
||||
// Arrange
|
||||
// language=cs
|
||||
@@ -58,7 +58,7 @@ public class MyCommand : ICommand
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyzer_does_not_report_an_error_if_the_specified_parameter_converter_derives_from_a_compatible_BindingConverter()
|
||||
public void Analyzer_does_not_report_an_error_if_a_parameter_has_a_converter_that_derives_from_a_compatible_BindingConverter()
|
||||
{
|
||||
// Arrange
|
||||
// language=cs
|
||||
@@ -81,6 +81,54 @@ public class MyCommand : ICommand
|
||||
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
|
||||
// language=cs
|
||||
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; set; }
|
||||
|
||||
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
|
||||
// language=cs
|
||||
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 IReadOnlyList<string> Foo { get; set; }
|
||||
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ public class ParameterMustHaveValidValidatorsAnalyzerSpecs
|
||||
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustHaveValidValidatorsAnalyzer();
|
||||
|
||||
[Fact]
|
||||
public void Analyzer_reports_an_error_if_one_of_the_specified_parameter_validators_does_not_derive_from_BindingValidator()
|
||||
public void Analyzer_reports_an_error_a_parameter_has_a_validator_that_does_not_derive_from_BindingValidator()
|
||||
{
|
||||
// Arrange
|
||||
// language=cs
|
||||
@@ -33,7 +33,7 @@ public class MyCommand : ICommand
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyzer_reports_an_error_if_one_of_the_specified_parameter_validators_does_not_derive_from_a_compatible_BindingValidator()
|
||||
public void Analyzer_reports_an_error_if_a_parameter_has_a_validator_that_does_not_derive_from_a_compatible_BindingValidator()
|
||||
{
|
||||
// Arrange
|
||||
// language=cs
|
||||
@@ -57,7 +57,7 @@ public class MyCommand : ICommand
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyzer_does_not_report_an_error_if_each_specified_parameter_validator_derives_from_a_compatible_BindingValidator()
|
||||
public void Analyzer_does_not_report_an_error_if_a_parameter_has_validators_that_all_derive_from_compatible_BindingValidators()
|
||||
{
|
||||
// Arrange
|
||||
// language=cs
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" PrivateAssets="all" />
|
||||
<!-- Keep this dependency as low as possible since we can't bundle it -->
|
||||
<!-- https://github.com/Tyrrrz/CliFx/issues/127 -->
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -5,8 +5,10 @@ using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CliFx.Analyzers.ObjectModel;
|
||||
|
||||
internal partial class CommandOptionSymbol
|
||||
internal partial class CommandOptionSymbol : ICommandMemberSymbol
|
||||
{
|
||||
public IPropertySymbol Property { get; }
|
||||
|
||||
public string? Name { get; }
|
||||
|
||||
public char? ShortName { get; }
|
||||
@@ -16,11 +18,13 @@ internal partial class CommandOptionSymbol
|
||||
public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; }
|
||||
|
||||
public CommandOptionSymbol(
|
||||
IPropertySymbol property,
|
||||
string? name,
|
||||
char? shortName,
|
||||
ITypeSymbol? converterType,
|
||||
IReadOnlyList<ITypeSymbol> validatorTypes)
|
||||
{
|
||||
Property = property;
|
||||
Name = name;
|
||||
ShortName = shortName;
|
||||
ConverterType = converterType;
|
||||
@@ -30,22 +34,25 @@ internal partial class CommandOptionSymbol
|
||||
|
||||
internal partial class CommandOptionSymbol
|
||||
{
|
||||
private static AttributeData? TryGetOptionAttribute(IPropertySymbol property) =>
|
||||
property
|
||||
.GetAttributes()
|
||||
.FirstOrDefault(a => a.AttributeClass.DisplayNameMatches(SymbolNames.CliFxCommandOptionAttribute));
|
||||
private static AttributeData? TryGetOptionAttribute(IPropertySymbol property) => property
|
||||
.GetAttributes()
|
||||
.FirstOrDefault(a => a.AttributeClass?.DisplayNameMatches(SymbolNames.CliFxCommandOptionAttribute) == true);
|
||||
|
||||
private static CommandOptionSymbol FromAttribute(AttributeData attribute)
|
||||
public static CommandOptionSymbol? TryResolve(IPropertySymbol property)
|
||||
{
|
||||
var attribute = TryGetOptionAttribute(property);
|
||||
if (attribute is null)
|
||||
return null;
|
||||
|
||||
var name = attribute
|
||||
.ConstructorArguments
|
||||
.Where(a => a.Type.DisplayNameMatches("string") || a.Type.DisplayNameMatches("System.String"))
|
||||
.Where(a => a.Type?.SpecialType == SpecialType.System_String)
|
||||
.Select(a => a.Value)
|
||||
.FirstOrDefault() as string;
|
||||
|
||||
var shortName = attribute
|
||||
.ConstructorArguments
|
||||
.Where(a => a.Type.DisplayNameMatches("char") || a.Type.DisplayNameMatches("System.Char"))
|
||||
.Where(a => a.Type?.SpecialType == SpecialType.System_Char)
|
||||
.Select(a => a.Value)
|
||||
.FirstOrDefault() as char?;
|
||||
|
||||
@@ -64,16 +71,7 @@ internal partial class CommandOptionSymbol
|
||||
.Cast<ITypeSymbol>()
|
||||
.ToArray();
|
||||
|
||||
return new CommandOptionSymbol(name, shortName, converter, validators);
|
||||
}
|
||||
|
||||
public static CommandOptionSymbol? TryResolve(IPropertySymbol property)
|
||||
{
|
||||
var attribute = TryGetOptionAttribute(property);
|
||||
|
||||
return attribute is not null
|
||||
? FromAttribute(attribute)
|
||||
: null;
|
||||
return new CommandOptionSymbol(property, name, shortName, converter, validators);
|
||||
}
|
||||
|
||||
public static bool IsOptionProperty(IPropertySymbol property) =>
|
||||
|
||||
@@ -5,8 +5,10 @@ using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CliFx.Analyzers.ObjectModel;
|
||||
|
||||
internal partial class CommandParameterSymbol
|
||||
internal partial class CommandParameterSymbol : ICommandMemberSymbol
|
||||
{
|
||||
public IPropertySymbol Property { get; }
|
||||
|
||||
public int Order { get; }
|
||||
|
||||
public string? Name { get; }
|
||||
@@ -18,12 +20,14 @@ internal partial class CommandParameterSymbol
|
||||
public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; }
|
||||
|
||||
public CommandParameterSymbol(
|
||||
IPropertySymbol property,
|
||||
int order,
|
||||
string? name,
|
||||
bool? isRequired,
|
||||
ITypeSymbol? converterType,
|
||||
IReadOnlyList<ITypeSymbol> validatorTypes)
|
||||
{
|
||||
Property = property;
|
||||
Order = order;
|
||||
Name = name;
|
||||
IsRequired = isRequired;
|
||||
@@ -34,13 +38,16 @@ internal partial class CommandParameterSymbol
|
||||
|
||||
internal partial class CommandParameterSymbol
|
||||
{
|
||||
private static AttributeData? TryGetParameterAttribute(IPropertySymbol property) =>
|
||||
property
|
||||
.GetAttributes()
|
||||
.FirstOrDefault(a => a.AttributeClass.DisplayNameMatches(SymbolNames.CliFxCommandParameterAttribute));
|
||||
private static AttributeData? TryGetParameterAttribute(IPropertySymbol property) => property
|
||||
.GetAttributes()
|
||||
.FirstOrDefault(a => a.AttributeClass?.DisplayNameMatches(SymbolNames.CliFxCommandParameterAttribute) == true);
|
||||
|
||||
private static CommandParameterSymbol FromAttribute(AttributeData attribute)
|
||||
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)
|
||||
@@ -73,16 +80,7 @@ internal partial class CommandParameterSymbol
|
||||
.Cast<ITypeSymbol>()
|
||||
.ToArray();
|
||||
|
||||
return new CommandParameterSymbol(order, name, isRequired, converter, validators);
|
||||
}
|
||||
|
||||
public static CommandParameterSymbol? TryResolve(IPropertySymbol property)
|
||||
{
|
||||
var attribute = TryGetParameterAttribute(property);
|
||||
|
||||
return attribute is not null
|
||||
? FromAttribute(attribute)
|
||||
: null;
|
||||
return new CommandParameterSymbol(property, order, name, isRequired, converter, validators);
|
||||
}
|
||||
|
||||
public static bool IsParameterProperty(IPropertySymbol property) =>
|
||||
|
||||
21
CliFx.Analyzers/ObjectModel/ICommandMemberSymbol.cs
Normal file
21
CliFx.Analyzers/ObjectModel/ICommandMemberSymbol.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Analyzers.Utils.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CliFx.Analyzers.ObjectModel;
|
||||
|
||||
internal interface ICommandMemberSymbol
|
||||
{
|
||||
IPropertySymbol Property { get; }
|
||||
|
||||
ITypeSymbol? ConverterType { get; }
|
||||
|
||||
IReadOnlyList<ITypeSymbol> ValidatorTypes { get; }
|
||||
}
|
||||
|
||||
internal static class CommandMemberSymbolExtensions
|
||||
{
|
||||
public static bool IsScalar(this ICommandMemberSymbol member) =>
|
||||
member.Property.Type.SpecialType == SpecialType.System_String ||
|
||||
member.Property.Type.TryGetEnumerableUnderlyingType() is null;
|
||||
}
|
||||
@@ -15,7 +15,8 @@ public class OptionMustHaveUniqueNameAnalyzer : AnalyzerBase
|
||||
: base(
|
||||
"Options must have unique names",
|
||||
"This option's name must be unique within the command (comparison IS NOT case sensitive). " +
|
||||
"Specified name: '{0}'.")
|
||||
"Specified name: `{0}`. " +
|
||||
"Property bound to another option with the same name: `{1}`.")
|
||||
{
|
||||
}
|
||||
|
||||
@@ -55,7 +56,8 @@ public class OptionMustHaveUniqueNameAnalyzer : AnalyzerBase
|
||||
context.ReportDiagnostic(
|
||||
CreateDiagnostic(
|
||||
propertyDeclaration.Identifier.GetLocation(),
|
||||
option.Name
|
||||
option.Name,
|
||||
otherProperty.Name
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ public class OptionMustHaveUniqueShortNameAnalyzer : AnalyzerBase
|
||||
: base(
|
||||
"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}'.")
|
||||
"Specified short name: `{0}` " +
|
||||
"Property bound to another option with the same short name: `{1}`.")
|
||||
{
|
||||
}
|
||||
|
||||
@@ -54,7 +55,8 @@ public class OptionMustHaveUniqueShortNameAnalyzer : AnalyzerBase
|
||||
context.ReportDiagnostic(
|
||||
CreateDiagnostic(
|
||||
propertyDeclaration.Identifier.GetLocation(),
|
||||
option.ShortName
|
||||
option.ShortName,
|
||||
otherProperty.Name
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,16 @@ public class OptionMustHaveValidConverterAnalyzer : AnalyzerBase
|
||||
.FirstOrDefault();
|
||||
|
||||
// Value returned by the converter must be assignable to the property type
|
||||
if (converterValueType is null || !property.Type.IsAssignableFrom(converterValueType))
|
||||
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())
|
||||
|
||||
@@ -13,7 +13,7 @@ public class OptionMustHaveValidNameAnalyzer : AnalyzerBase
|
||||
: base(
|
||||
"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}'.")
|
||||
"Specified name: `{0}`.")
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ public class OptionMustHaveValidShortNameAnalyzer : AnalyzerBase
|
||||
: base(
|
||||
"Option short names must be letter characters",
|
||||
"This option's short name must be a single letter character. " +
|
||||
"Specified short name: '{0}'.")
|
||||
"Specified short name: `{0}`.")
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,11 @@ public class OptionMustHaveValidValidatorsAnalyzer : AnalyzerBase
|
||||
.FirstOrDefault();
|
||||
|
||||
// Value passed to the validator must be assignable from the property type
|
||||
if (validatorValueType is null || !validatorValueType.IsAssignableFrom(property.Type))
|
||||
var isCompatible =
|
||||
validatorValueType is not null &&
|
||||
context.Compilation.IsAssignable(property.Type, validatorValueType);
|
||||
|
||||
if (!isCompatible)
|
||||
{
|
||||
context.ReportDiagnostic(
|
||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
||||
|
||||
@@ -13,7 +13,8 @@ public class ParameterMustBeLastIfNonRequiredAnalyzer : AnalyzerBase
|
||||
public ParameterMustBeLastIfNonRequiredAnalyzer()
|
||||
: base(
|
||||
"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).")
|
||||
"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}`.")
|
||||
{
|
||||
}
|
||||
|
||||
@@ -48,7 +49,10 @@ public class ParameterMustBeLastIfNonRequiredAnalyzer : AnalyzerBase
|
||||
if (otherParameter.Order > parameter.Order)
|
||||
{
|
||||
context.ReportDiagnostic(
|
||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
||||
CreateDiagnostic(
|
||||
propertyDeclaration.Identifier.GetLocation(),
|
||||
otherProperty.Name
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,11 @@ public class ParameterMustBeLastIfNonScalarAnalyzer : AnalyzerBase
|
||||
public ParameterMustBeLastIfNonScalarAnalyzer()
|
||||
: base(
|
||||
"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).")
|
||||
"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 static bool IsScalar(ITypeSymbol type) =>
|
||||
type.DisplayNameMatches("string") ||
|
||||
type.DisplayNameMatches("System.String") ||
|
||||
!type.AllInterfaces
|
||||
.Select(i => i.ConstructedFrom)
|
||||
.Any(t => t.DisplayNameMatches("System.Collections.Generic.IEnumerable<T>"));
|
||||
|
||||
private void Analyze(
|
||||
SyntaxNodeAnalysisContext context,
|
||||
PropertyDeclarationSyntax propertyDeclaration,
|
||||
@@ -32,13 +26,13 @@ public class ParameterMustBeLastIfNonScalarAnalyzer : AnalyzerBase
|
||||
if (property.ContainingType is null)
|
||||
return;
|
||||
|
||||
if (IsScalar(property.Type))
|
||||
return;
|
||||
|
||||
var parameter = CommandParameterSymbol.TryResolve(property);
|
||||
if (parameter is null)
|
||||
return;
|
||||
|
||||
if (parameter.IsScalar())
|
||||
return;
|
||||
|
||||
var otherProperties = property
|
||||
.ContainingType
|
||||
.GetMembers()
|
||||
@@ -55,7 +49,10 @@ public class ParameterMustBeLastIfNonScalarAnalyzer : AnalyzerBase
|
||||
if (otherParameter.Order > parameter.Order)
|
||||
{
|
||||
context.ReportDiagnostic(
|
||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
||||
CreateDiagnostic(
|
||||
propertyDeclaration.Identifier.GetLocation(),
|
||||
otherProperty.Name
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ public class ParameterMustBeSingleIfNonRequiredAnalyzer : AnalyzerBase
|
||||
public ParameterMustBeSingleIfNonRequiredAnalyzer()
|
||||
: base(
|
||||
"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.")
|
||||
"This parameter is non-required so it must be the only such parameter in the command. " +
|
||||
"Property bound to another non-required parameter: `{0}`.")
|
||||
{
|
||||
}
|
||||
|
||||
@@ -48,7 +49,10 @@ public class ParameterMustBeSingleIfNonRequiredAnalyzer : AnalyzerBase
|
||||
if (otherParameter.IsRequired == false)
|
||||
{
|
||||
context.ReportDiagnostic(
|
||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
||||
CreateDiagnostic(
|
||||
propertyDeclaration.Identifier.GetLocation(),
|
||||
otherProperty.Name
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,11 @@ public class ParameterMustBeSingleIfNonScalarAnalyzer : AnalyzerBase
|
||||
public ParameterMustBeSingleIfNonScalarAnalyzer()
|
||||
: base(
|
||||
"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.")
|
||||
"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 static bool IsScalar(ITypeSymbol type) =>
|
||||
type.DisplayNameMatches("string") ||
|
||||
type.DisplayNameMatches("System.String") ||
|
||||
!type.AllInterfaces
|
||||
.Select(i => i.ConstructedFrom)
|
||||
.Any(t => t.DisplayNameMatches("System.Collections.Generic.IEnumerable<T>"));
|
||||
|
||||
private void Analyze(
|
||||
SyntaxNodeAnalysisContext context,
|
||||
PropertyDeclarationSyntax propertyDeclaration,
|
||||
@@ -32,10 +26,11 @@ public class ParameterMustBeSingleIfNonScalarAnalyzer : AnalyzerBase
|
||||
if (property.ContainingType is null)
|
||||
return;
|
||||
|
||||
if (!CommandParameterSymbol.IsParameterProperty(property))
|
||||
var parameter = CommandParameterSymbol.TryResolve(property);
|
||||
if (parameter is null)
|
||||
return;
|
||||
|
||||
if (IsScalar(property.Type))
|
||||
if (parameter.IsScalar())
|
||||
return;
|
||||
|
||||
var otherProperties = property
|
||||
@@ -47,13 +42,17 @@ public class ParameterMustBeSingleIfNonScalarAnalyzer : AnalyzerBase
|
||||
|
||||
foreach (var otherProperty in otherProperties)
|
||||
{
|
||||
if (!CommandParameterSymbol.IsParameterProperty(otherProperty))
|
||||
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
|
||||
if (otherParameter is null)
|
||||
continue;
|
||||
|
||||
if (!IsScalar(otherProperty.Type))
|
||||
if (!otherParameter.IsScalar())
|
||||
{
|
||||
context.ReportDiagnostic(
|
||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
||||
CreateDiagnostic(
|
||||
propertyDeclaration.Identifier.GetLocation(),
|
||||
otherProperty.Name
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ public class ParameterMustHaveUniqueNameAnalyzer : AnalyzerBase
|
||||
: base(
|
||||
"Parameters must have unique names",
|
||||
"This parameter's name must be unique within the command (comparison IS NOT case sensitive). " +
|
||||
"Specified name: '{0}'.")
|
||||
"Specified name: `{0}`. " +
|
||||
"Property bound to another parameter with the same name: `{1}`.")
|
||||
{
|
||||
}
|
||||
|
||||
@@ -55,7 +56,8 @@ public class ParameterMustHaveUniqueNameAnalyzer : AnalyzerBase
|
||||
context.ReportDiagnostic(
|
||||
CreateDiagnostic(
|
||||
propertyDeclaration.Identifier.GetLocation(),
|
||||
parameter.Name
|
||||
parameter.Name,
|
||||
otherProperty.Name
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ public class ParameterMustHaveUniqueOrderAnalyzer : AnalyzerBase
|
||||
: base(
|
||||
"Parameters must have unique order",
|
||||
"This parameter's order must be unique within the command. " +
|
||||
"Specified order: {0}.")
|
||||
"Specified order: {0}. " +
|
||||
"Property bound to another parameter with the same order: `{1}`.")
|
||||
{
|
||||
}
|
||||
|
||||
@@ -48,7 +49,8 @@ public class ParameterMustHaveUniqueOrderAnalyzer : AnalyzerBase
|
||||
context.ReportDiagnostic(
|
||||
CreateDiagnostic(
|
||||
propertyDeclaration.Identifier.GetLocation(),
|
||||
parameter.Order
|
||||
parameter.Order,
|
||||
otherProperty.Name
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,16 @@ public class ParameterMustHaveValidConverterAnalyzer : AnalyzerBase
|
||||
.FirstOrDefault();
|
||||
|
||||
// Value returned by the converter must be assignable to the property type
|
||||
if (converterValueType is null || !property.Type.IsAssignableFrom(converterValueType))
|
||||
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())
|
||||
|
||||
@@ -35,7 +35,11 @@ public class ParameterMustHaveValidValidatorsAnalyzer : AnalyzerBase
|
||||
.FirstOrDefault();
|
||||
|
||||
// Value passed to the validator must be assignable from the property type
|
||||
if (validatorValueType is null || !validatorValueType.IsAssignableFrom(property.Type))
|
||||
var isCompatible =
|
||||
validatorValueType is not null &&
|
||||
context.Compilation.IsAssignable(property.Type, validatorValueType);
|
||||
|
||||
if (!isCompatible)
|
||||
{
|
||||
context.ReportDiagnostic(
|
||||
CreateDiagnostic(propertyDeclaration.Identifier.GetLocation())
|
||||
|
||||
@@ -29,10 +29,13 @@ internal static class RoslynExtensions
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsAssignableFrom(this ITypeSymbol target, ITypeSymbol source) =>
|
||||
SymbolEqualityComparer.Default.Equals(target, source) ||
|
||||
source.GetBaseTypes().Contains(target, SymbolEqualityComparer.Default) ||
|
||||
source.AllInterfaces.Contains(target, SymbolEqualityComparer.Default);
|
||||
public static ITypeSymbol? TryGetEnumerableUnderlyingType(this ITypeSymbol type) => type
|
||||
.AllInterfaces
|
||||
.FirstOrDefault(i => i.ConstructedFrom.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T)?
|
||||
.TypeArguments[0];
|
||||
|
||||
public static bool IsAssignable(this Compilation compilation, ITypeSymbol source, ITypeSymbol destination) =>
|
||||
compilation.ClassifyConversion(source, destination).Exists;
|
||||
|
||||
public static void HandleClassDeclaration(
|
||||
this AnalysisContext analysisContext,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
|
||||
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
|
||||
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Reflection;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CliFx.Tests.Dummy;
|
||||
@@ -14,9 +15,17 @@ public static partial class Program
|
||||
|
||||
public static partial class Program
|
||||
{
|
||||
public static async Task Main() =>
|
||||
public static async Task Main()
|
||||
{
|
||||
// Make sure color codes are not produced because we rely on the output in tests
|
||||
Environment.SetEnvironmentVariable(
|
||||
"DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION",
|
||||
"false"
|
||||
);
|
||||
|
||||
await new CliApplicationBuilder()
|
||||
.AddCommandsFromThisAssembly()
|
||||
.Build()
|
||||
.RunAsync();
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Basic.Reference.Assemblies" Version="1.2.4" />
|
||||
<PackageReference Include="CliWrap" Version="3.4.1" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.5.1" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.3.0" PrivateAssets="all" />
|
||||
<PackageReference Include="CliWrap" Version="3.4.3" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.6.0" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.4.1" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
|
||||
@@ -33,10 +33,8 @@
|
||||
|
||||
<!-- Pack the analyzer assembly inside the package -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../CliFx.Analyzers/CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
|
||||
<ProjectReference Include="../CliFx.Analyzers/CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/CliFx.Analyzers.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/Microsoft.CodeAnalysis.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/Microsoft.CodeAnalysis.CSharp.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" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>2.2.3</Version>
|
||||
<Version>2.2.4</Version>
|
||||
<Company>Tyrrrz</Company>
|
||||
<Copyright>Copyright (C) Oleksii Holub</Copyright>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
<config>
|
||||
<add key="defaultPushSource" value="https://api.nuget.org/v3/index.json" />
|
||||
</config>
|
||||
</configuration>
|
||||
@@ -45,7 +45,7 @@ To learn more about the war and how you can help, [click here](https://tyrrrz.me
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
@@ -716,4 +716,4 @@ In such case, the values of the environment variable will be split by `Path.Path
|
||||
|
||||
## Etymology
|
||||
|
||||
**CliFx** is made out of "Cli" for "Command Line Interface" and "Fx" for "Framework". It's pronounced as "cliff ex".
|
||||
**CliFx** is made out of "Cli" for "Command Line Interface" and "Fx" for "Framework". It's pronounced as "cliff ex".
|
||||
Reference in New Issue
Block a user