Skip to content

Commit

Permalink
Support for structurally detecting cases also for record syntax (Walk…
Browse files Browse the repository at this point in the history
…erCodeRanger#44).

Sets dependencies to Roslyn 3.9.0.
  • Loading branch information
csharper2010 committed Apr 5, 2022
1 parent c5361a1 commit 5938b70
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 4 deletions.
37 changes: 37 additions & 0 deletions ExhaustiveMatching.Analyzer.Tests/CodeContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,43 @@ public sealed class Error : Result<TSuccess, TError> {{
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<TSuccess, TError> {{
private Result() {{ }}
public sealed record Success(TSuccess Value) : Result<TSuccess, TError>;
public sealed record Error(TError value) : Result<TSuccess, TError>;
}}
}}
namespace System.Runtime.CompilerServices {{
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
internal static class IsExternalInit {{
}}
}}";
return string.Format(context, args, body);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.3.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
Expand Down
54 changes: 54 additions & 0 deletions ExhaustiveMatching.Analyzer.Tests/SwitchExpressionAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,60 @@ public async Task SwitchOnStructurallyClosedThrowingExhaustiveMatchAllowNull()
await VerifyCSharpDiagnosticsAsync(source);
}

[Fact]
public async Task SwitchOnStructurallyClosedRecordThrowingExhaustiveMatchFailedIsNotExhaustiveReportsDiagnostic()
{
const string args = "Result<string, string> result";
const string test = @"
var x = result ◊1⟦switch⟧
{
Result<string, string>.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<string, string> result";
const string test = @"
var x = result ◊1⟦switch⟧
{
Result<string, string>.Error error => ""Error: "" + error,
Result<string, string>.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<string, string> result";
const string test = @"
var x = result ◊1⟦switch⟧
{
Result<string, string>.Error error => ""Error: "" + error,
Result<string, string>.Success success => ""Success: "" + success,
null => ""null"",
_ => throw ExhaustiveMatch.Failed(result),
};";

var source = CodeContext.ResultRecord(args, test);

await VerifyCSharpDiagnosticsAsync(source);
}

[Fact]
public async Task SwitchOnStruct()
{
Expand Down
69 changes: 69 additions & 0 deletions ExhaustiveMatching.Analyzer.Tests/SwitchStatementAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,75 @@ public async Task SwitchOnStructurallyClosedThrowingExhaustiveMatchAllowNull()
await VerifyCSharpDiagnosticsAsync(source);
}

[Fact]
public async Task SwitchOnStructurallyClosedRecordThrowingExhaustiveMatchFailedIsNotExhaustiveReportsDiagnostic()
{
const string args = "Result<string, string> result";
const string test = @"
◊1⟦switch⟧ (result)
{
case Result<string, string>.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<string, string> result";
const string test = @"
◊1⟦switch⟧ (result)
{
case Result<string, string>.Error error:
Console.WriteLine(""Error: "" + error);
break;
case Result<string, string>.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<string, string> result";
const string test = @"
◊1⟦switch⟧ (result)
{
case Result<string, string>.Error error:
Console.WriteLine(""Error: "" + error);
break;
case Result<string, string>.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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,10 @@ private static Project CreateProject(IReadOnlyCollection<string> 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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.3.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.9.0" PrivateAssets="all" />
<PackageReference Update="NETStandard.Library" PrivateAssets="all" />
</ItemGroup>

Expand Down
4 changes: 3 additions & 1 deletion ExhaustiveMatching.Analyzer/TypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ITypeSymbol>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RootNamespace>Examples</RootNamespace>
<LangVersion>9.0</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
11 changes: 11 additions & 0 deletions ExhaustiveMatching.Examples/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.ComponentModel;

namespace System.Runtime.CompilerServices {
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit {
}
}
10 changes: 10 additions & 0 deletions ExhaustiveMatching.Examples/ResultRecord.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Examples {
public abstract record ResultRecord<TSuccess, TError>
{
private ResultRecord() { }

public sealed record Success(TSuccess Value) : ResultRecord<TSuccess, TError>;

public sealed record Error(TError Value) : ResultRecord<TSuccess, TError>;
}
}

0 comments on commit 5938b70

Please sign in to comment.