diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoal.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoal.java index a427ce595bca..0dc73c968c18 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoal.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoal.java @@ -310,7 +310,7 @@ protected void fixIncompatibleExtensions(UpgradeContext context) { boolean modified = false; boolean needsNisseCompat = false; - List extensions = root.children("extension").toList(); + List extensions = root.childElements("extension").toList(); List toRemove = new ArrayList<>(); for (Element ext : extensions) { 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 eaaf888e48da..d5e55e9e568c 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 pomMap) Set modifiedPoms = new HashSet<>(); Set errorPoms = new HashSet<>(); + // Collect all properties defined across all project POMs for cross-POM analysis + Set allDefinedProperties = collectAllDefinedProperties(pomMap); + for (Map.Entry entry : pomMap.entrySet()) { Path pomPath = entry.getKey(); Document pomDocument = entry.getValue(); @@ -135,6 +145,7 @@ public UpgradeResult doApply(UpgradeContext context, Map 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"); @@ -345,6 +356,152 @@ private boolean fixIncorrectParentRelativePaths( return false; } + /** + * Collects all property names defined in properties sections across all project POMs. + */ + private Set collectAllDefinedProperties(Map pomMap) { + Set properties = new HashSet<>(); + + for (Document document : pomMap.values()) { + Element root = document.root(); + + root.childElement(PROPERTIES) + .ifPresent(propsElement -> + propsElement.childElements().forEach(child -> properties.add(child.name()))); + + root.childElement(PROFILES).ifPresent(profiles -> profiles.childElements(PROFILE) + .forEach(profile -> profile.childElement(PROPERTIES) + .ifPresent(propsElement -> + propsElement.childElements().forEach(child -> properties.add(child.name()))))); + } + + return properties; + } + + /** + * Fixes dependencies with undefined property expressions by commenting them out. + */ + private boolean fixUndefinedPropertyExpressions( + Document pomDocument, Set allDefinedProperties, UpgradeContext context) { + Element root = pomDocument.root(); + + Stream dependencyContainers = Stream.concat( + Stream.of( + new DependencyContainer( + root.childElement(DEPENDENCIES).orElse(null), DEPENDENCIES), + new DependencyContainer( + root.childElement(DEPENDENCY_MANAGEMENT) + .flatMap(dm -> dm.childElement(DEPENDENCIES)) + .orElse(null), + DEPENDENCY_MANAGEMENT)) + .filter(container -> container.element != null), + root.childElement(PROFILES).stream() + .flatMap(profiles -> profiles.childElements(PROFILE)) + .flatMap(profile -> Stream.of( + new DependencyContainer( + profile.childElement(DEPENDENCIES) + .orElse(null), + "profile dependencies"), + new DependencyContainer( + profile.childElement(DEPENDENCY_MANAGEMENT) + .flatMap(dm -> dm.childElement(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 allDefinedProperties, + Document pomDocument, + UpgradeContext context, + String sectionName) { + boolean fixed = false; + List dependencies = + dependenciesElement.childElements(DEPENDENCY).toList(); + Editor editor = new Editor(pomDocument); + + for (Element dependency : dependencies) { + Set 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 findUndefinedProperties(Element dependency, Set allDefinedProperties) { + Set 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 allDefinedProperties, Set 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 6144f4ca3265..15ea0630168e 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 @@ -478,6 +478,176 @@ void shouldNotModifyUrlsWithoutDeprecatedExpressions() 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 = """ + + + 4.0.0 + test + test + 1.0.0 + + + + com.google.guava + guava + ${guava-version} + + + + + """; + + Document document = Document.of(pomXml); + Map 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.childElements("dependency").count(), "Should have no dependency elements"); + } + + @Test + @DisplayName("should not comment out dependency with defined property") + void shouldNotCommentOutDependencyWithDefinedProperty() throws Exception { + String pomXml = """ + + + 4.0.0 + test + test + 1.0.0 + + 30.0-jre + + + + + com.google.guava + guava + ${guava-version} + + + + + """; + + Document document = Document.of(pomXml); + Map 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.childElements("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 = """ + + + 4.0.0 + test + test + 1.0.0 + + + test + test-dep + ${project.version} + + + + """; + + Document document = Document.of(pomXml); + Map 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.childElements("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 = """ + + + 4.0.0 + test + parent + 1.0.0 + + 30.0-jre + + + """; + + String childPom = """ + + + 4.0.0 + + test + parent + 1.0.0 + + child + + + com.google.guava + guava + ${guava-version} + + + + """; + + Document parentDoc = Document.of(parentPom); + Document childDoc = Document.of(childPom); + Map 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.childElements("dependency").count(), + "Dependency should not be commented out when property is defined in another POM"); + } + } + @Nested @DisplayName("Strategy Description") class StrategyDescriptionTests {