Skip to content

Commit 3e0ba42

Browse files
authored
Add checkstyle rule to Include Caught Exceptions as Cause for Subsequently thrown exceptions (Azure#26818)
1 parent fec3cf8 commit 3e0ba42

File tree

45 files changed

+437
-48
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+437
-48
lines changed

eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/BlacklistedWordsCheck.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@
1010
import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
1111
import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
1212

13-
import java.util.Arrays;
1413
import java.util.Collections;
1514
import java.util.HashSet;
1615
import java.util.Set;
17-
import java.util.stream.Collectors;
1816

1917
/**
2018
* Ensure that code is not using words or abbreviations that are blacklisted by this Checkstyle.

eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/JavadocThrowsChecks.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
import java.util.HashSet;
1818
import java.util.Map;
1919

20+
/**
21+
* Verifies that all throws in the public API have JavaDocs explaining why and when they are thrown.
22+
*/
2023
public class JavadocThrowsChecks extends AbstractCheck {
2124
static final String MISSING_DESCRIPTION_MESSAGE =
2225
"@throws tag requires a description explaining when the error is thrown.";

eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/NoImplInPublicAPI.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
import java.util.regex.Matcher;
1818
import java.util.regex.Pattern;
1919

20+
/**
21+
* Verifies that:
22+
* 1) no return classes are from the implementation package
23+
* 2) no class of implementation package as method's parameters
24+
*/
2025
public class NoImplInPublicAPI extends AbstractCheck {
2126
private static final String ALTERNATIVE_MOVE_TO_PUBLIC_API = "Alternatively, it can be removed from the "
2227
+ "implementation package and made public API, after appropriate API review.";
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.tools.checkstyle.checks;
5+
6+
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
7+
import com.puppycrawl.tools.checkstyle.api.DetailAST;
8+
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
9+
10+
import java.util.LinkedList;
11+
import java.util.List;
12+
import java.util.stream.Collectors;
13+
14+
/**
15+
* This check ensures that an exception thrown includes the current caught exception cause.
16+
* New exception should use the original/cause exception object to provide the full stack trace for the problem.
17+
*
18+
* // DO
19+
* try {
20+
* url = new URL(urlString);
21+
* } catch (MalformedURLException ex) {
22+
* throw new RuntimeException(ex);
23+
* }
24+
*
25+
* // DON'T
26+
* try {
27+
* url = new URL(urlString);
28+
* } catch (MalformedURLException ex) {
29+
* throw new RuntimeException("Invalid URL string was given."); // "ex" is ignored.
30+
* }
31+
*/
32+
public class UseCaughtExceptionCauseCheck extends AbstractCheck {
33+
static final String UNUSED_CAUGHT_EXCEPTION_ERROR = "Caught and rethrown exceptions should include the caught"
34+
+ " exception as the cause in the rethrown exception. Dropping the causal exception makes it more difficult"
35+
+ " to troubleshoot issues when they arise. Include the caught exception variable %s as the cause.";
36+
37+
@Override
38+
public int[] getDefaultTokens() {
39+
return getRequiredTokens();
40+
}
41+
42+
@Override
43+
public int[] getAcceptableTokens() {
44+
return getRequiredTokens();
45+
}
46+
47+
@Override
48+
public int[] getRequiredTokens() {
49+
return new int[] {TokenTypes.LITERAL_CATCH};
50+
}
51+
52+
@Override
53+
public void visitToken(DetailAST catchBlockToken) {
54+
// get the caught exception variable name from the catch block
55+
final DetailAST catchStatement = catchBlockToken.findFirstToken(TokenTypes.PARAMETER_DEF);
56+
final String caughtExceptionVariableName = catchStatement.findFirstToken(TokenTypes.IDENT).getText();
57+
58+
// get all throw statements from current catch block
59+
final List<DetailAST> throwStatements = getThrowStatements(catchBlockToken);
60+
61+
// get possible exception names to which the original exception might be assigned to
62+
final List<String> wrappedExceptions =
63+
getWrappedExceptions(catchBlockToken, catchBlockToken, caughtExceptionVariableName);
64+
65+
throwStatements.forEach(throwToken -> {
66+
final List<String> throwParamNames = new LinkedList<>();
67+
getThrowParamNames(throwToken, throwParamNames);
68+
// include the original exception name to the list to look for in throw statements
69+
wrappedExceptions.add(caughtExceptionVariableName);
70+
71+
// throwParamNames = [ex, p]
72+
// exceptionsList = [ex, cause]
73+
// Caught exception variable is used if an intersection is between the throw statements param names
74+
// used and the actual exception names being thrown.
75+
List<String> intersect =
76+
wrappedExceptions.stream().filter(throwParamNames::contains).collect(Collectors.toList());
77+
if (intersect.size() == 0) {
78+
log(throwToken, String.format(UNUSED_CAUGHT_EXCEPTION_ERROR, caughtExceptionVariableName));
79+
}
80+
});
81+
}
82+
83+
/**
84+
* Returns the list of exceptions that wrapped the current exception tokens
85+
*
86+
* @param currentCatchAST current catch block token
87+
* @param detailAST catch block throw parent token
88+
* @param caughtExceptionVariableName list containing the exception tokens
89+
* @return list of wrapped exception tokens
90+
*/
91+
private List<String> getWrappedExceptions(DetailAST currentCatchAST, DetailAST detailAST,
92+
String caughtExceptionVariableName) {
93+
94+
final List<String> wrappedExceptionNames = new LinkedList<>();
95+
96+
for (DetailAST currentNode : getChildrenNodes(detailAST)) {
97+
// Recursively traverse through the children of the parent node to collect references where the
98+
// caught exception variable is used.
99+
if (currentNode.getType() == TokenTypes.IDENT
100+
&& currentNode.getText().equals(caughtExceptionVariableName)) {
101+
getWrappedExceptionVariable(currentCatchAST, wrappedExceptionNames, currentNode);
102+
}
103+
104+
// add collection in case of last node on this level
105+
if (currentNode.getFirstChild() != null) {
106+
wrappedExceptionNames.addAll(
107+
getWrappedExceptions(currentCatchAST, currentNode, caughtExceptionVariableName));
108+
}
109+
}
110+
return wrappedExceptionNames;
111+
}
112+
113+
/**
114+
* Returns the wrapped exception variable name
115+
*/
116+
private void getWrappedExceptionVariable(DetailAST currentCatchBlock, List<String> wrappedExceptionNames,
117+
DetailAST currentToken) {
118+
DetailAST temp = currentToken;
119+
120+
// Get the node assigning the caught exception variable, traversing upwards starting from the current node.
121+
while (!temp.equals(currentCatchBlock) && temp.getType() != TokenTypes.ASSIGN) {
122+
temp = temp.getParent();
123+
}
124+
125+
if (temp.getType() == TokenTypes.ASSIGN) {
126+
final DetailAST wrappedException;
127+
// Get the variable definition param name to which the caught exception variable is assigned.
128+
if (temp.getParent().getType() == TokenTypes.VARIABLE_DEF) {
129+
wrappedException = temp.getParent().findFirstToken(TokenTypes.IDENT);
130+
} else if (temp.findFirstToken(TokenTypes.DOT) != null) {
131+
// Get the variable name if assigned to a 'this' variable
132+
wrappedException = temp.findFirstToken(TokenTypes.DOT).findFirstToken(TokenTypes.IDENT);
133+
} else {
134+
wrappedException = temp.findFirstToken(TokenTypes.IDENT);
135+
}
136+
if (wrappedException != null) {
137+
wrappedExceptionNames.add(wrappedException.getText());
138+
}
139+
}
140+
}
141+
142+
/**
143+
* Returns the parameter names for current throw keyword.
144+
*
145+
* @param throwParent The parent throw token
146+
* @param paramNames The list containing the parameter names
147+
* @return list of throw param names
148+
*/
149+
private List<String> getThrowParamNames(DetailAST throwParent, List<String> paramNames) {
150+
// get all param names by recursively going through all the throw statements retrieving the token type IDENT
151+
// for the text
152+
getChildrenNodes(throwParent).forEach(currentNode -> {
153+
if (currentNode.getType() == TokenTypes.IDENT) {
154+
paramNames.add(currentNode.getText());
155+
}
156+
if (currentNode.getFirstChild() != null) {
157+
getThrowParamNames(currentNode, paramNames);
158+
}
159+
});
160+
return paramNames;
161+
}
162+
163+
/**
164+
* Recursive method that searches for all the LITERAL_THROW on the current catch token.
165+
*
166+
* @param catchBlockToken A start token.
167+
* @return list of throw tokens
168+
*/
169+
private List<DetailAST> getThrowStatements(DetailAST catchBlockToken) {
170+
final List<DetailAST> throwStatements = new LinkedList<>();
171+
getChildrenNodes(catchBlockToken).forEach(currentNode -> {
172+
if (TokenTypes.LITERAL_THROW == currentNode.getType()) {
173+
throwStatements.add(currentNode);
174+
}
175+
if (currentNode.getFirstChild() != null) {
176+
throwStatements.addAll(getThrowStatements(currentNode));
177+
}
178+
});
179+
return throwStatements;
180+
}
181+
182+
/**
183+
* Gets all the children by traversing the tree generated from the current parent node.
184+
*
185+
* @param token parent node.
186+
* @return List of children of the current node.
187+
*/
188+
private static List<DetailAST> getChildrenNodes(DetailAST token) {
189+
final List<DetailAST> result = new LinkedList<>();
190+
191+
DetailAST currNode = token.getFirstChild();
192+
193+
// Add all the nodes on the current level of the tree and then move to the next level
194+
while (currNode != null) {
195+
result.add(currNode);
196+
currNode = currNode.getNextSibling();
197+
}
198+
199+
return result;
200+
}
201+
}

eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ the main ServiceBusClientBuilder. -->
109109
<suppress checks="com.azure.tools.checkstyle.checks.ServiceClientCheck" files="ServiceBusSessionReceiverAsyncClient.java"/>
110110
<suppress checks="com.azure.tools.checkstyle.checks.ServiceClientCheck" files="ServiceBusSessionReceiverClient.java"/>
111111

112+
<suppress checks="com.azure.tools.checkstyle.checks.UseCaughtExceptionCauseCheck" files="ServiceBusMessageBatch.java"/>
113+
112114
<!-- Suppress warning for ServiceClients to have Client in its name as Azure Container Registry has Clientlets which do not follow the same convention. -->
113115
<suppress checks="com.azure.tools.checkstyle.checks.ServiceClientCheck" files="ContainerRepository.java"/>
114116
<suppress checks="com.azure.tools.checkstyle.checks.ServiceClientCheck" files="ContainerRepositoryAsync.java"/>
@@ -141,6 +143,8 @@ the main ServiceBusClientBuilder. -->
141143
<!-- Suppress IO exception in the NIO package because they are required by the interface -->
142144
<suppress checks="com.azure.tools.checkstyle.checks.ThrowFromClientLoggerCheck"
143145
files="com.azure.storage.blob.nio/*"/>
146+
<suppress checks="com.azure.tools.checkstyle.checks.UseCaughtExceptionCauseCheck"
147+
files="com.azure.storage.blob.nio.AzureFileSystemProvider.java"/>
144148

145149
<suppress checks="com.azure.tools.checkstyle.checks.ThrowFromClientLoggerCheck"
146150
files="com.azure.storage.blob.batch.BlobBatchOperationResponse.java"
@@ -348,11 +352,12 @@ the main ServiceBusClientBuilder. -->
348352

349353
<!-- Checkstyle suppression for Event Hubs client APIs that use Flux instead of PagedFlux for methods that return a collection -->
350354
<suppress checks="com.azure.tools.checkstyle.checks.ServiceClientCheck" files="com.azure.messaging.eventhubs.(EventHubConsumerClient|EventHubConsumerAsyncClient|EventHubProducerClient|EventHubProducerAsyncClient).java"/>
355+
<suppress checks="com.azure.tools.checkstyle.checks.UseCaughtExceptionCauseCheck" files="com.azure.messaging.eventhubs.EventDataBatch.java"/>
351356

352357
<!-- ### begin: Spring related suppression -->
353358
<!-- TODO: (https://github.com/Azure/azure-sdk-for-java/issues/18291) -->
354359
<!-- GoodLoggingCheck: Spring as a framework, will not directly use ClientLogger-->
355-
<suppress checks="com.azure.tools.checkstyle.checks.(GoodLoggingCheck|ThrowFromClientLoggerCheck)" files=".*[/\\]com[/\\]azure[/\\]spring[/\\].*"/>
360+
<suppress checks="com.azure.tools.checkstyle.checks.(GoodLoggingCheck|ThrowFromClientLoggerCheck|UseCaughtExceptionCauseCheck)" files=".*[/\\]com[/\\]azure[/\\]spring[/\\].*"/>
356361

357362
<!-- ExternalDependencyExposedCheck: Spring will directly use classes from spring dependencies in methods-->
358363
<suppress checks="com.azure.tools.checkstyle.checks.ExternalDependencyExposedCheck" files=".*[/\\]com[/\\]azure[/\\]spring[/\\].*"/>

eng/code-quality-reports/src/main/resources/checkstyle/checkstyle.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,5 +398,9 @@ page at http://checkstyle.sourceforge.net/config.html -->
398398
<!-- <module name="com.azure.tools.checkstyle.checks.FluentMethodNameCheck">-->
399399
<!-- <property name="avoidStartWords" value="with,set"/>-->
400400
<!-- </module>-->
401+
402+
<!-- CUSTOM CHECKS -->
403+
<!-- Ensures caught exceptions are included as exception cause in subsequently thrown exception -->
404+
<module name="com.azure.tools.checkstyle.checks.UseCaughtExceptionCauseCheck"/>
401405
</module>
402406
</module>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.tools.checkstyle.checks;
5+
6+
import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport;
7+
import com.puppycrawl.tools.checkstyle.Checker;
8+
import org.junit.After;
9+
import org.junit.Before;
10+
import org.junit.Test;
11+
12+
import java.util.Collection;
13+
import java.util.Collections;
14+
15+
import static com.azure.tools.checkstyle.checks.UseCaughtExceptionCauseCheck.UNUSED_CAUGHT_EXCEPTION_ERROR;
16+
17+
/**
18+
* Tests for using the caught exception variable, UseCaughtExceptionTest.
19+
*/
20+
public class UseCaughtExceptionTest extends AbstractModuleTestSupport {
21+
private Checker checker;
22+
23+
@Before
24+
public void prepare() throws Exception {
25+
checker = createChecker(createModuleConfig(UseCaughtExceptionCauseCheck.class));
26+
}
27+
28+
@After
29+
public void cleanup() {
30+
checker.destroy();
31+
}
32+
33+
@Override
34+
protected String getPackageLocation() {
35+
return "com/azure/tools/checkstyle/checks/UseCaughtExceptionCheck";
36+
}
37+
38+
39+
@Test
40+
public void unusedCaughtExceptionTestData() throws Exception {
41+
String[] expected = {
42+
expectedErrorMessage(17, 13, String.format(UNUSED_CAUGHT_EXCEPTION_ERROR, "e"))
43+
};
44+
verify(checker, getPath("UseCaughtExceptionTestData.java"), expected);
45+
}
46+
47+
private String expectedErrorMessage(int line, int column, String errorMessage) {
48+
return String.format("%d:%d: %s", line, column, errorMessage);
49+
}
50+
}

0 commit comments

Comments
 (0)