[MNG-7344] Track dependencyManagement import location in effective Model for MPH-183 (#603)

Co-authored-by: Maarten Mulders <mthmulders@apache.org>
Co-authored-by: Juul Hobert <juul.hobert@infosupport.com>
Co-authored-by: Giovanni van der Schelde <gvdschelde@gmail.com>
Co-authored-by: Guillaume Nodet <gnodet@gmail.com>
This commit is contained in:
Jan-Jelle Kester 2024-08-15 15:24:49 +02:00 committed by GitHub
parent 1ee18d36ce
commit 5b61e95f23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 365 additions and 9 deletions

View File

@ -32,12 +32,14 @@ public class InputLocation implements Serializable, InputLocationTracker {
private final int columnNumber;
private final InputSource source;
private final Map<Object, InputLocation> locations;
private final InputLocation importedFrom;
public InputLocation(InputSource source) {
this.lineNumber = -1;
this.columnNumber = -1;
this.source = source;
this.locations = Collections.singletonMap(0, this);
this.importedFrom = null;
}
public InputLocation(int lineNumber, int columnNumber) {
@ -54,6 +56,7 @@ public class InputLocation implements Serializable, InputLocationTracker {
this.source = source;
this.locations =
selfLocationKey != null ? Collections.singletonMap(selfLocationKey, this) : Collections.emptyMap();
this.importedFrom = null;
}
public InputLocation(int lineNumber, int columnNumber, InputSource source, Map<Object, InputLocation> locations) {
@ -61,6 +64,15 @@ public class InputLocation implements Serializable, InputLocationTracker {
this.columnNumber = columnNumber;
this.source = source;
this.locations = ImmutableCollections.copy(locations);
this.importedFrom = null;
}
public InputLocation(InputLocation original) {
this.lineNumber = original.lineNumber;
this.columnNumber = original.columnNumber;
this.source = original.source;
this.locations = original.locations;
this.importedFrom = original.importedFrom;
}
public int getLineNumber() {
@ -83,6 +95,17 @@ public class InputLocation implements Serializable, InputLocationTracker {
return locations;
}
/**
* Gets the parent InputLocation where this InputLocation may have been imported from.
* Can return {@code null}.
*
* @return InputLocation
* @since 4.0.0
*/
public InputLocation getImportedFrom() {
return importedFrom;
}
/**
* Merges the {@code source} location into the {@code target} location.
*
@ -152,4 +175,26 @@ public class InputLocation implements Serializable, InputLocationTracker {
return new InputLocation(-1, -1, InputSource.merge(source.getSource(), target.getSource()), locations);
} // -- InputLocation merge( InputLocation, InputLocation, java.util.Collection )
/**
* Class StringFormatter.
*
* @version $Revision$ $Date$
*/
public interface StringFormatter {
// -----------/
// - Methods -/
// -----------/
/**
* Method toString.
*/
String toString(InputLocation location);
}
@Override
public String toString() {
return String.format("%s @ %d:%d", source.getLocation(), lineNumber, columnNumber);
}
}

View File

@ -20,4 +20,13 @@ package org.apache.maven.api.model;
public interface InputLocationTracker {
InputLocation getLocation(Object field);
/**
* Gets the parent InputLocation where this InputLocation may have been imported from.
* Can return {@code null}.
*
* @return InputLocation
* @since 4.0.0
*/
InputLocation getImportedFrom();
}

View File

@ -33,17 +33,24 @@ public class InputSource implements Serializable {
private final String modelId;
private final String location;
private final List<InputSource> inputs;
private final InputLocation importedFrom;
public InputSource(String modelId, String location) {
this(modelId, location, null);
}
public InputSource(String modelId, String location, InputLocation importedFrom) {
this.modelId = modelId;
this.location = location;
this.inputs = null;
this.importedFrom = importedFrom;
}
public InputSource(Collection<InputSource> inputs) {
this.modelId = null;
this.location = null;
this.inputs = ImmutableCollections.copy(inputs);
this.importedFrom = null;
}
/**
@ -64,6 +71,17 @@ public class InputSource implements Serializable {
return this.modelId;
}
/**
* Gets the parent InputLocation where this InputLocation may have been imported from.
* Can return {@code null}.
*
* @return InputLocation
* @since 4.0.0
*/
public InputLocation getImportedFrom() {
return importedFrom;
}
@Override
public boolean equals(Object o) {
if (this == o) {

View File

@ -123,6 +123,10 @@ under the License.
<artifactId>hamcrest</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-di</artifactId>

View File

@ -32,6 +32,8 @@ import org.apache.maven.api.di.Singleton;
import org.apache.maven.api.model.Dependency;
import org.apache.maven.api.model.DependencyManagement;
import org.apache.maven.api.model.Exclusion;
import org.apache.maven.api.model.InputLocation;
import org.apache.maven.api.model.InputSource;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.services.BuilderProblem.Severity;
import org.apache.maven.api.services.ModelBuilderRequest;
@ -81,6 +83,10 @@ public class DefaultDependencyManagementImporter implements DependencyManagement
+ toString(present) + ". Add the conflicting managed dependency directly "
+ "to the dependencyManagement section of the POM.");
}
if (present == null && request.isLocationTracking()) {
Dependency updatedDependency = updateWithImportedFrom(dependency, source);
dependencies.put(key, updatedDependency);
}
}
}
@ -144,4 +150,43 @@ public class DefaultDependencyManagementImporter implements DependencyManagement
return Objects.equals(e1.getGroupId(), e2.getGroupId())
&& Objects.equals(e1.getArtifactId(), e2.getArtifactId());
}
static Dependency updateWithImportedFrom(Dependency dependency, DependencyManagement bom) {
// We are only interested in the InputSource, so the location of the <dependency> element is sufficient
InputLocation dependencyLocation = dependency.getLocation("");
InputLocation bomLocation = bom.getLocation("");
if (dependencyLocation == null || bomLocation == null) {
return dependency;
}
InputSource dependencySource = dependencyLocation.getSource();
InputSource bomSource = bomLocation.getSource();
// If the dependency and BOM have the same source, it means we found the root where the dependency is declared.
if (dependencySource == null
|| bomSource == null
|| Objects.equals(dependencySource.getModelId(), bomSource.getModelId())) {
return Dependency.newBuilder(dependency, true)
.importedFrom(bomLocation)
.build();
}
while (dependencySource.getImportedFrom() != null) {
InputLocation importedFrom = dependencySource.getImportedFrom();
// Stop if the BOM is already in the list, no update necessary
if (Objects.equals(importedFrom.getSource().getModelId(), bomSource.getModelId())) {
return dependency;
}
dependencySource = importedFrom.getSource();
}
// We modify the input location that is used for the whole file.
// This is likely correct because the POM hierarchy applies to the whole POM, not just one dependency.
return Dependency.newBuilder(dependency, true)
.importedFrom(new InputLocation(bomLocation))
.build();
}
}

View File

@ -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.maven.internal.impl.model;
import org.apache.maven.api.model.Dependency;
import org.apache.maven.api.model.DependencyManagement;
import org.apache.maven.api.model.InputLocation;
import org.apache.maven.api.model.InputSource;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class DefaultDependencyManagementImporterTest {
@Test
void testUpdateWithImportedFrom_dependencyLocationAndBomLocationAreNull_dependencyReturned() {
final Dependency dependency = Dependency.newBuilder().build();
final DependencyManagement depMgmt = DependencyManagement.newBuilder().build();
final Dependency result = DefaultDependencyManagementImporter.updateWithImportedFrom(dependency, depMgmt);
assertThat(dependency).isEqualTo(result);
}
@Test
void testUpdateWithImportedFrom_dependencyManagementAndDependencyHaveSameSource_dependencyImportedFromSameSource() {
final InputSource source = new InputSource("SINGLE_SOURCE", "");
final Dependency dependency = Dependency.newBuilder()
.location("", new InputLocation(1, 1, source))
.build();
final DependencyManagement bom = DependencyManagement.newBuilder()
.location("", new InputLocation(1, 1, source))
.build();
final Dependency result = DefaultDependencyManagementImporter.updateWithImportedFrom(dependency, bom);
assertThat(result).isNotNull();
assertThat(result.getImportedFrom().toString())
.isEqualTo(bom.getLocation("").toString());
}
@Test
public void testUpdateWithImportedFrom_singleLevel_importedFromSet() {
// Arrange
final InputSource dependencySource = new InputSource("DEPENDENCY", "DEPENDENCY");
final InputSource bomSource = new InputSource("BOM", "BOM");
final Dependency dependency = Dependency.newBuilder()
.location("", new InputLocation(1, 1, dependencySource))
.build();
final DependencyManagement bom = DependencyManagement.newBuilder()
.location("", new InputLocation(2, 2, bomSource))
.build();
// Act
final Dependency result = DefaultDependencyManagementImporter.updateWithImportedFrom(dependency, bom);
// Assert
assertThat(result).isNotNull();
assertThat(result.getImportedFrom().toString())
.isEqualTo(bom.getLocation("").toString());
}
@Test
public void testUpdateWithImportedFrom_multiLevel_importedFromSetChanged() {
// Arrange
final InputSource bomSource = new InputSource("BOM", "BOM");
final InputSource intermediateSource =
new InputSource("INTERMEDIATE", "INTERMEDIATE", new InputLocation(bomSource));
final InputSource dependencySource =
new InputSource("DEPENDENCY", "DEPENDENCY", new InputLocation(intermediateSource));
final InputLocation bomLocation = new InputLocation(2, 2, bomSource);
final Dependency dependency = Dependency.newBuilder()
.location("", new InputLocation(1, 1, dependencySource))
.importedFrom(bomLocation)
.build();
final DependencyManagement bom =
DependencyManagement.newBuilder().location("", bomLocation).build();
// Act
final Dependency result = DefaultDependencyManagementImporter.updateWithImportedFrom(dependency, bom);
// Assert
assertThat(result.getImportedFrom().toString())
.isEqualTo(bom.getLocation("").toString());
}
@Test
public void testUpdateWithImportedFrom_multiLevelAlreadyFoundInDifferentSource_importedFromSetMaintained() {
// Arrange
final InputSource bomSource = new InputSource("BOM", "BOM");
final InputSource intermediateSource =
new InputSource("INTERMEDIATE", "INTERMEDIATE", new InputLocation(bomSource));
final InputSource dependencySource =
new InputSource("DEPENDENCY", "DEPENDENCY", new InputLocation(intermediateSource));
final Dependency dependency = Dependency.newBuilder()
.location("", new InputLocation(1, 1, dependencySource))
.build();
final DependencyManagement differentSource = DependencyManagement.newBuilder()
.location("", new InputLocation(2, 2, new InputSource("BOM2", "BOM2")))
.build();
// Act
final Dependency result =
DefaultDependencyManagementImporter.updateWithImportedFrom(dependency, differentSource);
// Assert
assertThat(result.getImportedFrom().toString())
.isEqualTo(differentSource.getLocation("").toString());
}
}

View File

@ -1722,7 +1722,8 @@ public class DefaultModelBuilder implements ModelBuilder {
importIds.add(importing);
List<org.apache.maven.api.model.DependencyManagement> importMgmts = null;
// Model v4
List<org.apache.maven.api.model.DependencyManagement> importMgmts = new ArrayList<>();
for (Iterator<Dependency> it = depMgmt.getDependencies().iterator(); it.hasNext(); ) {
Dependency dependency = it.next();
@ -1734,13 +1735,19 @@ public class DefaultModelBuilder implements ModelBuilder {
it.remove();
// Model v3
DependencyManagement importMgmt = loadDependencyManagement(model, request, problems, dependency, importIds);
if (importMgmt != null) {
if (importMgmts == null) {
importMgmts = new ArrayList<>();
if (importMgmt == null) {
continue;
}
if (request.isLocationTracking()) {
// Keep track of why this DependencyManagement was imported.
// And map model v3 to model v4 -> importMgmt(v3).getDelegate() returns a v4 object
importMgmts.add(
org.apache.maven.api.model.DependencyManagement.newBuilder(importMgmt.getDelegate(), true)
.build());
} else {
importMgmts.add(importMgmt.getDelegate());
}
}

View File

@ -59,6 +59,11 @@ public final class InputLocation implements java.io.Serializable, Cloneable, Inp
*/
private InputLocation location;
/**
* Field importedFrom.
*/
private InputLocation importedFrom;
// ----------------/
// - Constructors -/
// ----------------/
@ -73,6 +78,7 @@ public final class InputLocation implements java.io.Serializable, Cloneable, Inp
.collect(Collectors.toMap(
e -> e.getKey(),
e -> e.getValue() == location ? this : new InputLocation(e.getValue())));
this.importedFrom = location.getImportedFrom() != null ? new InputLocation(location.getImportedFrom()) : null;
}
public InputLocation(int lineNumber, int columnNumber) {
@ -217,6 +223,26 @@ public final class InputLocation implements java.io.Serializable, Cloneable, Inp
return this.source;
} // -- InputSource getSource()
/**
* Gets the parent InputLocation where this InputLocation may have been imported from.
* Can return {@code null}.
*
* @return InputLocation
* @since 4.0.0
*/
public InputLocation getImportedFrom() {
return importedFrom;
}
/**
* Set the imported from location.
*
* @param importedFrom
*/
public void setImportedFrom(InputLocation importedFrom) {
this.importedFrom = importedFrom;
}
/**
* Method merge.
*

View File

@ -50,6 +50,14 @@ public class InputSource implements java.io.Serializable, Cloneable {
*/
private String location;
/**
*
*
* The location of the POM from which this POM was
* imported from or {@code null} if unknown.
*/
private InputLocation importedFrom;
// ----------------/
// - Constructors -/
// ----------------/
@ -59,6 +67,7 @@ public class InputSource implements java.io.Serializable, Cloneable {
public InputSource(org.apache.maven.api.model.InputSource source) {
this.modelId = source.getModelId();
this.location = source.getLocation();
this.importedFrom = source.getImportedFrom() != null ? new InputLocation(source.getImportedFrom()) : null;
}
// -----------/
@ -119,6 +128,24 @@ public class InputSource implements java.io.Serializable, Cloneable {
this.modelId = modelId;
} // -- void setModelId( String )
/**
* Get the location of the POM from which this POM was
*
* @return
*/
public InputLocation getImportedFrom() {
return importedFrom;
}
/**
* Set the location of the POM from which this POM was imported from.
*
* @param importedFrom
*/
public void setImportedFrom(InputLocation importedFrom) {
this.importedFrom = importedFrom;
}
@Override
public String toString() {
return getModelId() + " " + getLocation();

View File

@ -161,6 +161,7 @@ under the License.
<maven.site.path>ref/4-LATEST</maven.site.path>
<project.build.outputTimestamp>2024-05-22T14:07:09Z</project.build.outputTimestamp>
<!-- various versions -->
<assertjVersion>3.26.0</assertjVersion>
<asmVersion>9.7</asmVersion>
<byteBuddyVersion>1.14.18</byteBuddyVersion>
<cipherVersion>2.0</cipherVersion>
@ -606,6 +607,11 @@ under the License.
<version>${hamcrestVersion}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertjVersion}</version>
</dependency>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-testing</artifactId>

View File

@ -52,6 +52,7 @@
#set ( $dummy = $imports.add( "java.util.HashMap" ) )
#set ( $dummy = $imports.add( "java.util.List" ) )
#set ( $dummy = $imports.add( "java.util.Map" ) )
#set ( $dummy = $imports.add( "java.util.Set" ) )
#set ( $dummy = $imports.add( "java.util.Objects" ) )
#set ( $dummy = $imports.add( "java.util.stream.Collectors" ) )
#set ( $dummy = $imports.add( "java.util.stream.Stream" ) )
@ -282,6 +283,20 @@ public class ${class.name}
.location(key, location.toApiLocation()).build());
}
public InputLocation getImportedFrom() {
${packageModelV4}.InputLocation loc = getDelegate().getImportedFrom();
return loc != null ? new InputLocation(loc) : null;
}
public void setImportedFrom(InputLocation location) {
update(${packageModelV4}.${class.name}.newBuilder(getDelegate(), true)
.importedFrom(location.toApiLocation()).build());
}
public Set<Object> getLocationKeys() {
return getDelegate().getLocationKeys();
}
#end
protected boolean replace(Object oldDelegate, Object newDelegate) {
if (super.replace(oldDelegate, newDelegate)) {

View File

@ -49,6 +49,7 @@
#set ( $dummy = $imports.add( "java.util.Collections" ) )
#set ( $dummy = $imports.add( "java.util.HashMap" ) )
#set ( $dummy = $imports.add( "java.util.Map" ) )
#set ( $dummy = $imports.add( "java.util.Set" ) )
#set ( $dummy = $imports.add( "org.apache.maven.api.annotations.Experimental" ) )
#set ( $dummy = $imports.add( "org.apache.maven.api.annotations.Generated" ) )
#set ( $dummy = $imports.add( "org.apache.maven.api.annotations.Immutable" ) )
@ -138,6 +139,8 @@ public class ${class.name}
#if ( ! $class.superClass )
/** Locations */
final Map<Object, InputLocation> locations;
/** Location tracking */
final InputLocation importedFrom;
#end
#end
@ -159,7 +162,8 @@ public class ${class.name}
$type $field.name${sep}
#end
#if ( $locationTracking )
Map<Object, InputLocation> locations
Map<Object, InputLocation> locations,
InputLocation importedFrom
#end
) {
#if ( $class.superClass )
@ -169,7 +173,8 @@ public class ${class.name}
${field.name}${sep}
#end
#if ( $locationTracking )
locations
locations,
importedFrom
#end
);
#end
@ -187,6 +192,7 @@ public class ${class.name}
#if ( $locationTracking )
#if ( ! $class.superClass )
this.locations = ImmutableCollections.copy(locations);
this.importedFrom = importedFrom;
#end
#end
}
@ -252,6 +258,21 @@ public class ${class.name}
return locations != null ? locations.get(key) : null;
}
/**
* Gets the keys of the locations of the input source.
*/
public Set<Object> getLocationKeys() {
return locations != null ? locations.keySet() : null;
}
/**
* Gets the input location that caused this model to be read.
*/
public InputLocation getImportedFrom()
{
return importedFrom;
}
#end
/**
* Creates a new builder with this object as the basis.
@ -382,6 +403,7 @@ public class ${class.name}
#end
#if ( ! $class.superClass && $locationTracking )
Map<Object, InputLocation> locations;
InputLocation importedFrom;
#end
Builder(boolean withDefaults) {
@ -416,6 +438,7 @@ public class ${class.name}
#end
#if ( $locationTracking )
this.locations = base.locations;
this.importedFrom = base.importedFrom;
#end
} else {
this.base = base;
@ -461,6 +484,12 @@ public class ${class.name}
return this;
}
@Nonnull
public Builder importedFrom(InputLocation importedFrom) {
this.importedFrom = importedFrom;
return this;
}
#end
@Nonnull
public ${class.name} build() {
@ -494,7 +523,8 @@ public class ${class.name}
#end
#end
#if ( $locationTracking )
locations
locations,
importedFrom
#end
);
}