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

rombert pushed a commit to branch master
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-analyser.git


The following commit(s) were added to refs/heads/master by this push:
     new 04d2559  SLING-13205  Add a validator to detect conflicting RepoInit 
statements  (#57)
04d2559 is described below

commit 04d25597c14d1a4c3965d865dba751ee8c7cd5e5
Author: michalwedzik <[email protected]>
AuthorDate: Wed May 27 13:24:16 2026 +0200

    SLING-13205  Add a validator to detect conflicting RepoInit statements  
(#57)
    
    Under some circumstances sling produces wrong paths in the repoinit, I 
added logging to have the possibility to observe this situation in the logs
    
    ---------
    
    Co-authored-by: sii12877 <[email protected]>
---
 readme.md                                          |   4 +
 .../analyser/task/impl/CheckRepoInitConflicts.java |  62 +++++++++++
 .../RepoInitConflictsValidator.java                | 124 +++++++++++++++++++++
 .../impl/repoinitconflicts/ValidationReport.java   |  69 ++++++++++++
 .../task/impl/CheckRepoInitConflictsTest.java      | 124 +++++++++++++++++++++
 .../RepoInitConflictsValidatorTest.java            | 121 ++++++++++++++++++++
 6 files changed, 504 insertions(+)

diff --git a/readme.md b/readme.md
index fe2ce18..3861ba1 100644
--- a/readme.md
+++ b/readme.md
@@ -106,6 +106,10 @@ This analyser requires additional configuration:
 
 Checks the syntax of all repoinit sections.
 
+## `repoinit-conflicts`
+
+Validates repoinit sections for potential conflicts and reports findings as 
warnings.
+
 ## `requirements-capabilities`
 
 Checks bundle requirements/capabilities for consistency and completeness.
diff --git 
a/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckRepoInitConflicts.java
 
b/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckRepoInitConflicts.java
new file mode 100644
index 0000000..edf714b
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckRepoInitConflicts.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.feature.analyser.task.impl;
+
+import java.util.List;
+
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+import 
org.apache.sling.feature.analyser.task.impl.repoinitconflicts.RepoInitConflictsValidator;
+import 
org.apache.sling.feature.analyser.task.impl.repoinitconflicts.ValidationReport;
+
+public class CheckRepoInitConflicts implements AnalyserTask {
+
+    @Override
+    public String getId() {
+        return "repoinit-conflicts";
+    }
+
+    @Override
+    public String getName() {
+        return "Repoinit Conflicts check";
+    }
+
+    /**
+     * Executes the Repoinit conflict validation.
+     * <p>
+     * This method retrieves the feature from the context, validates it using
+     * {@link RepoInitConflictsValidator}, and reports a warning if any 
conflicts are found.
+     *
+     * @param context analyser task context containing the feature to validate
+     */
+    @Override
+    public void execute(final AnalyserTaskContext context) {
+        Feature feature = context.getFeature();
+
+        ValidationReport report = RepoInitConflictsValidator.validate(feature);
+
+        if (!report.hasConflicts()) {
+            return;
+        }
+
+        List<String> messages = report.generate();
+        messages.forEach(context::reportWarning);
+    }
+}
diff --git 
a/src/main/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/RepoInitConflictsValidator.java
 
b/src/main/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/RepoInitConflictsValidator.java
new file mode 100644
index 0000000..ae4ffbc
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/RepoInitConflictsValidator.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.feature.analyser.task.impl.repoinitconflicts;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.repoinit.parser.impl.ParseException;
+import org.apache.sling.repoinit.parser.impl.RepoInitParserImpl;
+import org.apache.sling.repoinit.parser.operations.CreatePath;
+import org.apache.sling.repoinit.parser.operations.Operation;
+import org.apache.sling.repoinit.parser.operations.PathSegmentDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@SuppressWarnings("java:S1874")
+public class RepoInitConflictsValidator {
+    private RepoInitConflictsValidator() {}
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(RepoInitConflictsValidator.class);
+
+    public static ValidationReport validate(final Feature feature) {
+        ValidationReport report = new ValidationReport();
+
+        final Extension repoinitExtension = 
feature.getExtensions().getByName(Extension.EXTENSION_NAME_REPOINIT);
+
+        if (repoinitExtension == null
+                || repoinitExtension.getText() == null
+                || repoinitExtension.getType() != ExtensionType.TEXT) {
+            return report;
+        }
+
+        List<CreatePath[]> conflicts = findConflicts(repoinitExtension);
+
+        report.addConflicts(feature, conflicts);
+
+        return report;
+    }
+
+    private static List<CreatePath[]> findConflicts(Extension 
repoinitExtension) {
+        try {
+            List<Operation> operations = new RepoInitParserImpl(new 
StringReader(repoinitExtension.getText())).parse();
+
+            List<CreatePath> createPaths = operations.stream()
+                    .filter(CreatePath.class::isInstance)
+                    .map(op -> (CreatePath) op)
+                    .collect(Collectors.toList());
+
+            return findConflictingPairs(createPaths);
+        } catch (ParseException e) {
+            LOGGER.error(
+                    "Failed to parse repoinit statements, skipping conflict 
validation. Error: {}", e.getMessage(), e);
+            return Collections.emptyList();
+        }
+    }
+
+    private static List<CreatePath[]> findConflictingPairs(List<CreatePath> 
createPaths) {
+        int size = createPaths.size();
+        List<CreatePath[]> conflicts = new ArrayList<>();
+        for (int i = 0; i < size; i++) {
+            for (int j = i + 1; j < size; j++) {
+                List<CreatePath[]> conflict = 
findConflictPair(createPaths.get(i), createPaths.get(j));
+                if (!conflict.isEmpty()) {
+                    conflicts.addAll(conflict);
+                }
+            }
+        }
+        return conflicts;
+    }
+
+    private static List<CreatePath[]> findConflictPair(CreatePath a, 
CreatePath b) {
+        List<PathSegmentDefinition> aDefs = a.getDefinitions();
+        List<PathSegmentDefinition> bDefs = b.getDefinitions();
+
+        // different depth → no conflict
+        if (aDefs.size() != bDefs.size()) {
+            return Collections.emptyList();
+        }
+        List<CreatePath[]> conflicts = new ArrayList<>();
+        for (int i = 0; i < aDefs.size(); i++) {
+            PathSegmentDefinition aSeg = aDefs.get(i);
+            PathSegmentDefinition bSeg = bDefs.get(i);
+
+            // segments diverge → stop comparing this pair
+            if (!Objects.equals(aSeg.getSegment(), bSeg.getSegment())) {
+                return Collections.emptyList();
+            }
+
+            // same segment but different type → conflict
+            if (!Objects.equals(aSeg.getPrimaryType(), bSeg.getPrimaryType())) 
{
+                CreatePath[] conflict = new CreatePath[2];
+                conflict[0] = a;
+                conflict[1] = b;
+
+                conflicts.add(conflict);
+                break;
+            }
+        }
+        return conflicts;
+    }
+}
diff --git 
a/src/main/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/ValidationReport.java
 
b/src/main/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/ValidationReport.java
new file mode 100644
index 0000000..34945f6
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/ValidationReport.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.feature.analyser.task.impl.repoinitconflicts;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.feature.Feature;
+import org.apache.sling.repoinit.parser.operations.CreatePath;
+
+@SuppressWarnings("java:S1874")
+public class ValidationReport {
+
+    private final Map<Feature, List<CreatePath[]>> conflicts = new 
LinkedHashMap<>();
+
+    public void addConflicts(Feature feature, List<CreatePath[]> 
featureConflicts) {
+        if (!featureConflicts.isEmpty()) {
+            conflicts.put(feature, featureConflicts);
+        }
+    }
+
+    public boolean hasConflicts() {
+        return !conflicts.isEmpty();
+    }
+
+    public List<String> generate() {
+        List<String> messages = new ArrayList<>();
+        messages.add("Repoinit validation results:");
+
+        if (!hasConflicts()) {
+            messages.add("No issues found");
+        } else {
+            for (Map.Entry<Feature, List<CreatePath[]>> entry : 
conflicts.entrySet()) {
+                Feature feature = entry.getKey();
+                List<CreatePath[]> featureConflicts = entry.getValue();
+
+                messages.add("Incorrect repoinit for feature " + feature);
+                messages.add("Found " + featureConflicts.size() + " sets of 
conflicting repoinit statements");
+
+                for (CreatePath[] conflict : featureConflicts) {
+                    messages.add("Conflicting statement :\n  "
+                            + conflict[0].asRepoInitString()
+                            + "\n"
+                            + conflict[1].asRepoInitString());
+                }
+            }
+        }
+
+        return messages;
+    }
+}
diff --git 
a/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckRepoInitConflictsTest.java
 
b/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckRepoInitConflictsTest.java
new file mode 100644
index 0000000..35c17c7
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckRepoInitConflictsTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.feature.analyser.task.impl;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class CheckRepoInitConflictsTest {
+
+    @Test
+    void testConstants() {
+        final AnalyserTask task = new CheckRepoInitConflicts();
+
+        assertEquals("repoinit-conflicts", task.getId());
+        assertEquals("Repoinit Conflicts check", task.getName());
+    }
+
+    @Test
+    void shouldNotReportWarningWhenNoRepoinit() {
+        final AnalyserTaskContext ctx = 
Mockito.mock(AnalyserTaskContext.class);
+
+        Feature feature = Mockito.mock(Feature.class);
+        org.apache.sling.feature.Extensions extensions = 
Mockito.mock(org.apache.sling.feature.Extensions.class);
+
+        Mockito.when(ctx.getFeature()).thenReturn(feature);
+        Mockito.when(feature.getExtensions()).thenReturn(extensions);
+        Mockito.when(extensions.getByName("repoinit")).thenReturn(null);
+
+        CheckRepoInitConflicts task = new CheckRepoInitConflicts();
+        task.execute(ctx);
+
+        Mockito.verify(ctx).getFeature();
+        Mockito.verifyNoMoreInteractions(ctx);
+    }
+
+    @Test
+    void shouldNotReportWarningWhenNoConflicts() {
+        final AnalyserTaskContext ctx = 
Mockito.mock(AnalyserTaskContext.class);
+
+        Feature feature = featureWithExtension(
+                textExtension("create path (sling:Folder) /apps/a/b\n" + 
"create path (sling:Folder) /apps/a/c"));
+
+        Mockito.when(ctx.getFeature()).thenReturn(feature);
+
+        CheckRepoInitConflicts task = new CheckRepoInitConflicts();
+        task.execute(ctx);
+
+        Mockito.verify(ctx).getFeature();
+        Mockito.verifyNoMoreInteractions(ctx);
+    }
+
+    @Test
+    void shouldReportWarningWhenConflictExists() {
+        final AnalyserTaskContext ctx = 
Mockito.mock(AnalyserTaskContext.class);
+
+        Feature feature =
+                featureWithExtension(textExtension("create path (sling:Folder) 
/apps/a/b(cq:ClientLibraryFolder)\n"
+                        + "create path (sling:Folder) /apps/a/b"));
+
+        Mockito.when(ctx.getFeature()).thenReturn(feature);
+
+        CheckRepoInitConflicts task = new CheckRepoInitConflicts();
+        task.execute(ctx);
+
+        Mockito.verify(ctx).getFeature();
+        Mockito.verify(ctx).reportWarning(Mockito.contains("conflicting 
repoinit"));
+        Mockito.verify(ctx).reportWarning(Mockito.contains("Conflicting 
statement"));
+    }
+
+    @Test
+    void shouldIgnoreInvalidRepoinitSyntax() {
+        final AnalyserTaskContext ctx = 
Mockito.mock(AnalyserTaskContext.class);
+
+        Feature feature = featureWithExtension(textExtension("invalid $$$"));
+
+        Mockito.when(ctx.getFeature()).thenReturn(feature);
+
+        CheckRepoInitConflicts task = new CheckRepoInitConflicts();
+        task.execute(ctx);
+
+        Mockito.verify(ctx).getFeature();
+        Mockito.verifyNoMoreInteractions(ctx);
+    }
+
+    private Extension textExtension(String text) {
+        Extension extension = Mockito.mock(Extension.class);
+        Mockito.when(extension.getType()).thenReturn(ExtensionType.TEXT);
+        Mockito.when(extension.getText()).thenReturn(text);
+        return extension;
+    }
+
+    private Feature featureWithExtension(Extension extension) {
+        Feature feature = Mockito.mock(Feature.class);
+        org.apache.sling.feature.Extensions extensions = 
Mockito.mock(org.apache.sling.feature.Extensions.class);
+
+        Mockito.when(feature.getExtensions()).thenReturn(extensions);
+        Mockito.when(extensions.getByName("repoinit")).thenReturn(extension);
+
+        return feature;
+    }
+}
diff --git 
a/src/test/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/RepoInitConflictsValidatorTest.java
 
b/src/test/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/RepoInitConflictsValidatorTest.java
new file mode 100644
index 0000000..b2e538b
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/feature/analyser/task/impl/repoinitconflicts/RepoInitConflictsValidatorTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.feature.analyser.task.impl.repoinitconflicts;
+
+import java.util.List;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class RepoInitConflictsValidatorTest {
+
+    @Test
+    void shouldReturnNoIssuesWhenFeatureHasNoRepoinit() {
+        Feature feature = mock(Feature.class);
+        
when(feature.getExtensions()).thenReturn(mock(org.apache.sling.feature.Extensions.class));
+        when(feature.getExtensions().getByName("repoinit")).thenReturn(null);
+
+        ValidationReport report = RepoInitConflictsValidator.validate(feature);
+
+        assertFalse(report.hasConflicts());
+        assertTrue(report.generate().contains("No issues found"));
+    }
+
+    @Test
+    void shouldReturnNoIssuesForCorrectRepoinit() {
+        Extension extension =
+                textExtension("create path (sling:Folder) /apps/a/b\n" + 
"create path (sling:Folder) /apps/a/c\n"
+                        + "create path (sling:Folder) 
/apps/a/c/d(sling:OrderedFolder)");
+
+        Feature feature = featureWithExtension(extension);
+
+        ValidationReport report = RepoInitConflictsValidator.validate(feature);
+
+        assertFalse(report.hasConflicts());
+        assertTrue(report.generate().contains("No issues found"));
+    }
+
+    @Test
+    void shouldReportConflictForSamePathDifferentType() {
+        Extension extension = textExtension(
+                "create path (sling:Folder) /apps/a/b(sling:OrderedFolder)\n" 
+ "create path (sling:Folder) /apps/a/b");
+
+        Feature feature = featureWithExtension(extension);
+
+        ValidationReport report = RepoInitConflictsValidator.validate(feature);
+        assertTrue(report.hasConflicts());
+
+        List<String> result = report.generate();
+        assertTrue(result.get(1).contains("Incorrect repoinit for feature"));
+        assertTrue(result.get(2).contains("Found 1 sets of conflicting 
repoinit statements"));
+        assertTrue(result.get(3).contains("/apps/a/b"));
+    }
+
+    @Test
+    void shouldReportMultipleConflicts() {
+        Extension extension = textExtension("create path (sling:Folder) 
/apps/a/b(sling:OrderedFolder)\n"
+                + "create path (sling:Folder) /apps/a/b\n"
+                + "create path (sling:Folder) /apps/x/y(sling:OrderedFolder)\n"
+                + "create path (sling:Folder) /apps/x/y");
+
+        Feature feature = featureWithExtension(extension);
+
+        ValidationReport report = RepoInitConflictsValidator.validate(feature);
+        assertTrue(report.hasConflicts());
+
+        List<String> result = report.generate();
+        assertTrue(result.get(2).contains("Found 2 sets of conflicting 
repoinit statements"));
+    }
+
+    @Test
+    void shouldIgnoreInvalidRepoinitSyntax() {
+        Extension extension = textExtension("invalid $$$");
+
+        Feature feature = featureWithExtension(extension);
+
+        ValidationReport report = RepoInitConflictsValidator.validate(feature);
+
+        assertFalse(report.hasConflicts());
+        assertTrue(report.generate().contains("No issues found"));
+    }
+
+    private Extension textExtension(String text) {
+        Extension extension = mock(Extension.class);
+        when(extension.getType()).thenReturn(ExtensionType.TEXT);
+        when(extension.getText()).thenReturn(text);
+        return extension;
+    }
+
+    private Feature featureWithExtension(Extension extension) {
+        Feature feature = mock(Feature.class);
+        org.apache.sling.feature.Extensions extensions = 
mock(org.apache.sling.feature.Extensions.class);
+
+        when(feature.getExtensions()).thenReturn(extensions);
+        when(extensions.getByName("repoinit")).thenReturn(extension);
+
+        return feature;
+    }
+}

Reply via email to