#3054: Allow abstract return type when all directly sealed subtypes are covered by subclass mappings

Co-authored-by: Ben Zegveld <Ben.Zegveld@gmail.com>
This commit is contained in:
Zegveld 2023-05-01 11:54:24 +02:00 committed by GitHub
parent be94569791
commit bc5a877121
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 537 additions and 2 deletions

View File

@ -112,6 +112,11 @@ public class MavenIntegrationTest {
void protobufBuilderTest() {
}
@ProcessorTest(baseDir = "sealedSubclassTest")
@EnabledForJreRange(min = JRE.JAVA_17)
void sealedSubclassTest() {
}
@ProcessorTest(baseDir = "recordsTest", processorTypes = {
ProcessorTest.ProcessorType.JAVAC
})

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright MapStruct Authors.
Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-it-parent</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>sealedSubclassTest</artifactId>
<packaging>jar</packaging>
<profiles>
<profile>
<id>generate-via-compiler-plugin</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgument
combine.self="override"></compilerArgument>
<compilerId>\${compiler-id}</compilerId>
<compilerArgs>--enable-preview</compilerArgs>
</configuration>
<dependencies>
<dependency>
<groupId>org.eclipse.tycho</groupId>
<artifactId>tycho-compiler-jdt</artifactId>
<version>${org.eclipse.tycho.compiler-jdt.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</profile>
<profile>
<id>debug-forked-javac</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<fork>true</fork>
<compilerArgs>
<arg>--enable-preview</arg>
<arg>-J-Xdebug</arg>
<arg>-J-Xnoagent</arg>
<arg>-J-Djava.compiler=NONE</arg>
<arg>-J-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000</arg>
</compilerArgs>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkCount>1</forkCount>
<argLine>--enable-preview</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,18 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
public final class Bike extends Vehicle {
private int numberOfGears;
public int getNumberOfGears() {
return numberOfGears;
}
public void setNumberOfGears(int numberOfGears) {
this.numberOfGears = numberOfGears;
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
public final class BikeDto extends VehicleDto {
private int numberOfGears;
public int getNumberOfGears() {
return numberOfGears;
}
public void setNumberOfGears(int numberOfGears) {
this.numberOfGears = numberOfGears;
}
}

View File

@ -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.itest.sealedsubclass;
public final class Car extends Vehicle {
private boolean manual;
public boolean isManual() {
return manual;
}
public void setManual(boolean manual) {
this.manual = manual;
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
public final class CarDto extends VehicleDto {
private boolean manual;
public boolean isManual() {
return manual;
}
public void setManual(boolean manual) {
this.manual = manual;
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
public final class Davidson extends Motor {
private int numberOfExhausts;
public int getNumberOfExhausts() {
return numberOfExhausts;
}
public void setNumberOfExhausts(int numberOfExhausts) {
this.numberOfExhausts = numberOfExhausts;
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
public final class DavidsonDto extends MotorDto {
private int numberOfExhausts;
public int getNumberOfExhausts() {
return numberOfExhausts;
}
public void setNumberOfExhausts(int numberOfExhausts) {
this.numberOfExhausts = numberOfExhausts;
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
public final class Harley extends Motor {
private int engineDb;
public int getEngineDb() {
return engineDb;
}
public void setEngineDb(int engineDb) {
this.engineDb = engineDb;
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
public final class HarleyDto extends MotorDto {
private int engineDb;
public int getEngineDb() {
return engineDb;
}
public void setEngineDb(int engineDb) {
this.engineDb = engineDb;
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
public sealed abstract class Motor extends Vehicle permits Harley, Davidson {
private int cc;
public int getCc() {
return cc;
}
public void setCc(int cc) {
this.cc = cc;
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
public sealed abstract class MotorDto extends VehicleDto permits HarleyDto, DavidsonDto {
private int cc;
public int getCc() {
return cc;
}
public void setCc(int cc) {
this.cc = cc;
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.SubclassMapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface SealedSubclassMapper {
SealedSubclassMapper INSTANCE = Mappers.getMapper( SealedSubclassMapper.class );
VehicleCollectionDto map(VehicleCollection vehicles);
@SubclassMapping( source = Car.class, target = CarDto.class )
@SubclassMapping( source = Bike.class, target = BikeDto.class )
@SubclassMapping( source = Harley.class, target = HarleyDto.class )
@SubclassMapping( source = Davidson.class, target = DavidsonDto.class )
@Mapping( source = "vehicleManufacturingCompany", target = "maker")
VehicleDto map(Vehicle vehicle);
VehicleCollection mapInverse(VehicleCollectionDto vehicles);
@InheritInverseConfiguration
Vehicle mapInverse(VehicleDto dto);
}

View File

@ -0,0 +1,27 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
public abstract sealed class Vehicle permits Bike, Car, Motor {
private String name;
private String vehicleManufacturingCompany;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getVehicleManufacturingCompany() {
return vehicleManufacturingCompany;
}
public void setVehicleManufacturingCompany(String vehicleManufacturingCompany) {
this.vehicleManufacturingCompany = vehicleManufacturingCompany;
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
import java.util.ArrayList;
import java.util.Collection;
public class VehicleCollection {
private Collection<Vehicle> vehicles = new ArrayList<>();
public Collection<Vehicle> getVehicles() {
return vehicles;
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
import java.util.ArrayList;
import java.util.Collection;
public class VehicleCollectionDto {
private Collection<VehicleDto> vehicles = new ArrayList<>();
public Collection<VehicleDto> getVehicles() {
return vehicles;
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
public abstract sealed class VehicleDto permits CarDto, BikeDto, MotorDto {
private String name;
private String maker;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMaker() {
return maker;
}
public void setMaker(String maker) {
this.maker = maker;
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.itest.sealedsubclass;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.Test;
public class SealedSubclassTest {
@Test
public void mappingIsDoneUsingSubclassMapping() {
VehicleCollection vehicles = new VehicleCollection();
vehicles.getVehicles().add( new Car() );
vehicles.getVehicles().add( new Bike() );
vehicles.getVehicles().add( new Harley() );
vehicles.getVehicles().add( new Davidson() );
VehicleCollectionDto result = SealedSubclassMapper.INSTANCE.map( vehicles );
assertThat( result.getVehicles() ).doesNotContainNull();
assertThat( result.getVehicles() ) // remove generic so that test works.
.extracting( vehicle -> (Class) vehicle.getClass() )
.containsExactly( CarDto.class, BikeDto.class, HarleyDto.class, DavidsonDto.class );
}
@Test
public void inverseMappingIsDoneUsingSubclassMapping() {
VehicleCollectionDto vehicles = new VehicleCollectionDto();
vehicles.getVehicles().add( new CarDto() );
vehicles.getVehicles().add( new BikeDto() );
vehicles.getVehicles().add( new HarleyDto() );
vehicles.getVehicles().add( new DavidsonDto() );
VehicleCollection result = SealedSubclassMapper.INSTANCE.mapInverse( vehicles );
assertThat( result.getVehicles() ).doesNotContainNull();
assertThat( result.getVehicles() ) // remove generic so that test works.
.extracting( vehicle -> (Class) vehicle.getClass() )
.containsExactly( Car.class, Bike.class, Harley.class, Davidson.class );
}
@Test
public void subclassMappingInheritsInverseMapping() {
VehicleCollectionDto vehiclesDto = new VehicleCollectionDto();
CarDto carDto = new CarDto();
carDto.setMaker( "BenZ" );
vehiclesDto.getVehicles().add( carDto );
VehicleCollection result = SealedSubclassMapper.INSTANCE.mapInverse( vehiclesDto );
assertThat( result.getVehicles() )
.extracting( Vehicle::getVehicleManufacturingCompany )
.containsExactly( "BenZ" );
}
}

View File

@ -446,8 +446,39 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
}
private boolean isAbstractReturnTypeAllowed() {
return method.getOptions().getBeanMapping().getSubclassExhaustiveStrategy().isAbstractReturnTypeAllowed()
&& !method.getOptions().getSubclassMappings().isEmpty();
return !method.getOptions().getSubclassMappings().isEmpty()
&& ( method.getOptions().getBeanMapping().getSubclassExhaustiveStrategy().isAbstractReturnTypeAllowed()
|| isCorrectlySealed() );
}
private boolean isCorrectlySealed() {
Type mappingSourceType = method.getMappingSourceType();
return isCorrectlySealed( mappingSourceType );
}
private boolean isCorrectlySealed(Type mappingSourceType) {
if ( mappingSourceType.isSealed() ) {
List<? extends TypeMirror> unusedPermittedSubclasses =
new ArrayList<>( mappingSourceType.getPermittedSubclasses() );
method.getOptions().getSubclassMappings().forEach( subClassOption -> {
for (Iterator<? extends TypeMirror> iterator = unusedPermittedSubclasses.iterator();
iterator.hasNext(); ) {
if ( ctx.getTypeUtils().isSameType( iterator.next(), subClassOption.getSource() ) ) {
iterator.remove();
}
}
} );
for ( Iterator<? extends TypeMirror> iterator = unusedPermittedSubclasses.iterator();
iterator.hasNext(); ) {
TypeMirror typeMirror = iterator.next();
Type type = ctx.getTypeFactory().getType( typeMirror );
if ( type.isAbstract() && isCorrectlySealed( type ) ) {
iterator.remove();
}
}
return unusedPermittedSubclasses.isEmpty();
}
return false;
}
private void initializeMappingReferencesIfNeeded(Type resultTypeToMap) {

View File

@ -5,6 +5,8 @@
*/
package org.mapstruct.ap.internal.model.common;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
@ -53,6 +55,7 @@ import org.mapstruct.ap.internal.util.accessor.MapValueAccessor;
import org.mapstruct.ap.internal.util.accessor.PresenceCheckAccessor;
import org.mapstruct.ap.internal.util.accessor.ReadAccessor;
import static java.util.Collections.emptyList;
import static org.mapstruct.ap.internal.util.Collections.first;
/**
@ -67,6 +70,18 @@ import static org.mapstruct.ap.internal.util.Collections.first;
* @author Filip Hrisafov
*/
public class Type extends ModelElement implements Comparable<Type> {
private static final Method SEALED_PERMITTED_SUBCLASSES_METHOD;
static {
Method permittedSubclassesMethod;
try {
permittedSubclassesMethod = TypeElement.class.getMethod( "getPermittedSubclasses" );
}
catch ( NoSuchMethodException e ) {
permittedSubclassesMethod = null;
}
SEALED_PERMITTED_SUBCLASSES_METHOD = permittedSubclassesMethod;
}
private final TypeUtils typeUtils;
private final ElementUtils elementUtils;
@ -1661,4 +1676,27 @@ public class Type extends ModelElement implements Comparable<Type> {
return "java.util.EnumSet".equals( getFullyQualifiedName() );
}
/**
* return true if this type is a java 17+ sealed class
*/
public boolean isSealed() {
return typeElement.getModifiers().stream().map( Modifier::name ).anyMatch( "SEALED"::equals );
}
/**
* return the list of permitted TypeMirrors for the java 17+ sealed class
*/
@SuppressWarnings( "unchecked" )
public List<? extends TypeMirror> getPermittedSubclasses() {
if (SEALED_PERMITTED_SUBCLASSES_METHOD == null) {
return emptyList();
}
try {
return (List<? extends TypeMirror>) SEALED_PERMITTED_SUBCLASSES_METHOD.invoke( typeElement );
}
catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
return emptyList();
}
}
}