This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch backport-12083-to-4.0.x in repository https://gitbox.apache.org/repos/asf/maven.git
commit 1403fc87e51051d3e4d49449d807946a704bd08f 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 {
