You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by kw...@apache.org on 2026/05/09 10:07:44 UTC

(sling-whiteboard) branch bugfix/service-values-in-scr-skill created (now b918f466)

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

kwin pushed a change to branch bugfix/service-values-in-scr-skill
in repository https://gitbox.apache.org/repos/asf/sling-whiteboard.git


      at b918f466 Register all transitive interfaces as services as well

This branch includes the following new commits:

     new b918f466 Register all transitive interfaces as services as well

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(sling-whiteboard) 01/01: Register all transitive interfaces as services as well

Posted by kw...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kwin pushed a commit to branch bugfix/service-values-in-scr-skill
in repository https://gitbox.apache.org/repos/asf/sling-whiteboard.git

commit b918f4661bfc5c2d369177495b5b174300eacc94
Author: Konrad Windszus <kw...@apache.org>
AuthorDate: Sat May 9 12:07:32 2026 +0200

    Register all transitive interfaces as services as well
---
 .../scripts/migrate_annotations.py                 | 121 +++++++++-
 .../scripts/tests/test_migrate_annotations.py      | 251 +++++++++++++++++++++
 2 files changed, 369 insertions(+), 3 deletions(-)

diff --git a/skills/osgi-scr-migrator/scripts/migrate_annotations.py b/skills/osgi-scr-migrator/scripts/migrate_annotations.py
index 70385722..6e87ee86 100755
--- a/skills/osgi-scr-migrator/scripts/migrate_annotations.py
+++ b/skills/osgi-scr-migrator/scripts/migrate_annotations.py
@@ -737,7 +737,10 @@ class AnnotationMigrator:
         return annotations
 
     def _find_service_class_for_component(self, component_idx: int) -> Optional[str]:
-        """Find the @Service annotation near this @Component."""
+        """Find the @Service annotation and either infer service class from class interfaces or the given value.
+        
+        Checks for explicit @Service annotation.
+        """
         # Look in the next few lines for @Service
         for i in range(component_idx + 1, min(component_idx + 5, len(self.lines))):
             line = self.lines[i]
@@ -746,9 +749,121 @@ class AnnotationMigrator:
                 service_match = re.search(r'@Service\s*\(\s*(?:value\s*=\s*)?([^)]+)\)', line)
                 if service_match:
                     return service_match.group(1).strip()
-                # If no explicit class, return empty to signal service registration
-                return ""
+                # If @Service is present but no class specified, we will infer from interfaces
+                else:
+                    return self._get_transitive_interfaces_from_class(component_idx)
+        
+        # No @Service annotation found don't auto register as service
         return None
