diff --git a/ClosedTypeHierarchyDiagnosticSuppressor.Tests/SwitchExpressionSuppressorTests.cs b/ClosedTypeHierarchyDiagnosticSuppressor.Tests/SwitchExpressionSuppressorTests.cs index 9ad05db..dab8ec5 100644 --- a/ClosedTypeHierarchyDiagnosticSuppressor.Tests/SwitchExpressionSuppressorTests.cs +++ b/ClosedTypeHierarchyDiagnosticSuppressor.Tests/SwitchExpressionSuppressorTests.cs @@ -87,6 +87,86 @@ public static int DoSwitch(Root root) return EnsureNotSuppressed(code, NullableContextOptions.Disable); } + + [Test] + public Task When_type_with_destructure_And_all_subtypes_match_Then_suppress() + { + var code = CodeHelper.WrapInNamespace(TypeHierarchies.Closed.Deconstruct + @" +static class SwitchTest +{ + public static int DoSwitch(Root root) + { + return root switch + { + Root.Leaf1(object value) => 0, + Root.Leaf2 => 1, + }; + } +} +"); + + return EnsureSuppressed(code, NullableContextOptions.Enable); + } + + [Test] + public Task When_type_with_destructure_And_other_Deconstruct_methods_has_all_subtypes_match_Then_suppress() + { + var code = CodeHelper.WrapInNamespace(TypeHierarchies.Closed.Deconstruct + @" +static class SwitchTest +{ + public static int DoSwitch(Root root) + { + return root switch + { + Root.Leaf1(object value, string s) => 0, + Root.Leaf2 => 1, + }; + } +} +"); + + return EnsureSuppressed(code, NullableContextOptions.Enable); + } + + [Test] + public Task When_type_with_destructure_extension_method_has_all_subtypes_match_Then_suppress() + { + var code = CodeHelper.WrapInNamespace(TypeHierarchies.Closed.Deconstruct + @" +static class SwitchTest +{ + public static int DoSwitch(Root root) + { + return root switch + { + Root.Leaf1(object value, string s, object otherValue) => 0, + Root.Leaf2 => 1, + }; + } +} +"); + + return EnsureSuppressed(code, NullableContextOptions.Enable); + } + + [Test] + public Task When_type_with_destructure_And_only_base_type_is_matched_Then_do_not_suppress() + { + var code = CodeHelper.WrapInNamespace(TypeHierarchies.Closed.Deconstruct + @" +static class SwitchTest +{ + public static int DoSwitch(Root root) + { + return root switch + { + Root.Leaf1(string value) => 0, + Root.Leaf2 => 1, + }; + } +} +"); + + return EnsureNotSuppressed(code, NullableContextOptions.Enable); + } [Test] public Task When_nullable_is_disabled_And_null_is_matched_on_its_own_Then_suppress() diff --git a/ClosedTypeHierarchyDiagnosticSuppressor.Tests/SwitchStatementSuppressorTests.cs b/ClosedTypeHierarchyDiagnosticSuppressor.Tests/SwitchStatementSuppressorTests.cs index 6a21293..5fef3fb 100644 --- a/ClosedTypeHierarchyDiagnosticSuppressor.Tests/SwitchStatementSuppressorTests.cs +++ b/ClosedTypeHierarchyDiagnosticSuppressor.Tests/SwitchStatementSuppressorTests.cs @@ -94,6 +94,94 @@ public static void DoSwitch(Root root) return EnsureNotSuppressed(code, NullableContextOptions.Disable); } + [Test] + public Task When_type_with_destructure_And_all_subtypes_match_Then_suppress() + { + var code = CodeHelper.WrapInNamespace(TypeHierarchies.Closed.Deconstruct + @" +static class SwitchTest +{ + public static void DoSwitch(Root root) + { + switch(root) + { + case Root.Leaf1(object value): + break; + case Root.Leaf2: + break; + } + } +} +"); + + return EnsureSuppressed(code, NullableContextOptions.Enable); + } + + [Test] + public Task When_type_with_destructure_And_other_Deconstruct_methods_has_all_subtypes_match_Then_suppress() + { + var code = CodeHelper.WrapInNamespace(TypeHierarchies.Closed.Deconstruct + @" +static class SwitchTest +{ + public static void DoSwitch(Root root) + { + switch(root) + { + case Root.Leaf1(object value, string s): + break; + case Root.Leaf2: + break; + } + } +} +"); + + return EnsureSuppressed(code, NullableContextOptions.Enable); + } + + [Test] + public Task When_type_with_destructure_extension_method_has_all_subtypes_match_Then_suppress() + { + var code = CodeHelper.WrapInNamespace(TypeHierarchies.Closed.Deconstruct + @" +static class SwitchTest +{ + public static void DoSwitch(Root root) + { + switch(root) + { + case Root.Leaf1(object value, string s, object otherValue): + break; + case Root.Leaf2: + break; + } + } +} +"); + + return EnsureSuppressed(code, NullableContextOptions.Enable); + } + + [Test] + public Task When_type_with_destructure_And_only_base_type_is_matched_Then_do_not_suppress() + { + var code = CodeHelper.WrapInNamespace(TypeHierarchies.Closed.Deconstruct + @" +static class SwitchTest +{ + public static void DoSwitch(Root root) + { + switch(root) + { + case Root.Leaf1(string value): + break; + case Root.Leaf2: + break; + }; + } +} +"); + + return EnsureNotSuppressed(code, NullableContextOptions.Enable); + } + [Test] public Task When_nullable_is_disabled_And_null_is_matched_on_its_own_Then_suppress() { diff --git a/ClosedTypeHierarchyDiagnosticSuppressor.Tests/TypeHierarchies.cs b/ClosedTypeHierarchyDiagnosticSuppressor.Tests/TypeHierarchies.cs index 371b9bb..b0b7c0b 100644 --- a/ClosedTypeHierarchyDiagnosticSuppressor.Tests/TypeHierarchies.cs +++ b/ClosedTypeHierarchyDiagnosticSuppressor.Tests/TypeHierarchies.cs @@ -37,6 +37,48 @@ public sealed class Leaf2 : Root { public Leaf2(T value) : base(value) {} } public T Value { get; } } +"; + public const string Deconstruct = @" +abstract class Root +{ + Root() {} + public sealed class Leaf1 : Root + { + public Leaf1(object value, string s, object otherValue) + { + Value = value; + S = s; + OtherValue = otherValue; + } + + public object Value { get; } + public string S { get; } + public object OtherValue { get; } + + public void Deconstruct(out object value) + { + value = Value; + } + + public void Deconstruct(out object value, out string s) + { + value = Value; + s = S; + } + } + + public sealed class Leaf2 : Root {} +} + +static class RootExtensions +{ + public static void Deconstruct(this Root.Leaf1 leaf1, out object value, out string s, out object otherValue) + { + value = leaf1.Value; + s = leaf1.S; + otherValue = leaf1.OtherValue; + } +} "; } diff --git a/ClosedTypeHierarchyDiagnosticSuppressor/PatternHelper.cs b/ClosedTypeHierarchyDiagnosticSuppressor/PatternHelper.cs index 16dd2b6..6d84730 100644 --- a/ClosedTypeHierarchyDiagnosticSuppressor/PatternHelper.cs +++ b/ClosedTypeHierarchyDiagnosticSuppressor/PatternHelper.cs @@ -39,21 +39,23 @@ public static bool HandlesTypeWithoutRestrictions(PatternSyntax patternSyntax, I _ => matchedTypeIsSubtype }; - bool IsSubpatternNonRestrictive(SubpatternSyntax subpatternSyntax) => + bool IsRecursivePatternNonRestrictive(RecursivePatternSyntax recursivePatternSyntax) => + (recursivePatternSyntax.PropertyPatternClause?.Subpatterns ?? recursivePatternSyntax.PositionalPatternClause?.Subpatterns) + ?.Select((p, i) => (Pattern: p, Index: i)).All(pair => IsSubpatternNonRestrictive(pair.Pattern, pair.Index)) ?? false; + + bool IsSubpatternNonRestrictive(SubpatternSyntax subpatternSyntax, int index) => subpatternSyntax.Pattern switch { VarPatternSyntax => true, RecursivePatternSyntax sub => IsRecursivePatternNonRestrictive(sub), - DeclarationPatternSyntax declaration => IsDeclarationPatternNonRestrictive(declaration, subpatternSyntax), + DeclarationPatternSyntax declaration => IsDeclarationPatternNonRestrictive(declaration, subpatternSyntax, index), _ => false - - }; - bool IsRecursivePatternNonRestrictive(RecursivePatternSyntax recursivePatternSyntax) => - recursivePatternSyntax.PropertyPatternClause?.Subpatterns.All(IsSubpatternNonRestrictive) ?? false; - - bool IsDeclarationPatternNonRestrictive(DeclarationPatternSyntax declarationPatternSyntax, SubpatternSyntax containingSubpatternSyntax) + bool IsDeclarationPatternNonRestrictive( + DeclarationPatternSyntax declarationPatternSyntax, + SubpatternSyntax containingSubpatternSyntax, + int index) { SymbolInfo declaredSymbolInfo = model.GetSymbolInfo(declarationPatternSyntax.Type); @@ -70,6 +72,23 @@ bool IsDeclarationPatternNonRestrictive(DeclarationPatternSyntax declarationPatt } } } + + if (containingSubpatternSyntax is { Parent: PositionalPatternClauseSyntax positionalPattern, Pattern: DeclarationPatternSyntax declaration }) + { + var symbol = model.GetSymbolInfo(positionalPattern).Symbol; + if (symbol is IMethodSymbol { Name: "Deconstruct" } deconstructMethod) + { + var correspondingParameterIndex = deconstructMethod.IsExtensionMethod + ? index + 1 + : index; + var positionalType = deconstructMethod.Parameters[correspondingParameterIndex].Type; + + if (compilation.HasImplicitConversion(positionalType, declaredType)) + { + return true; + } + } + } } return false;