This is an automated email from the ASF dual-hosted git repository.

cziegeler pushed a commit to branch SLING-13114
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-extension-apiregions.git


The following commit(s) were added to refs/heads/SLING-13114 by this push:
     new 5f952c8  SLING-13114 : Check for deprecated API does not consider 
other bundles exporting the API
5f952c8 is described below

commit 5f952c8aa692715b7ad2a5bafc85a04b83bdf1f9
Author: Carsten Ziegeler <[email protected]>
AuthorDate: Sat Feb 14 14:37:53 2026 +0100

    SLING-13114 : Check for deprecated API does not consider other bundles 
exporting the API
---
 .../apiregions/analyser/CheckDeprecatedApi.java    | 125 ++++++++++++-----
 .../analyser/CheckDeprecatedApiTest.java           | 156 ++++++++++++++++++++-
 2 files changed, 242 insertions(+), 39 deletions(-)

diff --git 
a/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckDeprecatedApi.java
 
b/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckDeprecatedApi.java
index 944a761..fdd7d7f 100644
--- 
a/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckDeprecatedApi.java
+++ 
b/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckDeprecatedApi.java
@@ -18,15 +18,18 @@
  */
 package org.apache.sling.feature.extension.apiregions.analyser;
 
+import java.util.ArrayList;
 import java.util.Calendar;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import org.apache.sling.feature.ArtifactId;
 import org.apache.sling.feature.analyser.task.AnalyserTask;
 import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
 import org.apache.sling.feature.extension.apiregions.api.ApiExport;
