Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import java.util.Collection;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeMirror;

import org.mapstruct.ap.internal.util.TypeUtils;
Expand Down Expand Up @@ -112,6 +114,8 @@ else if ( typeUtils.isSameType( builder.getTypeMirror(), builderCreationOwner )
builder = owner;
}

builder = resolveTypeParameters( builder, typeToBuild, builderInfo.getBuildMethods(), typeFactory, typeUtils );

return new BuilderType(
builder,
owner,
Expand All @@ -120,4 +124,40 @@ else if ( typeUtils.isSameType( builder.getTypeMirror(), builderCreationOwner )
builderInfo.getBuildMethods()
);
}

/**
* Resolves the type parameters of a generic builder against the type that needs to be built.
* <p>
* For example, when building {@code Record<String>} with a {@code RecordBuilder<T>} whose build method
* returns {@code Record<T>}, the resolved builder type is {@code RecordBuilder<String>}.
*
* @param builder the builder type
* @param typeToBuild the type that needs to be built
* @param buildMethods the build methods of the builder
* @param typeFactory the type factory
* @param typeUtils the type utils
*
* @return the builder with its type parameters resolved, or the given builder if they cannot be resolved
*/
private static Type resolveTypeParameters(Type builder, Type typeToBuild,
Collection<ExecutableElement> buildMethods, TypeFactory typeFactory, TypeUtils typeUtils) {
if ( builder.getTypeParameters().isEmpty() || typeToBuild.getTypeParameters().isEmpty()
|| !( builder.getTypeMirror() instanceof DeclaredType ) ) {
return builder;
}

for ( ExecutableElement buildMethod : buildMethods ) {
TypeMirror buildMethodType = typeUtils.asMemberOf( (DeclaredType) builder.getTypeMirror(), buildMethod );
if ( !( buildMethodType instanceof ExecutableType ) ) {
continue;
}
TypeMirror builtType = ( (ExecutableType) buildMethodType ).getReturnType();
TypeMirror erasedTypeToBuild = typeUtils.erasure( typeToBuild.getTypeMirror() );
if ( typeUtils.isSameType( typeUtils.erasure( builtType ), erasedTypeToBuild ) ) {
return builder.resolveGenericTypeParameters( typeToBuild, typeFactory.getType( builtType ) );
}
}

return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,11 @@ protected boolean isBuildMethod(ExecutableElement buildMethod, DeclaredType buil
}
TypeMirror buildMethodType = typeUtils.asMemberOf( builderType, buildMethod );
if ( buildMethodType instanceof ExecutableType ) {
return typeUtils.isAssignable( ( (ExecutableType) buildMethodType ).getReturnType(), typeElement.asType() );
// Compare erasures, the type variables of a generic builder are not the ones of the type being built
return typeUtils.isAssignable(
typeUtils.erasure( ( (ExecutableType) buildMethodType ).getReturnType() ),
typeUtils.erasure( typeElement.asType() )
);
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.bugs._2932;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface Issue2932Mapper {

Issue2932Mapper INSTANCE = Mappers.getMapper( Issue2932Mapper.class );

Record<String> map(RecordDto source);

Pair<String, Integer> map(PairDto source);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.bugs._2932;

import org.mapstruct.ap.testutil.IssueKey;
import org.mapstruct.ap.testutil.ProcessorTest;
import org.mapstruct.ap.testutil.WithClasses;

import static org.assertj.core.api.Assertions.assertThat;

/**
* @author Seonwoo Jung
*/
@IssueKey("2932")
@WithClasses({
Issue2932Mapper.class,
Pair.class,
PairDto.class,
Record.class,
RecordDto.class
})
class Issue2932Test {

@ProcessorTest
void shouldUseBuilderOfGenericTarget() {
Record<String> target = Issue2932Mapper.INSTANCE.map( new RecordDto( "answer", "42" ) );

assertThat( target.getName() ).isEqualTo( "answer" );
assertThat( target.getValue() ).isEqualTo( "42" );
}

@ProcessorTest
void shouldUseConstructorCreatedBuilderOfGenericTarget() {
Pair<String, Integer> target = Issue2932Mapper.INSTANCE.map( new PairDto( "answer", 42 ) );

assertThat( target.getKey() ).isEqualTo( "answer" );
assertThat( target.getValue() ).isEqualTo( 42 );
}
}
51 changes: 51 additions & 0 deletions processor/src/test/java/org/mapstruct/ap/test/bugs/_2932/Pair.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.bugs._2932;

/**
* A generic type with a builder that is created through its public constructor.
*/
public class Pair<K, V> {

private final K key;
private final V value;

private Pair(K key, V value) {
this.key = key;
this.value = value;
}

public K getKey() {
return key;
}

public V getValue() {
return value;
}

public static class PairBuilder<K, V> {

private K key;
private V value;

public PairBuilder() {
}

public PairBuilder<K, V> key(K key) {
this.key = key;
return this;
}

public PairBuilder<K, V> value(V value) {
this.value = value;
return this;
}

public Pair<K, V> build() {
return new Pair<>( key, value );
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.bugs._2932;

public class PairDto {

private final String key;
private final int value;

public PairDto(String key, int value) {
this.key = key;
this.value = value;
}

public String getKey() {
return key;
}

public int getValue() {
return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.bugs._2932;

/**
* Mirrors the shape Lombok generates for {@code @Builder} on a generic class.
*/
public class Record<T> {

private final String name;
private final T value;

private Record(String name, T value) {
this.name = name;
this.value = value;
}

public String getName() {
return name;
}

public T getValue() {
return value;
}

public static <T> RecordBuilder<T> builder() {
return new RecordBuilder<>();
}

public static class RecordBuilder<T> {

private String name;
private T value;

RecordBuilder() {
}

public RecordBuilder<T> name(String name) {
this.name = name;
return this;
}

public RecordBuilder<T> value(T value) {
this.value = value;
return this;
}

public Record<T> build() {
return new Record<>( name, value );
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.bugs._2932;

public class RecordDto {

private final String name;
private final String value;

public RecordDto(String name, String value) {
this.name = name;
this.value = value;
}

public String getName() {
return name;
}

public String getValue() {
return value;
}
}
Loading