Skip to content

Commit 12283cf

Browse files
stefanodallapalmastefanodgithub-actions[bot]timtebeek
authored
Add OnlyCatchDeclaredExceptions recipe to address RSPEC-S2221 (#601)
* (RSPEC-S2221) SpecifyGenericExceptionCatches recipe * Update src/main/java/org/openrewrite/staticanalysis/SpecifyGenericExceptionCatches.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update src/test/java/org/openrewrite/staticanalysis/SpecifyGenericExceptionCatchesTest.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Apply suggestions from code review Applied suggestions Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Fixed conflict introduced by applying batch suggestions * Apply formatter * No need for a named internal class * Show difference between declared and undeclared runtime exceptions * Use `reduce` * Use `ListUtils.map` * Handle existing multi catch that's incomplete * Rename recipe to make intention clear * Drop need for `hasGenericCatch` * Rename method to show intention * Add a precondition to limit where recipe applies * Generate template with shortened types and imports * Always add imports in case of missing types --------- Co-authored-by: stefanod <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tim te Beek <[email protected]>
1 parent 30ff5ae commit 12283cf

File tree

2 files changed

+594
-0
lines changed

2 files changed

+594
-0
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.staticanalysis;
17+
18+
import org.jspecify.annotations.Nullable;
19+
import org.openrewrite.*;
20+
import org.openrewrite.internal.ListUtils;
21+
import org.openrewrite.java.JavaIsoVisitor;
22+
import org.openrewrite.java.JavaTemplate;
23+
import org.openrewrite.java.search.UsesType;
24+
import org.openrewrite.java.tree.J;
25+
import org.openrewrite.java.tree.JavaType;
26+
import org.openrewrite.java.tree.JavaType.FullyQualified;
27+
import org.openrewrite.java.tree.TypeUtils;
28+
29+
import java.util.*;
30+
31+
import static java.util.stream.Collectors.joining;
32+
import static java.util.stream.Collectors.toList;
33+
34+
public class OnlyCatchDeclaredExceptions extends Recipe {
35+
36+
private static final String JAVA_LANG_EXCEPTION = "java.lang.Exception";
37+
38+
@Override
39+
public String getDisplayName() {
40+
return "Replace `catch(Exception)` with specific declared exceptions thrown in the try block";
41+
}
42+
43+
@Override
44+
public String getDescription() {
45+
return "Replaces `catch(Exception e)` blocks with a multi-catch block " +
46+
"(`catch (SpecificException1 | SpecificException2 e)`) containing only the exceptions declared " +
47+
"thrown by method or constructor invocations within the `try` block that are not already caught " +
48+
"by more specific `catch` clauses.";
49+
}
50+
51+
@Override
52+
public Set<String> getTags() {
53+
return new HashSet<>(Arrays.asList("CWE-396", "RSPEC-S2221"));
54+
}
55+
56+
@Override
57+
public TreeVisitor<?, ExecutionContext> getVisitor() {
58+
JavaIsoVisitor<ExecutionContext> visitor = new JavaIsoVisitor<ExecutionContext>() {
59+
@Override
60+
public J.Try visitTry(J.Try aTry, ExecutionContext ctx) {
61+
J.Try t = super.visitTry(aTry, ctx);
62+
return t.withCatches(ListUtils.map(t.getCatches(), c -> {
63+
if (isGenericCatch(c)) {
64+
// Find declared thrown exceptions that are not already specifically caught
65+
Set<JavaType> declaredThrown = getDeclaredThrownExceptions(t);
66+
declaredThrown.removeAll(getCaughtExceptions(t));
67+
if (!declaredThrown.isEmpty()) {
68+
return multiCatchWithDeclaredExceptions(c, declaredThrown);
69+
}
70+
}
71+
return c;
72+
}));
73+
}
74+
75+
private boolean isGenericCatch(J.Try.Catch aCatch) {
76+
FullyQualified fq = TypeUtils.asFullyQualified(aCatch.getParameter().getType());
77+
if (fq != null) {
78+
String fqn = fq.getFullyQualifiedName();
79+
return TypeUtils.fullyQualifiedNamesAreEqual(JAVA_LANG_EXCEPTION, fqn);
80+
}
81+
return false;
82+
}
83+
84+
private Set<JavaType> getCaughtExceptions(J.Try aTry) {
85+
Set<JavaType> caughtExceptions = new HashSet<>();
86+
for (J.Try.Catch c : aTry.getCatches()) {
87+
JavaType type = c.getParameter().getType();
88+
if (type instanceof JavaType.MultiCatch) {
89+
caughtExceptions.addAll(((JavaType.MultiCatch) type).getThrowableTypes());
90+
} else if (type != null) {
91+
caughtExceptions.add(type);
92+
}
93+
}
94+
return caughtExceptions;
95+
}
96+
97+
private Set<JavaType> getDeclaredThrownExceptions(J.Try aTry) {
98+
return new JavaIsoVisitor<Set<JavaType>>() {
99+
@Override
100+
public @Nullable JavaType visitType(@Nullable JavaType javaType, Set<JavaType> javaTypes) {
101+
if (javaType instanceof JavaType.Method) {
102+
javaTypes.addAll(((JavaType.Method) javaType).getThrownExceptions());
103+
}
104+
return super.visitType(javaType, javaTypes);
105+
}
106+
}.reduce(aTry.getBody(), new HashSet<>());
107+
}
108+
109+
private J.Try.Catch multiCatchWithDeclaredExceptions(J.Try.Catch aCatch, Set<JavaType> thrownExceptions) {
110+
List<FullyQualified> fqs = thrownExceptions.stream()
111+
.map(TypeUtils::asFullyQualified)
112+
.filter(Objects::nonNull)
113+
.sorted(Comparator.comparing(FullyQualified::getClassName))
114+
.collect(toList());
115+
String throwableTypes = fqs
116+
.stream()
117+
.map(FullyQualified::getClassName)
118+
.collect(joining("|"));
119+
String[] imports = fqs.stream().map(FullyQualified::getFullyQualifiedName).toArray(String[]::new);
120+
for (String s : imports) {
121+
maybeAddImport(s, false);
122+
}
123+
124+
J.Try surroundingTry = getCursor().firstEnclosing(J.Try.class);
125+
assert surroundingTry != null;
126+
127+
// Preserve the existing variable name from the original generic catch block
128+
String variableName = aCatch.getParameter().getTree().getVariables().get(0).getSimpleName();
129+
J.Try generatedTry = JavaTemplate.builder(String.format("try {} catch (%s %s) {}", throwableTypes, variableName))
130+
.imports(imports)
131+
.build()
132+
.apply(new Cursor(getCursor(), surroundingTry), surroundingTry.getCoordinates().replace());
133+
return aCatch.withParameter(generatedTry.getCatches().get(0).getParameter());
134+
}
135+
};
136+
return Preconditions.check(new UsesType<>(JAVA_LANG_EXCEPTION, false), visitor);
137+
}
138+
}

0 commit comments

Comments
 (0)