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

gnodet pushed a commit to branch maven-4.0.x-test-fixes
in repository https://gitbox.apache.org/repos/asf/maven.git

commit 14f4eefb13af88109edf5497adaed2d30e4e54c5
Author: Guillaume Nodet <[email protected]>
AuthorDate: Mon May 18 18:19:01 2026 +0200

    Fix #12080: mvnup - comment out dependencies with undefined property 
expressions
    
    Add a new compatibility fix to CompatibilityFixStrategy that detects
    dependencies whose coordinate fields (groupId, artifactId, version)
    contain ${...} expressions referencing properties not defined in any
    project POM. These orphan dependencies cause Maven 4's validator to
    reject them with "Not fully interpolated dependency" errors.
    
    The fix collects all properties from <properties> sections across the
    reactor (including profiles), skips well-known built-in properties
    (project.*, env.*, settings.*, maven.*, revision, sha1, changelist,
    etc.), and comments out dependencies with truly undefined expressions
    using <!-- mvnup: commented out - undefined property '...' --> format.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../mvnup/goals/CompatibilityFixStrategy.java      | 153 +++++++++++++++++++
 .../mvnup/goals/CompatibilityFixStrategyTest.java  | 170 +++++++++++++++++++++
 2 files changed, 323 insertions(+)

diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/CompatibilityFixStrategy.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/CompatibilityFixStrategy.java
index 266af03dff..8601a9b664 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/CompatibilityFixStrategy.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/CompatibilityFixStrategy.java
@@ -25,9 +25,13 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Stream;
 
+import eu.maveniverse.domtrip.Comment;
 import eu.maveniverse.domtrip.Document;
+import eu.maveniverse.domtrip.Editor;
 import eu.maveniverse.domtrip.Element;
 import eu.maveniverse.domtrip.maven.Coordinates;
 import eu.maveniverse.domtrip.maven.MavenPomElements;
@@ -54,6 +58,7 @@
 import static 
eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_REPOSITORY;
 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILE;
 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILES;
+import static 
eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROPERTIES;
 import static 
eu.maveniverse.domtrip.maven.MavenPomElements.Elements.RELATIVE_PATH;
 import static 
eu.maveniverse.domtrip.maven.MavenPomElements.Elements.REPOSITORIES;
 import static 
