Skip to content
This repository was archived by the owner on Sep 24, 2020. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace ICSharpCode.NRefactory.CSharp.Refactoring.CodeActions
{
[ContextAction(
"Enable exhaustiveness check",
Description = "Enable exhaustiveness check for this switch")]
public class EnableExhaustivenessCheckAction: SpecializedCodeAction<SwitchStatement>
{
protected override CodeAction GetAction(RefactoringContext context, SwitchStatement node)
{
if (!node.SwitchToken.Contains(context.Location))
return null;

if (SomeOfEnumValuesWasNotHandledInSwitchStatementIssue.IsExhaustivenessCheckEnabled(node))
return null;

var switchData = SomeOfEnumValuesWasNotHandledInSwitchStatementIssue.BuildSwitchData(node, context);
if (switchData == null)
return null;

return new CodeAction(
context.TranslateString("Enabled exhaustiveness check"),
script => EnableCheck(script, node),
node
);
}

static void EnableCheck(Script script, SwitchStatement node)
{
var comment = new Comment(" " + SomeOfEnumValuesWasNotHandledInSwitchStatementIssue.EnableCheckComment);
script.InsertBefore(node, comment);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using System.Collections.Generic;
using System.Linq;
using ICSharpCode.NRefactory.Refactoring;
using ICSharpCode.NRefactory.Semantics;
using ICSharpCode.NRefactory.TypeSystem;

namespace ICSharpCode.NRefactory.CSharp.Refactoring
{
[IssueDescription("Some of enum values was not handled in switch statement",
Description = "Some of enum values was not handled in switch statement.",
Category = IssueCategories.CodeQualityIssues,
Severity = Severity.Warning)]
public class SomeOfEnumValuesWasNotHandledInSwitchStatementIssue: GatherVisitorCodeIssueProvider
{
internal static readonly string EnableCheckComment = "Check exhaustiveness";

internal static bool IsExhaustivenessCheckEnabled(SwitchStatement switchStatement)
{
var comment = switchStatement.Parent.GetChildByRole(Roles.Comment);
return comment != null && comment.Content.Trim() == EnableCheckComment;
}

internal static SwitchData BuildSwitchData(SwitchStatement switchStatement, BaseRefactoringContext context)
{
return new SwitchDataBuilder(switchStatement, context).Build();
}

protected override IGatherVisitor CreateVisitor(BaseRefactoringContext context)
{
return new GatherVisitor(context);
}

internal class SwitchData
{
public readonly IType EnumType;
public readonly IEnumerable<MemberReferenceExpression> LabelsExpressions;

public SwitchData(IType enumType, IEnumerable<MemberReferenceExpression> labelsExpressions)
{
EnumType = enumType;
LabelsExpressions = labelsExpressions.ToArray();
}
}

class SwitchDataBuilder
{
readonly SwitchStatement _switchStatement;
readonly BaseRefactoringContext _context;

public SwitchDataBuilder(SwitchStatement switchStatement, BaseRefactoringContext context)
{
_switchStatement = switchStatement;
_context = context;
}

public SwitchData Build()
{
var enumType = GetEnumType();
if (enumType == null)
return null;

var labelsExpressions = GatherCaseLabelsExpressions();
if (!AreAllExpressionsHasEnumType(labelsExpressions, enumType))
return null;

return new SwitchData(enumType, labelsExpressions.Cast<MemberReferenceExpression>());
}

IType GetEnumType()
{
var resolveResult = _context.Resolve(_switchStatement.Expression);
return resolveResult.Type.Kind == TypeKind.Enum ? resolveResult.Type : null;
}

IEnumerable<Expression> GatherCaseLabelsExpressions()
{
var labels = _switchStatement.SwitchSections.SelectMany(_ => _.CaseLabels);
var nonDefaultLabels = labels.Where(_ => !_.Expression.IsNull);

return nonDefaultLabels.Select(_ => _.Expression);
}

bool AreAllExpressionsHasEnumType(IEnumerable<Expression> expressions, IType type)
{
var resolveResults = expressions.Select(_ => _context.Resolve(_)).ToArray();
return resolveResults.Any() && resolveResults.All(_ => _.Type == type);
}
}

class GatherVisitor: GatherVisitorBase<SomeOfEnumValuesWasNotHandledInSwitchStatementIssue>
{
public GatherVisitor(BaseRefactoringContext ctx) : base(ctx)
{
}

public override void VisitSwitchStatement(SwitchStatement switchStatement)
{
base.VisitSwitchStatement(switchStatement);

if (IsExhaustivenessCheckEnabled(switchStatement)) {
var switchData = BuildSwitchData(switchStatement, ctx);

if (switchData != null) {
var missingValues = GetMissingEnumValues(switchData).ToArray();

if (missingValues.Any())
AddIssue(new CodeIssue(
switchStatement.SwitchToken,
"Some of enum values was not handled",
"Handle missing values",
script => GenerateMissingCasesForMissingValues(script, switchStatement, missingValues)
));
}
}
}

static IEnumerable<IField> GetMissingEnumValues(SwitchData switchData)
{
var handledValues = switchData.LabelsExpressions.Select(_ => _.MemberName).ToArray();
var allValues = switchData.EnumType.GetFields(_ => _.IsConst && _.IsPublic);

return allValues.Where(_ => !handledValues.Contains(_.Name));
}

static void GenerateMissingCasesForMissingValues(Script script, SwitchStatement switchStatement, IEnumerable<IField> values)
{
var astType = new SimpleType(values.First().Type.Name);
var newSwitchStatement = (SwitchStatement)switchStatement.Clone();

var previousSection = GetDefaultSection(newSwitchStatement);
foreach (var value in values.Reverse()) {
var newSection = new SwitchSection {
CaseLabels = {
new CaseLabel(new MemberReferenceExpression(astType.Clone(), value.Name))
},
Statements = {
new ThrowStatement(new ObjectCreateExpression(new SimpleType("System.NotImplementedException")))
}
};

if (previousSection != null)
newSwitchStatement.SwitchSections.InsertBefore(previousSection, newSection);
else
newSwitchStatement.SwitchSections.Add(newSection);

previousSection = newSection;
}

script.Replace(switchStatement, newSwitchStatement);
}

static SwitchSection GetDefaultSection(SwitchStatement switchStatement)
{
var sections = switchStatement.SwitchSections;
return sections.FirstOrDefault(s => s.CaseLabels.Any(l => l.Expression.IsNull));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@
<Compile Include="CodeIssues\TODO\PartOfBodyCanBeConvertedToQueryIssue.cs" />
<Compile Include="CodeIssues\TODO\RedundantTypeArgumentsOfMethodIssue.cs" />
<Compile Include="CodeIssues\Synced\PracticesAndImprovements\PossibleMistakenCallToGetTypeIssue.cs" />
<Compile Include="CodeActions\EnableExhaustivenessCheckAction.cs" />
<Compile Include="CodeIssues\Uncategorized\SomeOfEnumValuesWasNotHandledInSwitchStatementIssue.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using NUnit.Framework;
using ICSharpCode.NRefactory.CSharp.Refactoring.CodeActions;

namespace ICSharpCode.NRefactory.CSharp.CodeActions
{
[TestFixture]
public class EnableExhaustivenessCheckActionTests: ContextActionTestBase
{
[Test]
public void TestNotEnumSwitchLabelsExpressions()
{
TestWrongContext<EnableExhaustivenessCheckAction>(@"
enum Foo { First, Second, Third }

class TestClass
{
void TestMethod(Foo foo)
{
$switch (foo)
{
case 0: return;
case Foo.Second: return;
default: return;
}
}
}");
}

[Test]
public void TestCheckAlreadyEnabled()
{
TestWrongContext<EnableExhaustivenessCheckAction>(@"
enum Foo { First, Second, Third }

class TestClass
{
void TestMethod(Foo foo)
{
// Check exhaustiveness
$switch (foo)
{
case Foo.Second: return;
default: return;
}
}
}");
}

[Test]
public void TestCheckDisabled()
{
Test<EnableExhaustivenessCheckAction>(@"
enum Foo { First, Second, Third }

class TestClass
{
void TestMethod(Foo foo)
{
$switch (foo)
{
case Foo.Second: return;
default: return;
}
}
}", @"
enum Foo { First, Second, Third }

class TestClass
{
void TestMethod(Foo foo)
{
// Check exhaustiveness
switch (foo)
{
case Foo.Second: return;
default: return;
}
}
}");
}
}
}
Loading