From f1cf4ee6b1a41fc8b4d898c18a78a385dc3f5c13 Mon Sep 17 00:00:00 2001 From: Jeff Walker Date: Sun, 3 Nov 2019 16:50:32 -0800 Subject: [PATCH] add EM0015 Open Interface Subtype of a Closed Type Must Be a Case --- .../TypeDeclarationAnalyzerTests.cs | 28 ++++++++++++++++++- ExhaustiveMatching.Analyzer.nuspec | 2 +- .../ExhaustiveMatchAnalyzer.cs | 11 +++++++- .../ExhaustiveMatching.Analyzer.csproj | 6 ++-- .../Resources.Designer.cs | 27 ++++++++++++++++++ ExhaustiveMatching.Analyzer/Resources.resx | 9 ++++++ .../TypeDeclarationAnalyzer.cs | 18 +++++++++--- ExhaustiveMatching/ExhaustiveMatching.csproj | 6 ++-- README.md | 4 +++ 9 files changed, 98 insertions(+), 13 deletions(-) diff --git a/ExhaustiveMatching.Analyzer.Tests/TypeDeclarationAnalyzerTests.cs b/ExhaustiveMatching.Analyzer.Tests/TypeDeclarationAnalyzerTests.cs index 1448c2e..080f7ba 100644 --- a/ExhaustiveMatching.Analyzer.Tests/TypeDeclarationAnalyzerTests.cs +++ b/ExhaustiveMatching.Analyzer.Tests/TypeDeclarationAnalyzerTests.cs @@ -10,7 +10,7 @@ namespace ExhaustiveMatching.Analyzer.Tests public class TypeDeclarationAnalyzerTests : CodeFixVerifier { [TestMethod] - public void SubtypeOfClosedTypeMustBeCase() + public void ConcreteSubtypeOfClosedTypeMustBeCase() { const string test = @"using ExhaustiveMatching; namespace TestNamespace @@ -35,6 +35,32 @@ public class Triangle : Shape { } VerifyCSharpDiagnostic(test, expected); } + [TestMethod] + public void OpenInterfaceSubtypeOfClosedTypeMustBeCase() + { + const string test = @"using ExhaustiveMatching; +namespace TestNamespace +{ + [Closed( + typeof(ISquare), + typeof(ICircle))] + public interface IShape { } + public interface ISquare : IShape { } + public interface ICircle : IShape { } + public interface ITriangle : IShape { } +}"; + + var expected = new DiagnosticResult + { + Id = "EM0015", + Message = "Open interface TestNamespace.ITriangle is not a case of its closed supertype: TestNamespace.IShape", + Severity = DiagnosticSeverity.Error, + Locations = new[] { new DiagnosticResultLocation("Test0.cs", 10, 22, 9) } + }; + + VerifyCSharpDiagnostic(test, expected); + } + [TestMethod] public void CaseTypeMustBeSubtype() { diff --git a/ExhaustiveMatching.Analyzer.nuspec b/ExhaustiveMatching.Analyzer.nuspec index 5db73a3..1eec971 100644 --- a/ExhaustiveMatching.Analyzer.nuspec +++ b/ExhaustiveMatching.Analyzer.nuspec @@ -2,7 +2,7 @@ ExhaustiveMatching.Analyzer - 0.3.2 + 0.4.0 Jeff Walker Jeff Walker BSD-3-Clause diff --git a/ExhaustiveMatching.Analyzer/ExhaustiveMatchAnalyzer.cs b/ExhaustiveMatching.Analyzer/ExhaustiveMatchAnalyzer.cs index 652b4ff..e853f44 100644 --- a/ExhaustiveMatching.Analyzer/ExhaustiveMatchAnalyzer.cs +++ b/ExhaustiveMatching.Analyzer/ExhaustiveMatchAnalyzer.cs @@ -40,6 +40,10 @@ public class ExhaustiveMatchAnalyzer : DiagnosticAnalyzer private static readonly LocalizableString EM0014Message = LoadString(nameof(Resources.EM0014Message)); private static readonly LocalizableString EM0014Description = LoadString(Resources.EM0014Description); + private static readonly LocalizableString EM0015Title = LoadString(nameof(Resources.EM0015Title)); + private static readonly LocalizableString EM0015Message = LoadString(nameof(Resources.EM0015Message)); + private static readonly LocalizableString EM0015Description = LoadString(Resources.EM0015Description); + private static readonly LocalizableString EM0100Title = LoadString(nameof(Resources.EM0100Title)); private static readonly LocalizableString EM0100Message = LoadString(nameof(Resources.EM0100Message)); private static readonly LocalizableString EM0100Description = LoadString(Resources.EM0100Description); @@ -86,6 +90,10 @@ public class ExhaustiveMatchAnalyzer : DiagnosticAnalyzer new DiagnosticDescriptor("EM0014", EM0014Title, EM0014Message, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, EM0014Description); + public static readonly DiagnosticDescriptor OpenInterfaceSubtypeMustBeCaseOfClosedType = + new DiagnosticDescriptor("EM0015", EM0015Title, EM0015Message, Category, + DiagnosticSeverity.Error, isEnabledByDefault: true, EM0015Description); + public static readonly DiagnosticDescriptor WhenGuardNotSupported = new DiagnosticDescriptor("EM0100", EM0100Title, EM0100Message, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, EM0100Description); @@ -105,7 +113,8 @@ public class ExhaustiveMatchAnalyzer : DiagnosticAnalyzer public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(NotExhaustiveEnumSwitch, NotExhaustiveObjectSwitch, NotExhaustiveNullableEnumSwitch, ConcreteSubtypeMustBeCaseOfClosedType, - MustBeDirectSubtype, MustBeSubtype, SubtypeMustBeCovered, WhenGuardNotSupported, + MustBeDirectSubtype, MustBeSubtype, SubtypeMustBeCovered, + OpenInterfaceSubtypeMustBeCaseOfClosedType, WhenGuardNotSupported, CaseClauseTypeNotSupported, OpenTypeNotSupported, MatchMustBeOnCaseType); public override void Initialize(AnalysisContext context) diff --git a/ExhaustiveMatching.Analyzer/ExhaustiveMatching.Analyzer.csproj b/ExhaustiveMatching.Analyzer/ExhaustiveMatching.Analyzer.csproj index ce439f2..926a55a 100644 --- a/ExhaustiveMatching.Analyzer/ExhaustiveMatching.Analyzer.csproj +++ b/ExhaustiveMatching.Analyzer/ExhaustiveMatching.Analyzer.csproj @@ -3,9 +3,9 @@ netstandard2.0 false - 0.3.2.0 - 0.3.2.0 - 0.3.2 + 0.4.0.0 + 0.4.0.0 + 0.4.0 diff --git a/ExhaustiveMatching.Analyzer/Resources.Designer.cs b/ExhaustiveMatching.Analyzer/Resources.Designer.cs index 699b491..a951798 100644 --- a/ExhaustiveMatching.Analyzer/Resources.Designer.cs +++ b/ExhaustiveMatching.Analyzer/Resources.Designer.cs @@ -249,6 +249,33 @@ internal static string EM0014Title { } } + /// + /// Looks up a localized string similar to An open interface subtype of a type marked with the "Closed" attribute must be a case of the closed type.. + /// + internal static string EM0015Description { + get { + return ResourceManager.GetString("EM0015Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open interface {0} is not a case of its closed supertype: {1}. + /// + internal static string EM0015Message { + get { + return ResourceManager.GetString("EM0015Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open Interface Subtype of a Closed Type Must Be a Case. + /// + internal static string EM0015Title { + get { + return ResourceManager.GetString("EM0015Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to An exhustive switch statement does not support case clauses with "when" guards. /// diff --git a/ExhaustiveMatching.Analyzer/Resources.resx b/ExhaustiveMatching.Analyzer/Resources.resx index d792b8f..2495789 100644 --- a/ExhaustiveMatching.Analyzer/Resources.resx +++ b/ExhaustiveMatching.Analyzer/Resources.resx @@ -180,6 +180,15 @@ Concrete Subtype of a Closed Type Must Be a Covered + + An open interface subtype of a type marked with the "Closed" attribute must be a case of the closed type. + + + Open interface {0} is not a case of its closed supertype: {1} + + + Open Interface Subtype of a Closed Type Must Be a Case + An exhustive switch statement does not support case clauses with "when" guards diff --git a/ExhaustiveMatching.Analyzer/TypeDeclarationAnalyzer.cs b/ExhaustiveMatching.Analyzer/TypeDeclarationAnalyzer.cs index 7d8a179..e8370c3 100644 --- a/ExhaustiveMatching.Analyzer/TypeDeclarationAnalyzer.cs +++ b/ExhaustiveMatching.Analyzer/TypeDeclarationAnalyzer.cs @@ -28,10 +28,13 @@ private static void MustBeCase( ITypeSymbol typeSymbol, INamedTypeSymbol closedAttribute) { - if (typeSymbol.IsAbstract) + var isConcrete = !typeSymbol.IsAbstract; + var isOpenInterface = typeSymbol.TypeKind == TypeKind.Interface + && !typeSymbol.HasAttribute(closedAttribute); + if (!isConcrete && !isOpenInterface) return; - // Any concrete type directly inheriting from a closed type must be listed in the cases + // Any concrete type or open interface directly inheriting from a closed type must be listed in the cases var directSuperTypes = typeSymbol.DirectSuperTypes(); var closedSuperTypes = directSuperTypes .Where(t => t.HasAttribute(closedAttribute)) @@ -44,11 +47,18 @@ private static void MustBeCase( if (isMember) continue; - var diagnostic = Diagnostic.Create(ExhaustiveMatchAnalyzer.ConcreteSubtypeMustBeCaseOfClosedType, - typeDeclaration.Identifier.GetLocation(), typeSymbol.GetFullName(), superType.GetFullName()); + var descriptor = isConcrete + ? ExhaustiveMatchAnalyzer.ConcreteSubtypeMustBeCaseOfClosedType + // else isOpenInterface is always true + : ExhaustiveMatchAnalyzer.OpenInterfaceSubtypeMustBeCaseOfClosedType; + + var diagnostic = Diagnostic.Create(descriptor, typeDeclaration.Identifier.GetLocation(), + typeSymbol.GetFullName(), superType.GetFullName()); context.ReportDiagnostic(diagnostic); } + if (!isConcrete) return; + // Any concrete type indirectly inheriting from a closed type must be covered by a case type // that isn't itself closed. If it were closed, then it could be matched by all the cases, and // this type would not be matched. diff --git a/ExhaustiveMatching/ExhaustiveMatching.csproj b/ExhaustiveMatching/ExhaustiveMatching.csproj index c645477..afac980 100644 --- a/ExhaustiveMatching/ExhaustiveMatching.csproj +++ b/ExhaustiveMatching/ExhaustiveMatching.csproj @@ -2,9 +2,9 @@ netstandard2.0 - 0.3.2.0 - 0.3.2.0 - 0.3.2 + 0.4.0.0 + 0.4.0.0 + 0.4.0 diff --git a/README.md b/README.md index 8fdbe60..d1820a1 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,10 @@ The analyzer reports various errors for incorrect code. The table below gives a EM0014 A concrete subtype of a closed type is not covered by some case + + EM0015 + An open interface is not listed as a case in a closed type it is a direct subtype of + EM0100 An exhaustive switch can't contain when guards