eu.maveniverse.domtrip.maven.MavenPomElements.Elements.REPOSITORY;
@@ -70,6 +75,8 @@
 @Priority(20)
 public class CompatibilityFixStrategy extends AbstractUpgradeStrategy {
 
+    private static final Pattern EXPRESSION_PATTERN = 
Pattern.compile("\\$\\{([^}]+)}");
+
     @Override
     public boolean isApplicable(UpgradeContext context) {
         UpgradeOptions options = getOptions(context);
@@ -117,6 +124,9 @@ public UpgradeResult doApply(UpgradeContext context, 
Map<Path, Document> pomMap)
         Set<Path> modifiedPoms = new HashSet<>();
         Set<Path> errorPoms = new HashSet<>();
 
+        // Collect all properties defined across all project POMs for 
cross-POM analysis
+        Set<String> allDefinedProperties = collectAllDefinedProperties(pomMap);
+
         for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
             Path pomPath = entry.getKey();
             Document pomDocument = entry.getValue();
@@ -135,6 +145,7 @@ public UpgradeResult doApply(UpgradeContext context, 
Map<Path, Document> pomMap)
                 hasIssues |= fixDuplicatePlugins(pomDocument, context);
                 hasIssues |= fixUnsupportedRepositoryExpressions(pomDocument, 
context);
                 hasIssues |= fixIncorrectParentRelativePaths(pomDocument, 
pomPath, pomMap, context);
+                hasIssues |= fixUndefinedPropertyExpressions(pomDocument, 
allDefinedProperties, context);
 
                 if (hasIssues) {
                     context.success("Maven 4 compatibility issues fixed");
@@ -340,6 +351,148 @@ private boolean fixIncorrectParentRelativePaths(
         return false;
     }
 
+    /**
+     * Collects all property names defined in properties sections across all 
project POMs.
+     */
+    private Set<String> collectAllDefinedProperties(Map<Path, Document> 
pomMap) {
+        Set<String> properties = new HashSet<>();
+
+        for (Document document : pomMap.values()) {
+            Element root = document.root();
+
+            root.child(PROPERTIES)
+                    .ifPresent(propsElement -> 
propsElement.children().forEach(child -> properties.add(child.name())));
+
+            root.child(PROFILES)
+                    .ifPresent(profiles -> profiles.children(PROFILE)
+                            .forEach(profile -> profile.child(PROPERTIES)
+                                    .ifPresent(propsElement ->
+                                            
propsElement.children().forEach(child -> properties.add(child.name())))));
+        }
+
+        return properties;
+    }
+
+    /**
+     * Fixes dependencies with undefined property expressions by commenting 
them out.
+     */
+    private boolean fixUndefinedPropertyExpressions(
+            Document pomDocument, Set<String> allDefinedProperties, 
UpgradeContext context) {
+        Element root = pomDocument.root();
+
+        Stream<DependencyContainer> dependencyContainers = Stream.concat(
+                Stream.of(
+                                new 
DependencyContainer(root.child(DEPENDENCIES).orElse(null), DEPENDENCIES),
+                                new DependencyContainer(
+                                        root.child(DEPENDENCY_MANAGEMENT)
+                                                .flatMap(dm -> 
dm.child(DEPENDENCIES))
+                                                .orElse(null),
+                                        DEPENDENCY_MANAGEMENT))
+                        .filter(container -> container.element != null),
+                root.child(PROFILES).stream()
+                        .flatMap(profiles -> profiles.children(PROFILE))
+                        .flatMap(profile -> Stream.of(
+                                        new DependencyContainer(
+                                                
profile.child(DEPENDENCIES).orElse(null), "profile dependencies"),
+                                        new DependencyContainer(
+                                                
profile.child(DEPENDENCY_MANAGEMENT)
+                                                        .flatMap(dm -> 
dm.child(DEPENDENCIES))
+                                                        .orElse(null),
+                                                "profile 
dependencyManagement"))
+                                .filter(container -> container.element != 
null)));
+
+        return dependencyContainers
+                .map(container -> fixUndefinedPropertyExpressionsInSection(
+                        container.element, allDefinedProperties, pomDocument, 
context, container.sectionName))
+                .reduce(false, Boolean::logicalOr);
+    }
+
+    /**
+     * Fixes undefined property expressions in a specific dependencies section.
+     */
+    private boolean fixUndefinedPropertyExpressionsInSection(
+            Element dependenciesElement,
+            Set<String> allDefinedProperties,
+            Document pomDocument,
+            UpgradeContext context,
+            String sectionName) {
+        boolean fixed = false;
+        List<Element> dependencies = 
dependenciesElement.children(DEPENDENCY).toList();
+        Editor editor = new Editor(pomDocument);
+
+        for (Element dependency : dependencies) {
+            Set<String> undefinedProps = findUndefinedProperties(dependency, 
allDefinedProperties);
+            if (!undefinedProps.isEmpty()) {
+                String propLabel = undefinedProps.size() > 1 ? "properties" : 
"property";
+                String propsStr = "'" + String.join("', '", undefinedProps) + 
"'";
+
+                Comment comment = editor.commentOutElement(dependency);
+                String elementXml = comment.content().trim();
+                comment.content(
+                        " mvnup: commented out - undefined " + propLabel + " " 
+ propsStr + "\n" + elementXml + " ");
+
+                context.detail("Fixed: Commented out dependency with undefined 
" + propLabel + " " + propsStr + " in "
+                        + sectionName);
+                fixed = true;
+            }
+        }
+
+        return fixed;
+    }
+
+    /**
+     * Finds undefined property expressions in a dependency's coordinate 
fields.
+     */
+    private Set<String> findUndefinedProperties(Element dependency, 
Set<String> allDefinedProperties) {
+        Set<String> undefinedProperties = new HashSet<>();
+
+        String groupId = 
dependency.childText(MavenPomElements.Elements.GROUP_ID);
+        String artifactId = 
dependency.childText(MavenPomElements.Elements.ARTIFACT_ID);
+        String version = 
dependency.childText(MavenPomElements.Elements.VERSION);
+
+        collectUndefinedExpressions(groupId, allDefinedProperties, 
undefinedProperties);
+        collectUndefinedExpressions(artifactId, allDefinedProperties, 
undefinedProperties);
+        collectUndefinedExpressions(version, allDefinedProperties, 
undefinedProperties);
+
+        return undefinedProperties;
+    }
+
+    private void collectUndefinedExpressions(String value, Set<String> 
allDefinedProperties, Set<String> result) {
+        if (value == null) {
+            return;
+        }
+        Matcher matcher = EXPRESSION_PATTERN.matcher(value);
+        while (matcher.find()) {
+            String propertyName = matcher.group(1);
+            if (!isWellKnownProperty(propertyName) && 
!allDefinedProperties.contains(propertyName)) {
+                result.add(propertyName);
+            }
+        }
+    }
+
+    private static boolean isWellKnownProperty(String propertyName) {
+        if (propertyName.startsWith("project.")
+                || propertyName.startsWith("pom.")
+                || propertyName.startsWith("env.")
+                || propertyName.startsWith("settings.")
+                || propertyName.startsWith("maven.")) {
+            return true;
+        }
+        if (propertyName.startsWith("java.")
+                || propertyName.startsWith("os.")
+                || propertyName.startsWith("user.")
+                || propertyName.startsWith("file.")
+                || propertyName.startsWith("line.")
+                || propertyName.startsWith("path.")
+                || propertyName.startsWith("sun.")) {
+            return true;
+        }
+        return "basedir".equals(propertyName)
+                || "revision".equals(propertyName)
+                || "sha1".equals(propertyName)
+                || "changelist".equals(propertyName);
+    }
+
     /**
      * Recursively finds all elements with a specific attribute value.
      */
diff --git 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/CompatibilityFixStrategyTest.java
 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/CompatibilityFixStrategyTest.java
index af8b0774c7..c2e002d3bf 100644
--- 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/CompatibilityFixStrategyTest.java
+++ 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/CompatibilityFixStrategyTest.java
@@ -281,6 +281,176 @@ void shouldRemoveDuplicatePluginsInPluginManagement() 
throws Exception {
         }
     }
 
+    @Nested
+    @DisplayName("Undefined Property Expression Fixes")
+    class UndefinedPropertyExpressionFixesTests {
+
+        @Test
+        @DisplayName("should comment out dependency with undefined property 
expression")
+        void shouldCommentOutDependencyWithUndefinedProperty() throws 
Exception {
+            String pomXml = """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <project xmlns="http://maven.apache.org/POM/4.0.0";>
+                    <modelVersion>4.0.0</modelVersion>
+                    <groupId>test</groupId>
+                    <artifactId>test</artifactId>
+                    <version>1.0.0</version>
+                    <dependencyManagement>
+                        <dependencies>
+                            <dependency>
+                                <groupId>com.google.guava</groupId>
+                                <artifactId>guava</artifactId>
+                                <version>${guava-version}</version>
+                            </dependency>
+                        </dependencies>
+                    </dependencyManagement>
+                </project>
+                """;
+
+            Document document = Document.of(pomXml);
+            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), 
document);
+
+            UpgradeContext context = createMockContext();
+            UpgradeResult result = strategy.doApply(context, pomMap);
+
+            assertTrue(result.success(), "Compatibility fix should succeed");
+            assertTrue(result.modifiedCount() > 0, "Should have commented out 
dependency");
+
+            String xml = DomUtils.toXml(document);
+            assertTrue(xml.contains("mvnup: commented out"), "Should contain 
comment-out marker");
+            assertTrue(xml.contains("guava-version"), "Should mention the 
undefined property");
+
+            Element root = document.root();
+            Element depMgmt = DomUtils.findChildElement(root, 
"dependencyManagement");
+            Element deps = DomUtils.findChildElement(depMgmt, "dependencies");
+            assertEquals(0, deps.children("dependency").count(), "Should have 
no dependency elements");
+        }
+
+        @Test
+        @DisplayName("should not comment out dependency with defined property")
+        void shouldNotCommentOutDependencyWithDefinedProperty() throws 
Exception {
+            String pomXml = """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <project xmlns="http://maven.apache.org/POM/4.0.0";>
+                    <modelVersion>4.0.0</modelVersion>
+                    <groupId>test</groupId>
+                    <artifactId>test</artifactId>
+                    <version>1.0.0</version>
+                    <properties>
+                        <guava-version>30.0-jre</guava-version>
+                    </properties>
+                    <dependencyManagement>
+                        <dependencies>
+                            <dependency>
+                                <groupId>com.google.guava</groupId>
+                                <artifactId>guava</artifactId>
+                                <version>${guava-version}</version>
+                            </dependency>
+                        </dependencies>
+                    </dependencyManagement>
+                </project>
+                """;
+
+            Document document = Document.of(pomXml);
+            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), 
document);
+
+            UpgradeContext context = createMockContext();
+            strategy.doApply(context, pomMap);
+
+            Element root = document.root();
+            Element depMgmt = DomUtils.findChildElement(root, 
"dependencyManagement");
+            Element deps = DomUtils.findChildElement(depMgmt, "dependencies");
+            assertEquals(1, deps.children("dependency").count(), "Dependency 
should still be present");
+        }
+
+        @Test
+        @DisplayName("should not comment out dependency with well-known 
built-in property")
+        void shouldNotCommentOutDependencyWithBuiltinProperty() throws 
Exception {
+            String pomXml = """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <project xmlns="http://maven.apache.org/POM/4.0.0";>
+                    <modelVersion>4.0.0</modelVersion>
+                    <groupId>test</groupId>
+                    <artifactId>test</artifactId>
+                    <version>1.0.0</version>
+                    <dependencies>
+                        <dependency>
+                            <groupId>test</groupId>
+                            <artifactId>test-dep</artifactId>
+                            <version>${project.version}</version>
+                        </dependency>
+                    </dependencies>
+                </project>
+                """;
+
+            Document document = Document.of(pomXml);
+            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), 
document);
+
+            UpgradeContext context = createMockContext();
+            strategy.doApply(context, pomMap);
+
+            Element root = document.root();
+            Element deps = DomUtils.findChildElement(root, "dependencies");
+            assertEquals(
+                    1,
+                    deps.children("dependency").count(),
+                    "Dependency with built-in property should still be 
present");
+        }
+
+        @Test
+        @DisplayName("should recognize property defined in another module POM")
+        void shouldRecognizePropertyFromOtherPom() throws Exception {
+            String parentPom = """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <project xmlns="http://maven.apache.org/POM/4.0.0";>
+                    <modelVersion>4.0.0</modelVersion>
+                    <groupId>test</groupId>
+                    <artifactId>parent</artifactId>
+                    <version>1.0.0</version>
+                    <properties>
+                        <guava-version>30.0-jre</guava-version>
+                    </properties>
+                </project>
+                """;
+
+            String childPom = """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <project xmlns="http://maven.apache.org/POM/4.0.0";>
+                    <modelVersion>4.0.0</modelVersion>
+                    <parent>
+                        <groupId>test</groupId>
+                        <artifactId>parent</artifactId>
+                        <version>1.0.0</version>
+                    </parent>
+                    <artifactId>child</artifactId>
+                    <dependencies>
+                        <dependency>
+                            <groupId>com.google.guava</groupId>
+                            <artifactId>guava</artifactId>
+                            <version>${guava-version}</version>
+                        </dependency>
+                    </dependencies>
+                </project>
+                """;
+
+            Document parentDoc = Document.of(parentPom);
+            Document childDoc = Document.of(childPom);
+            Map<Path, Document> pomMap = Map.of(
+                    Paths.get("pom.xml"), parentDoc,
+                    Paths.get("child/pom.xml"), childDoc);
+
+            UpgradeContext context = createMockContext();
+            strategy.doApply(context, pomMap);
+
+            Element root = childDoc.root();
+            Element deps = DomUtils.findChildElement(root, "dependencies");
+            assertEquals(
+                    1,
+                    deps.children("dependency").count(),
+                    "Dependency should not be commented out when property is 
defined in another POM");
+        }
+    }
+
     @Nested
     @DisplayName("Strategy Description")
     class StrategyDescriptionTests {

Reply via email to