From 864efd317970e154aa61604efb762318fd278202 Mon Sep 17 00:00:00 2001 From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com> Date: Wed, 20 Apr 2022 20:09:14 +0300 Subject: [PATCH] Fix converter analyzer false positive when handling non-scalars or nullable types --- ...tionMustHaveValidConverterAnalyzerSpecs.cs | 54 +++++++++++++++++-- ...ionMustHaveValidValidatorsAnalyzerSpecs.cs | 6 +-- ...eterMustHaveValidConverterAnalyzerSpecs.cs | 54 +++++++++++++++++-- ...terMustHaveValidValidatorsAnalyzerSpecs.cs | 6 +-- .../ObjectModel/CommandOptionSymbol.cs | 34 ++++++------ .../ObjectModel/CommandParameterSymbol.cs | 30 +++++------ .../ObjectModel/ICommandMemberSymbol.cs | 21 ++++++++ .../OptionMustHaveValidConverterAnalyzer.cs | 11 +++- .../OptionMustHaveValidValidatorsAnalyzer.cs | 6 ++- .../ParameterMustBeLastIfNonScalarAnalyzer.cs | 13 ++--- ...arameterMustBeSingleIfNonScalarAnalyzer.cs | 17 +++--- ...ParameterMustHaveValidConverterAnalyzer.cs | 11 +++- ...arameterMustHaveValidValidatorsAnalyzer.cs | 6 ++- .../Utils/Extensions/RoslynExtensions.cs | 11 ++-- 14 files changed, 205 insertions(+), 75 deletions(-) create mode 100644 CliFx.Analyzers/ObjectModel/ICommandMemberSymbol.cs diff --git a/CliFx.Analyzers.Tests/OptionMustHaveValidConverterAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveValidConverterAnalyzerSpecs.cs index 447f254..73788b6 100644 --- a/CliFx.Analyzers.Tests/OptionMustHaveValidConverterAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/OptionMustHaveValidConverterAnalyzerSpecs.cs @@ -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 +{ + 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 +{ + public override string Convert(string rawValue) => rawValue; +} + +[Command] +public class MyCommand : ICommand +{ + [CommandOption(""foo"", Converter = typeof(MyConverter))] + public IReadOnlyList 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() { diff --git a/CliFx.Analyzers.Tests/OptionMustHaveValidValidatorsAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/OptionMustHaveValidValidatorsAnalyzerSpecs.cs index 35b0d0c..c9ae82a 100644 --- a/CliFx.Analyzers.Tests/OptionMustHaveValidValidatorsAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/OptionMustHaveValidValidatorsAnalyzerSpecs.cs @@ -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 diff --git a/CliFx.Analyzers.Tests/ParameterMustHaveValidConverterAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustHaveValidConverterAnalyzerSpecs.cs index eed9ef2..8df0bc6 100644 --- a/CliFx.Analyzers.Tests/ParameterMustHaveValidConverterAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/ParameterMustHaveValidConverterAnalyzerSpecs.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 +{ + 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 +{ + public override string Convert(string rawValue) => rawValue; +} + +[Command] +public class MyCommand : ICommand +{ + [CommandParameter(0, Converter = typeof(MyConverter))] + public IReadOnlyList 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() { diff --git a/CliFx.Analyzers.Tests/ParameterMustHaveValidValidatorsAnalyzerSpecs.cs b/CliFx.Analyzers.Tests/ParameterMustHaveValidValidatorsAnalyzerSpecs.cs index 66f67a8..921fc25 100644 --- a/CliFx.Analyzers.Tests/ParameterMustHaveValidValidatorsAnalyzerSpecs.cs +++ b/CliFx.Analyzers.Tests/ParameterMustHaveValidValidatorsAnalyzerSpecs.cs @@ -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 diff --git a/CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs b/CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs index 0f18bd1..e9cf8f9 100644 --- a/CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs +++ b/CliFx.Analyzers/ObjectModel/CommandOptionSymbol.cs @@ -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 ValidatorTypes { get; } public CommandOptionSymbol( + IPropertySymbol property, string? name, char? shortName, ITypeSymbol? converterType, IReadOnlyList 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() .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) => diff --git a/CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs b/CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs index 5a29829..cbda9d7 100644 --- a/CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs +++ b/CliFx.Analyzers/ObjectModel/CommandParameterSymbol.cs @@ -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 ValidatorTypes { get; } public CommandParameterSymbol( + IPropertySymbol property, int order, string? name, bool? isRequired, ITypeSymbol? converterType, IReadOnlyList 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() .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) => diff --git a/CliFx.Analyzers/ObjectModel/ICommandMemberSymbol.cs b/CliFx.Analyzers/ObjectModel/ICommandMemberSymbol.cs new file mode 100644 index 0000000..3fb81d8 --- /dev/null +++ b/CliFx.Analyzers/ObjectModel/ICommandMemberSymbol.cs @@ -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 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; +} \ No newline at end of file diff --git a/CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs index 6fcf08e..7e8bd4c 100644 --- a/CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs +++ b/CliFx.Analyzers/OptionMustHaveValidConverterAnalyzer.cs @@ -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()) diff --git a/CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs b/CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs index ce0afad..091dfc9 100644 --- a/CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs +++ b/CliFx.Analyzers/OptionMustHaveValidValidatorsAnalyzer.cs @@ -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()) diff --git a/CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs b/CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs index f71282a..f698a89 100644 --- a/CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs +++ b/CliFx.Analyzers/ParameterMustBeLastIfNonScalarAnalyzer.cs @@ -17,13 +17,6 @@ public class ParameterMustBeLastIfNonScalarAnalyzer : AnalyzerBase { } - 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")); - private void Analyze( SyntaxNodeAnalysisContext context, PropertyDeclarationSyntax propertyDeclaration, @@ -32,13 +25,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() diff --git a/CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs b/CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs index 5f27252..e014c14 100644 --- a/CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs +++ b/CliFx.Analyzers/ParameterMustBeSingleIfNonScalarAnalyzer.cs @@ -17,13 +17,6 @@ public class ParameterMustBeSingleIfNonScalarAnalyzer : AnalyzerBase { } - 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")); - private void Analyze( SyntaxNodeAnalysisContext context, PropertyDeclarationSyntax propertyDeclaration, @@ -32,10 +25,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,10 +41,11 @@ 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()) diff --git a/CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs b/CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs index dc77fea..f04bdd9 100644 --- a/CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs +++ b/CliFx.Analyzers/ParameterMustHaveValidConverterAnalyzer.cs @@ -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()) diff --git a/CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs b/CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs index 7311b5f..275df82 100644 --- a/CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs +++ b/CliFx.Analyzers/ParameterMustHaveValidValidatorsAnalyzer.cs @@ -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()) diff --git a/CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs b/CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs index 870faf9..d386014 100644 --- a/CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs +++ b/CliFx.Analyzers/Utils/Extensions/RoslynExtensions.cs @@ -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,