From acebc1255d49d139842e5f4813c26c2a3c92a10a Mon Sep 17 00:00:00 2001 From: CSharper2010 Date: Tue, 5 Apr 2022 08:53:52 +0200 Subject: [PATCH] Support for structurally detecting cases also for record syntax (#44). Sets dependencies to Roslyn 3.9.0. --- .../CodeContext.cs | 37 ++++++++++ .../ExhaustiveMatching.Analyzer.Tests.csproj | 2 +- .../SwitchExpressionAnalyzerTests.cs | 54 +++++++++++++++ .../SwitchStatementAnalyzerTests.cs | 69 +++++++++++++++++++ .../Verifiers/DiagnosticVerifier.Helper.cs | 5 +- .../ExhaustiveMatching.Analyzer.csproj | 2 +- .../TypeSymbolExtensions.cs | 4 +- .../ExhaustiveMatching.Examples.csproj | 1 + ExhaustiveMatching.Examples/IsExternalInit.cs | 11 +++ ExhaustiveMatching.Examples/ResultRecord.cs | 12 ++++ 10 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 ExhaustiveMatching.Examples/IsExternalInit.cs create mode 100644 ExhaustiveMatching.Examples/ResultRecord.cs diff --git a/ExhaustiveMatching.Analyzer.Tests/CodeContext.cs b/ExhaustiveMatching.Analyzer.Tests/CodeContext.cs index 153349c..e464ff2 100644 --- a/ExhaustiveMatching.Analyzer.Tests/CodeContext.cs +++ b/ExhaustiveMatching.Analyzer.Tests/CodeContext.cs @@ -94,6 +94,43 @@ public sealed class Error : Result {{ public Error(TError value) {{ Value = value; }} }} }} +}}"; + return string.Format(context, args, body); + } + + public static string ResultRecord(string args, string body) + { + const string context = @"using System; // Result type +using System; +using System.Collections.Generic; +using ExhaustiveMatching; +using TestNamespace; + +class TestClass +{{ + void TestMethod({0}) + {{{1} + }} +}} + +namespace TestNamespace +{{ + public abstract record Result {{ + private Result() {{ }} + + public sealed record Success(TSuccess Value) : Result; + + public sealed record Error(TError value) : Result; + }} +}} + +namespace System.Runtime.CompilerServices {{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + internal static class IsExternalInit {{ + }} }}"; return string.Format(context, args, body); } diff --git a/ExhaustiveMatching.Analyzer.Tests/ExhaustiveMatching.Analyzer.Tests.csproj b/ExhaustiveMatching.Analyzer.Tests/ExhaustiveMatching.Analyzer.Tests.csproj index 6bbf901..ed35a64 100644 --- a/ExhaustiveMatching.Analyzer.Tests/ExhaustiveMatching.Analyzer.Tests.csproj +++ b/ExhaustiveMatching.Analyzer.Tests/ExhaustiveMatching.Analyzer.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/ExhaustiveMatching.Analyzer.Tests/SwitchExpressionAnalyzerTests.cs b/ExhaustiveMatching.Analyzer.Tests/SwitchExpressionAnalyzerTests.cs index ed3d360..bf2dc42 100644 --- a/ExhaustiveMatching.Analyzer.Tests/SwitchExpressionAnalyzerTests.cs +++ b/ExhaustiveMatching.Analyzer.Tests/SwitchExpressionAnalyzerTests.cs @@ -395,6 +395,60 @@ public async Task SwitchOnStructurallyClosedThrowingExhaustiveMatchAllowNull() await VerifyCSharpDiagnosticsAsync(source); } + [Fact] + public async Task SwitchOnStructurallyClosedRecordThrowingExhaustiveMatchFailedIsNotExhaustiveReportsDiagnostic() + { + const string args = "Result result"; + const string test = @" + var x = result ◊1⟦switch⟧ + { + Result.Error error => ""Error: "" + error, + _ => throw ExhaustiveMatch.Failed(result), + };"; + + var source = CodeContext.ResultRecord(args, test); + var expectedSuccess = DiagnosticResult + .Error("EM0003", "Subtype not handled by switch: TestNamespace.Success") + .AddLocation(source, 1); + + await VerifyCSharpDiagnosticsAsync(source, expectedSuccess); + } + + [Fact] + public async Task SwitchOnStructurallyClosedRecordThrowingExhaustiveMatchDoesNotReportsDiagnostic() + { + const string args = "Result result"; + const string test = @" + var x = result ◊1⟦switch⟧ + { + Result.Error error => ""Error: "" + error, + Result.Success success => ""Success: "" + success, + _ => throw ExhaustiveMatch.Failed(result), + };"; + + var source = CodeContext.ResultRecord(args, test); + + await VerifyCSharpDiagnosticsAsync(source); + } + + [Fact] + public async Task SwitchOnStructurallyClosedRecordThrowingExhaustiveMatchAllowNull() + { + const string args = "Result result"; + const string test = @" + var x = result ◊1⟦switch⟧ + { + Result.Error error => ""Error: "" + error, + Result.Success success => ""Success: "" + success, + null => ""null"", + _ => throw ExhaustiveMatch.Failed(result), + };"; + + var source = CodeContext.ResultRecord(args, test); + + await VerifyCSharpDiagnosticsAsync(source); + } + [Fact] public async Task SwitchOnStruct() { diff --git a/ExhaustiveMatching.Analyzer.Tests/SwitchStatementAnalyzerTests.cs b/ExhaustiveMatching.Analyzer.Tests/SwitchStatementAnalyzerTests.cs index d3aff3b..7d4ef1b 100644 --- a/ExhaustiveMatching.Analyzer.Tests/SwitchStatementAnalyzerTests.cs +++ b/ExhaustiveMatching.Analyzer.Tests/SwitchStatementAnalyzerTests.cs @@ -512,6 +512,75 @@ public async Task SwitchOnStructurallyClosedThrowingExhaustiveMatchAllowNull() await VerifyCSharpDiagnosticsAsync(source); } + [Fact] + public async Task SwitchOnStructurallyClosedRecordThrowingExhaustiveMatchFailedIsNotExhaustiveReportsDiagnostic() + { + const string args = "Result result"; + const string test = @" + ◊1⟦switch⟧ (result) + { + case Result.Error error: + Console.WriteLine(""Error: "" + error); + break; + default: + throw ExhaustiveMatch.Failed(result); + }"; + + var source = CodeContext.ResultRecord(args, test); + var expectedSuccess = DiagnosticResult + .Error("EM0003", "Subtype not handled by switch: TestNamespace.Success") + .AddLocation(source, 1); + + await VerifyCSharpDiagnosticsAsync(source, expectedSuccess); + } + + [Fact] + public async Task SwitchOnStructurallyClosedRecordThrowingExhaustiveMatchDoesNotReportsDiagnostic() + { + const string args = "Result result"; + const string test = @" + ◊1⟦switch⟧ (result) + { + case Result.Error error: + Console.WriteLine(""Error: "" + error); + break; + case Result.Success success: + Console.WriteLine(""Success: "" + success); + break; + default: + throw ExhaustiveMatch.Failed(result); + }"; + + var source = CodeContext.ResultRecord(args, test); + + await VerifyCSharpDiagnosticsAsync(source); + } + + [Fact] + public async Task SwitchOnStructurallyClosedRecordThrowingExhaustiveMatchAllowNull() + { + const string args = "Result result"; + const string test = @" + ◊1⟦switch⟧ (result) + { + case Result.Error error: + Console.WriteLine(""Error: "" + error); + break; + case Result.Success success: + Console.WriteLine(""Success: "" + success); + break; + case null: + Console.WriteLine(""null""); + break; + default: + throw ExhaustiveMatch.Failed(result); + }"; + + var source = CodeContext.ResultRecord(args, test); + + await VerifyCSharpDiagnosticsAsync(source); + } + [Fact] public async Task SwitchOnStruct() { diff --git a/ExhaustiveMatching.Analyzer.Tests/Verifiers/DiagnosticVerifier.Helper.cs b/ExhaustiveMatching.Analyzer.Tests/Verifiers/DiagnosticVerifier.Helper.cs index bd69fe5..4303cd2 100644 --- a/ExhaustiveMatching.Analyzer.Tests/Verifiers/DiagnosticVerifier.Helper.cs +++ b/ExhaustiveMatching.Analyzer.Tests/Verifiers/DiagnosticVerifier.Helper.cs @@ -167,7 +167,10 @@ private static Project CreateProject(IReadOnlyCollection sources) solution = solution.AddDocument(documentId, newFileName, SourceText.From(source)); count++; } - return solution.GetProject(projectId); + + var project = solution.GetProject(projectId); + project = project?.WithParseOptions(((CSharpParseOptions) project.ParseOptions ?? new CSharpParseOptions()).WithLanguageVersion(LanguageVersion.CSharp9)); + return project; } #endregion } diff --git a/ExhaustiveMatching.Analyzer/ExhaustiveMatching.Analyzer.csproj b/ExhaustiveMatching.Analyzer/ExhaustiveMatching.Analyzer.csproj index 6a92289..e95880c 100644 --- a/ExhaustiveMatching.Analyzer/ExhaustiveMatching.Analyzer.csproj +++ b/ExhaustiveMatching.Analyzer/ExhaustiveMatching.Analyzer.csproj @@ -9,7 +9,7 @@ - + diff --git a/ExhaustiveMatching.Analyzer/TypeSymbolExtensions.cs b/ExhaustiveMatching.Analyzer/TypeSymbolExtensions.cs index 5f56715..34da21c 100644 --- a/ExhaustiveMatching.Analyzer/TypeSymbolExtensions.cs +++ b/ExhaustiveMatching.Analyzer/TypeSymbolExtensions.cs @@ -211,7 +211,9 @@ public static bool TryGetStructurallyClosedTypeCases(this ITypeSymbol rootType, if (rootType is INamedTypeSymbol namedType && rootType.TypeKind != TypeKind.Error - && namedType.InstanceConstructors.All(c => c.DeclaredAccessibility == Accessibility.Private)) { + && namedType.InstanceConstructors + .All(c => c.DeclaredAccessibility == Accessibility.Private + || rootType.IsRecord && c.DeclaredAccessibility == Accessibility.Protected && c.Parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(c.Parameters[0].Type, rootType))) { var nestedTypes = context.SemanticModel.LookupSymbols(0, rootType) .OfType() diff --git a/ExhaustiveMatching.Examples/ExhaustiveMatching.Examples.csproj b/ExhaustiveMatching.Examples/ExhaustiveMatching.Examples.csproj index 90ad006..c7760ac 100644 --- a/ExhaustiveMatching.Examples/ExhaustiveMatching.Examples.csproj +++ b/ExhaustiveMatching.Examples/ExhaustiveMatching.Examples.csproj @@ -3,6 +3,7 @@ netstandard2.1 Examples + 9.0 diff --git a/ExhaustiveMatching.Examples/IsExternalInit.cs b/ExhaustiveMatching.Examples/IsExternalInit.cs new file mode 100644 index 0000000..eac0316 --- /dev/null +++ b/ExhaustiveMatching.Examples/IsExternalInit.cs @@ -0,0 +1,11 @@ +using System.ComponentModel; + +namespace System.Runtime.CompilerServices { + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit { + } +} diff --git a/ExhaustiveMatching.Examples/ResultRecord.cs b/ExhaustiveMatching.Examples/ResultRecord.cs new file mode 100644 index 0000000..125c9d1 --- /dev/null +++ b/ExhaustiveMatching.Examples/ResultRecord.cs @@ -0,0 +1,12 @@ +namespace Examples { + public abstract record ResultRecord + { + private ResultRecord() { } + + public sealed record Success(TSuccess Value) : ResultRecord; + + public sealed record Error(TError Value) : ResultRecord; + } + + public record OtherResult(string Value) : ResultRecord(new Error(Value)); +}