+    
+    def _get_transitive_interfaces_from_class(self, component_idx: int) -> Optional[str]:
+        """Extract all transitive interfaces from the class declaration.
+        
+        Looks for the class declaration after the @Component annotation and extracts
+        all interfaces it implements. Also resolves transitive interfaces (interfaces
+        that the declared interfaces extend).
+        
+        Returns:
+            - A single interface name if exactly one interface is found
+            - A list of interfaces formatted as "{ Interface1.class, Interface2.class, ... }"
+            - Empty string "" if interfaces are found but unable to resolve
+            - None if no class declaration or interfaces are found
+        """
+        # Find the class declaration after @Component
+        class_line = None
+        for i in range(component_idx + 1, min(component_idx + 10, len(self.lines))):
+            line = self.lines[i]
+            if re.search(r'(public|private|protected)?\s*(abstract\s+)?class\s+\w+', line):
+                class_line = i
+                break
+        
+        if class_line is None:
+            return None
+        
+        # Extract the class declaration line(s) - may span multiple lines
+        class_decl = self.lines[class_line]
+        paren_count = class_decl.count('(') - class_decl.count(')')
+        j = class_line + 1
+        
+        # Collect until we find opening brace or complete the declaration
+        while j < len(self.lines) and paren_count >= 0 and '{' not in class_decl:
+            class_decl += ' ' + self.lines[j].strip()
+            paren_count += self.lines[j].count('(') - self.lines[j].count(')')
+            j += 1
+        
+        # Extract interfaces from "implements Interface1, Interface2, ..."
+        interfaces_match = re.search(r'implements\s+([^{]+)', class_decl)
+        if not interfaces_match:
+            return None
+        
+        interfaces_str = interfaces_match.group(1).strip()
+        # Split by comma and clean up each interface
+        declared_interfaces = [iface.strip() for iface in interfaces_str.split(',')]
+        
+        if not declared_interfaces:
+            return None
+        
+        # Get all transitive interfaces
+        all_interfaces = set()
+        for iface in declared_interfaces:
+            # Add the direct interface
+            all_interfaces.add(iface)
+            # Get transitive interfaces
+            transitive = self._get_transitive_extends(iface)
+            all_interfaces.update(transitive)
+        
+        # Remove any empty strings
+        all_interfaces.discard('')
+        
+        if not all_interfaces:
+            return None
+        
+        # Format the result
+        if len(all_interfaces) == 1:
+            return list(all_interfaces)[0] + ".class"
+        else:
+            # Sort for consistent output
+            sorted_interfaces = sorted(all_interfaces)
+            interfaces_expr = ', '.join(f'{iface}.class' for iface in sorted_interfaces)
+            return f'{{ {interfaces_expr} }}'
+    
+    def _get_transitive_extends(self, interface_name: str) -> Set[str]:
+        """Find all interfaces that the given interface extends (recursively).
+        
+        Searches the current file for the interface definition and extracts all
+        interfaces it extends, then recursively resolves those.
+        
+        Args:
+            interface_name: The interface name to resolve (may include package prefix)
+        
+        Returns:
+            Set of all transitive parent interfaces
+        """
+        transitive = set()
+        
+        # Handle simple names (just look for "interface InterfaceName")
+        # and qualified names (com.example.InterfaceName - take last part)
+        simple_name = interface_name.split('.')[-1] if '.' in interface_name else interface_name
+        
+        # Search for interface definition in current file
+        for i, line in enumerate(self.lines):
+            # Look for "interface InterfaceName extends ..."
+            pattern = rf'interface\s+{re.escape(simple_name)}\s*(?:extends\s+([^{{]+))?'
+            match = re.search(pattern, line)
+            if match and match.group(1):
+                # Found the extends clause
+                extends_str = match.group(1).strip()
+                # Split by comma to get all parent interfaces
+                parent_interfaces = [iface.strip() for iface in extends_str.split(',')]
+                
+                for parent in parent_interfaces:
+                    if parent:
+                        transitive.add(parent)
+                        # Recursively get transitive interfaces
+                        transitive.update(self._get_transitive_extends(parent))
+                break
+        
+        return transitive
 
     def _transform_component_annotation(self, annotation: str, service_class: Optional[str] = None) -> str:
         """Transform SCR @Component to OSGi R6/R7 format."""
