Skip to content

Client core: new logging exceptions checks #45433

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
May 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
31b3abd
Add clientcore logging checks and update core to follow them
lmolkova May 21, 2025
d0dc0bd
up
lmolkova May 21, 2025
5531372
spotless
lmolkova May 21, 2025
87ba45d
up
lmolkova May 22, 2025
dee1e95
spotless
lmolkova May 22, 2025
1bed10a
clean up
lmolkova May 22, 2025
aa1f4ab
tests
lmolkova May 22, 2025
b3de204
checkstyle fixes and suppressions
lmolkova May 22, 2025
1e0ed58
checkstyle suppressions kv
lmolkova May 22, 2025
978508c
checkstyle suppressions identity
lmolkova May 22, 2025
09dad86
annotation processor and appconfig
lmolkova May 22, 2025
12ec5b7
up
lmolkova May 22, 2025
6ea581a
spotless
lmolkova May 22, 2025
3bd89f0
add string concat
lmolkova May 23, 2025
9b3b6ab
static exception text
lmolkova May 23, 2025
8119358
spotless
lmolkova May 23, 2025
3c11f46
review comments: part 1
lmolkova May 27, 2025
05e8edc
Merge branch 'main' into client-core-logging-exceptions-checks
lmolkova May 27, 2025
eb7165a
clean up
lmolkova May 27, 2025
47cd590
Merge branch 'main' into client-core-logging-exceptions-checks
lmolkova May 27, 2025
f1ee4ce
clean up identity
lmolkova May 27, 2025
1772c72
appconfig: coreexception and spotless
lmolkova May 27, 2025
a393556
add illegal state to the list of ignored exceptions
lmolkova May 27, 2025
1714d3f
formatting
lmolkova May 27, 2025
77e0f0b
Merge branch 'main' into client-core-logging-exceptions-checks
lmolkova May 27, 2025
98266bd
merge
lmolkova May 27, 2025
51e57c6
run exception checks on generated code
lmolkova May 28, 2025
d897fdd
Merge branch 'main' into client-core-logging-exceptions-checks
lmolkova May 28, 2025
6ec99cd
update kv suppressions
lmolkova May 28, 2025
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,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.tools.checkstyle.checks;

import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FullIdent;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;

import java.util.Arrays;
import java.util.List;

