diff --git a/core b/core
index f4a8257154..c9bc84b64e 160000
--- a/core
+++ b/core
@@ -1 +1 @@
-Subproject commit f4a8257154ab26279a20dad3f8fbae5e496f8105
+Subproject commit c9bc84b64e757d42826e45a6dd5b69290da49a54
diff --git a/customization-base/src/main/java/com/azure/autorest/customization/implementation/ls/EclipseLanguageServerFacade.java b/customization-base/src/main/java/com/azure/autorest/customization/implementation/ls/EclipseLanguageServerFacade.java
index fb97c69902..c5c61cd9a4 100644
--- a/customization-base/src/main/java/com/azure/autorest/customization/implementation/ls/EclipseLanguageServerFacade.java
+++ b/customization-base/src/main/java/com/azure/autorest/customization/implementation/ls/EclipseLanguageServerFacade.java
@@ -53,12 +53,9 @@ public EclipseLanguageServerFacade(String pathToLanguageServerPlugin, Logger log
if (javaVersion < 17) {
// JAR to start v1.12.0
command.add("./plugins/org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar");
- } else if (javaVersion < 21) {
- // JAR to start v1.29.0
- command.add("./plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar");
} else {
- // JAR to start v1.31.0
- command.add("./plugins/org.eclipse.equinox.launcher_1.6.700.v20231214-2017.jar");
+ // JAR to start v1.39.0
+ command.add("./plugins/org.eclipse.equinox.launcher_1.6.900.v20240613-2009.jar");
}
command.add("--add-modules=ALL-SYSTEM");
@@ -76,8 +73,7 @@ public EclipseLanguageServerFacade(String pathToLanguageServerPlugin, Logger log
}
logger.info("Starting Eclipse JDT language server at {}", languageServerPath);
- server = new ProcessBuilder(command)
- .redirectOutput(ProcessBuilder.Redirect.PIPE)
+ server = new ProcessBuilder(command).redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectInput(ProcessBuilder.Redirect.PIPE)
.redirectErrorStream(true)
.directory(languageServerPath.toFile())
@@ -96,23 +92,16 @@ private static Path getLanguageServerDirectory(int javaVersion, Logger logger) t
if (javaVersion < 17) {
// Eclipse JDT language server version 1.12.0 is the last version that supports Java 11, which is
// autorest.java's baseline.
- downloadUrl = URI.create(DOWNLOAD_BASE_URL + "1.12.0/jdt-language-server-1.12.0-202206011637.tar.gz")
- .toURL();
+ downloadUrl
+ = URI.create(DOWNLOAD_BASE_URL + "1.12.0/jdt-language-server-1.12.0-202206011637.tar.gz").toURL();
languageServerPath = autorestLanguageServer.resolve("1.12.0");
- } else if (javaVersion < 21) {
- // Eclipse JDT language server version 1.29.0 is the latest version that supports Java 17.
- // In the future this else statement may need to be replaced with an else if as newer versions of
- // Eclipse JDT language server may baseline on Java 21 (or later).
- downloadUrl = URI.create(DOWNLOAD_BASE_URL + "1.29.0/jdt-language-server-1.29.0-202310261436.tar.gz")
- .toURL();
- languageServerPath = autorestLanguageServer.resolve("1.29.0");
} else {
- // Eclipse JDT language server version 1.31.0 is the latest version that supports Java 21.
+ // Eclipse JDT language server version 1.39.0 is the latest version that supports Java 17.
// In the future this else statement may need to be replaced with an else if as newer versions of
- // Eclipse JDT language server may baseline on Java 25 (or later).
- downloadUrl = URI.create(DOWNLOAD_BASE_URL + "1.31.0/jdt-language-server-1.31.0-202401111522.tar.gz")
- .toURL();
- languageServerPath = autorestLanguageServer.resolve("1.31.0");
+ // Eclipse JDT language server may baseline on Java 21 (or later).
+ downloadUrl
+ = URI.create(DOWNLOAD_BASE_URL + "1.39.0/jdt-language-server-1.39.0-202408291433.tar.gz").toURL();
+ languageServerPath = autorestLanguageServer.resolve("1.39.0");
}
Path languageServer = languageServerPath.resolve("jdt-language-server");
diff --git a/package-lock.json b/package-lock.json
index be7b4ec1dd..fc6fb4e5d2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@autorest/java",
- "version": "4.1.37",
+ "version": "4.1.38",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@autorest/java",
- "version": "4.1.37",
+ "version": "4.1.38",
"license": "MIT",
"devDependencies": {
"@microsoft.azure/autorest.testserver": "3.3.50",
diff --git a/package.json b/package.json
index e9997b75d7..b61fc97eb4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@autorest/java",
- "version": "4.1.37",
+ "version": "4.1.38",
"description": "The Java extension for classic generators in AutoRest.",
"scripts": {
"autorest": "autorest",
diff --git a/protocol-sdk-integration-tests/.vscode/eclipse-format-azure-sdk-for-java.xml b/protocol-sdk-integration-tests/.vscode/eclipse-format-azure-sdk-for-java.xml
new file mode 100644
index 0000000000..7866b52cda
--- /dev/null
+++ b/protocol-sdk-integration-tests/.vscode/eclipse-format-azure-sdk-for-java.xml
@@ -0,0 +1,401 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/README.md b/protocol-sdk-integration-tests/eng/code-quality-reports/README.md
index 5bda4a678f..fbfed68343 100644
--- a/protocol-sdk-integration-tests/eng/code-quality-reports/README.md
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/README.md
@@ -2,3 +2,5 @@
This module defines/configures the rules for code quality analysis tools such as checkstyle and spotbugs.
For more information refer to [Checkstyle](https://checkstyle.org/) and [Spotbugs](https://spotbugs.github.io/).
+
+![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-java%2Feng%2Fcode-quality-reports%2FREADME.png)
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/pom.xml b/protocol-sdk-integration-tests/eng/code-quality-reports/pom.xml
index 6b1b7869cf..1297b297ca 100644
--- a/protocol-sdk-integration-tests/eng/code-quality-reports/pom.xml
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/pom.xml
@@ -1,5 +1,5 @@
-
+
@@ -31,6 +31,15 @@
+
+
+
+
+ com.google.guava
+ guava
+ 33.1.0-jre
+
+
com.puppycrawl.toolscheckstyle
@@ -109,17 +118,17 @@
org.apache.maven.pluginsmaven-site-plugin
- 3.7.1
+ 3.12.1org.apache.maven.pluginsmaven-project-info-reports-plugin
- 3.0.0
+ 3.5.0org.apache.maven.pluginsmaven-surefire-plugin
- 3.1.0
+ 3.2.5
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/BlacklistedWordsCheck.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/DenyListedWordsCheck.java
similarity index 76%
rename from protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/BlacklistedWordsCheck.java
rename to protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/DenyListedWordsCheck.java
index 35c4cf3308..33be27d49d 100644
--- a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/BlacklistedWordsCheck.java
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/DenyListedWordsCheck.java
@@ -10,32 +10,30 @@
import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
-import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
-import java.util.stream.Collectors;
/**
- * Ensure that code is not using words or abbreviations that are blacklisted by this Checkstyle.
- * blacklistedWords: the words that have been blacklisted in the checkstyle.xml config file
- *
- * Prints out a message stating the location and the class, method or variable as well as the list
- * blacklisted words.
+ * Ensure that code is not using words or abbreviations that are deny listed by this Checkstyle. denyListedWords: the
+ * words that have been denied in the checkstyle.xml config file
+ *
+ * Prints out a message stating the location and the class, method or variable as well as the list of deny listed words.
*/
-public class BlacklistedWordsCheck extends AbstractCheck {
- private final Set blacklistedWords = new HashSet<>();
+public class DenyListedWordsCheck extends AbstractCheck {
+ private final Set denyListedWords = new HashSet<>();
static final String ERROR_MESSAGE = "%s, All Public API Classes, Fields and Methods should follow "
+ "Camelcase standards for the following words: %s.";
/**
* Adds words that Classes, Methods and Variables that should follow Camelcasing standards
- * @param blacklistedWords words that should follow normal Camelcasing standards
+ *
+ * @param denyListedWords words that should follow normal Camelcasing standards
*/
- public final void setBlacklistedWords(String... blacklistedWords) {
- if (blacklistedWords != null) {
- Collections.addAll(this.blacklistedWords, blacklistedWords);
+ public final void setDenyListedWords(String... denyListedWords) {
+ if (denyListedWords != null) {
+ Collections.addAll(this.denyListedWords, denyListedWords);
}
}
@@ -51,7 +49,7 @@ public int[] getAcceptableTokens() {
@Override
public int[] getRequiredTokens() {
- return new int[] {TokenTypes.CLASS_DEF,
+ return new int[]{TokenTypes.CLASS_DEF,
TokenTypes.METHOD_DEF,
TokenTypes.VARIABLE_DEF};
}
@@ -67,7 +65,7 @@ public void visitToken(DetailAST token) {
}
final String tokenName = token.findFirstToken(TokenTypes.IDENT).getText();
- if (!hasBlacklistedWords(tokenName)) {
+ if (!hasDenyListedWords(tokenName)) {
break;
}
@@ -76,7 +74,7 @@ public void visitToken(DetailAST token) {
break;
}
- log(token, String.format(ERROR_MESSAGE, tokenName, String.join(", ", this.blacklistedWords)));
+ log(token, String.format(ERROR_MESSAGE, tokenName, String.join(", ", this.denyListedWords)));
break;
default:
@@ -100,13 +98,13 @@ private boolean isPublicApi(DetailAST token) {
/**
* Gets the disallowed abbreviation contained in given String.
+ *
* @param tokenName the given String.
- * @return the disallowed abbreviation contained in given String as a
- * separate String.
+ * @return the disallowed abbreviation contained in given String as a separate String.
*/
- private boolean hasBlacklistedWords(String tokenName) {
- for (String blacklistedWord : blacklistedWords) {
- if (tokenName.contains(blacklistedWord)) {
+ private boolean hasDenyListedWords(String tokenName) {
+ for (String denyListedWord : denyListedWords) {
+ if (tokenName.contains(denyListedWord)) {
return true;
}
}
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/EnforceFinalFieldsCheck.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/EnforceFinalFieldsCheck.java
index 34b71ddc48..f52fe3f70b 100644
--- a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/EnforceFinalFieldsCheck.java
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/EnforceFinalFieldsCheck.java
@@ -5,12 +5,17 @@
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 com.puppycrawl.tools.checkstyle.utils.CheckUtil;
import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import java.util.Set;
/**
@@ -19,7 +24,7 @@
* nonFinalFields: keep an array of non private fields as tokens (to keep line number)
* assignmentsFromConstructor: Save a set of string for each field name that gets its value assigned in constructor
* assignmentsFromMethods: Save a set of strings for each field name that gets updated in any method
- *
+ *
* On finish tree, check what non-final fields get a value only in constructor and nowhere else by looking for
* strings inside nonFinalFields AND assignmentsFromConstructor but NOT in assignmentsFromMethods
*/
@@ -36,6 +41,7 @@ public class EnforceFinalFieldsCheck extends AbstractCheck {
private Set assignmentsFromMethods;
private DetailAST scopeParent = null;
private Set currentScopeParameterSet = null;
+ private Map variablesInScope = null;
private String currentClassName = null;
@Override
@@ -108,6 +114,7 @@ public void visitToken(DetailAST token) {
case TokenTypes.METHOD_DEF:
case TokenTypes.CTOR_DEF:
scopeParent = token;
+ variablesInScope = new HashMap<>();
break;
default:
// Checkstyle complains if there's no default block in switch
@@ -122,6 +129,7 @@ public void leaveToken(DetailAST token) {
case TokenTypes.CTOR_DEF:
scopeParent = null;
currentScopeParameterSet = null;
+ variablesInScope = null;
break;
default:
break;
@@ -160,6 +168,15 @@ private DetailAST getAssignedField(final DetailAST assignationToken) {
token -> token.getText().equals(this.currentClassName)).isPresent()) {
// Case when referencing same class for private static fields
return assignationWithDot.getLastChild();
+ } else if (assignationWithDot.getFirstChild().getType() == TokenTypes.IDENT) {
+ // Case where setting a field on a variable.
+ String variableNameToken = assignationWithDot.getFirstChild().getText();
+ DetailAST variableDeclaration = variablesInScope.get(variableNameToken);
+ DetailAST parentScope = getParentScope(assignationToken);
+ if (variableDeclaration != null && parentScope != null
+ && CheckUtil.isBeforeInSource(variableDeclaration, parentScope)) {
+ return assignationWithDot.getLastChild();
+ }
}
} else {
final DetailAST variableNameToken = assignationToken.getFirstChild();
@@ -172,6 +189,17 @@ private DetailAST getAssignedField(final DetailAST assignationToken) {
return null;
}
+ private static DetailAST getParentScope(DetailAST ast) {
+ DetailAST parent = ast.getParent();
+ do {
+ if (parent.getType() == TokenTypes.SLIST) {
+ return parent;
+ }
+ } while ((parent = parent.getParent()) != null);
+
+ return null;
+ }
+
/*
* Saves a field name to a container depending on the provided type
*/
@@ -197,7 +225,12 @@ private void checkAssignation(final DetailAST assignationToken) {
final DetailAST assignationParent = assignationToken.getParent();
if (assignationParent != null && TokenTypes.VARIABLE_DEF == assignationParent.getType()) {
- // Assignation for a variable definition. No need to check this assignation
+ String variableType = FullIdent.createFullIdentBelow(assignationParent.findFirstToken(TokenTypes.TYPE)).getText();
+ if (Objects.equals(currentClassName, variableType)) {
+ // Track variable definitions of the class we're currently in.
+ variablesInScope.put(assignationParent.findFirstToken(TokenTypes.IDENT).getText(), assignationParent);
+ }
+
return;
}
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/ExternalDependencyExposedCheck.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/ExternalDependencyExposedCheck.java
index 0cc3238066..4271830977 100644
--- a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/ExternalDependencyExposedCheck.java
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/ExternalDependencyExposedCheck.java
@@ -10,12 +10,8 @@
import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
-import java.util.Arrays;
-import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Map;
-import java.util.Set;
/**
* No external dependency exposed in public API
@@ -23,9 +19,6 @@
public class ExternalDependencyExposedCheck extends AbstractCheck {
private static final String EXTERNAL_DEPENDENCY_ERROR =
"Class ''%s'', is a class from external dependency. You should not use it as a %s type.";
- private static final Set VALID_DEPENDENCY_SET = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
- "java", "com.azure", "reactor", "org.reactivestreams"
- )));
private final Map simpleClassNameToQualifiedNameMap = new HashMap<>();
@@ -198,7 +191,12 @@ private boolean isValidClassDependency(String typeName) {
}
final String qualifiedName = simpleClassNameToQualifiedNameMap.get(typeName);
- return VALID_DEPENDENCY_SET.stream()
- .anyMatch(validPackageName -> qualifiedName.startsWith(validPackageName));
+
+ return "com.azure.".regionMatches(0, qualifiedName, 0, 10)
+ || "io.clientcore.".regionMatches(0, qualifiedName, 0, 14)
+ || "java.".regionMatches(0, qualifiedName, 0, 5)
+ || "javax.".regionMatches(0, qualifiedName, 0, 6)
+ || "reactor.".regionMatches(0, qualifiedName, 0, 8)
+ || "org.reactivestreams.".regionMatches(0, qualifiedName, 0, 20);
}
}
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/GoodLoggingCheck.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/GoodLoggingCheck.java
index 5eef8042d3..18f48a3ff6 100644
--- a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/GoodLoggingCheck.java
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/GoodLoggingCheck.java
@@ -35,13 +35,13 @@ public class GoodLoggingCheck extends AbstractCheck {
private static final int[] REQUIRED_TOKENS = new int[]{
TokenTypes.IMPORT,
TokenTypes.INTERFACE_DEF,
+ TokenTypes.ENUM_DEF,
TokenTypes.CLASS_DEF,
TokenTypes.LITERAL_NEW,
TokenTypes.VARIABLE_DEF,
TokenTypes.METHOD_CALL
};
- static final String STATIC_LOGGER_ERROR = "Use a static ClientLogger instance in a static method.";
static final String LOGGER_NAME_ERROR = "ClientLogger instance naming: use \"%s\" instead of \"%s\" for consistency.";
static final String NOT_CLIENT_LOGGER_ERROR = "Do not use %s class. Use \"%s\" as a logging mechanism instead of \"%s\".";
static final String LOGGER_NAME_MISMATCH_ERROR = "Not newing a ClientLogger with matching class name. Use \"%s.class\" "
@@ -78,7 +78,9 @@ public void finishTree(DetailAST ast) {
@Override
public void leaveToken(DetailAST ast) {
- if (ast.getType() == TokenTypes.CLASS_DEF) {
+ if (ast.getType() == TokenTypes.CLASS_DEF
+ || ast.getType() == TokenTypes.INTERFACE_DEF
+ || ast.getType() == TokenTypes.ENUM_DEF) {
classNameDeque.poll();
}
}
@@ -98,6 +100,7 @@ public void visitToken(DetailAST ast) {
break;
case TokenTypes.CLASS_DEF:
case TokenTypes.INTERFACE_DEF:
+ case TokenTypes.ENUM_DEF:
classNameDeque.offer(ast.findFirstToken(TokenTypes.IDENT).getText());
break;
case TokenTypes.LITERAL_NEW:
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/ImmutableClassCheck.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/ImmutableClassCheck.java
new file mode 100644
index 0000000000..dd250dce15
--- /dev/null
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/ImmutableClassCheck.java
@@ -0,0 +1,140 @@
+// 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.Scope;
+import com.puppycrawl.tools.checkstyle.api.TokenTypes;
+import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
+import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
+
+import java.util.Stack;
+
+/**
+ * Verify the classes with annotation {@code @Immutable} should have the following rules:
+ *
+ *
No public or protected fields
+ *
No public or protected setter methods
+ *
+ */
+public class ImmutableClassCheck extends AbstractCheck {
+ private static final String IMMUTABLE_NOTATION = "Immutable";
+
+ static final String PUBLIC_FIELD_ERROR_TEMPLATE =
+ "Classes annotated with @Immutable cannot have non-final public or protect fields. "
+ + "Found non-final public field: %s.";
+ static final String SETTER_METHOD_ERROR_TEMPLATE =
+ "Classes annotated with @Immutable cannot have public or protected setter methods. "
+ + "Found public setter method: %s.";
+
+ private Stack hasImmutableAnnotationStack;
+
+ @Override
+ public int[] getDefaultTokens() {
+ return getRequiredTokens();
+ }
+
+ @Override
+ public int[] getAcceptableTokens() {
+ return getRequiredTokens();
+ }
+
+ @Override
+ public int[] getRequiredTokens() {
+ return new int[]{
+ TokenTypes.CLASS_DEF,
+ TokenTypes.VARIABLE_DEF,
+ TokenTypes.METHOD_DEF
+ };
+ }
+
+ @Override
+ public void beginTree(DetailAST root) {
+ hasImmutableAnnotationStack = new Stack<>();
+ }
+
+ @Override
+ public void visitToken(DetailAST token) {
+ switch (token.getType()) {
+ case TokenTypes.CLASS_DEF:
+ hasImmutableAnnotationStack.add(hasImmutableAnnotation(token));
+ break;
+
+ case TokenTypes.VARIABLE_DEF:
+ if (!hasImmutableAnnotationStack.isEmpty() && hasImmutableAnnotationStack.peek()) {
+ checkForPublicField(token);
+ }
+ break;
+
+ case TokenTypes.METHOD_DEF:
+ if (!hasImmutableAnnotationStack.isEmpty() && hasImmutableAnnotationStack.peek()) {
+ checkForSetterMethod(token);
+ }
+ break;
+
+ default:
+ // Checkstyle complains if there's no default block in switch
+ break;
+ }
+ }
+
+ @Override
+ public void leaveToken(DetailAST ast) {
+ if (ast.getType() == TokenTypes.CLASS_DEF && !hasImmutableAnnotationStack.isEmpty()) {
+ hasImmutableAnnotationStack.pop();
+ }
+ }
+
+ /*
+ * Checks if the class is annotated with annotation {@literal @Immutable}. A class could have multiple annotations.
+ *
+ * @param classDefToken the CLASS_DEF AST node
+ * @return true if the class is annotated with {@literal @Immutable}, false otherwise.
+ */
+ private static boolean hasImmutableAnnotation(DetailAST classDefinition) {
+ DetailAST immutableAnnotation = AnnotationUtil.getAnnotation(classDefinition, IMMUTABLE_NOTATION);
+ return immutableAnnotation != null;
+ }
+
+ private void checkForPublicField(DetailAST variableDefinition) {
+ DetailAST modifiers = variableDefinition.findFirstToken(TokenTypes.MODIFIERS);
+
+ // Field has no modifiers or is final, therefore it's immutable for all intents and purposes.
+ if (modifiers == null || modifiers.findFirstToken(TokenTypes.FINAL) != null) {
+ return;
+ }
+
+ if (isScopeAndSurroundingScopePublic(variableDefinition)) {
+ // Field is 'public' or 'protected', immutable classes cannot have public fields.
+ log(variableDefinition, String.format(PUBLIC_FIELD_ERROR_TEMPLATE,
+ variableDefinition.findFirstToken(TokenTypes.IDENT).getText()));
+ }
+ }
+
+ private void checkForSetterMethod(DetailAST methodDefinition) {
+ String methodName = methodDefinition.findFirstToken(TokenTypes.IDENT).getText();
+
+ if (!isSetterMethod(methodName)) {
+ return;
+ }
+
+ if (isScopeAndSurroundingScopePublic(methodDefinition)) {
+ // Setter method is 'public' or 'protected', immutable classes cannot have public setters.
+ log(methodDefinition, String.format(SETTER_METHOD_ERROR_TEMPLATE, methodName));
+ }
+ }
+
+ private static boolean isSetterMethod(String methodName) {
+ return methodName.startsWith("set") && methodName.length() >= 4 && Character.isUpperCase(methodName.charAt(3));
+ }
+
+ private static boolean isScopeAndSurroundingScopePublic(DetailAST detailAST) {
+ Scope scope = ScopeUtil.getScope(detailAST);
+ Scope surroundingScope = ScopeUtil.getSurroundingScope(detailAST);
+
+ return (scope == Scope.PUBLIC || scope == Scope.PROTECTED)
+ && (surroundingScope == Scope.PUBLIC || surroundingScope == Scope.PROTECTED);
+ }
+}
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/JavadocThrowsChecks.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/JavadocThrowsChecks.java
index e157bf2bab..d0fde3be06 100644
--- a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/JavadocThrowsChecks.java
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/JavadocThrowsChecks.java
@@ -17,6 +17,9 @@
import java.util.HashSet;
import java.util.Map;
+/**
+ * Verifies that all throws in the public API have JavaDocs explaining why and when they are thrown.
+ */
public class JavadocThrowsChecks extends AbstractCheck {
static final String MISSING_DESCRIPTION_MESSAGE =
"@throws tag requires a description explaining when the error is thrown.";
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/NoImplInPublicAPI.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/NoImplInPublicAPI.java
index 33d9d643b1..0398c34931 100644
--- a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/NoImplInPublicAPI.java
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/NoImplInPublicAPI.java
@@ -17,6 +17,11 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+/**
+ * Verifies that:
+ * 1) no return classes are from the implementation package
+ * 2) no class of implementation package as method's parameters
+ */
public class NoImplInPublicAPI extends AbstractCheck {
private static final String ALTERNATIVE_MOVE_TO_PUBLIC_API = "Alternatively, it can be removed from the "
+ "implementation package and made public API, after appropriate API review.";
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/OnlyFinalFieldsForImmutableClassCheck.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/OnlyFinalFieldsForImmutableClassCheck.java
deleted file mode 100644
index 916c753385..0000000000
--- a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/OnlyFinalFieldsForImmutableClassCheck.java
+++ /dev/null
@@ -1,94 +0,0 @@
-// 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.TokenTypes;
-import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
-import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
-
-import java.util.Optional;
-
-/**
- * Verify the classes with annotation {@code @Immutable} should have following rules:
- *
- *
Only final fields allowed
- *
- */
-public class OnlyFinalFieldsForImmutableClassCheck extends AbstractCheck {
- private static final String IMMUTABLE_NOTATION = "Immutable";
- private static final String ERROR_MSG = "The variable field ''%s'' should be final."
- + "Classes annotated with @Immutable are supposed to be immutable.";
-
- private boolean hasImmutableAnnotation;
-
- @Override
- public int[] getDefaultTokens() {
- return getRequiredTokens();
- }
-
- @Override
- public int[] getAcceptableTokens() {
- return getRequiredTokens();
- }
-
- @Override
- public int[] getRequiredTokens() {
- return new int[] {
- TokenTypes.CLASS_DEF,
- TokenTypes.OBJBLOCK
- };
- }
-
- @Override
- public void beginTree(DetailAST root) {
- hasImmutableAnnotation = false;
- }
-
- @Override
- public void visitToken(DetailAST token) {
- switch (token.getType()) {
- case TokenTypes.CLASS_DEF:
- hasImmutableAnnotation = hasImmutableAnnotation(token);
- break;
- case TokenTypes.OBJBLOCK:
- if (hasImmutableAnnotation) {
- checkForOnlyFinalFields(token);
- }
- break;
- default:
- // Checkstyle complains if there's no default block in switch
- break;
- }
- }
-
- /*
- * Checks if the class is annotated with annotation {@literal @Immutable}. A class could have multiple annotations.
- *
- * @param classDefToken the CLASS_DEF AST node
- * @return true if the class is annotated with {@literal @Immutable}, false otherwise.
- */
- private boolean hasImmutableAnnotation(DetailAST classDefToken) {
- DetailAST immutableAnnotation = AnnotationUtil.getAnnotation(classDefToken, IMMUTABLE_NOTATION);
- return immutableAnnotation != null;
- }
-
- /*
- * Checks all field definitions within the first level of a class are final
- *
- * @param objBlockToken the OBJBLOCK AST node
- */
- private void checkForOnlyFinalFields(DetailAST objBlockToken) {
- Optional nonFinalFieldFound = TokenUtil.findFirstTokenByPredicate(objBlockToken,
- node -> TokenTypes.VARIABLE_DEF == node.getType() && !node.branchContains(TokenTypes.FINAL)
- && !Utils.hasIllegalCombination(node.findFirstToken(TokenTypes.MODIFIERS)));
-
- if (nonFinalFieldFound.isPresent()) {
- DetailAST field = nonFinalFieldFound.get().findFirstToken(TokenTypes.IDENT);
- log(field, String.format(ERROR_MSG, field.getText()));
- }
- }
-
-}
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/StepVerifierCheck.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/StepVerifierCheck.java
new file mode 100644
index 0000000000..a92bf04f6f
--- /dev/null
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/StepVerifierCheck.java
@@ -0,0 +1,76 @@
+// 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;
+
+/**
+ * Ensures that test code doesn't use {@code StepVerifier.setDefaultTimeout}.
+ *
+ * This configures a default timeout used by all {@code StepVerifier} calls, which can lead to flaky tests as this may
+ * affect other tests.
+ */
+public class StepVerifierCheck extends AbstractCheck {
+ private static final String SET_DEFAULT_TIMEOUT = "setDefaultTimeout";
+ private static final String FULLY_QUALIFIED = "reactor.test.StepVerifier.setDefaultTimeout";
+ private static final String METHOD_CALL = "StepVerifier.setDefaultTimeout";
+
+ static final String ERROR_MESSAGE = "Do not use StepVerifier.setDefaultTimeout as it can affect other tests. "
+ + "Instead use expect* methods on StepVerifier and use verify(Duration) to "
+ + "set timeouts on a test-by-test basis.";
+
+ private boolean hasStaticImport;
+
+ @Override
+ public int[] getDefaultTokens() {
+ return new int[]{
+ TokenTypes.METHOD_CALL,
+ TokenTypes.STATIC_IMPORT
+ };
+ }
+
+ @Override
+ public int[] getAcceptableTokens() {
+ return getDefaultTokens();
+ }
+
+ @Override
+ public int[] getRequiredTokens() {
+ return getDefaultTokens();
+ }
+
+ @Override
+ public void init() {
+ super.init();
+ hasStaticImport = false;
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ hasStaticImport = false;
+ }
+
+ @Override
+ public void visitToken(DetailAST ast) {
+ if (ast.getType() == TokenTypes.STATIC_IMPORT) {
+ // Compare if the static import is for StepVerifier.setDefaultTimeout
+ hasStaticImport = FULLY_QUALIFIED.equals(
+ FullIdent.createFullIdent(ast.getFirstChild().getNextSibling()).getText());
+ } else {
+ // Compare the method call against StepVerifier.setDefaultTimeout or setDefaultTimeout if there is a static
+ // import for StepVerifier.setDefaultTimeout
+ FullIdent fullIdent = FullIdent.createFullIdentBelow(ast);
+ if (hasStaticImport && SET_DEFAULT_TIMEOUT.equals(fullIdent.getText())) {
+ log(ast.getLineNo(), fullIdent.getColumnNo(), ERROR_MESSAGE);
+ } else if (METHOD_CALL.equals(fullIdent.getText())) {
+ log(ast.getLineNo(), fullIdent.getColumnNo(), ERROR_MESSAGE);
+ } else if (FULLY_QUALIFIED.equals(fullIdent.getText())) {
+ log(ast.getLineNo(), fullIdent.getColumnNo(), ERROR_MESSAGE);
+ }
+ }
+ }
+}
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/UseCaughtExceptionCauseCheck.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/UseCaughtExceptionCauseCheck.java
new file mode 100644
index 0000000000..5598892b87
--- /dev/null
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/UseCaughtExceptionCauseCheck.java
@@ -0,0 +1,201 @@
+// 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.TokenTypes;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * This check ensures that an exception thrown includes the current caught exception cause.
+ * New exception should use the original/cause exception object to provide the full stack trace for the problem.
+ *
+ * // DO
+ * try {
+ * url = new URL(urlString);
+ * } catch (MalformedURLException ex) {
+ * throw new RuntimeException(ex);
+ * }
+ *
+ * // DON'T
+ * try {
+ * url = new URL(urlString);
+ * } catch (MalformedURLException ex) {
+ * throw new RuntimeException("Invalid URL string was given."); // "ex" is ignored.
+ * }
+ */
+public class UseCaughtExceptionCauseCheck extends AbstractCheck {
+ static final String UNUSED_CAUGHT_EXCEPTION_ERROR = "Caught and rethrown exceptions should include the caught"
+ + " exception as the cause in the rethrown exception. Dropping the causal exception makes it more difficult"
+ + " to troubleshoot issues when they arise. Include the caught exception variable %s as the cause.";
+
+ @Override
+ public int[] getDefaultTokens() {
+ return getRequiredTokens();
+ }
+
+ @Override
+ public int[] getAcceptableTokens() {
+ return getRequiredTokens();
+ }
+
+ @Override
+ public int[] getRequiredTokens() {
+ return new int[] {TokenTypes.LITERAL_CATCH};
+ }
+
+ @Override
+ public void visitToken(DetailAST catchBlockToken) {
+ // get the caught exception variable name from the catch block
+ final DetailAST catchStatement = catchBlockToken.findFirstToken(TokenTypes.PARAMETER_DEF);
+ final String caughtExceptionVariableName = catchStatement.findFirstToken(TokenTypes.IDENT).getText();
+
+ // get all throw statements from current catch block
+ final List throwStatements = getThrowStatements(catchBlockToken);
+
+ // get possible exception names to which the original exception might be assigned to
+ final List wrappedExceptions =
+ getWrappedExceptions(catchBlockToken, catchBlockToken, caughtExceptionVariableName);
+
+ throwStatements.forEach(throwToken -> {
+ final List throwParamNames = new LinkedList<>();
+ getThrowParamNames(throwToken, throwParamNames);
+ // include the original exception name to the list to look for in throw statements
+ wrappedExceptions.add(caughtExceptionVariableName);
+
+ // throwParamNames = [ex, p]
+ // exceptionsList = [ex, cause]
+ // Caught exception variable is used if an intersection is between the throw statements param names
+ // used and the actual exception names being thrown.
+ List intersect =
+ wrappedExceptions.stream().filter(throwParamNames::contains).collect(Collectors.toList());
+ if (intersect.size() == 0) {
+ log(throwToken, String.format(UNUSED_CAUGHT_EXCEPTION_ERROR, caughtExceptionVariableName));
+ }
+ });
+ }
+
+ /**
+ * Returns the list of exceptions that wrapped the current exception tokens
+ *
+ * @param currentCatchAST current catch block token
+ * @param detailAST catch block throw parent token
+ * @param caughtExceptionVariableName list containing the exception tokens
+ * @return list of wrapped exception tokens
+ */
+ private List getWrappedExceptions(DetailAST currentCatchAST, DetailAST detailAST,
+ String caughtExceptionVariableName) {
+
+ final List wrappedExceptionNames = new LinkedList<>();
+
+ for (DetailAST currentNode : getChildrenNodes(detailAST)) {
+ // Recursively traverse through the children of the parent node to collect references where the
+ // caught exception variable is used.
+ if (currentNode.getType() == TokenTypes.IDENT
+ && currentNode.getText().equals(caughtExceptionVariableName)) {
+ getWrappedExceptionVariable(currentCatchAST, wrappedExceptionNames, currentNode);
+ }
+
+ // add collection in case of last node on this level
+ if (currentNode.getFirstChild() != null) {
+ wrappedExceptionNames.addAll(
+ getWrappedExceptions(currentCatchAST, currentNode, caughtExceptionVariableName));
+ }
+ }
+ return wrappedExceptionNames;
+ }
+
+ /**
+ * Returns the wrapped exception variable name
+ */
+ private void getWrappedExceptionVariable(DetailAST currentCatchBlock, List wrappedExceptionNames,
+ DetailAST currentToken) {
+ DetailAST temp = currentToken;
+
+ // Get the node assigning the caught exception variable, traversing upwards starting from the current node.
+ while (!temp.equals(currentCatchBlock) && temp.getType() != TokenTypes.ASSIGN) {
+ temp = temp.getParent();
+ }
+
+ if (temp.getType() == TokenTypes.ASSIGN) {
+ final DetailAST wrappedException;
+ // Get the variable definition param name to which the caught exception variable is assigned.
+ if (temp.getParent().getType() == TokenTypes.VARIABLE_DEF) {
+ wrappedException = temp.getParent().findFirstToken(TokenTypes.IDENT);
+ } else if (temp.findFirstToken(TokenTypes.DOT) != null) {
+ // Get the variable name if assigned to a 'this' variable
+ wrappedException = temp.findFirstToken(TokenTypes.DOT).findFirstToken(TokenTypes.IDENT);
+ } else {
+ wrappedException = temp.findFirstToken(TokenTypes.IDENT);
+ }
+ if (wrappedException != null) {
+ wrappedExceptionNames.add(wrappedException.getText());
+ }
+ }
+ }
+
+ /**
+ * Returns the parameter names for current throw keyword.
+ *
+ * @param throwParent The parent throw token
+ * @param paramNames The list containing the parameter names
+ * @return list of throw param names
+ */
+ private List getThrowParamNames(DetailAST throwParent, List paramNames) {
+ // get all param names by recursively going through all the throw statements retrieving the token type IDENT
+ // for the text
+ getChildrenNodes(throwParent).forEach(currentNode -> {
+ if (currentNode.getType() == TokenTypes.IDENT) {
+ paramNames.add(currentNode.getText());
+ }
+ if (currentNode.getFirstChild() != null) {
+ getThrowParamNames(currentNode, paramNames);
+ }
+ });
+ return paramNames;
+ }
+
+ /**
+ * Recursive method that searches for all the LITERAL_THROW on the current catch token.
+ *
+ * @param catchBlockToken A start token.
+ * @return list of throw tokens
+ */
+ private List getThrowStatements(DetailAST catchBlockToken) {
+ final List throwStatements = new LinkedList<>();
+ getChildrenNodes(catchBlockToken).forEach(currentNode -> {
+ if (TokenTypes.LITERAL_THROW == currentNode.getType()) {
+ throwStatements.add(currentNode);
+ }
+ if (currentNode.getFirstChild() != null) {
+ throwStatements.addAll(getThrowStatements(currentNode));
+ }
+ });
+ return throwStatements;
+ }
+
+ /**
+ * Gets all the children by traversing the tree generated from the current parent node.
+ *
+ * @param token parent node.
+ * @return List of children of the current node.
+ */
+ private static List getChildrenNodes(DetailAST token) {
+ final List result = new LinkedList<>();
+
+ DetailAST currNode = token.getFirstChild();
+
+ // Add all the nodes on the current level of the tree and then move to the next level
+ while (currNode != null) {
+ result.add(currNode);
+ currNode = currNode.getNextSibling();
+ }
+
+ return result;
+ }
+}
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/Utils.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/Utils.java
index 903c0a706d..96cfc89b06 100644
--- a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/Utils.java
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/Utils.java
@@ -8,10 +8,7 @@
import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
import java.util.Optional;
-import java.util.Set;
/**
* Common utils amount custom checks
@@ -19,22 +16,34 @@
public class Utils {
/*
* Set of modifiers that cannot be combined with final because it causes a violation.
+ *
+ * This is an int array instead of a Set as there are so few values and cache locality is better.
*/
- private static final Set INVALID_FINAL_COMBINATION = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
- TokenTypes.LITERAL_TRANSIENT,
- TokenTypes.LITERAL_VOLATILE,
- TokenTypes.LITERAL_DEFAULT,
- TokenTypes.LITERAL_PROTECTED
- )));
+ private static final int[] INVALID_FINAL_COMBINATION;
/*
- * Set of annotations that cannot be combined with modifier 'final' because it would break serialization.
+ * Set of annotations that cannot be combined with modified 'final' because it would break serialization.
+ *
+ * This is a String array instead of a Set as there are so few values and cache locality is better.
*/
- private static final Set INVALID_FINAL_ANNOTATIONS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
- "JsonProperty",
- "JsonAlias",
- "JacksonXmlProperty"
- )));
+ private static final String[] INVALID_FINAL_ANNOTATIONS;
+
+ static {
+ INVALID_FINAL_COMBINATION = new int[] {
+ TokenTypes.LITERAL_TRANSIENT,
+ TokenTypes.LITERAL_VOLATILE,
+ TokenTypes.LITERAL_DEFAULT,
+ TokenTypes.LITERAL_PROTECTED
+ };
+ Arrays.sort(INVALID_FINAL_COMBINATION);
+
+ INVALID_FINAL_ANNOTATIONS = new String[] {
+ "JacksonXmlProperty",
+ "JsonAlias",
+ "JsonProperty"
+ };
+ Arrays.sort(INVALID_FINAL_ANNOTATIONS);
+ }
/**
* Check if variable modifiers contains any of the illegal combination with final modifier
@@ -51,10 +60,19 @@ protected static boolean hasIllegalCombination(DetailAST modifiers) {
Optional illegalCombination = TokenUtil.findFirstTokenByPredicate(modifiers, (node) -> {
final int type = node.getType();
- return INVALID_FINAL_COMBINATION.contains(node.getType()) || (TokenTypes.ANNOTATION == type
- && INVALID_FINAL_ANNOTATIONS.contains(node.findFirstToken(TokenTypes.IDENT).getText()));
+
+ return invalidFinalCombination(type) || invalidFinalAnnotation(type, node);
});
return illegalCombination.isPresent();
}
+
+ private static boolean invalidFinalCombination(int type) {
+ return Arrays.binarySearch(INVALID_FINAL_COMBINATION, type) != -1;
+ }
+
+ private static boolean invalidFinalAnnotation(int type, DetailAST ast) {
+ return type == TokenTypes.ANNOTATION
+ && Arrays.binarySearch(INVALID_FINAL_ANNOTATIONS, ast.findFirstToken(TokenTypes.IDENT).getText(), String::compareTo) != -1;
+ }
}
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/filters/AzureSdkFilter.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/filters/AzureSdkFilter.java
new file mode 100644
index 0000000000..1cf50aafb3
--- /dev/null
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/filters/AzureSdkFilter.java
@@ -0,0 +1,98 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.azure.tools.checkstyle.filters;
+
+import com.puppycrawl.tools.checkstyle.api.AuditEvent;
+import com.puppycrawl.tools.checkstyle.api.Filter;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A Checkstyle filter that filters out common Checkstyle violations that should be ignored by all SDKs.
+ *
+ * The following violations are ignored:
+ *
+ *
+ *
ExternalDependencyExposed checks in implementation code
+ *
Missing Javadoc comments in sample and test code
+ *
Star imports in test code
+ *
Nested blocks in test code
+ *
Azure SDK Checkstyle checks in sample and test code
+ *
Missing package-info in sample and test code
+ *
Line length in sample and test code
+ *
Equals avoid null in sample code
+ *
+ */
+public class AzureSdkFilter implements Filter {
+ // Pattern that matches sample, test, and test-shared files.
+ // This will capture the file type (sample, test, or test-shared) in group 1, which removes the need for multiple
+ // Patterns to match whether the file is a sample, test, or test-shared file to improve performance.
+ private static final Pattern SAMPLE_OR_TEST_FILE_PATTERN
+ = Pattern.compile(".*src[/\\\\](samples|test|test-shared)[/\\\\]java[/\\\\].*\\.java$");
+
+ // Pattern that matches implementation files.
+ private static final Pattern IMPLEMENTATION_FILE_PATTERN
+ = Pattern.compile(".*src[/\\\\].*[/\\\\]implementation[/\\\\].*\\.java$");
+
+ // The package name prefix for Azure SDK Checkstyle checks
+ private static final String AZURE_SDK_CHECK_START = "com.azure.tools.checkstyle.checks.";
+
+ // The ExternalDependencyExposed Azure SDK Checkstyle check
+ private static final String EXTERNAL_DEPENDENCY_EXPOSED = AZURE_SDK_CHECK_START + "ExternalDependencyExposed";
+
+ @Override
+ public boolean accept(AuditEvent event) {
+ boolean shouldSkip = isIgnoredImplementation(event) || isIgnoredSampleOrTest(event);
+
+ return !shouldSkip;
+ }
+
+ private static boolean isIgnoredSampleOrTest(AuditEvent event) {
+ Matcher matcher = SAMPLE_OR_TEST_FILE_PATTERN.matcher(event.getFileName());
+
+ if (!matcher.matches()) {
+ // Not a test or sample file, so don't filter
+ return false;
+ }
+
+ boolean isTestFile = matcher.group(1).startsWith("test");
+
+ String violation = event.getViolation().getSourceName();
+
+ if (violation.contains("Javadoc")) {
+ // Ignore missing Javadoc comments in test code
+ return true;
+ } else if (isTestFile && violation.contains("AvoidStarImport")) {
+ // Ignore star imports in test code
+ return true;
+ } else if (isTestFile && violation.contains("AvoidNestedBlocks")) {
+ // Ignore nested blocks in test code
+ return true;
+ } else if (violation.startsWith(AZURE_SDK_CHECK_START)) {
+ // Ignore Azure SDK Checkstyle checks in sample and test code
+ return true;
+ } else if (violation.contains("LineLength")) {
+ // Ignore line length in sample and test code
+ return true;
+ } else if (!isTestFile && violation.contains("EqualsAvoidNull")) {
+ // Ignore equals avoid null in sample code
+ return true;
+ }
+
+ return false;
+ }
+
+ private static boolean isIgnoredImplementation(AuditEvent event) {
+ Matcher matcher = IMPLEMENTATION_FILE_PATTERN.matcher(event.getFileName());
+
+ if (!matcher.matches()) {
+ // Not an implementation file, so don't filter
+ return false;
+ }
+
+ String violation = event.getViolation().getSourceName();
+
+ return violation.startsWith(EXTERNAL_DEPENDENCY_EXPOSED);
+ }
+}
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/filters/package-info.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/filters/package-info.java
new file mode 100644
index 0000000000..0fba146619
--- /dev/null
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/filters/package-info.java
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+/**
+ * This package contains classes for custom Checkstyle filters.
+ */
+package com.azure.tools.checkstyle.filters;
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkAllowedExternalApis.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkAllowedExternalApis.java
new file mode 100644
index 0000000000..0ca3a54187
--- /dev/null
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkAllowedExternalApis.java
@@ -0,0 +1,165 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.tools.revapi.transforms;
+
+import org.revapi.Difference;
+import org.revapi.Element;
+import org.revapi.TransformationResult;
+import org.revapi.base.BaseDifferenceTransform;
+import org.revapi.java.spi.JavaTypeElement;
+
+import javax.annotation.Nullable;
+import javax.lang.model.element.NestingKind;
+import javax.lang.model.element.TypeElement;
+import java.util.regex.Pattern;
+
+public final class AzureSdkAllowedExternalApis> extends BaseDifferenceTransform {
+ private static final Pattern DIFFERENCE_CODE_PATTERN = Pattern.compile("java.class.externalClassExposedInAPI",
+ Pattern.LITERAL);
+
+ @Override
+ public Pattern[] getDifferenceCodePatterns() {
+ return new Pattern[] { DIFFERENCE_CODE_PATTERN };
+ }
+
+ @Override
+ public TransformationResult tryTransform(@Nullable E oldElement, @Nullable E newElement, Difference difference) {
+ if (newElement == null) {
+ // Missing element to compare.
+ return TransformationResult.keep();
+ }
+
+ if (!(newElement instanceof JavaTypeElement)) {
+ // Unknown element type.
+ return TransformationResult.keep();
+ }
+
+ TypeElement outermostElement = findOuterMostClass(((JavaTypeElement) newElement).getDeclaringElement());
+ ExternalApiStatus shouldBeIgnored = shouldExternalApiBeIgnored(outermostElement);
+
+ return shouldBeIgnored.ignore ? TransformationResult.discard() : TransformationResult.keep();
+ }
+
+ @Override
+ public String getExtensionId() {
+ return "azure-sdk-allowed-external-apis";
+ }
+
+ private static TypeElement findOuterMostClass(javax.lang.model.element.Element el) {
+ while (el != null && !(el instanceof TypeElement)) {
+ el = el.getEnclosingElement();
+ }
+
+ if (el == null) {
+ return null;
+ }
+
+ return ((TypeElement) el).getNestingKind() == NestingKind.TOP_LEVEL
+ ? (TypeElement) el
+ : findOuterMostClass(el.getEnclosingElement());
+ }
+
+
+
+ private static ExternalApiStatus shouldExternalApiBeIgnored(TypeElement element) {
+ if (element == null) {
+ return ExternalApiStatus.KEEP;
+ }
+
+ String className = element.getQualifiedName().toString();
+
+ if (className.startsWith("com.")) {
+ if ("azure.".regionMatches(0, className, 4, 6)) {
+ if ("communication.common.".regionMatches(0, className, 10, 21)
+ || "core.".regionMatches(0, className, 10, 5)
+ || "cosmos.".regionMatches(0, className, 10, 7)
+ || "data.schemaregistry.".regionMatches(0, className, 10, 20)
+ || "data.appconfiguration.".regionMatches(0, className, 10, 22)
+ || "identity.".regionMatches(0, className, 10, 9)
+ || "json.".regionMatches(0, className, 10, 5)
+ || "messaging.eventgrid.".regionMatches(0, className, 10, 20)
+ || "messaging.eventhubs.".regionMatches(0, className, 10, 20)
+ || "messaging.servicebus.".regionMatches(0, className, 10, 21)
+ || "resourcemanager.".regionMatches(0, className, 10, 16)
+ || "security.keyvault.".regionMatches(0, className, 10, 18)
+ || "spring.cloud.appconfiguration.config.".regionMatches(0, className, 10, 20)
+ || "spring.cloud.feature.".regionMatches(0, className, 10, 21)
+ || "storage.".regionMatches(0, className, 10, 8)
+ || "xml.".regionMatches(0, className, 10, 4)) {
+ return ExternalApiStatus.SDK_CLASSES;
+ } else if ("perf.test.core.".regionMatches(0, className, 10, 15)) {
+ return ExternalApiStatus.PERF_TEST;
+ } else if (className.length() == 53
+ && className.endsWith("spring.cloud.config.AppConfigurationRefresh")) {
+ return ExternalApiStatus.APP_CONFIGURATION_REFRESH;
+ }
+ } else if ("mysql.cj.".regionMatches(0, className, 4, 9)) {
+ return ExternalApiStatus.MYSQL_CJ;
+ }
+ } else if (className.startsWith("io.")) {
+ if ("cloudevents.".regionMatches(0, className, 3, 12)) {
+ return ExternalApiStatus.CLOUD_EVENTS;
+ } else if ("opentelemetry.".regionMatches(0, className, 3, 14)) {
+ return ExternalApiStatus.OPEN_TELEMETRY;
+ } else if ("clientcore.".regionMatches(0, className, 3, 10)) {
+ return ExternalApiStatus.SDK_CLASSES;
+ }
+ } else if (className.startsWith("org.")) {
+ if ("json.".regionMatches(0, className, 4, 5)) {
+ return ExternalApiStatus.ORG_JSON;
+ } else if ("postgresql.".regionMatches(0, className, 4, 11)) {
+ return ExternalApiStatus.POSTGRESQL;
+ } else if ("reactivestreams.".regionMatches(0, className, 4, 16)) {
+ return ExternalApiStatus.REACTIVE_STREAMS;
+ } else if (className.length() == 37 && className.endsWith("springframework.util.ErrorHandler")) {
+ return ExternalApiStatus.SPRING_ERROR_HANDLER;
+ }
+ } else if (className.startsWith("redis.clients.jedis")) {
+ return ExternalApiStatus.JEDIS;
+ }
+
+ return ExternalApiStatus.KEEP;
+ }
+
+ private static final class ExternalApiStatus {
+ private static final ExternalApiStatus KEEP = new ExternalApiStatus(false);
+ private static final ExternalApiStatus MYSQL_CJ = new ExternalApiStatus("Mysql driver classes are allowed to "
+ + "be exposed by dependencies using them.");
+ private static final ExternalApiStatus SDK_CLASSES = new ExternalApiStatus("SDK classes are allowed to be "
+ + "exposed by dependencies using them.");
+ private static final ExternalApiStatus PERF_TEST = new ExternalApiStatus("perf-test classes are allowed to be "
+ + "exposed.");
+ private static final ExternalApiStatus APP_CONFIGURATION_REFRESH = new ExternalApiStatus("This isn't an "
+ + "external class");
+ private static final ExternalApiStatus CLOUD_EVENTS = new ExternalApiStatus("Azure Event Grid cloud native "
+ + "cloud event is allowed to use CloudEvents types in public APIs as it implements interfaces defined by "
+ + "CloudEvents");
+ private static final ExternalApiStatus OPEN_TELEMETRY = new ExternalApiStatus("Azure Monitor Exporter is "
+ + "allowed to use OpenTelemetry types in public APIs as it implements interfaces defined by OpenTelemetry");
+ private static final ExternalApiStatus ORG_JSON = new ExternalApiStatus("To support the EventHubs "
+ + "JedisRedisCheckpointStore constructor");
+ private static final ExternalApiStatus POSTGRESQL = new ExternalApiStatus("Postgresql driver classes are "
+ + "allowed to be exposed by dependencies using them.");
+ private static final ExternalApiStatus SPRING_ERROR_HANDLER = new ExternalApiStatus("Azure Spring Cloud "
+ + "Messaging need the Spring's public interface for error handler registration, it is a common class for "
+ + "users to handle runtime errors.");
+ private static final ExternalApiStatus REACTIVE_STREAMS = new ExternalApiStatus("Reactive streams types are "
+ + "allowed to be exposed.");
+
+ private static final ExternalApiStatus JEDIS = new ExternalApiStatus("To support the EventHubs "
+ + "JedisRedisCheckpointStore constructor");
+ private final boolean ignore;
+ private final String justification;
+
+ ExternalApiStatus(boolean ignore) {
+ this.ignore = ignore;
+ this.justification = null;
+ }
+
+ ExternalApiStatus(String justification) {
+ this.ignore = true;
+ this.justification = justification;
+ }
+ }
+}
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkTreeFilterProvider.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkTreeFilterProvider.java
new file mode 100644
index 0000000000..5819a50dc3
--- /dev/null
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkTreeFilterProvider.java
@@ -0,0 +1,214 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.tools.revapi.transforms;
+
+import org.revapi.AnalysisContext;
+import org.revapi.ArchiveAnalyzer;
+import org.revapi.Element;
+import org.revapi.FilterStartResult;
+import org.revapi.TreeFilter;
+import org.revapi.TreeFilterProvider;
+import org.revapi.base.IndependentTreeFilter;
+import org.revapi.java.spi.JavaTypeElement;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.lang.model.element.NestingKind;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import java.io.Reader;
+import java.util.Optional;
+
+public final class AzureSdkTreeFilterProvider implements TreeFilterProvider {
+ @Override
+ public > Optional> filterFor(ArchiveAnalyzer archiveAnalyzer) {
+ if (!"revapi.java".equals(archiveAnalyzer.getApiAnalyzer().getExtensionId())) {
+ return Optional.empty();
+ }
+
+ return Optional.of(new IndependentTreeFilter() {
+ @Override
+ protected FilterStartResult doStart(E element) {
+ if (!(element instanceof JavaTypeElement)) {
+ // Unknown element type.
+ return FilterStartResult.defaultResult();
+ }
+
+ TypeElement outermostClass = findOuterMostClass(((JavaTypeElement) element).getDeclaringElement());
+
+ // No guarantee there is an outermost class, the enclosing type could be an interface or enum.
+ boolean excludeClass = outermostClass != null
+ && excludeClass(outermostClass.getQualifiedName().toString());
+
+ if (excludeClass) {
+ // Class is being excluded, no need to inspect package.
+ return FilterStartResult.doesntMatch();
+ }
+
+ PackageElement packageElement = findPackage((JavaTypeElement) element);
+
+ if (packageElement == null) {
+ // No Java package.
+ return FilterStartResult.defaultResult();
+ }
+
+ String packageName = packageElement.getQualifiedName().toString();
+ boolean excludePackage = excludePackage(packageName);
+
+ return excludePackage ? FilterStartResult.doesntMatch() : FilterStartResult.matchAndDescend();
+ }
+ });
+ }
+
+ private static TypeElement findOuterMostClass(javax.lang.model.element.Element el) {
+ while (el != null && !(el instanceof TypeElement)) {
+ el = el.getEnclosingElement();
+ }
+
+ if (el == null) {
+ return null;
+ }
+
+ return ((TypeElement) el).getNestingKind() == NestingKind.TOP_LEVEL
+ ? (TypeElement) el
+ : findOuterMostClass(el.getEnclosingElement());
+ }
+
+ static boolean excludeClass(String className) {
+ if (!className.startsWith("com.azure.")) {
+ return false;
+ }
+
+ if ("core.".regionMatches(0, className, 10, 5)) {
+ // Exclude com.azure.core.util.Configuration
+ return className.length() == 33 && className.endsWith("util.Configuration");
+ } else if ("cosmos.".regionMatches(0, className, 10, 6)) {
+ // Exclude
+ //
+ // - com.azure.cosmos.BridgeInternal
+ // - com.azure.cosmos.CosmosBridgeInternal
+ // - com.azure.cosmos.models.ModelBridgeInternal
+ // - com.azure.cosmos.util.UtilBridgeInternal
+ return (className.length() == 31 && className.endsWith("BridgeInternal"))
+ || (className.length() == 37 && className.endsWith("CosmosBridgeInternal"))
+ || (className.length() == 43 && className.endsWith("models.ModelBridgeInternal"))
+ || (className.length() == 40 && className.endsWith("util.UtilBridgeInternal"));
+ } else if ("spring.cloud.config.".regionMatches(0, className, 10, 20)) {
+ // Exclude
+ //
+ // - com.azure.spring.cloud.config.AppConfigurationBootstrapConfiguration
+ // - com.azure.spring.cloud.config.AppConfigurationRefresh
+ // - com.azure.spring.cloud.config.properties.AppConfigurationProviderProperties
+ // - com.azure.spring.cloud.config.web.AppConfigurationEndpoint
+ // - com.azure.spring.cloud.config.web.pushrefresh.AppConfigurationRefreshEvent
+ return (className.length() == 68 && className.endsWith("AppConfigurationBootstrapConfiguration"))
+ || (className.length() == 53 && className.endsWith("AppConfigurationRefresh"))
+ || (className.length() == 75 && className.endsWith("properties.AppConfigurationProviderProperties"))
+ || (className.length() == 58 && className.endsWith("web.AppConfigurationEndpoint"))
+ || (className.length() == 74 && className.endsWith("web.pushrefresh.AppConfigurationRefreshEvent"));
+ }
+
+ return false;
+ }
+
+ private static PackageElement findPackage(JavaTypeElement element) {
+ javax.lang.model.element.Element el = element.getDeclaringElement();
+ while (el != null && !(el instanceof PackageElement)) {
+ el = el.getEnclosingElement();
+ }
+
+ return (PackageElement) el;
+ }
+
+ static boolean excludePackage(String packageName) {
+ if (packageName.startsWith("com.")) {
+ if ("azure.".regionMatches(0, packageName, 4, 6)) {
+ if ("data.cosmos".regionMatches(0, packageName, 10, 11)) {
+ // Exclude com.azure.data.cosmos*
+ return true;
+ } else if (packageName.indexOf("implementation", 10) != -1
+ || packageName.indexOf("samples", 10) != -1) {
+ // Exclude com.azure*.implementation*, com.azure.json*, com.azure*.samples*, and com.azure.xml*
+ return true;
+ } else if ("resourcemanager".regionMatches(0, packageName, 10, 15)) {
+ // Exclude com.azure.resourcemanager*.fluent.* but don't match fluentcore or confluent
+ int fluentIndex = packageName.indexOf("fluent", 25);
+ return fluentIndex != -1 && (fluentIndex + 6 == packageName.length()
+ || (packageName.charAt(fluentIndex - 1) == '.' && packageName.charAt(fluentIndex + 6) == '.'));
+ } else {
+ return false;
+ }
+ } else {
+ // Exclude com.fasterxml.jackson*, com.google.gson*, com.microsoft.azure*, and com.nimbusds*
+ return "fasterxml.jackson".regionMatches(0, packageName, 4, 17)
+ || "google.gson".regionMatches(0, packageName, 4, 11)
+ || "microsoft.azure".regionMatches(0, packageName, 4, 15)
+ || "nimbusds".regionMatches(0, packageName, 4, 8);
+ }
+ }
+
+ if (packageName.startsWith("io.")) {
+ // Exclude io.micrometer*, io.netty*, and io.vertx*
+ return "micrometer".regionMatches(0, packageName, 3, 10)
+ || "netty".regionMatches(0, packageName, 3, 5)
+ || "vertx".regionMatches(0, packageName, 3, 5);
+ }
+
+ if (packageName.startsWith("javax.")) {
+ // Exclude javax.jms* and javax.servlet*
+ return "jms".regionMatches(0, packageName, 6, 3)
+ || "servlet".regionMatches(0, packageName, 6, 7);
+ }
+
+ if (packageName.startsWith("kotlin")
+ || packageName.startsWith("okhttp3")
+ || packageName.startsWith("okio")) {
+ // Exclude kotlin*, okhttp3*, and okio*
+ return true;
+ }
+
+ if (packageName.startsWith("org.")) {
+ if ("apache.".regionMatches(0, packageName, 4, 7)) {
+ // Exclude org.apache.avro*, org.apache.commons*, and org.apache.qpid*
+ return "avro".regionMatches(0, packageName, 11, 4)
+ || "commons".regionMatches(0, packageName, 11, 7)
+ || "qpid".regionMatches(0, packageName, 11, 4);
+ } else {
+ // Exclude org.junit*, org.slf4j*, and org.springframework*
+ return "junit".regionMatches(0, packageName, 4, 5)
+ || "reactivestreams".regionMatches(0, packageName, 4, 15)
+ || "slf4j".regionMatches(0, packageName, 4, 5)
+ || "springframework".regionMatches(0, packageName, 4, 15);
+ }
+ }
+
+ if (packageName.startsWith("reactor.")) {
+ // Exclude reactor.core*, reactor.netty*, and reactor.util*
+ return "core".regionMatches(0, packageName, 8, 4)
+ || "netty".regionMatches(0, packageName, 8, 5)
+ || "util".regionMatches(0, packageName, 8, 4);
+ }
+
+ return false;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getExtensionId() {
+ return "azure-sdk-tree-provider";
+ }
+
+ @Nullable
+ @Override
+ public Reader getJSONSchema() {
+ return null;
+ }
+
+ @Override
+ public void initialize(@Nonnull AnalysisContext analysisContext) {
+ }
+}
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/JacksonDatabindRemovalTransform.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/JacksonDatabindRemovalTransform.java
new file mode 100644
index 0000000000..6efe648864
--- /dev/null
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/JacksonDatabindRemovalTransform.java
@@ -0,0 +1,152 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.azure.tools.revapi.transforms;
+
+import org.revapi.Criticality;
+import org.revapi.Difference;
+import org.revapi.Element;
+import org.revapi.TransformationResult;
+import org.revapi.base.BaseDifferenceTransform;
+
+import javax.annotation.Nullable;
+import java.util.regex.Pattern;
+
+/**
+ * Transform that runs after RevApi generates API differences that removes Jackson Databind changes from the flagged
+ * differences set.
+ *
+ * @param Type of element to transform.
+ */
+public final class JacksonDatabindRemovalTransform> extends BaseDifferenceTransform {
+ private static final Pattern DIFFERENCE_CODE_PATTERN = Pattern.compile("java\\.annotation\\.removed");
+
+ @Override
+ public Pattern[] getDifferenceCodePatterns() {
+ // This indicates to RevApi that all differences should be inspected by this transform.
+ return new Pattern[] { DIFFERENCE_CODE_PATTERN };
+ }
+
+ @Override
+ public String getExtensionId() {
+ // Used to configure this transform in the RevApi pipeline.
+ return "jackson-databind-removal";
+ }
+
+ @Override
+ public TransformationResult tryTransform(@Nullable E oldElement, @Nullable E newElement, Difference difference) {
+ // RevApi should add 'annotationType' as an attachment for 'java.annotation.removed' differences.
+ String annotationType = difference.attachments.get("annotationType");
+
+ if (difference.criticality != Criticality.ERROR) {
+ // Only transform the Difference if the criticality is an error. If this isn't guarded it results in an
+ // infinite transformation loop as RevApi will keep running the transformation pipeline until there are no
+ // transformations applied in the pipeline run.
+ return TransformationResult.keep();
+ }
+
+ if (annotationType == null || annotationType.isEmpty()) {
+ // But if the annotationType wasn't included keep the current result as we can't determine if this is a
+ // Jackson Databind change.
+ return TransformationResult.keep();
+ }
+
+ if (!annotationType.contains("fasterxml.jackson") || !annotationType.contains("annotation")) {
+ // The annotation isn't from Jackson Databind, keep the current result.
+ return TransformationResult.keep();
+ }
+
+ // Now verify that this change is in a package that is allowed to make the change.
+ String packageName = difference.attachments.get("package");
+
+ if (packageName == null || packageName.isEmpty()) {
+ // But if the package wasn't included keep the current result as we can't determine if this package is
+ // allowed to remove Jackson Databind annotations.
+ return TransformationResult.keep();
+ }
+
+ return shouldDiscard(packageName) ? TransformationResult.discard() : TransformationResult.keep();
+ }
+
+ private static boolean shouldDiscard(String packageName) {
+ if (!packageName.startsWith("com.azure.")) {
+ // The package isn't from the Azure SDK, keep the current result.
+ return false;
+ }
+
+ if (packageName.regionMatches(10, "containers.containerregistry.models", 0, 35)) {
+ // Container Registry
+ return true;
+ } else if (packageName.regionMatches(10, "search.documents", 0, 16)) {
+ // Search Documents
+ return packageName.regionMatches(26, ".models", 0, 7)
+ || packageName.regionMatches(26, ".indexes.models", 0, 13);
+ } else if (packageName.regionMatches(10, "security.", 0, 9)) {
+ if (packageName.regionMatches(19, "attestation.models", 0, 17)) {
+ // Attestation
+ return true;
+ } else if (packageName.regionMatches(19, "keyvault.", 0, 9)) {
+ // KeyVault
+ return packageName.regionMatches(28, "administration.models", 0, 21)
+ || packageName.regionMatches(28, "certificates.models", 0, 19)
+ || packageName.regionMatches(28, "keys.models", 0, 11)
+ || packageName.regionMatches(28, "keys.cryptography.models", 0, 24);
+ }
+ } else if (packageName.regionMatches(10, "ai.", 0, 3)) {
+ if (packageName.regionMatches(13, "textanalytics.models", 0, 20)) {
+ // Text Analytics
+ return true;
+ } else if (packageName.regionMatches(13, "formrecognizer.", 0, 15)) {
+ // Form Recognizer
+ return packageName.regionMatches(28, "models", 0, 6)
+ || packageName.regionMatches(28, "training.models", 0, 15)
+ || packageName.regionMatches(28, "documentanalysis.models", 0, 23)
+ || packageName.regionMatches(28, "documentanalysis.administration.models", 0, 38);
+ } else if (packageName.regionMatches(13, "metricsadvisor.", 0, 15)) {
+ // Metrics Advisor
+ return packageName.regionMatches(28, "models", 0, 6)
+ || packageName.regionMatches(28, "administration.models", 0, 21);
+ } else if (packageName.regionMatches(13, "contentsafety.models", 0, 20)) {
+ // Content Safety
+ return true;
+ }
+ } else if (packageName.regionMatches(10, "messaging.", 0, 10)) {
+ // Service Bus
+ if (packageName.regionMatches(20, "servicebus.", 0, 11)) {
+ return packageName.regionMatches(31, "models", 0, 6)
+ || packageName.regionMatches(31, "administration.models", 0, 21);
+ } else if (packageName.regionMatches(20, "eventgrid.systemevents", 0, 22)) {
+ // Event Grid
+ return true;
+ }
+ } else if (packageName.regionMatches(10, "monitor.query.models", 0, 20)) {
+ // Monitor Query
+ return true;
+ } else if (packageName.regionMatches(10, "data.tables.models", 0, 18)) {
+ // Tables
+ return true;
+ } else if (packageName.regionMatches(10, "storage.", 0, 8)) {
+ if (packageName.regionMatches(18, "file.datalake.models", 0, 20)) {
+ // DataLake
+ return true;
+ } else if (packageName.regionMatches(18, "file.share.models", 0, 17)) {
+ // Shares
+ return true;
+ } else if (packageName.regionMatches(18, "queue.models", 0, 12)) {
+ // Queue
+ return true;
+ } else if (packageName.regionMatches(18, "blob.", 0, 5)) {
+ // Blob
+ return packageName.regionMatches(23, "models", 0, 6)
+ || packageName.regionMatches(23, "options", 0, 7);
+ }
+ } else if (packageName.regionMatches(10, "communication.", 0, 14)) {
+ if (packageName.regionMatches(24, "jobrouter.models", 0, 16)) {
+ // Communication Job Router
+ return true;
+ }
+ }
+
+ // The package is from the Azure SDK, but not in the allowed list, keep the current result.
+ return false;
+ }
+}
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/TransitiveCoreChangesTransform.java b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/TransitiveCoreChangesTransform.java
index 587b4a1caf..cb040ee458 100644
--- a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/TransitiveCoreChangesTransform.java
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/TransitiveCoreChangesTransform.java
@@ -20,13 +20,13 @@
* @param Type of element to transform.
*/
public final class TransitiveCoreChangesTransform> extends BaseDifferenceTransform {
- private static final Pattern CORE_ARCHIVE = Pattern.compile("com\\.azure:azure-core:.*");
+ private static final Pattern DIFFERENCE_CODE_PATTERN = Pattern.compile(".*");
private static final String SUPPLEMENTARY = Archive.Role.SUPPLEMENTARY.toString();
@Override
public Pattern[] getDifferenceCodePatterns() {
// This indicates to RevApi that all differences should be inspected by this transform.
- return new Pattern[] { Pattern.compile(".*") };
+ return new Pattern[] { DIFFERENCE_CODE_PATTERN };
}
@Override
@@ -51,8 +51,10 @@ public TransformationResult tryTransform(@Nullable E oldElement, @Nullable E new
return TransformationResult.keep();
}
- if (!CORE_ARCHIVE.matcher(newArchive).matches()) {
- // The difference isn't from the azure-core SDK, keep the current result.
+ if (!newArchive.startsWith("com.azure:azure-core:")
+ && !newArchive.startsWith("com.azure:azure-json:")
+ && !newArchive.startsWith("com.azure:azure-xml:")) {
+ // The difference isn't from the azure-core, azure-json, or azure-xml SDK, keep the current result.
return TransformationResult.keep();
}
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/META-INF/services/org.revapi.DifferenceTransform b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/META-INF/services/org.revapi.DifferenceTransform
new file mode 100644
index 0000000000..2933e6c3e3
--- /dev/null
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/META-INF/services/org.revapi.DifferenceTransform
@@ -0,0 +1,3 @@
+com.azure.tools.revapi.transforms.AzureSdkAllowedExternalApis
+com.azure.tools.revapi.transforms.JacksonDatabindRemovalTransform
+com.azure.tools.revapi.transforms.TransitiveCoreChangesTransform
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/META-INF/services/org.revapi.TreeFilterProvider b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/META-INF/services/org.revapi.TreeFilterProvider
new file mode 100644
index 0000000000..28443e149c
--- /dev/null
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/META-INF/services/org.revapi.TreeFilterProvider
@@ -0,0 +1 @@
+com.azure.tools.revapi.transforms.AzureSdkTreeFilterProvider
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml
index 1df25e0a7f..5094af9ca2 100644
--- a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml
@@ -9,24 +9,399 @@
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle.xml b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle.xml
index 69c3b16c56..ac4dc59d23 100644
--- a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle.xml
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle.xml
@@ -9,12 +9,16 @@ what the following rules do, please see the checkstyle configuration
page at http://checkstyle.sourceforge.net/config.html -->
+
+
+
+
@@ -228,7 +232,13 @@ page at http://checkstyle.sourceforge.net/config.html -->
-
+
+
+
+
+
+
+
@@ -268,11 +278,11 @@ page at http://checkstyle.sourceforge.net/config.html -->
-
-
-
+
+
+
-
+
@@ -331,9 +341,10 @@ page at http://checkstyle.sourceforge.net/config.html -->
-
-
+
+
+
+
@@ -387,8 +398,8 @@ page at http://checkstyle.sourceforge.net/config.html -->
-
-
+
+
@@ -398,5 +409,11 @@ page at http://checkstyle.sourceforge.net/config.html -->
+
+
+
+
+
+
diff --git a/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/dependency-allowlist.html b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/dependency-allowlist.html
new file mode 100644
index 0000000000..bdd3c61fae
--- /dev/null
+++ b/protocol-sdk-integration-tests/eng/code-quality-reports/src/main/resources/dependency-allowlist.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+ Maven Allowlist Report
+
+
+
+
+
+
+
+
+
Maven Allowlist Report
+
+
+ This report displays the allowlist of all maven dependencies. It is based on the machine-readable JSON report.
+