diff --git a/eng/code-quality-reports/pom.xml b/eng/code-quality-reports/pom.xml index 66b42c4583f6..8e928bb08c29 100755 --- a/eng/code-quality-reports/pom.xml +++ b/eng/code-quality-reports/pom.xml @@ -40,6 +40,12 @@ 33.1.0-jre + + com.fasterxml.jackson.core + jackson-databind + 2.17.2 + + com.puppycrawl.tools checkstyle @@ -65,12 +71,6 @@ org.revapi revapi 0.14.4 - - - com.fasterxml.jackson.core - jackson-databind - - diff --git a/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AllowedExternalApis.java b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AllowedExternalApis.java new file mode 100644 index 000000000000..2a5966c2e5f3 --- /dev/null +++ b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AllowedExternalApis.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.tools.revapi.transforms; + +import com.fasterxml.jackson.databind.JsonNode; +import org.revapi.AnalysisContext; +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.Nonnull; +import javax.annotation.Nullable; +import javax.lang.model.element.TypeElement; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +import static com.azure.tools.revapi.transforms.RevApiUtils.createPrefixMatchersFromConfiguration; +import static com.azure.tools.revapi.transforms.RevApiUtils.findOuterMostClass; + +/** + * RevApi transformation that allows for ignoring external APIs that are allowed to be exposed. + * + * @param Type of element to transform. + */ +public final class AllowedExternalApis> extends BaseDifferenceTransform { + private static final Pattern DIFFERENCE_CODE_PATTERN = Pattern.compile("java.class.externalClassExposedInAPI", + Pattern.LITERAL); + + private boolean enabled = false; + private List allowedPrefixes = Collections.emptyList(); + + @Override + public Pattern[] getDifferenceCodePatterns() { + return new Pattern[] { DIFFERENCE_CODE_PATTERN }; + } + + @Override + public TransformationResult tryTransform(@Nullable E oldElement, @Nullable E newElement, Difference difference) { + if (!enabled || newElement == null || allowedPrefixes.isEmpty()) { + // Missing element to compare. + return TransformationResult.keep(); + } + + if (!(newElement instanceof JavaTypeElement)) { + // Unknown element type. + return TransformationResult.keep(); + } + + TypeElement outermostElement = findOuterMostClass(((JavaTypeElement) newElement).getDeclaringElement()); + + if (outermostElement == null) { + return TransformationResult.keep(); + } + + String className = outermostElement.getQualifiedName().toString(); + + for (PrefixMatcher prefixMatcher : allowedPrefixes) { + if (prefixMatcher.test(className)) { + return TransformationResult.discard(); + } + } + + return TransformationResult.keep(); + } + + @Override + public String getExtensionId() { + return "allowed-external-apis"; + } + + @Override + public void initialize(@Nonnull AnalysisContext analysisContext) { + JsonNode enabledNode = analysisContext.getConfigurationNode().get("enabled"); + this.enabled = enabledNode != null && enabledNode.isBoolean() && enabledNode.booleanValue(); + this.allowedPrefixes = createPrefixMatchersFromConfiguration(analysisContext, "allowedPrefixes"); + } +} diff --git a/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkAllowedExternalApis.java b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkAllowedExternalApis.java deleted file mode 100644 index 0ca3a54187d8..000000000000 --- a/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkAllowedExternalApis.java +++ /dev/null @@ -1,165 +0,0 @@ -// 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/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkTreeFilterProvider.java b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkTreeFilterProvider.java deleted file mode 100644 index 5819a50dc3d1..000000000000 --- a/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkTreeFilterProvider.java +++ /dev/null @@ -1,214 +0,0 @@ -// 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/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/ClassAndPackageTreeFilterProvider.java b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/ClassAndPackageTreeFilterProvider.java new file mode 100644 index 000000000000..1c180ff8cc23 --- /dev/null +++ b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/ClassAndPackageTreeFilterProvider.java @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.tools.revapi.transforms; + +import com.fasterxml.jackson.databind.JsonNode; +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.PackageElement; +import javax.lang.model.element.TypeElement; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +import static com.azure.tools.revapi.transforms.RevApiUtils.createPrefixMatchersFromConfiguration; +import static com.azure.tools.revapi.transforms.RevApiUtils.findOuterMostClass; + +/** + * A {@link TreeFilterProvider} that filters out classes and packages that shouldn't have RevApi analysis performed. + */ +public final class ClassAndPackageTreeFilterProvider implements TreeFilterProvider { + private static final String PATTERN_ERROR_MESSAGE = "Configuration 'ignoredPackagesPatterns' must be an array of " + + "strings representing regular expressions to ignore."; + + @Override + public String getExtensionId() { + return "class-and-package-tree-filter-provider"; + } + + @Nullable + @Override + public Reader getJSONSchema() { + return null; + } + + // TreeFilterProviders don't need to have a configuration for enabled as + private List ignoredClasses = Collections.emptyList(); + private List ignoredPackages = Collections.emptyList(); + private List ignoredPackagesPatterns = Collections.emptyList(); + + @Override + public void initialize(@Nonnull AnalysisContext analysisContext) { + this.ignoredClasses = createPrefixMatchersFromConfiguration(analysisContext, "ignoredClasses"); + this.ignoredPackages = createPrefixMatchersFromConfiguration(analysisContext, "ignoredPackages"); + + JsonNode patterns = analysisContext.getConfigurationNode().get("ignoredPackagesPatterns"); + if (patterns != null) { + if (!patterns.isArray()) { + throw new IllegalArgumentException(PATTERN_ERROR_MESSAGE); + } + + List ignoredPackagesPatterns = new ArrayList<>(); + for (JsonNode pattern : patterns) { + if (!pattern.isTextual()) { + throw new IllegalArgumentException(PATTERN_ERROR_MESSAGE); + } + + ignoredPackagesPatterns.add(Pattern.compile(pattern.asText())); + } + + this.ignoredPackagesPatterns = ignoredPackagesPatterns; + } + } + + @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(); + } + }); + } + + boolean excludeClass(String className) { + for (PrefixMatcher matcher : ignoredClasses) { + if (matcher.test(className)) { + return true; + } + } + + 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; + } + + boolean excludePackage(String packageName) { + for (PrefixMatcher matcher : ignoredPackages) { + if (matcher.test(packageName)) { + return true; + } + } + + for (Pattern pattern : ignoredPackagesPatterns) { + if (pattern.matcher(packageName).matches()) { + return true; + } + } + + return false; + } + + @Override + public void close() { + } +} diff --git a/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/JacksonDatabindRemovalTransform.java b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/IgnoredJacksonDatabindRemovalTransform.java similarity index 74% rename from eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/JacksonDatabindRemovalTransform.java rename to eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/IgnoredJacksonDatabindRemovalTransform.java index 6efe6488640d..326c54d22c95 100644 --- a/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/JacksonDatabindRemovalTransform.java +++ b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/IgnoredJacksonDatabindRemovalTransform.java @@ -2,12 +2,15 @@ // Licensed under the MIT License. package com.azure.tools.revapi.transforms; +import com.fasterxml.jackson.databind.JsonNode; +import org.revapi.AnalysisContext; import org.revapi.Criticality; import org.revapi.Difference; import org.revapi.Element; import org.revapi.TransformationResult; import org.revapi.base.BaseDifferenceTransform; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.regex.Pattern; @@ -17,9 +20,11 @@ * * @param Type of element to transform. */ -public final class JacksonDatabindRemovalTransform> extends BaseDifferenceTransform { +public final class IgnoredJacksonDatabindRemovalTransform> extends BaseDifferenceTransform { private static final Pattern DIFFERENCE_CODE_PATTERN = Pattern.compile("java\\.annotation\\.removed"); + private boolean enabled = false; + @Override public Pattern[] getDifferenceCodePatterns() { // This indicates to RevApi that all differences should be inspected by this transform. @@ -29,11 +34,22 @@ public Pattern[] getDifferenceCodePatterns() { @Override public String getExtensionId() { // Used to configure this transform in the RevApi pipeline. - return "jackson-databind-removal"; + return "ignored-jackson-databind-removal"; + } + + @Override + public void initialize(@Nonnull AnalysisContext analysisContext) { + JsonNode enabledNode = analysisContext.getConfigurationNode().get("enabled"); + this.enabled = enabledNode != null && enabledNode.isBoolean() && enabledNode.booleanValue(); } @Override public TransformationResult tryTransform(@Nullable E oldElement, @Nullable E newElement, Difference difference) { + if (!enabled) { + // If this transform isn't enabled, keep the current result. + return TransformationResult.keep(); + } + // RevApi should add 'annotationType' as an attachment for 'java.annotation.removed' differences. String annotationType = difference.attachments.get("annotationType"); @@ -117,6 +133,10 @@ private static boolean shouldDiscard(String packageName) { } else if (packageName.regionMatches(20, "eventgrid.systemevents", 0, 22)) { // Event Grid return true; + } else if (packageName.regionMatches(20, "webpubsub.", 0, 10)) { + // WebPubSub + return packageName.regionMatches(30, "models", 0, 6) + || packageName.regionMatches(30, "client.models", 0, 13); } } else if (packageName.regionMatches(10, "monitor.query.models", 0, 20)) { // Monitor Query @@ -140,10 +160,26 @@ private static boolean shouldDiscard(String packageName) { || 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; - } + return packageName.regionMatches(24, "jobrouter.models", 0, 16) // Communication Job Router + || packageName.regionMatches(24, "messages.models", 0, 15) // Communication Messages + || packageName.regionMatches(24, "callautomation.models", 0, 21) // Communication Call Automation + || packageName.regionMatches(24, "chat.models", 0, 11) // Communication Chat + || packageName.regionMatches(24, "rooms.models", 0, 12) // Communication Rooms + || packageName.regionMatches(24, "identity.models", 0, 15) // Communication Identity + || packageName.regionMatches(24, "email.models", 0, 12) // Communication Email + || packageName.regionMatches(24, "phonenumbers.models", 0, 19); // Communication Phone Numbers + } else if (packageName.regionMatches(10, "digitaltwins.core", 0, 17)) { + // Digital Twins Core + return true; + } else if (packageName.regionMatches(10, "developer.devcenter.models", 0, 26)) { + // Developer Dev Center + return true; + } else if (packageName.regionMatches(10, "compute.batch.models", 0, 20)) { + // Compute Batch + return true; + } else if (packageName.regionMatches(10, "ai.translation.text.models", 0, 26)) { + // Translation Text + return true; } // The package is from the Azure SDK, but not in the allowed list, keep the current result. diff --git a/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/TransitiveCoreChangesTransform.java b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/IgnoredTransitiveChangesTransform.java similarity index 51% rename from eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/TransitiveCoreChangesTransform.java rename to eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/IgnoredTransitiveChangesTransform.java index cb040ee4587c..57b44bcd7bab 100644 --- a/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/TransitiveCoreChangesTransform.java +++ b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/IgnoredTransitiveChangesTransform.java @@ -3,6 +3,8 @@ package com.azure.tools.revapi.transforms; +import com.fasterxml.jackson.databind.JsonNode; +import org.revapi.AnalysisContext; import org.revapi.Archive; import org.revapi.Criticality; import org.revapi.Difference; @@ -10,19 +12,29 @@ import org.revapi.TransformationResult; import org.revapi.base.BaseDifferenceTransform; +import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.regex.Pattern; /** - * Transform that runs after RevApi generates API differences that removes transitive azure-core changes from the - * flagged differences set. + * Transform that runs after RevApi generates API differences that removes transitive changes from the flagged + * differences set. * * @param Type of element to transform. */ -public final class TransitiveCoreChangesTransform> extends BaseDifferenceTransform { +public final class IgnoredTransitiveChangesTransform> extends BaseDifferenceTransform { + private static final String CONFIGURATION_ERROR_MESSAGE = "Configuration 'ignoredNewArchives' must be an array " + + "of strings representing a Maven groupId:artifactId pair to ignore transitive changes from."; + private static final Pattern DIFFERENCE_CODE_PATTERN = Pattern.compile(".*"); private static final String SUPPLEMENTARY = Archive.Role.SUPPLEMENTARY.toString(); + private boolean enabled = false; + private List ignoredNewArchives = Collections.emptyList(); + @Override public Pattern[] getDifferenceCodePatterns() { // This indicates to RevApi that all differences should be inspected by this transform. @@ -32,11 +44,39 @@ public Pattern[] getDifferenceCodePatterns() { @Override public String getExtensionId() { // Used to configure this transform in the RevApi pipeline. - return "transitive-core-changes"; + return "ignored-transitive-changes"; + } + + @Override + public void initialize(@Nonnull AnalysisContext analysisContext) { + JsonNode enabledNode = analysisContext.getConfigurationNode().get("enabled"); + this.enabled = enabledNode != null && enabledNode.isBoolean() && enabledNode.booleanValue(); + JsonNode configuration = analysisContext.getConfigurationNode().get("ignoredNewArchives"); + if (configuration != null) { + if (!configuration.isArray()) { + throw new IllegalArgumentException(CONFIGURATION_ERROR_MESSAGE); + } + + List ignoredNewArchives = new ArrayList<>(); + for (JsonNode node : configuration) { + if (!node.isTextual()) { + throw new IllegalArgumentException(CONFIGURATION_ERROR_MESSAGE); + } + + ignoredNewArchives.add(node.asText()); + } + + this.ignoredNewArchives = ignoredNewArchives; + } } @Override public TransformationResult tryTransform(@Nullable E oldElement, @Nullable E newElement, Difference difference) { + if (!enabled) { + // If this transform isn't enabled, keep the current result. + return TransformationResult.keep(); + } + // RevApi will always add newArchive and newArchiveRole together. String newArchive = difference.attachments.get("newArchive"); String newArchiveRole = difference.attachments.get("newArchiveRole"); @@ -51,10 +91,16 @@ public TransformationResult tryTransform(@Nullable E oldElement, @Nullable E new return TransformationResult.keep(); } - 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. + boolean shouldKeep = true; + for (String ignoredNewArchive : ignoredNewArchives) { + if (newArchive.startsWith(ignoredNewArchive)) { + shouldKeep = false; + break; + } + } + + if (shouldKeep) { + // The difference didn't match any 'ignoredNewArchives', keep the current result. return TransformationResult.keep(); } @@ -62,7 +108,7 @@ public TransformationResult tryTransform(@Nullable E oldElement, @Nullable E new // infinite transformation loop as RevApi will keep running the transformation pipeline until there are no // transformations applied in the pipeline run. if (difference.criticality == Criticality.ERROR) { - // The difference is from azure-core and azure-core is a dependency to this SDK, discard it for now. + // The difference is from an ignored new archive, discard it. // In the future this could retain it with a lower criticality level for informational reasons. return TransformationResult.discard(); } else { diff --git a/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/PrefixMatcher.java b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/PrefixMatcher.java new file mode 100644 index 000000000000..4c212367fd3c --- /dev/null +++ b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/PrefixMatcher.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.tools.revapi.transforms; + +import java.util.List; +import java.util.function.Predicate; + +/** + * A {@link Predicate} implementation that matches an input against an initial prefix and an optional list of + * sub-prefixes. + *

+ * The initial prefix must always be matched to be able to return true, then if there are any sub-prefixes only one of + * those must be matched to return true. If sub-prefixes are empty then only the initial prefix must be matched. + */ +final class PrefixMatcher implements Predicate { + private final String initialPrefix; + private final int initialPrefixLength; + private final List subPrefixes; + + PrefixMatcher(String initialPrefix, List subPrefixes) { + this.initialPrefix = initialPrefix; + this.initialPrefixLength = initialPrefix.length(); + this.subPrefixes = subPrefixes; + } + + @Override + public boolean test(String s) { + if (s == null) { + return false; + } + + if (!s.startsWith(initialPrefix)) { + return false; + } + + if (subPrefixes.isEmpty()) { + return true; + } + + for (String subPrefix : subPrefixes) { + if (s.regionMatches(initialPrefixLength, subPrefix, 0, subPrefix.length())) { + return true; + } + } + + return false; + } +} diff --git a/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/RevApiUtils.java b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/RevApiUtils.java new file mode 100644 index 000000000000..67056e1d4c10 --- /dev/null +++ b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/RevApiUtils.java @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.tools.revapi.transforms; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.revapi.AnalysisContext; + +import javax.annotation.Nonnull; +import javax.lang.model.element.Element; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.TypeElement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Utility class containing methods to work with RevApi. + */ +final class RevApiUtils { + private static final String CONFIGURATION_ERROR_MESSAGE = "Configuration '%s' must be an object where each key is " + + "an initial prefix to be matched and the value is an array of strings representing sub-prefixes to match, " + + "where an empty array of sub-prefixes indicates only the initial prefix needs to be matched."; + + /** + * Finds the outermost class {@link TypeElement} for the given {@link Element}. + * + * @param el The element to find the outermost class for. + * @return The outermost class for the given element, may return null. + */ + static TypeElement findOuterMostClass(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 List createPrefixMatchersFromConfiguration(@Nonnull AnalysisContext analysisContext, + String propertyName) { + JsonNode configuration = analysisContext.getConfigurationNode().get(propertyName); + if (configuration == null) { + return Collections.emptyList(); + } + + if (!configuration.isObject()) { + throw new IllegalArgumentException(String.format(CONFIGURATION_ERROR_MESSAGE, propertyName)); + } + + List prefixMatchers = new ArrayList<>(); + ObjectNode prefixesObject = (ObjectNode) configuration; + for (Iterator> it = prefixesObject.fields(); it.hasNext(); ) { + Map.Entry allowedPrefixKvp = it.next(); + String initialPrefix = allowedPrefixKvp.getKey(); + if (initialPrefix.isEmpty()) { + throw new IllegalArgumentException(String.format(CONFIGURATION_ERROR_MESSAGE, propertyName)); + } + + JsonNode allowedPrefix = allowedPrefixKvp.getValue(); + if (!allowedPrefix.isArray()) { + throw new IllegalArgumentException(String.format(CONFIGURATION_ERROR_MESSAGE, propertyName)); + } + + ArrayNode subPrefixes = (ArrayNode) allowedPrefix; + List subPrefixList = new ArrayList<>(); + + for (JsonNode subPrefix : subPrefixes) { + if (!subPrefix.isTextual()) { + throw new IllegalArgumentException(String.format(CONFIGURATION_ERROR_MESSAGE, propertyName)); + } + + subPrefixList.add(subPrefix.asText()); + } + + prefixMatchers.add(new PrefixMatcher(initialPrefix, subPrefixList)); + } + + return prefixMatchers; + } + + private RevApiUtils() { + // Private constructor to prevent instantiation. + } +} diff --git a/eng/code-quality-reports/src/main/resources/META-INF/services/org.revapi.DifferenceTransform b/eng/code-quality-reports/src/main/resources/META-INF/services/org.revapi.DifferenceTransform index 2933e6c3e323..7474ffc9e854 100644 --- a/eng/code-quality-reports/src/main/resources/META-INF/services/org.revapi.DifferenceTransform +++ b/eng/code-quality-reports/src/main/resources/META-INF/services/org.revapi.DifferenceTransform @@ -1,3 +1,3 @@ -com.azure.tools.revapi.transforms.AzureSdkAllowedExternalApis -com.azure.tools.revapi.transforms.JacksonDatabindRemovalTransform -com.azure.tools.revapi.transforms.TransitiveCoreChangesTransform +com.azure.tools.revapi.transforms.AllowedExternalApis +com.azure.tools.revapi.transforms.IgnoredJacksonDatabindRemovalTransform +com.azure.tools.revapi.transforms.IgnoredTransitiveChangesTransform diff --git a/eng/code-quality-reports/src/main/resources/META-INF/services/org.revapi.TreeFilterProvider b/eng/code-quality-reports/src/main/resources/META-INF/services/org.revapi.TreeFilterProvider index 28443e149c65..53e3a61e11e9 100644 --- a/eng/code-quality-reports/src/main/resources/META-INF/services/org.revapi.TreeFilterProvider +++ b/eng/code-quality-reports/src/main/resources/META-INF/services/org.revapi.TreeFilterProvider @@ -1 +1 @@ -com.azure.tools.revapi.transforms.AzureSdkTreeFilterProvider +com.azure.tools.revapi.transforms.ClassAndPackageTreeFilterProvider diff --git a/eng/code-quality-reports/src/main/resources/revapi/clientcore-revapi.json b/eng/code-quality-reports/src/main/resources/revapi/clientcore-revapi.json new file mode 100644 index 000000000000..d2efa6306151 --- /dev/null +++ b/eng/code-quality-reports/src/main/resources/revapi/clientcore-revapi.json @@ -0,0 +1,91 @@ +[ + { + "extension": "revapi.java", + "configuration": { + "missing-classes": { + "behavior": "ignore", + "ignoreMissingAnnotations": false + }, + "matchOverloads": true + } + }, + { + "extension": "revapi.versions", + "configuration": { + "enabled": true, + "semantic0": false, + "versionIncreaseAllows": { + "major": { + "severity": "BREAKING" + }, + "minor": { + "severity": "NON_BREAKING" + }, + "patch": { + "severity": "EQUIVALENT" + } + }, + "onAllowed": { + "remove": true, + "attachments": { + "breaksVersioningRules": "false" + } + }, + "onDisallowed": { + "criticality": "error", + "attachments": { + "breaksVersioningRules": "true" + } + }, + "passThroughDifferences": [ + "java.class.nonPublicPartOfAPI" + ] + } + }, + { + "extension": "allowed-external-apis", + "configuration": { + "enabled": true, + "allowedPrefixes": { + "io.": [ + "clientcore.core" + ] + } + } + }, + { + "extension": "ignored-transitive-changes", + "configuration": { + "enabled": true, + "ignoredNewArchives": [ + "io.clientcore:core:" + ] + } + }, + { + "extension": "class-and-package-tree-filter-provider", + "configuration": { + "ignoredClasses": { + }, + "ignoredPackages": { + "kotlin": [], + "okhttp3": [], + "okio": [], + "org.": [ + "junit" + ] + }, + "ignoredPackagesPatterns": [ + "io\\.clientcore\\..*(implementation|samples).*" + ] + } + }, + { + "extension": "revapi.differences", + "configuration": { + "ignore": true, + "differences": [ + ] + } + } +] diff --git a/eng/code-quality-reports/src/main/resources/revapi/revapi.json b/eng/code-quality-reports/src/main/resources/revapi/revapi.json index c273b71cff54..923af61d9e10 100644 --- a/eng/code-quality-reports/src/main/resources/revapi/revapi.json +++ b/eng/code-quality-reports/src/main/resources/revapi/revapi.json @@ -42,6 +42,121 @@ ] } }, + { + "extension": "allowed-external-apis", + "configuration": { + "enabled": true, + "allowedPrefixes": { + "com.azure.": [ + "communication.common.", + "core.", + "cosmos.", + "data.appconfiguration.", + "data.schemaregistry.", + "identity.", + "json.", + "messaging.eventgrid.", + "messaging.eventhub.", + "messaging.servicebus.", + "perf.test.core.", + "resourcemanager.", + "security.keyvault.", + "spring.cloud.appconfiguration.config.", + "spring.cloud.feature.", + "storage.", + "xml." + ], + "com.mysql.cj.": [], + "io.": [ + "clientcore.", + "cloudevents.", + "opentelemetry." + ], + "org.": [ + "json.", + "postgresql.", + "reactivestreams.", + "springframework.util.ErrorHandler" + ], + "redis.clients.jedis": [] + } + } + }, + { + "extension": "ignored-jackson-databind-removal", + "configuration": { + "enabled": true + } + }, + { + "extension": "ignored-transitive-changes", + "configuration": { + "enabled": true, + "ignoredNewArchives": [ + "com.azure:azure-core:", + "com.azure:azure-json:", + "com.azure:azure-xml:" + ] + } + }, + { + "extension": "class-and-package-tree-filter-provider", + "configuration": { + "ignoredClasses": { + "com.azure.": [ + "core.util.Configuration", + "cosmos.BridgeInternal", + "cosmos.CosmosBridgeInternal", + "cosmos.models.ModelBridgeInternal", + "cosmos.util.UtilBridgeInternal", + "spring.cloud.config.AppConfigurationBootstrapConfiguration", + "spring.cloud.config.AppConfigurationRefresh", + "spring.cloud.config.properties.AppConfigurationProviderProperties", + "spring.cloud.config.web.AppConfigurationEndpoint", + "spring.cloud.config.web.pushrefresh.AppConfigurationRefreshEvent" + ] + }, + "ignoredPackages": { + "com.": [ + "azure.data.cosmos", + "fasterxml.jackson", + "google.gson", + "microsoft.azure", + "numbusds" + ], + "io.": [ + "micrometer", + "netty", + "vertx" + ], + "javax.": [ + "jms", + "servlet" + ], + "kotlin": [], + "okhttp3": [], + "okio": [], + "org.": [ + "apache.avro", + "apache.commons", + "apache.qpid", + "junit", + "reactivestreams", + "slf4j", + "springframework" + ], + "reactor.": [ + "core", + "netty", + "util" + ] + }, + "ignoredPackagesPatterns": [ + "com\\.azure\\..*(implementation|samples).*", + "com\\.azure\\.resourcemanager\\..*(fluent)(\\..*)?$" + ] + } + }, { "extension": "revapi.differences", "configuration": { @@ -479,68 +594,6 @@ "old": ".*? com\\.azure\\.resourcemanager\\..*\\.models.*", "justification": "Migration to azure-json." }, - { - "regex": true, - "code": "java\\.annotation\\.(attributeRemoved|attributeAdded)", - "old": ".*? com\\.azure\\.communication\\.messages\\.models.*", - "justification": "Jackson annotation changed." - }, - { - "regex": true, - "code": "java\\.annotation\\.removed", - "old": ".*? com\\.azure\\.communication\\.phonenumbers\\.models.*", - "new": ".*? com\\.azure\\.communication\\.phonenumbers\\.models.*", - "justification": "Migration to azure-json" - }, - { - "regex": true, - "code": "java\\.annotation\\.removed", - "old": ".*? com\\.azure\\.digitaltwins\\.core(\\.models)?.*", - "new": ".*? com\\.azure\\.digitaltwins\\.core(\\.models)?.*", - "justification": "Migration to azure-json" - }, - { - "regex": true, - "code": "java\\.annotation\\.removed", - "old": ".*? com\\.azure\\.messaging\\.webpubsub\\.client\\.models.*", - "new": ".*? com\\.azure\\.messaging\\.webpubsub\\.client\\.models.*", - "justification": "Migration to azure-json" - }, - { - "regex": true, - "code": "java\\.annotation\\.removed", - "old": ".*? com\\.azure\\.messaging\\.webpubsub\\.models.*", - "new": ".*? com\\.azure\\.messaging\\.webpubsub\\.models.*", - "justification": "Migration to azure-json" - }, - { - "regex": true, - "code": "java\\.annotation\\.removed", - "old": ".*? com\\.azure\\.ai\\.translation\\.text\\.models.*", - "new": ".*? com\\.azure\\.ai\\.translation\\.text\\.models.*", - "justification": "Migration to azure-json" - }, - { - "regex": true, - "code": "java\\.annotation\\.removed", - "old": ".*? com\\.azure\\.communication\\.callautomation\\.models.*", - "new": ".*? com\\.azure\\.communication\\.callautomation\\.models.*", - "justification": "Migration to azure-json" - }, - { - "regex": true, - "code": "java\\.annotation\\.removed", - "old": ".*? com\\.azure\\.communication\\.chat\\.models.*", - "new": ".*? com\\.azure\\.communication\\.chat\\.models.*", - "justification": "Migration to azure-json" - }, - { - "regex": true, - "code": "java\\.annotation\\.removed", - "old": ".*? com\\.azure\\.communication\\.email\\.models.*", - "new": ".*? com\\.azure\\.communication\\.email\\.models.*", - "justification": "Migration to azure-json" - }, { "regex": true, "code": "java.class.nowFinal", @@ -555,21 +608,6 @@ "new": "class com.azure.resourcemanager.(eventhubs|servicebus).models.UserAssignedIdentity", "justification": "Class is now final." }, - { - "ignore": true, - "code": "java.annotation.removed", - "old": "method com.azure.communication.identity.models.CommunicationTokenScope com.azure.communication.identity.models.CommunicationTokenScope::fromString(java.lang.String)", - "new": "method com.azure.communication.identity.models.CommunicationTokenScope com.azure.communication.identity.models.CommunicationTokenScope::fromString(java.lang.String)", - "annotation": "@com.fasterxml.jackson.annotation.JsonCreator", - "justification": "Migration to azure-json" - }, - { - "regex": true, - "code": "java\\.annotation\\.removed", - "old": ".*? com\\.azure\\.communication\\.messages\\.models(\\.channels)?.*", - "new": ".*? com\\.azure\\.communication\\.messages\\.models(\\.channels)?.*", - "justification": "Migration to azure-json" - }, { "code": "java.method.returnTypeErasureChanged", "old": "method T com.azure.identity.AadCredentialBuilderBase>::clientId(java.lang.String) @ com.azure.identity.InteractiveBrowserCredentialBuilder", @@ -582,11 +620,6 @@ "new": "method com.azure.identity.DeviceCodeCredentialBuilder com.azure.identity.DeviceCodeCredentialBuilder::clientId(java.lang.String)", "justification": "Override flags this as a potential binary breaking change, but it isn't." }, - { - "code": "java\\.annotation\\.removed", - "old": ".*? com\\.azure\\.compute\\.batch\\.models.*", - "justification": "Removing Jackson annotations from Azure Batch in transition to stream-style." - }, { "ignore": true, "code": "java.method.returnTypeChanged", @@ -675,27 +708,12 @@ "new": "class com.azure.resourcemanager.sql.models.UserIdentity", "justification": "Customer unlikely to subclass this class." }, - { - "ignore": true, - "code": "java.annotation.removed", - "old": "method com.azure.communication.rooms.models.ParticipantRole com.azure.communication.rooms.models.ParticipantRole::fromString(java.lang.String)", - "new": "method com.azure.communication.rooms.models.ParticipantRole com.azure.communication.rooms.models.ParticipantRole::fromString(java.lang.String)", - "annotation": "@com.fasterxml.jackson.annotation.JsonCreator", - "justification": "Migration to azure-json" - }, { "ignore": true, "code": "java.method.removed", "old": "method com.azure.resourcemanager.compute.models.WindowsConfiguration com.azure.resourcemanager.compute.models.WindowsConfiguration::withEnableVMAgentPlatformUpdates(java.lang.Boolean)", "justification": "Service changed the property to readOnly." }, - { - "regex": true, - "code" : "java\\.annotation\\.removed", - "old" : ".*? com\\.azure\\.developer\\.devcenter\\.models.*", - "new" : ".*? com\\.azure\\.developer\\.devcenter\\.models.*", - "justification": "Migration to azure-json" - }, { "ignore": true, "code" : "java.method.removed", diff --git a/eng/code-quality-reports/src/test/java/com/azure/tools/revapi/transforms/AzureSdkTreeFilterProviderTest.java b/eng/code-quality-reports/src/test/java/com/azure/tools/revapi/transforms/AzureSdkTreeFilterProviderTest.java deleted file mode 100644 index 78f3fd074839..000000000000 --- a/eng/code-quality-reports/src/test/java/com/azure/tools/revapi/transforms/AzureSdkTreeFilterProviderTest.java +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.tools.revapi.transforms; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import static com.azure.tools.revapi.transforms.AzureSdkTreeFilterProvider.excludeClass; -import static com.azure.tools.revapi.transforms.AzureSdkTreeFilterProvider.excludePackage; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Tests exclusion logic for {@link AzureSdkTreeFilterProvider}. - */ -public class AzureSdkTreeFilterProviderTest { - @ParameterizedTest - @ValueSource(strings = { - "com.azure.core.util.Configuration", "com.azure.cosmos.BridgeInternal", "com.azure.cosmos.CosmosBridgeInternal", - "com.azure.cosmos.models.ModelBridgeInternal", "com.azure.cosmos.util.UtilBridgeInternal", - "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" - }) - public void classesThatShouldBeExcluded(String className) { - assertTrue(excludeClass(className)); - } - - @ParameterizedTest - @ValueSource(strings = { - "com.azure.core.util.CoreUtils", "com.azure.cosmos.ConnectionMode", "com.azure.cosmos.CosmosClient", - "com.azure.cosmos.models.ChangeFeedPolicy", "com.azure.cosmos.util.CosmosPagedFlux", - "com.azure.spring.cloud.config.AppConfigurationConstants", "com.azure.spring.cloud.config.HostType", - "com.azure.spring.cloud.config.properties.ConfigStore", - "com.azure.spring.cloud.config.web.AppConfigurationWebAutoConfiguration", - "com.azure.spring.cloud.config.web.pushrefresh.AppConfigurationRefreshEndpoint" - }) - public void classesThatShouldNotBeExcluded(String className) { - assertFalse(excludeClass(className)); - } - - @ParameterizedTest - @ValueSource(strings = { - "com.azure.data.cosmos", "com.azure.core.implementation", "com.azure.resourcemanager.fluent", - "com.azure.resourcemanager.fluent.models", "com.fasterxml.jackson", "com.google.gson", "com.microsoft.azure", - "com.nimbusds", "io.micrometer", "io.netty", "io.vertx", "javax.jms", "javax.servlet", "kotlin", "okhttp3", - "okio", "org.apache.avro", "org.apache.commons", "org.apache.qpid", "org.junit", "org.slf4j", - "org.springframework", "reactor.core", "reactor.netty", "reactor.util" - }) - public void packagesThatShouldBeExcluded(String packageName) { - assertTrue(excludePackage(packageName)); - } - - @ParameterizedTest - @ValueSource(strings = { - "com.azure.cosmos", "com.azure.core.util", "com.azure.resourcemanager.fluentcore" - }) - public void packagesThatShouldBeNotExcluded(String packageName) { - assertFalse(excludePackage(packageName)); - } -} diff --git a/eng/code-quality-reports/src/test/java/com/azure/tools/revapi/transforms/ClassAndPackageTreeFilterProviderTest.java b/eng/code-quality-reports/src/test/java/com/azure/tools/revapi/transforms/ClassAndPackageTreeFilterProviderTest.java new file mode 100644 index 000000000000..3bc8995eeeea --- /dev/null +++ b/eng/code-quality-reports/src/test/java/com/azure/tools/revapi/transforms/ClassAndPackageTreeFilterProviderTest.java @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.tools.revapi.transforms; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.revapi.AnalysisContext; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests exclusion logic for {@link ClassAndPackageTreeFilterProvider}. + */ +public class ClassAndPackageTreeFilterProviderTest { + @ParameterizedTest + @ValueSource(strings = { + "com.azure.core.util.Configuration", "com.azure.cosmos.BridgeInternal", "com.azure.cosmos.CosmosBridgeInternal", + "com.azure.cosmos.models.ModelBridgeInternal", "com.azure.cosmos.util.UtilBridgeInternal", + "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" + }) + public void classesThatShouldBeExcluded(String className) { + AnalysisContext context = AnalysisContext.builder().build().copyWithConfiguration(createDefaultConfiguration()); + + try (ClassAndPackageTreeFilterProvider treeFilterProvider = new ClassAndPackageTreeFilterProvider()) { + treeFilterProvider.initialize(context); + assertTrue(treeFilterProvider.excludeClass(className)); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "com.azure.core.util.CoreUtils", "com.azure.cosmos.ConnectionMode", "com.azure.cosmos.CosmosClient", + "com.azure.cosmos.models.ChangeFeedPolicy", "com.azure.cosmos.util.CosmosPagedFlux", + "com.azure.spring.cloud.config.AppConfigurationConstants", "com.azure.spring.cloud.config.HostType", + "com.azure.spring.cloud.config.properties.ConfigStore", + "com.azure.spring.cloud.config.web.AppConfigurationWebAutoConfiguration", + "com.azure.spring.cloud.config.web.pushrefresh.AppConfigurationRefreshEndpoint" + }) + public void classesThatShouldNotBeExcluded(String className) { + AnalysisContext context = AnalysisContext.builder().build() + .copyWithConfiguration(createDefaultConfiguration()); + + try (ClassAndPackageTreeFilterProvider treeFilterProvider = new ClassAndPackageTreeFilterProvider()) { + treeFilterProvider.initialize(context); + assertFalse(treeFilterProvider.excludeClass(className)); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "com.azure.data.cosmos", "com.azure.core.implementation", "com.azure.resourcemanager.fluent", + "com.azure.resourcemanager.fluent.models", "com.fasterxml.jackson", "com.google.gson", "com.microsoft.azure", + "com.nimbusds", "io.micrometer", "io.netty", "io.vertx", "javax.jms", "javax.servlet", "kotlin", "okhttp3", + "okio", "org.apache.avro", "org.apache.commons", "org.apache.qpid", "org.junit", "org.slf4j", + "org.springframework", "reactor.core", "reactor.netty", "reactor.util" + }) + public void packagesThatShouldBeExcluded(String packageName) { + AnalysisContext context = AnalysisContext.builder().build() + .copyWithConfiguration(createDefaultConfiguration()); + + try (ClassAndPackageTreeFilterProvider treeFilterProvider = new ClassAndPackageTreeFilterProvider()) { + treeFilterProvider.initialize(context); + assertTrue(treeFilterProvider.excludePackage(packageName)); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "com.azure.cosmos", "com.azure.core.util", "com.azure.resourcemanager.fluentcore" + }) + public void packagesThatShouldBeNotExcluded(String packageName) { + AnalysisContext context = AnalysisContext.builder().build() + .copyWithConfiguration(createDefaultConfiguration()); + + try (ClassAndPackageTreeFilterProvider treeFilterProvider = new ClassAndPackageTreeFilterProvider()) { + treeFilterProvider.initialize(context); + assertFalse(treeFilterProvider.excludePackage(packageName)); + } + } + + private static JsonNode createDefaultConfiguration() { + ObjectNode configuration = JsonNodeFactory.instance.objectNode(); + configuration.putObject("ignoredClasses") + .putArray("com.azure.") + .add("core.util.Configuration") + .add("cosmos.BridgeInternal") + .add("cosmos.CosmosBridgeInternal") + .add("cosmos.models.ModelBridgeInternal") + .add("cosmos.util.UtilBridgeInternal") + .add("spring.cloud.config.AppConfigurationBootstrapConfiguration") + .add("spring.cloud.config.AppConfigurationRefresh") + .add("spring.cloud.config.properties.AppConfigurationProviderProperties") + .add("spring.cloud.config.web.AppConfigurationEndpoint") + .add("spring.cloud.config.web.pushrefresh.AppConfigurationRefreshEvent"); + + ObjectNode ignoredPackages = configuration.putObject("ignoredPackages"); + ignoredPackages.putArray("com.") + .add("azure.data.cosmos") + .add("fasterxml.jackson") + .add("google.gson") + .add("microsoft.azure") + .add("nimbusds"); + ignoredPackages.putArray("io.") + .add("micrometer") + .add("netty") + .add("vertx"); + ignoredPackages.putArray("javax.") + .add("jms") + .add("servlet"); + ignoredPackages.putArray("kotlin"); + ignoredPackages.putArray("okhttp3"); + ignoredPackages.putArray("okio"); + ignoredPackages.putArray("org.") + .add("apache.avro") + .add("apache.commons") + .add("apache.qpid") + .add("junit") + .add("reactivestreams") + .add("slf4j") + .add("springframework"); + ignoredPackages.putArray("reactor.") + .add("core") + .add("netty") + .add("util"); + + configuration.putArray("ignoredPackagesPatterns") + .add("com\\.azure\\..*(implementation|samples).*") + .add("com\\.azure\\.resourcemanager\\..*(fluent)(\\..*)?$"); + + return configuration; + } +} diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index d8870096d251..3fd84877aaad 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -234,6 +234,9 @@ org.revapi:revapi-maven-plugin;0.14.6 # sdk\keyvault\microsoft-azure-keyvault-test\pom.xml test_jar_com.microsoft.azure:azure-mgmt-resources;1.3.1-SNAPSHOT +# Special test dependencies for clientcore integrations +clientcore_dep_tests_org.slf4j:slf4j-simple;2.0.16 + # everything under sdk\cosmos cosmos_com.google.guava:guava;33.0.0-jre cosmos_io.dropwizard.metrics:metrics-core;4.1.0 diff --git a/sdk/clientcore/optional-dependency-tests/pom.xml b/sdk/clientcore/optional-dependency-tests/pom.xml index 4b241bec4626..1bbe325dffe4 100644 --- a/sdk/clientcore/optional-dependency-tests/pom.xml +++ b/sdk/clientcore/optional-dependency-tests/pom.xml @@ -70,7 +70,7 @@ org.slf4j slf4j-simple - 2.0.16 + 2.0.16 test diff --git a/sdk/parents/azure-client-sdk-parent/pom.xml b/sdk/parents/azure-client-sdk-parent/pom.xml index 30d2b1f4df50..e9150052305b 100644 --- a/sdk/parents/azure-client-sdk-parent/pom.xml +++ b/sdk/parents/azure-client-sdk-parent/pom.xml @@ -1034,7 +1034,7 @@ - azure-sdk-tree-provider + class-and-package-tree-filter-provider @@ -1055,6 +1055,11 @@ revapi-reporter-json 0.4.5 + + com.fasterxml.jackson.core + jackson-databind + 2.17.2 + diff --git a/sdk/parents/clientcore-parent/pom.xml b/sdk/parents/clientcore-parent/pom.xml index 799af97d104b..4617a1c40aa9 100644 --- a/sdk/parents/clientcore-parent/pom.xml +++ b/sdk/parents/clientcore-parent/pom.xml @@ -962,7 +962,7 @@ true - revapi/revapi.json + revapi/clientcore-revapi.json ^\d+\.\d+\.\d+$ @@ -981,7 +981,7 @@ - azure-sdk-tree-provider + class-and-package-tree-filter-provider @@ -1002,6 +1002,11 @@ revapi-reporter-json 0.4.5 + + com.fasterxml.jackson.core + jackson-databind + 2.17.2 + diff --git a/sdk/tools/azure-sdk-archetype/pom.xml b/sdk/tools/azure-sdk-archetype/pom.xml index 0eecbc00ad6b..e993ac178112 100644 --- a/sdk/tools/azure-sdk-archetype/pom.xml +++ b/sdk/tools/azure-sdk-archetype/pom.xml @@ -251,7 +251,7 @@ - azure-sdk-tree-provider + class-and-package-tree-filter-provider