Repository navigation
Expand file tree
/
Copy pathExhaustiveSwitchHasDefaultRule.java
More file actions
177 lines (162 loc) · 7.29 KB
/
Copy pathExhaustiveSwitchHasDefaultRule.java
File metadata and controls
177 lines (162 loc) · 7.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.rule.bestpractices;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import net.sourceforge.pmd.lang.java.ast.ASTAssignableExpr;
import net.sourceforge.pmd.lang.java.ast.ASTBlock;
import net.sourceforge.pmd.lang.java.ast.ASTExpression;
import net.sourceforge.pmd.lang.java.ast.ASTFieldAccess;
import net.sourceforge.pmd.lang.java.ast.ASTNullLiteral;
import net.sourceforge.pmd.lang.java.ast.ASTSwitchArrowBranch;
import net.sourceforge.pmd.lang.java.ast.ASTSwitchBranch;
import net.sourceforge.pmd.lang.java.ast.ASTSwitchExpression;
import net.sourceforge.pmd.lang.java.ast.ASTSwitchFallthroughBranch;
import net.sourceforge.pmd.lang.java.ast.ASTSwitchLike;
import net.sourceforge.pmd.lang.java.ast.ASTSwitchStatement;
import net.sourceforge.pmd.lang.java.ast.ASTThrowStatement;
import net.sourceforge.pmd.lang.java.ast.ASTTypePattern;
import net.sourceforge.pmd.lang.java.ast.ASTVariableAccess;
import net.sourceforge.pmd.lang.java.ast.internal.JavaAstUtils;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule;
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
import net.sourceforge.pmd.lang.java.symbols.JFieldSymbol;
import net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol;
import net.sourceforge.pmd.reporting.RuleContext;
/**
* @since 7.10.0 (as XPath) / 7.27.0 (as Java)
*/
public class ExhaustiveSwitchHasDefaultRule extends AbstractJavaRulechainRule {
static final int MAX_NAMED_CASES = 3;
public ExhaustiveSwitchHasDefaultRule() {
super(ASTSwitchExpression.class, ASTSwitchStatement.class);
}
@Override
public Object visit(ASTSwitchExpression node, Object data) {
visitSwitchLike(node, (RuleContext) data);
return null;
}
@Override
public Object visit(ASTSwitchStatement node, Object data) {
visitSwitchLike(node, (RuleContext) data);
return null;
}
private void visitSwitchLike(ASTSwitchLike node, RuleContext ctx) {
if (!node.isExhaustive() || !node.hasDefaultCase()) {
return;
}
if (!defaultBranchIsNecessary(node)) {
List<String> missing = missingCases(node);
if (node instanceof ASTSwitchExpression) {
// switch expressions - the compiler will do the exhaustive check
if (missing.isEmpty()) {
ctx.addViolationWithMessage(node, "Remove default case to make sure the compiler keeps checking that all cases are present.");
} else {
ctx.addViolation(node, formatMissingCases(missing));
}
} else {
// switch statement - no compiler checks. The rule NonExhaustiveSwitch should be used.
if (missing.isEmpty()) {
ctx.addViolationWithMessage(node, "Remove default case and use the rule NonExhaustiveSwitch to make sure the switch stays exhaustive.");
}
}
} else if (!branchJustThrows(node.getDefaultCase())) {
ctx.addViolationWithMessage(node, "The switch block is exhaustive. The default case should only throw, nothing else.");
}
}
/**
* returns true iff the only thing this branch does is throw an exception
*/
// visible for testing
/* private */ static boolean branchJustThrows(ASTSwitchBranch branch) {
if (branch instanceof ASTSwitchFallthroughBranch) {
ASTSwitchFallthroughBranch fallthroughBranch = (ASTSwitchFallthroughBranch) branch;
return fallthroughBranch.getStatements().count() == 1
&& fallthroughBranch.getStatements().first() instanceof ASTThrowStatement;
}
ASTSwitchArrowBranch arrowBranch = (ASTSwitchArrowBranch) branch;
if (arrowBranch.getRightHandSide() instanceof ASTThrowStatement) {
return true;
}
if (arrowBranch.getRightHandSide() instanceof ASTBlock) {
ASTBlock block = (ASTBlock) arrowBranch.getRightHandSide();
return block.size() == 1 && block.get(0) instanceof ASTThrowStatement;
}
return false;
}
/**
* returns true if the default branch of this switch is necessary to make the code compile.
* This happens, if you initialize a final variable in your other branches.
*/
// visible for testing
/* private */ static boolean defaultBranchIsNecessary(ASTSwitchLike switchLike) {
return switchLike.descendants(ASTAssignableExpr.ASTNamedReferenceExpr.class)
.filter(expr -> expr.getReferencedSym() != null)
.filter(expr -> expr.getReferencedSym().isFinal())
.any(JavaAstUtils::isVarAccessStrictlyWrite);
}
// visible for testing
/* private */ static List<String> missingCases(ASTSwitchLike node) {
JTypeDeclSymbol symbol = node.getTestedExpression().getTypeMirror().getSymbol();
if (!(symbol instanceof JClassSymbol)) {
return Collections.emptyList();
}
JClassSymbol classSymbol = (JClassSymbol) symbol;
Set<String> covered = coveredCaseNames(node);
List<String> missing = new ArrayList<>();
if (classSymbol.isEnum()) {
for (JFieldSymbol constant : classSymbol.getEnumConstants()) {
if (!covered.contains(constant.getSimpleName())) {
missing.add(constant.getSimpleName());
}
}
} else if (classSymbol.isSealed()) {
for (JClassSymbol subtype : classSymbol.getPermittedSubtypes()) {
if (!covered.contains(subtype.getSimpleName())) {
missing.add(subtype.getSimpleName());
}
}
}
Collections.sort(missing);
return missing;
}
private static Set<String> coveredCaseNames(ASTSwitchLike node) {
Set<String> names = new HashSet<>();
for (ASTSwitchBranch branch : node.getBranches()) {
for (ASTExpression expr : branch.getLabel().getExprList()) {
if (expr instanceof ASTNullLiteral) {
continue;
}
if (expr instanceof ASTVariableAccess) {
names.add(((ASTVariableAccess) expr).getName());
} else if (expr instanceof ASTFieldAccess) {
names.add(((ASTFieldAccess) expr).getName());
}
}
for (ASTTypePattern pattern : branch.getLabel().children(ASTTypePattern.class)) {
JTypeDeclSymbol typeSym = pattern.getTypeNode().getTypeMirror().getSymbol();
if (typeSym instanceof JClassSymbol) {
names.add(typeSym.getSimpleName());
}
}
}
return names;
}
// visible for testing
/* private */ static String formatMissingCases(List<String> names) {
if (names.isEmpty()) {
return "";
}
boolean truncated = names.size() > MAX_NAMED_CASES;
List<String> shown = truncated ? names.subList(0, MAX_NAMED_CASES) : names;
String joined = String.join(", ", shown);
if (truncated) {
joined += ", ...";
}
return " (" + joined + ")";
}
}