12 Commits
2.2.3 ... 2.2.4

Author SHA1 Message Date
Oleksii Holub
6d33c5cdad Update version 2022-04-22 19:15:44 +03:00
Oleksii Holub
e4c899c6c2 Reduce version of Microsoft.CodeAnalysis.CSharp further 2022-04-22 18:55:47 +03:00
Oleksii Holub
35b3ad0d63 Don't use Fody but also don't bundle Microsoft.CodeAnalysis.CSharp 2022-04-22 18:45:46 +03:00
Oleksii Holub
4e70557b47 Reorganize assets 2022-04-22 17:16:51 +03:00
Oleksii Holub
0a8d58255a Update NuGet.config 2022-04-22 16:58:21 +03:00
Oleksii Holub
d3fbc9c643 Merge analyzer project dependencies using Fody.Costura
Closes #127
2022-04-22 16:41:24 +03:00
Oleksii Holub
1cbf8776be Don't produce color codes in tests 2022-04-21 22:57:14 +03:00
Oleksii Holub
16e33f7b8f Update workflow 2022-04-21 22:51:11 +03:00
Oleksii Holub
5c848056c5 Add more contextual information to diagnostics 2022-04-20 20:27:53 +03:00
Oleksii Holub
864efd3179 Fix converter analyzer false positive when handling non-scalars or nullable types 2022-04-20 20:09:14 +03:00
Oleksii Holub
7f206a0c77 Update test logger 2022-04-19 01:31:40 +00:00
Oleksii Holub
22c15f8ec6 Update test logger 2022-04-18 19:53:52 +00:00
36 changed files with 286 additions and 112 deletions

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -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 }}

View File

@@ -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).

View File

@@ -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" />

View File

@@ -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()
{

View File

@@ -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

View File

@@ -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()
{

View File

@@ -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

View File

@@ -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>

View File

@@ -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) =>

View File

@@ -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) =>

View 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;
}

View File

@@ -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
)
);
}

View File

@@ -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
)
);
}

View File

@@ -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())

View File

@@ -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}`.")
{
}

View File

@@ -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}`.")
{
}

View File

@@ -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())

View File

@@ -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
)
);
}
}

View File

@@ -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
)
);
}
}

View File

@@ -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
)
);
}
}

View File

@@ -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
)
);
}
}

View File

@@ -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
)
);
}

View File

@@ -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
)
);
}

View File

@@ -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())

View File

@@ -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())

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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>

View File

@@ -45,7 +45,7 @@ To learn more about the war and how you can help, [click here](https://tyrrrz.me
## Screenshots
![help screen](.screenshots/help.png)
![help screen](.assets/help-screen.png)
## Usage

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB