Skip to content

Commit c0645d9

Browse files
committed
Implement ExhaustiveEnumCase check
1 parent 2fa0766 commit c0645d9

File tree

6 files changed

+443
-0
lines changed

6 files changed

+443
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- `ExhaustiveEnumCase` analysis rule, which flags `case` statements that do not handle all values in an enumeration.
1213
- **API:** `EnumeratorOccurrence` type.
1314
- **API:** `EnumeratorOccurrence::getGetEnumerator` method.
1415
- **API:** `EnumeratorOccurrence::getMoveNext` method.

delphi-checks/src/main/java/au/com/integradev/delphi/checks/CheckList.java

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public final class CheckList {
6666
EmptyRoutineCheck.class,
6767
EmptyVisibilitySectionCheck.class,
6868
EnumNameCheck.class,
69+
ExhaustiveEnumCaseCheck.class,
6970
ExplicitDefaultPropertyReferenceCheck.class,
7071
ExplicitTObjectInheritanceCheck.class,
7172
FieldNameCheck.class,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Sonar Delphi Plugin
3+
* Copyright (C) 2025 Integrated Application Development
4+
*
5+
* This program is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU Lesser General Public
7+
* License as published by the Free Software Foundation; either
8+
* version 3 of the License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
* Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public
16+
* License along with this program; if not, write to the Free Software
17+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
18+
*/
19+
package au.com.integradev.delphi.checks;
20+
21+
import java.util.List;
22+
import java.util.Objects;
23+
import java.util.Set;
24+
import java.util.stream.Collectors;
25+
import org.sonar.check.Rule;
26+
import org.sonar.plugins.communitydelphi.api.ast.CaseItemStatementNode;
27+
import org.sonar.plugins.communitydelphi.api.ast.CaseStatementNode;
28+
import org.sonar.plugins.communitydelphi.api.ast.DelphiNode;
29+
import org.sonar.plugins.communitydelphi.api.ast.ExpressionNode;
30+
import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode;
31+
import org.sonar.plugins.communitydelphi.api.ast.PrimaryExpressionNode;
32+
import org.sonar.plugins.communitydelphi.api.check.DelphiCheck;
33+
import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext;
34+
import org.sonar.plugins.communitydelphi.api.check.FilePosition;
35+
import org.sonar.plugins.communitydelphi.api.symbol.Invocable;
36+
import org.sonar.plugins.communitydelphi.api.symbol.declaration.EnumElementNameDeclaration;
37+
import org.sonar.plugins.communitydelphi.api.symbol.declaration.NameDeclaration;
38+
import org.sonar.plugins.communitydelphi.api.symbol.declaration.TypedDeclaration;
39+
import org.sonar.plugins.communitydelphi.api.type.Type;
40+
import org.sonar.plugins.communitydelphi.api.type.Type.EnumType;
41+
42+
@Rule(key = "ExhaustiveEnumCase")
43+
public class ExhaustiveEnumCaseCheck extends DelphiCheck {
44+
45+
@Override
46+
public DelphiCheckContext visit(CaseStatementNode node, DelphiCheckContext context) {
47+
if (node.getElseBlockNode() == null) {
48+
EnumType enumType = getSelectorExpressionType(node);
49+
if (enumType != null) {
50+
Set<EnumElementNameDeclaration> enumElements = getEnumElements(enumType);
51+
52+
node.getCaseItems().stream()
53+
.map(CaseItemStatementNode::getExpressions)
54+
.flatMap(List::stream)
55+
.map(this::getElementNameDeclaration)
56+
.filter(Objects::nonNull)
57+
.forEach(enumElements::remove);
58+
59+
if (!enumElements.isEmpty()) {
60+
context
61+
.newIssue()
62+
.onFilePosition(FilePosition.from(node.getToken()))
63+
.withMessage(
64+
String.format(
65+
"Make this case statement exhaustive (%d unhandled element%s)",
66+
enumElements.size(), enumElements.size() == 1 ? "" : "s"))
67+
.report();
68+
}
69+
}
70+
}
71+
72+
return super.visit(node, context);
73+
}
74+
75+
private NameDeclaration unpackExpressionDeclaration(ExpressionNode expression) {
76+
expression = expression.skipParentheses();
77+
if (!(expression instanceof PrimaryExpressionNode)) {
78+
return null;
79+
}
80+
81+
DelphiNode maybeNameReference = expression.getChild(0);
82+
if (!(maybeNameReference instanceof NameReferenceNode)) {
83+
return null;
84+
}
85+
86+
NameReferenceNode nameReference = ((NameReferenceNode) maybeNameReference).getLastName();
87+
return nameReference.getNameDeclaration();
88+
}
89+
90+
private EnumElementNameDeclaration getElementNameDeclaration(ExpressionNode expression) {
91+
var declaration = unpackExpressionDeclaration(expression);
92+
if (declaration instanceof EnumElementNameDeclaration) {
93+
return (EnumElementNameDeclaration) declaration;
94+
} else {
95+
return null;
96+
}
97+
}
98+
99+
private EnumType getSelectorExpressionType(CaseStatementNode node) {
100+
var declaration = unpackExpressionDeclaration(node.getSelectorExpression());
101+
Type type;
102+
103+
if (declaration instanceof Invocable) {
104+
var invocable = (Invocable) declaration;
105+
if (invocable.getRequiredParametersCount() != 0) {
106+
return null;
107+
}
108+
109+
type = invocable.getReturnType();
110+
} else if (declaration instanceof TypedDeclaration) {
111+
type = ((TypedDeclaration) declaration).getType();
112+
} else {
113+
return null;
114+
}
115+
116+
if (type == null || !type.isEnum()) {
117+
return null;
118+
}
119+
120+
return (EnumType) type;
121+
}
122+
123+
private Set<EnumElementNameDeclaration> getEnumElements(EnumType enumType) {
124+
return enumType.typeScope().getAllDeclarations().stream()
125+
.filter(EnumElementNameDeclaration.class::isInstance)
126+
.map(EnumElementNameDeclaration.class::cast)
127+
.collect(Collectors.toSet());
128+
}
129+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<h2>Why is this an issue?</h2>
2+
<p>
3+
When using a <code>case</code> statement to alternate between different values of an enumeration,
4+
all values should be handled. This could be done explicitly by including all values in the
5+
<code>case</code> arms, or implicitly by adding a <code>default</code> branch.
6+
</p>
7+
<p>
8+
An exhaustive <code>case</code> statement makes it clear that all behaviour is intentionally
9+
defined for all values, and guards against accidental omissions - for example, forgetting to
10+
update the case statement when a new value is added to the enumeration.
11+
</p>
12+
<h2>How to fix it</h2>
13+
<p>
14+
Add the missing enumeration values to the <code>case</code>:
15+
</p>
16+
<pre data-diff-id="1" data-diff-type="noncompliant">
17+
type
18+
TBeverageKind = (bvCold, bvFrozen, bvHot, bvRoomTemp);
19+
20+
procedure PrepareBeverage(Kind: TBeverageKind);
21+
begin
22+
case Kind of
23+
bvCold, bvFrozen:
24+
Refrigerate;
25+
bvHot:
26+
Microwave;
27+
end;
28+
end;
29+
</pre>
30+
<pre data-diff-id="1" data-diff-type="compliant">
31+
type
32+
TBeverageKind = (bvCold, bvFrozen, bvHot, bvRoomTemp);
33+
34+
procedure PrepareBeverage(Kind: TBeverageKind);
35+
begin
36+
case Kind of
37+
bvCold, bvFrozen:
38+
Refrigerate;
39+
bvHot:
40+
Microwave;
41+
bvRoomTemp:
42+
// No action required
43+
end;
44+
end;
45+
</pre>
46+
<p>
47+
Alternatively, add an <code>else</code> block to implicitly handle all remaining values:
48+
</p>
49+
<pre data-diff-id="2" data-diff-type="noncompliant">
50+
type
51+
TBeverageKind = (bvCold, bvFrozen, bvHot, bvRoomTemp);
52+
53+
procedure PrepareBeverage(Kind: TBeverageKind);
54+
begin
55+
case Kind of
56+
bvCold, bvFrozen:
57+
Refrigerate;
58+
bvHot:
59+
Microwave;
60+
end;
61+
end;
62+
</pre>
63+
<pre data-diff-id="2" data-diff-type="compliant">
64+
type
65+
TBeverageKind = (bvCold, bvFrozen, bvHot, bvRoomTemp);
66+
67+
procedure PrepareBeverage(Kind: TBeverageKind);
68+
begin
69+
case Kind of
70+
bvCold, bvFrozen:
71+
Refrigerate;
72+
bvHot:
73+
Microwave;
74+
else
75+
// No action required
76+
end;
77+
end;
78+
</pre>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"title": "Case statements should be exhaustive",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant/Issue",
7+
"constantCost": "3min"
8+
},
9+
"code": {
10+
"attribute": "COMPLETE",
11+
"impacts": {
12+
"MAINTAINABILITY": "MEDIUM"
13+
}
14+
},
15+
"tags": ["pitfall"],
16+
"defaultSeverity": "Minor",
17+
"scope": "ALL",
18+
"quickfix": "unknown"
19+
}

0 commit comments

Comments
 (0)