diff --git a/skills/osgi-scr-migrator/scripts/tests/test_migrate_annotations.py b/skills/osgi-scr-migrator/scripts/tests/test_migrate_annotations.py
index f15eb00d..ee476b95 100644
--- a/skills/osgi-scr-migrator/scripts/tests/test_migrate_annotations.py
+++ b/skills/osgi-scr-migrator/scripts/tests/test_migrate_annotations.py
@@ -708,6 +708,257 @@ public class MyService {
         self.assertIn('service_ranking()', new_content)
 
 
+class TestTransitiveInterfaceResolution(unittest.TestCase):
+    """Test transitive interface retrieval from class declarations."""
+
+    def test_single_interface_implementation(self):
+        """Test retrieving a single interface from class declaration."""
+        content = '''package com.example;
+
+import org.apache.felix.scr.annotations.Component;
+
+public interface MyService {
+    void doSomething();
+}
+
+@Component
+public class MyServiceImpl implements MyService {
+    public void doSomething() {}
+}
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        # Index of @Component annotation
+        result = migrator._get_transitive_interfaces_from_class(6)
+        
+        self.assertIsNotNone(result)
+        self.assertIn('MyService.class', result)
+
+    def test_multiple_interfaces_implementation(self):
+        """Test retrieving multiple interfaces from class declaration."""
+        content = '''package com.example;
+
+import org.apache.felix.scr.annotations.Component;
+
+public interface Service1 { void method1(); }
+public interface Service2 { void method2(); }
+
+@Component
+public class MultiImpl implements Service1, Service2 {
+    public void method1() {}
+    public void method2() {}
+}
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        result = migrator._get_transitive_interfaces_from_class(6)
+        
+        self.assertIsNotNone(result)
+        # Result should contain both interfaces
+        self.assertIn('Service1', result)
+        self.assertIn('Service2', result)
+        self.assertIn('.class', result)
+
+    def test_transitive_extends_hierarchy(self):
+        """Test retrieving interfaces with transitive extends hierarchy."""
+        content = '''package com.example;
+
+import org.apache.felix.scr.annotations.Component;
+
+public interface BaseService {
+    void base();
+}
+
+public interface ExtendedService extends BaseService {
+    void extended();
+}
+
+@Component
+public class ServiceImpl implements ExtendedService {
+    public void extended() {}
+    public void base() {}
+}
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        result = migrator._get_transitive_interfaces_from_class(9)
+        
+        self.assertIsNotNone(result)
+        # Should include both the direct interface and transitive parent
+        self.assertIn('BaseService', result)
+        self.assertIn('ExtendedService', result)
+
+    def test_complex_transitive_hierarchy(self):
+        """Test complex transitive interface hierarchy (A -> B -> C)."""
+        content = '''package com.example;
+
+import org.apache.felix.scr.annotations.Component;
+
+public interface A { void a(); }
+public interface B extends A { void b(); }
+public interface C extends B { void c(); }
+
+@Component
+public class ComplexImpl implements C {
+    public void c() {}
+    public void b() {}
+    public void a() {}
+}
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        result = migrator._get_transitive_interfaces_from_class(7)
+        
+        self.assertIsNotNone(result)
+        # Should include all three interfaces in the hierarchy
+        self.assertIn('A', result)
+        self.assertIn('B', result)
+        self.assertIn('C', result)
+
+    def test_service_annotation_takes_precedence(self):
+        """Test that explicit @Service annotation takes precedence over class interfaces."""
+        content = '''package com.example;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+
+public interface MyInterface {
+    void doIt();
+}
+
+@Component
+@Service(ExplicitService.class)
+public class MyImpl implements MyInterface {
+    public void doIt() {}
+}
+
+public interface ExplicitService {
+    void explicit();
+}
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        result = migrator._find_service_class_for_component(8)
+        
+        # @Service annotation should take precedence
+        self.assertEqual(result, 'ExplicitService.class')
+
+    def test_no_interfaces_returns_none(self):
+        """Test that components with no interfaces return None."""
+        content = '''package com.example;
+
+import org.apache.felix.scr.annotations.Component;
+
+@Component
+public class PlainComponent {
+    public void doSomething() {}
+}
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        result = migrator._get_transitive_interfaces_from_class(3)
+        
+        self.assertIsNone(result)
+
+    def test_service_annotation_without_value(self):
+        """Test @Service annotation without explicit class value."""
+        content = '''package com.example;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+
+public interface MyInterface {
+    void doIt();
+}
+
+@Component
+@Service
+public class MyImpl implements MyInterface {
+    public void doIt() {}
+}
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        result = migrator._find_service_class_for_component(8)
+        
+        # Should return "MyInterface" for @Service without value as the semantics of SCR Service and the service attribute in OSGi R6/R7 @Component are different for empty values.
+        self.assertEqual(result, "MyInterface.class")
+
+    def test_multiline_class_declaration(self):
+        """Test retrieving interfaces from multi-line class declaration."""
+        content = '''package com.example;
+
+import org.apache.felix.scr.annotations.Component;
+
+public interface Service1 { void m1(); }
+public interface Service2 { void m2(); }
+
+@Component
+public class MultilineImpl
+    implements Service1, Service2 {
+    
+    public void m1() {}
+    public void m2() {}
+}
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        result = migrator._get_transitive_interfaces_from_class(7)
+        
+        self.assertIsNotNone(result)
+        self.assertIn('Service1', result)
+        self.assertIn('Service2', result)
+
+    def test_multiple_interface_extends(self):
+        """Test interfaces that extend multiple parent interfaces."""
+        content = '''package com.example;
+
+import org.apache.felix.scr.annotations.Component;
+
+public interface A { void a(); }
+public interface B { void b(); }
+public interface C extends A, B { void c(); }
+
+@Component
+public class MultiExtendImpl implements C {
+    public void c() {}
+    public void a() {}
+    public void b() {}
+}
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        result = migrator._get_transitive_interfaces_from_class(7)
+        
+        self.assertIsNotNone(result)
+        # Should resolve all transitive parents
+        self.assertIn('A', result)
+        self.assertIn('B', result)
+        self.assertIn('C', result)
+
+    def test_fully_qualified_interface_names(self):
+        """Test that interfaces are properly resolved even with package names."""
+        content = '''package com.example.impl;
+
+import org.apache.felix.scr.annotations.Component;
+import com.example.api.MyService;
+import com.example.api.OtherService;
+
+@Component
+public class ServiceImpl implements MyService, OtherService {
+    public void doIt() {}
+}
+'''
+        stats = MigrationStats()
+        migrator = AnnotationMigrator(content, Path("test.java"), stats)
+        result = migrator._get_transitive_interfaces_from_class(5)
+        
+        self.assertIsNotNone(result)
+        # Should contain the interface names (last part after package)
+        self.assertIn('MyService', result)
+        self.assertIn('OtherService', result)
+
+
 class TestMigrationStats(unittest.TestCase):
     """Test migration statistics tracking."""