From dbc76b15364fab248e6a009f094b1e348ef1a2e6 Mon Sep 17 00:00:00 2001 From: webbrain-one <295484252+webbrain-one@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:03:45 +0300 Subject: [PATCH] Add analyzer for switch not covering all enum values Issue #91: report switch statements that do not cover all enum values. --- .../BlowinCleanCodeAnalyzer.cs | 1 + ...itchNotCoverAllEnumValuesFeatureAnalyze.cs | 125 ++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 BlowinCleanCode/BlowinCleanCode/Feature/GoodPractice/SwitchNotCoverAllEnumValuesFeatureAnalyze.cs diff --git a/BlowinCleanCode/BlowinCleanCode/BlowinCleanCodeAnalyzer.cs b/BlowinCleanCode/BlowinCleanCode/BlowinCleanCodeAnalyzer.cs index c48d9fb..934e13a 100644 --- a/BlowinCleanCode/BlowinCleanCode/BlowinCleanCodeAnalyzer.cs +++ b/BlowinCleanCode/BlowinCleanCode/BlowinCleanCodeAnalyzer.cs @@ -33,6 +33,7 @@ public class BlowinCleanCodeAnalyzer : DiagnosticAnalyzer new StaticClassFeatureSymbolAnalyze(), new DisposableMemberInNonDisposableClassFeatureAnalyze(), new SwitchStatementsShouldHaveAtLeast2CaseClausesFeatureAnalyze(), + new SwitchNotCoverAllEnumValuesFeatureAnalyze(), new FinalizersShouldNotBeEmptyFeatureAnalyze(), new TypeThatProvideEqualsShouldImplementIEquatableFeatureAnalyze(), new ThreadStaticFieldsShouldNotBeInitializedFeatureAnalyze(), diff --git a/BlowinCleanCode/BlowinCleanCode/Feature/GoodPractice/SwitchNotCoverAllEnumValuesFeatureAnalyze.cs b/BlowinCleanCode/BlowinCleanCode/Feature/GoodPractice/SwitchNotCoverAllEnumValuesFeatureAnalyze.cs new file mode 100644 index 0000000..89edbcf --- /dev/null +++ b/BlowinCleanCode/BlowinCleanCode/Feature/GoodPractice/SwitchNotCoverAllEnumValuesFeatureAnalyze.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using BlowinCleanCode.Feature.Base; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace BlowinCleanCode.Feature.GoodPractice +{ + public sealed class SwitchNotCoverAllEnumValuesFeatureAnalyze : FeatureSyntaxNodeAnalyzerBase + { + public const string DiagnosticId = "BCC1000"; + + public override DiagnosticDescriptor DiagnosticDescriptor { get; } = new DiagnosticDescriptor( + DiagnosticId, + "Switch does not cover all enum values", + "Switch does not cover all enum values", + "Good practice", + DiagnosticSeverity.Warning, + "A switch statement on an enum type should handle every enum value or provide a default clause", + true); + + protected override SyntaxKind SyntaxKind => SyntaxKind.SwitchStatement; + + protected override void Analyze(SyntaxNodeAnalysisContext context, SwitchStatementSyntax syntax) + { + if (AnalyzerCommentSkipCheck.Skip(syntax)) + return; + + var enumType = context.SemanticModel.GetTypeInfo(syntax.Expression).Type as INamedTypeSymbol; + if (enumType == null || enumType.TypeKind != TypeKind.Enum) + return; + + if (IsFlagsEnum(enumType)) + return; + + if (HasDefaultClause(syntax)) + return; + + var coveredValues = GetCoveredValues(context, syntax); + foreach (var member in enumType.GetMembers()) + { + if (!(member is IFieldSymbol field) || !field.IsEnumMember) + continue; + + if (field.ConstantValue == null) + continue; + + if (TryGetValue(field.ConstantValue, out var memberValue) && coveredValues.Contains(memberValue)) + continue; + + ReportDiagnostic(context, syntax.GetLocation()); + return; + } + } + + private static bool HasDefaultClause(SwitchStatementSyntax syntax) + => syntax.Sections.SelectMany(s => s.Labels).OfType().Any(); + + private static bool IsFlagsEnum(INamedTypeSymbol enumType) + => enumType.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "System.FlagsAttribute"); + + private static HashSet GetCoveredValues(SyntaxNodeAnalysisContext context, SwitchStatementSyntax syntax) + { + var values = new HashSet(); + foreach (var section in syntax.Sections) + { + foreach (var label in section.Labels) + { + if (label is CaseLabelSyntax caseLabel && caseLabel.Value != null) + { + var constant = context.SemanticModel.GetConstantValue(caseLabel.Value); + if (constant.HasValue && TryGetValue(constant.Value, out var value)) + values.Add(value); + } + } + } + + return values; + } + + private static bool TryGetValue(object value, out decimal result) + { + if (value is null) + return false; + + if (value.GetType().IsEnum) + { + value = Convert.ChangeType(value, value.GetType().BaseType, CultureInfo.InvariantCulture); + } + + switch (value) + { + case byte b: + result = b; + return true; + case sbyte sb: + result = sb; + return true; + case short s: + result = s; + return true; + case ushort us: + result = us; + return true; + case int i: + result = i; + return true; + case uint ui: + result = ui; + return true; + case long l: + result = (decimal)l; + return true; + case ulong ul: + result = (decimal)ul; + return true; + default: + return false; + } + } + } +}