@@ -49,8 +52,6 @@ public class CheckDeprecatedApi implements AnalyserTask {
 
     private static final String CFG_CHECK_OPTIONAL_IMPORTS = 
"check-optional-imports";
 
-    private static final String PROP_VERSION = "version";
-
     @Override
     public String getId() {
         return "region-deprecated-api";
@@ -112,7 +113,8 @@ public class CheckDeprecatedApi implements AnalyserTask {
             checkDate = null;
         }
 
-        final Set<ApiExport> exports = 
this.calculateDeprecatedPackages(region, bundleRegions);
+        final Map<String, DeprecatedPackage> deprecatedPackages =
+                this.calculateDeprecatedPackages(region, bundleRegions);
 
         final Set<String> allowedNames = getAllowedRegions(region);
 
@@ -122,16 +124,10 @@ public class CheckDeprecatedApi implements AnalyserTask {
                     if (!checkOptionalImports && pi.isOptional()) {
                         continue;
                     }
-                    final VersionRange importRange = 
pi.getPackageVersionRange();
                     DeprecationInfo deprecationInfo = null;
-                    for (final ApiExport exp : exports) {
-                        if (pi.getName().equals(exp.getName())) {
-                            String version = 
exp.getProperties().get(PROP_VERSION);
-                            if (version == null || importRange == null || 
importRange.includes(new Version(version))) {
-                                deprecationInfo = 
exp.getDeprecation().getPackageInfo();
-                                break;
-                            }
-                        }
+                    final DeprecatedPackage deprecatedPackage = 
deprecatedPackages.get(pi.getName());
+                    if (deprecatedPackage != null) {
+                        deprecationInfo = deprecatedPackage.isDeprecated(pi);
                     }
                     if (deprecationInfo != null) {
                         String msg = "Usage of deprecated package found : "
@@ -199,41 +195,45 @@ public class CheckDeprecatedApi implements AnalyserTask {
         return allowedNames;
     }
 
-    Set<ApiExport> calculateDeprecatedPackages(
+    Map<String, DeprecatedPackage> calculateDeprecatedPackages(
             final ApiRegion region, final Map<BundleDescriptor, Set<String>> 
bundleRegions) {
-        final Set<ApiExport> result = new LinkedHashSet<>();
-        for (final ApiExport export : region.listAllExports()) {
-            if (export.getDeprecation().getPackageInfo() != null) {
-                final String version = getVersion(bundleRegions, 
region.getName(), export.getName());
-                // create new ApiExport to add version
-                final ApiExport clone = new ApiExport(export.getName());
-                
clone.getDeprecation().setPackageInfo(export.getDeprecation().getPackageInfo());
-                if (version != null) {
-                    clone.getProperties().put(PROP_VERSION, version);
+        final Map<String, DeprecatedPackage> result = new HashMap<>();
+        ApiRegion current = region;
+        while (current != null) {
+            for (final ApiExport export : current.listExports()) {
+                if (export.getDeprecation().getPackageInfo() != null && 
!result.containsKey(export.getName())) {
+                    final DeprecatedPackage pck = 
getDeprecatedPackage(bundleRegions, current, export);
+                    result.put(export.getName(), pck);
                 }
-                result.add(clone);
             }
+            current = current.getParent();
         }
         return result;
     }
 
-    String getVersion(
-            final Map<BundleDescriptor, Set<String>> bundleRegions, final 
String regionName, final String packageName) {
-        String version = null;
+    DeprecatedPackage getDeprecatedPackage(
+            final Map<BundleDescriptor, Set<String>> bundleRegions, final 
ApiRegion region, final ApiExport export) {
+        final List<PackageInfo> deprecatedList = new ArrayList<>();
+        final List<PackageInfo> nonDeprecatedList = new ArrayList<>();
+
+        final ArtifactId[] regionOrigins = region.getFeatureOrigins();
+
         for (final Map.Entry<BundleDescriptor, Set<String>> entry : 
bundleRegions.entrySet()) {
-            if (entry.getValue().contains(regionName)) {
+            final ArtifactId[] bundleOrigins = 
entry.getKey().getArtifact().getFeatureOrigins();
+            if (entry.getValue().contains(region.getName())) {
                 for (final PackageInfo info : 
entry.getKey().getExportedPackages()) {
-                    if (info.getName().equals(packageName)) {
-                        version = info.getVersion();
-                        break;
+                    if (info.getName().equals(export.getName())) {
+                        if (regionOrigins.length == 0
+                                || (bundleOrigins.length > 0 && 
bundleOrigins[0].isSame(regionOrigins[0]))) {
+                            deprecatedList.add(info);
+                        } else {
+                            nonDeprecatedList.add(info);
+                        }
                     }
                 }
-                if (version != null) {
-                    break;
-                }
             }
         }
-        return version;
+        return new DeprecatedPackage(export.getDeprecation().getPackageInfo(), 
deprecatedList, nonDeprecatedList);
     }
 
     private Set<String> getBundleRegions(final BundleDescriptor info, final 
ApiRegions regions) {
@@ -243,4 +243,59 @@ public class CheckDeprecatedApi implements AnalyserTask {
                 .map(ApiRegion::getName)
                 .collect(Collectors.toSet());
     }
+
+    /**
+     * Represents a deprecated package with its deprecation information and 
package versions.
+     */
+    static final class DeprecatedPackage {
+        private final DeprecationInfo deprecationInfo;
+        private final List<PackageInfo> deprecatedList;
+        private final List<PackageInfo> nonDeprecatedList;
+
+        public DeprecationInfo getDeprecationInfo() {
+            return deprecationInfo;
+        }
+
+        /**
+         * Constructs a DeprecatedPackage with deprecation info and lists of 
package versions.
+         *
+         * @param deprecationInfo the deprecation information for this package
+         * @param deprecatedList list of deprecated package versions
+         * @param nonDeprecatedList list of non-deprecated package versions
+         */
+        public DeprecatedPackage(
+                final DeprecationInfo deprecationInfo,
+                final List<PackageInfo> deprecatedList,
+                final List<PackageInfo> nonDeprecatedList) {
+            this.deprecationInfo = deprecationInfo;
+            this.deprecatedList = deprecatedList;
+            this.nonDeprecatedList = nonDeprecatedList;
+        }
+
+        /**
+         * Checks if the given package info is deprecated.
+         *
+         * @param pi the package info to check
+         * @return the deprecation info if the package is deprecated, null 
otherwise
+         */
+        public DeprecationInfo isDeprecated(final PackageInfo pi) {
+            final VersionRange importVersion = pi.getVersion() != null ? 
pi.getPackageVersionRange() : null;
+            // check non deprecated list first
+            for (final PackageInfo nonDeprecated : nonDeprecatedList) {
+                final Version exportVersion = 
nonDeprecated.getPackageVersion();
+                if (exportVersion == null || importVersion == null || 
importVersion.includes(exportVersion)) {
+                    return null;
+                }
+            }
+            // check deprecated list
+            for (final PackageInfo deprecated : deprecatedList) {
+                final Version exportVersion = deprecated.getPackageVersion();
+                if (exportVersion == null || importVersion == null || 
importVersion.includes(exportVersion)) {
+                    return deprecationInfo;
+                }
+            }
+            // not found, do not report
+            return null;
+        }
+    }
 }
diff --git 
a/src/test/java/org/apache/sling/feature/extension/apiregions/analyser/CheckDeprecatedApiTest.java
 
b/src/test/java/org/apache/sling/feature/extension/apiregions/analyser/CheckDeprecatedApiTest.java
index 479765b..1525d0e 100644
--- 
a/src/test/java/org/apache/sling/feature/extension/apiregions/analyser/CheckDeprecatedApiTest.java
+++ 
b/src/test/java/org/apache/sling/feature/extension/apiregions/analyser/CheckDeprecatedApiTest.java
@@ -34,6 +34,7 @@ import org.apache.sling.feature.ExtensionState;
 import org.apache.sling.feature.ExtensionType;
 import org.apache.sling.feature.Feature;
 import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+import 
org.apache.sling.feature.extension.apiregions.analyser.CheckDeprecatedApi.DeprecatedPackage;
 import org.apache.sling.feature.extension.apiregions.api.ApiExport;
 import org.apache.sling.feature.extension.apiregions.api.ApiRegion;
 import org.apache.sling.feature.extension.apiregions.api.ApiRegions;
@@ -47,6 +48,7 @@ import org.mockito.Mockito;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.never;
 
@@ -104,13 +106,14 @@ public class CheckDeprecatedApiTest {
         region.add(e3);
 
         // only e1 should be returned
-        final Set<ApiExport> exports = 
analyser.calculateDeprecatedPackages(region, Collections.emptyMap());
+        final Map<String, DeprecatedPackage> exports =
+                analyser.calculateDeprecatedPackages(region, 
Collections.emptyMap());
         assertEquals(1, exports.size());
-        final ApiExport exp = exports.iterator().next();
-        assertEquals(e1.getName(), exp.getName());
+        final DeprecatedPackage exp = exports.get("e1");
+        assertNotNull(exp);
         assertEquals(
                 e1.getDeprecation().getPackageInfo().getMessage(),
-                exp.getDeprecation().getPackageInfo().getMessage());
+                exp.getDeprecationInfo().getMessage());
     }
 
     @Test
@@ -138,6 +141,145 @@ public class CheckDeprecatedApiTest {
                         Mockito.eq(ArtifactId.fromMvnId("g:b:1.0.0")), 
Mockito.contains("org.foo.deprecated"));
     }
 
+    @Test
+    public void testVersionRangeChecking() throws Exception {
+        // Test that version ranges are properly checked
+        final CheckDeprecatedApi analyser = new CheckDeprecatedApi();
+
+        // Create a feature with deprecated package exported at version 2.0.0
+        final Feature feature = new 
Feature(ArtifactId.fromMvnId("g:feature:1"));
+        final Extension extension =
+                new Extension(ExtensionType.JSON, ApiRegions.EXTENSION_NAME, 
ExtensionState.OPTIONAL);
+        
extension.setJSON("[{\"name\":\"global\",\"feature-origins\":[\"g:feature:1\"],"
+                + 
"\"exports\":[{\"name\":\"org.foo.deprecated\",\"deprecated\":\"This package is 
deprecated\"}]}]");
+        feature.getExtensions().add(extension);
+
+        final FeatureDescriptor fd = new FeatureDescriptorImpl(feature);
+
+        // Bundle that exports the deprecated package at version 2.0.0
+        final Artifact exportBundle = new 
Artifact(ArtifactId.fromMvnId("g:exporter:1.0.0"));
+        exportBundle.setFeatureOrigins(feature.getId());
+        final BundleDescriptor exportBd = new 
TestBundleDescriptor(exportBundle);
+        exportBd.getExportedPackages()
+                .add(new PackageInfo("org.foo.deprecated", "2.0.0", false, 
Collections.emptySet()));
+        fd.getBundleDescriptors().add(exportBd);
+
+        // Bundle that imports with version range [1.0.0,2.0.0) - should NOT 
be flagged
+        final Artifact importBundle1 = new 
Artifact(ArtifactId.fromMvnId("g:importer1:1.0.0"));
+        importBundle1.setFeatureOrigins(feature.getId());
+        final BundleDescriptor importBd1 = new 
TestBundleDescriptor(importBundle1);
+        final PackageInfo import1 =
+                new PackageInfo("org.foo.deprecated", "[1.0.0,2.0.0)", false, 
Collections.emptySet());
+        importBd1.getImportedPackages().add(import1);
+        fd.getBundleDescriptors().add(importBd1);
+
+        // Bundle that imports with version range [2.0.0,3.0.0) - SHOULD be 
flagged
+        final Artifact importBundle2 = new 
Artifact(ArtifactId.fromMvnId("g:importer2:1.0.0"));
+        importBundle2.setFeatureOrigins(feature.getId());
+        final BundleDescriptor importBd2 = new 
TestBundleDescriptor(importBundle2);
+        final PackageInfo import2 =
+                new PackageInfo("org.foo.deprecated", "[2.0.0,3.0.0)", false, 
Collections.emptySet());
+        importBd2.getImportedPackages().add(import2);
+        fd.getBundleDescriptors().add(importBd2);
+
+        // Bundle that imports without version - SHOULD be flagged (matches 
any version)
+        final Artifact importBundle3 = new 
Artifact(ArtifactId.fromMvnId("g:importer3:1.0.0"));
+        importBundle3.setFeatureOrigins(feature.getId());
+        final BundleDescriptor importBd3 = new 
TestBundleDescriptor(importBundle3);
+        final PackageInfo import3 = new PackageInfo("org.foo.deprecated", 
null, false, Collections.emptySet());
+        importBd3.getImportedPackages().add(import3);
+        fd.getBundleDescriptors().add(importBd3);
+
+        final AnalyserTaskContext ctx = 
Mockito.mock(AnalyserTaskContext.class);
+        Mockito.when(ctx.getFeature()).thenReturn(feature);
+        Mockito.when(ctx.getFeatureDescriptor()).thenReturn(fd);
+        
Mockito.when(ctx.getConfiguration()).thenReturn(Collections.emptyMap());
+
+        analyser.execute(ctx);
+
+        // importer1 should NOT be flagged (version range excludes 2.0.0)
+        Mockito.verify(ctx, never())
+                
.reportArtifactWarning(Mockito.eq(ArtifactId.fromMvnId("g:importer1:1.0.0")), 
Mockito.anyString());
+
+        // importer2 SHOULD be flagged (version range includes 2.0.0)
+        Mockito.verify(ctx)
+                .reportArtifactWarning(
+                        Mockito.eq(ArtifactId.fromMvnId("g:importer2:1.0.0")), 
Mockito.contains("org.foo.deprecated"));
+
+        // importer3 SHOULD be flagged (no version constraint)
+        Mockito.verify(ctx)
+                .reportArtifactWarning(
+                        Mockito.eq(ArtifactId.fromMvnId("g:importer3:1.0.0")), 
Mockito.contains("org.foo.deprecated"));
+    }
+
+    @Test
+    public void testMultipleExportVersions() throws Exception {
+        // Test that when multiple versions of a deprecated package exist,
+        // only imports matching the actual exported versions are flagged
+        final CheckDeprecatedApi analyser = new CheckDeprecatedApi();
+
+        final Feature feature = new 
Feature(ArtifactId.fromMvnId("g:feature:1"));
+        final Extension extension =
+                new Extension(ExtensionType.JSON, ApiRegions.EXTENSION_NAME, 
ExtensionState.OPTIONAL);
+        
extension.setJSON("[{\"name\":\"global\",\"feature-origins\":[\"g:feature:1\"],"
+                + 
"\"exports\":[{\"name\":\"org.foo.deprecated\",\"deprecated\":\"This package is 
deprecated\"}]}]");
+        feature.getExtensions().add(extension);
+
+        final FeatureDescriptor fd = new FeatureDescriptorImpl(feature);
+
+        // Bundle that exports the deprecated package at version 1.0.0
+        final Artifact exportBundle1 = new 
Artifact(ArtifactId.fromMvnId("g:exporter1:1.0.0"));
+        exportBundle1.setFeatureOrigins(feature.getId());
+        final BundleDescriptor exportBd1 = new 
TestBundleDescriptor(exportBundle1);
+        exportBd1
+                .getExportedPackages()
+                .add(new PackageInfo("org.foo.deprecated", "1.0.0", false, 
Collections.emptySet()));
+        fd.getBundleDescriptors().add(exportBd1);
+
+        // DIFFERENT bundle that exports the SAME package at version 3.0.0
+        final Artifact exportBundle2 = new 
Artifact(ArtifactId.fromMvnId("g:exporter2:1.0.0"));
+        exportBundle2.setFeatureOrigins(feature.getId());
+        final BundleDescriptor exportBd2 = new 
TestBundleDescriptor(exportBundle2);
+        exportBd2
+                .getExportedPackages()
+                .add(new PackageInfo("org.foo.deprecated", "3.0.0", false, 
Collections.emptySet()));
+        fd.getBundleDescriptors().add(exportBd2);
+
+        // Bundle that imports version [1.0.0,2.0.0) - matches only the 1.0.0 
export
+        final Artifact importBundle1 = new 
Artifact(ArtifactId.fromMvnId("g:importer1:1.0.0"));
+        importBundle1.setFeatureOrigins(feature.getId());
+        final BundleDescriptor importBd1 = new 
TestBundleDescriptor(importBundle1);
+        importBd1
+                .getImportedPackages()
+                .add(new PackageInfo("org.foo.deprecated", "[1.0.0,2.0.0)", 
false, Collections.emptySet()));
+        fd.getBundleDescriptors().add(importBd1);
+
+        // Bundle that imports version [2.5.0,4.0.0) - matches only the 3.0.0 
export
+        final Artifact importBundle2 = new 
Artifact(ArtifactId.fromMvnId("g:importer2:1.0.0"));
+        importBundle2.setFeatureOrigins(feature.getId());
+        final BundleDescriptor importBd2 = new 
TestBundleDescriptor(importBundle2);
+        importBd2
+                .getImportedPackages()
+                .add(new PackageInfo("org.foo.deprecated", "[2.5.0,4.0.0)", 
false, Collections.emptySet()));
+        fd.getBundleDescriptors().add(importBd2);
+
+        final AnalyserTaskContext ctx = 
Mockito.mock(AnalyserTaskContext.class);
+        Mockito.when(ctx.getFeature()).thenReturn(feature);
+        Mockito.when(ctx.getFeatureDescriptor()).thenReturn(fd);
+        
Mockito.when(ctx.getConfiguration()).thenReturn(Collections.emptyMap());
+
+        analyser.execute(ctx);
+
+        // BOTH importers should be flagged since both versions (1.0.0 and 
3.0.0) are available
+        // The implementation checks all matching exported versions for this 
package.
+        Mockito.verify(ctx)
+                .reportArtifactWarning(
+                        Mockito.eq(ArtifactId.fromMvnId("g:importer1:1.0.0")), 
Mockito.contains("org.foo.deprecated"));
+        Mockito.verify(ctx)
+                .reportArtifactWarning(
+                        Mockito.eq(ArtifactId.fromMvnId("g:importer2:1.0.0")), 
Mockito.contains("org.foo.deprecated"));
+    }
+
     private AnalyserTaskContext createContext(final Map<String, String> 
config, final boolean optionalImport) {
         final Feature feature = new 
Feature(ArtifactId.fromMvnId("g:feature:1"));
         final Extension extension =
@@ -154,6 +296,12 @@ public class CheckDeprecatedApiTest {
                 .add(new PackageInfo("org.foo.deprecated", "1.0", 
optionalImport, Collections.emptySet()));
         fd.getBundleDescriptors().add(bd);
 
+        final Artifact apiBundle = new 
Artifact(ArtifactId.fromMvnId("g:c:1.0.0"));
+        apiBundle.setFeatureOrigins(feature.getId());
+        final BundleDescriptor apiDesc = new TestBundleDescriptor(apiBundle);
+        apiDesc.getExportedPackages().add(new 
PackageInfo("org.foo.deprecated", "1.0", false, Collections.emptySet()));
+        fd.getBundleDescriptors().add(apiDesc);
+
         final AnalyserTaskContext ctx = 
Mockito.mock(AnalyserTaskContext.class);
         Mockito.when(ctx.getFeature()).thenReturn(feature);
         Mockito.when(ctx.getFeatureDescriptor()).thenReturn(fd);

Reply via email to