/**
* Checks that the exception is created and logged is also thrown.
*/
public class ExceptionCreatedButNotThrownCheck extends AbstractCheck {
static final String ERROR_MESSAGE = "An exception is created and logged, but not thrown. Ensure the exception is either thrown or not created at all. "
+ "See https://github.com/Azure/azure-sdk-for-java/wiki/Client-core:-logging-exceptions-best-practices for more details.";
private static final List<String> THROWABLE_AT_LOGGING_METHODS = Arrays.asList(
".throwableAtError",
".throwableAtWarning");

@Override
public int[] getDefaultTokens() {
return getRequiredTokens();
}

@Override
public int[] getAcceptableTokens() {
return getRequiredTokens();
}

@Override
public int[] getRequiredTokens() {
return new int[] {
TokenTypes.METHOD_CALL
};
}

@Override
public void visitToken(DetailAST token) {
String name = FullIdent.createFullIdentBelow(token).getText();
if (THROWABLE_AT_LOGGING_METHODS.stream().anyMatch(name::endsWith) && !isInsideThrow(token)) {
DetailAST logMethodCall = token.getParent().getParent();
if (logMethodCall == null || logMethodCall.getType() != TokenTypes.METHOD_CALL) {
return;
}

String nextName = FullIdent.createFullIdentBelow(logMethodCall).getText();
if (nextName.endsWith(".log")) {
log(token, ERROR_MESSAGE);
}
}
}

private boolean isInsideThrow(DetailAST methodCallAst) {
DetailAST parent = methodCallAst.getParent();

// Walk up to skip DOT or EXPR
while (parent != null && (parent.getType() == TokenTypes.DOT
|| parent.getType() == TokenTypes.EXPR
|| parent.getType() == TokenTypes.TYPECAST
|| parent.getType() == TokenTypes.METHOD_CALL)) {
parent = parent.getParent();
}

return parent != null && parent.getType() == TokenTypes.LITERAL_THROW;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.tools.checkstyle.checks;

import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FullIdent;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;

import java.util.Arrays;
import java.util.List;

/**
* Check that verifies that exceptions are logged when they are created.
* The check is skipped if:
* - the exception is created in a generated method or service interface
* - the exception is a NullPointerException, IllegalArgumentException or UnsupportedOperationException that are used for immediate input validation
*/
public class RawExceptionThrowCheck extends AbstractCheck {
static final String ERROR_MESSAGE = "Directly throwing a new exception is disallowed. Use the \"io.clientcore.core.instrumentation.logging.ClientLogger\" API instead, "
+ "such as \"logger.throwableAtError()\" or \"logger.throwableAtWarning()\"."
+ "See https://github.com/Azure/azure-sdk-for-java/wiki/Client-core:-logging-exceptions-best-practices for more details.";

private static final List<String> IGNORED_EXCEPTIONS = Arrays.asList("NullPointerException",
"IllegalArgumentException",
"IllegalStateException",
"UnsupportedOperationException");

private static final String CORE_EXCEPTION_FACTORY_NAME = "CoreException.from";

@Override
public int[] getDefaultTokens() {
return getRequiredTokens();
}

@Override
public int[] getAcceptableTokens() {
return getRequiredTokens();
}

@Override
public int[] getRequiredTokens() {
return new int[] {
TokenTypes.LITERAL_THROW
};
}

@Override
public void visitToken(DetailAST token) {
DetailAST expr = token.findFirstToken(TokenTypes.EXPR);
if (expr == null) {
return;
}

DetailAST newToken = expr.findFirstToken(TokenTypes.LITERAL_NEW);
if (newToken != null) {
String name = FullIdent.createFullIdentBelow(newToken).getText();
if (IGNORED_EXCEPTIONS.stream().noneMatch(name::endsWith)) {
log(newToken, ERROR_MESSAGE);
}

return;
}

DetailAST methodCallToken = expr.findFirstToken(TokenTypes.METHOD_CALL);
if (methodCallToken != null) {
if (CORE_EXCEPTION_FACTORY_NAME.equals(FullIdent.createFullIdentBelow(methodCallToken).getText()))
{
log(methodCallToken, ERROR_MESSAGE);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ public class ServiceClientCheck extends AbstractCheck {
"''%s'' service client with ''%s'', should always use return type ''%s'' if method name ends with ''%s'' or "
+ "should always named method name ends with ''%s'' if the return type is ''%s''.";
private static final String ASYNC_CONTEXT_ERROR =
"Asynchronous method with annotation @ServiceMethod must not has ''%s'' as a method parameter.";
"Asynchronous method with annotation @ServiceMethod must not have ''%s'' as a method parameter.";
private static final String SYNC_CONTEXT_ERROR =
"Synchronous method with annotation @ServiceMethod must has ''%s'' or ''%s'' as a method parameter.";
"Synchronous method with annotation @ServiceMethod must have ''%s'' or ''%s'' as a method parameter.";

// Add all imported classes into a map, key is the name of class and value is the full package path of class.
private final Map<String, String> simpleClassNameToQualifiedNameMap = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.tools.checkstyle.checks;

import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FullIdent;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;

import java.util.Arrays;
import java.util.List;

/**
* Checks that the message provided to "logger.throwableAt*().log" is static (not created using String.format).
*/
public class StringFormattedExceptionMessageCheck extends AbstractCheck {
static final String ERROR_MESSAGE = "Short message passed to \"logger.throwableAt*().log\" should be static. "
+ "Provide dynamic components using the \"addKeyValue(key, value)\" method instead. "
+ "See https://github.com/Azure/azure-sdk-for-java/wiki/Client-core:-logging-exceptions-best-practices for more details.";
private static final List<String> THROWABLE_AT_LOGGING_METHODS = Arrays.asList(
".throwableAtError",
".throwableAtWarning");

private static final String LOG_METHOD_NAME = ".log";
private static final String STRING_FORMAT_METHOD_NAME = "String.format";

@Override
public int[] getDefaultTokens() {
return getRequiredTokens();
}

@Override
public int[] getAcceptableTokens() {
return getRequiredTokens();
}

@Override
public int[] getRequiredTokens() {
return new int[] {
TokenTypes.METHOD_CALL
};
}

@Override
public void visitToken(DetailAST token) {
String name = FullIdent.createFullIdentBelow(token).getText();
if (THROWABLE_AT_LOGGING_METHODS.stream().anyMatch(name::endsWith)) {
DetailAST logMethodCall = token.getParent().getParent();
if (logMethodCall == null || logMethodCall.getType() != TokenTypes.METHOD_CALL) {
return;
}

String nextName = FullIdent.createFullIdentBelow(logMethodCall).getText();
if (nextName.endsWith(LOG_METHOD_NAME)) {
DetailAST elist = logMethodCall.findFirstToken(TokenTypes.ELIST);
if (elist == null) {
return;
}

// first param of `log()` method
DetailAST logExpr = elist.findFirstToken(TokenTypes.EXPR);
if (logExpr == null) {
return;
}

DetailAST firstParam = logExpr.getFirstChild();
// flag String.format
if (firstParam.getType() == TokenTypes.METHOD_CALL) {
String logFirstArgMethod = FullIdent.createFullIdentBelow(logExpr.getFirstChild()).getText();
if (logFirstArgMethod.endsWith(STRING_FORMAT_METHOD_NAME)) {
log(logExpr, ERROR_MESSAGE);
}
} else if (firstParam.getType() == TokenTypes.PLUS) {
// flag if dynamic, i.e. a combination of string literal and ident
DetailAST firstIdent = logExpr.getFirstChild().findFirstToken(TokenTypes.IDENT);
DetailAST firstLiteral = logExpr.getFirstChild().findFirstToken(TokenTypes.STRING_LITERAL);
if (firstIdent != null && firstLiteral != null) {
log(firstParam, ERROR_MESSAGE);
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,11 @@ page at http://checkstyle.sourceforge.net/config.html -->
<module name="ArrayTypeStyle"/>

<!--Checks for package naming and name length limit. Package names have to starting with either-->
<!--'com.microsoft.azure', 'com.azure', or 'io.clientcore' and then the extension names start with-->
<!--'com.azure.v2', or 'io.clientcore' and then the extension names start with-->
<!--characters 'a' to 'z' and only contains 'a' to 'z' or '0' to '9' with no more than 32 characters-->
<!--and the package names should be no more than 80 characters.-->
<module name="PackageName">
<property name="format" value="^(?=.{9,80}$)((io.clientcore)(\.[a-z][a-z0-9]{1,31})*)+$"/>
<property name="format" value="^(?=.{9,80}$)((io.clientcore|com.azure.v2)(\.[a-z][a-z0-9]{1,31})*)+$"/>
</module>

<!-- Javadoc checks -->
Expand Down Expand Up @@ -357,8 +357,16 @@ page at http://checkstyle.sourceforge.net/config.html -->
<module name="com.azure.tools.checkstyle.checks.JavadocFormatting"/>

<!-- CUSTOM CHECKS -->
<!-- Must use 'logger.logAndThrow' but not directly calling 'throw exception' -->
<module name="com.azure.tools.checkstyle.checks.ThrowFromClientLoggerCheck"/>
<!-- Don't format exception messages -->
<module name="com.azure.tools.checkstyle.checks.StringFormattedExceptionMessageCheck"/>

<!-- CUSTOM CHECKS -->
<!-- Log exceptions at creation time -->
<module name="com.azure.tools.checkstyle.checks.RawExceptionThrowCheck"/>

<!-- CUSTOM CHECKS -->
<!-- Throw created and logged exceptions -->
<module name="com.azure.tools.checkstyle.checks.ExceptionCreatedButNotThrownCheck"/>

<!-- CUSTOM CHECKS -->
<!-- Any class that implements the HttpPipelinePolicy interface should:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.tools.checkstyle.checks;

import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport;
import com.puppycrawl.tools.checkstyle.Checker;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class ExceptionCreatedButNotThrownCheckTest extends AbstractModuleTestSupport {
private Checker checker;

@BeforeEach
public void prepare() throws Exception {
checker = createChecker(createModuleConfig(ExceptionCreatedButNotThrownCheck.class));
}

@AfterEach
public void cleanup() {
checker.destroy();
}

@Override
protected String getPackageLocation() {
return "com/azure/tools/checkstyle/checks/ExceptionLoggingChecks";
}

@Test
public void throwCreatedException() throws Exception {
String[] expected = {
expectedErrorMessage(8, 32, ExceptionCreatedButNotThrownCheck.ERROR_MESSAGE),
};
verify(checker, getPath("ThrowCreatedExceptionCheckTestData.java"), expected);
}

private String expectedErrorMessage(int line, int column, String message) {
return String.format("%d:%d: %s", line, column, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.tools.checkstyle.checks;

import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport;
import com.puppycrawl.tools.checkstyle.Checker;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class RawExceptionThrowCheckTest extends AbstractModuleTestSupport {
private Checker checker;

@BeforeEach
public void prepare() throws Exception {
checker = createChecker(createModuleConfig(RawExceptionThrowCheck.class));
}

@AfterEach
public void cleanup() {
checker.destroy();
}

@Override
protected String getPackageLocation() {
return "com/azure/tools/checkstyle/checks/ExceptionLoggingChecks";
}

@Test
public void logNewExceptionTestData() throws Exception {
String[] expected = {
expectedErrorMessage(8, 15, RawExceptionThrowCheck.ERROR_MESSAGE),
expectedErrorMessage(13, 33, RawExceptionThrowCheck.ERROR_MESSAGE),
};
verify(checker, getPath("LogNewExceptionCheckTestData.java"), expected);
}

private String expectedErrorMessage(int line, int column, String message) {
return String.format("%d:%d: %s", line, column, message);
}
}
Loading
Loading