From f9e342b99d69ae967f05a36c02b3bcb397eba384 Mon Sep 17 00:00:00 2001 From: "s.zebzeev" Date: Mon, 20 Jul 2026 00:42:53 +0500 Subject: [PATCH] add generics support --- build.gradle | 2 +- .../references/MapstructBaseReference.java | 5 + .../references/MapstructSourceReference.java | 38 +++--- .../references/MapstructTargetReference.java | 35 +++--- .../MapstructBaseCompletionTestCase.java | 9 +- .../intellij/MapstructCompletionTestCase.java | 109 ++++++++++++++++++ ...cCarWrapperSourceAutoCompleteAfterCar.java | 56 +++++++++ ...ompleteAfterCarWithoutParameterPrefix.java | 45 ++++++++ ...cCarWrapperTargetAutoCompleteAfterCar.java | 56 +++++++++ .../mapping/GenericConstructorCarMapper.java | 39 +++++++ ...cConstructorMappingTargetUpdateMapper.java | 46 ++++++++ .../GenericNestedGenericTargetMapper.java | 68 +++++++++++ .../GenericRecordConstructorTargetMapper.java | 31 +++++ ...cTwoTypeParamsConstructorTargetMapper.java | 84 ++++++++++++++ 14 files changed, 593 insertions(+), 30 deletions(-) create mode 100644 testData/mapping/GenericCarWrapperSourceAutoCompleteAfterCar.java create mode 100644 testData/mapping/GenericCarWrapperSourceAutoCompleteAfterCarWithoutParameterPrefix.java create mode 100644 testData/mapping/GenericCarWrapperTargetAutoCompleteAfterCar.java create mode 100644 testData/mapping/GenericConstructorCarMapper.java create mode 100644 testData/mapping/GenericConstructorMappingTargetUpdateMapper.java create mode 100644 testData/mapping/GenericNestedGenericTargetMapper.java create mode 100644 testData/mapping/GenericRecordConstructorTargetMapper.java create mode 100644 testData/mapping/GenericTwoTypeParamsConstructorTargetMapper.java diff --git a/build.gradle b/build.gradle index 6c8f86ad..31022996 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,7 @@ licenseFormat.dependsOn licenseFormatForKotlin licenseTest.dependsOn licenseTestData checkstyle { - toolVersion = '8.36.1' + toolVersion = '10.12.0' config = resources.text.fromUri("https://raw.githubusercontent.com/mapstruct/mapstruct/master/build-config/src/main/resources/build-config/checkstyle.xml") configProperties = [ 'checkstyle.cache.file': layout.buildDirectory.get().asFile.toPath( ).resolve( 'checkstyle-cachefile').toString(), diff --git a/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructBaseReference.java b/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructBaseReference.java index 83ccc5ac..4c90c1c0 100644 --- a/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructBaseReference.java +++ b/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructBaseReference.java @@ -60,6 +60,11 @@ public String getValue() { return super.getValue(); } + @Nullable + public MapstructBaseReference getPrevious() { + return this.previous; + } + @Nullable @Override public final PsiElement resolve() { diff --git a/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructSourceReference.java b/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructSourceReference.java index b9b2387d..66d314ac 100644 --- a/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructSourceReference.java +++ b/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructSourceReference.java @@ -6,6 +6,7 @@ package org.mapstruct.intellij.codeinsight.references; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; import com.intellij.codeInsight.lookup.LookupElement; @@ -23,6 +24,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.mapstruct.intellij.util.MapstructUtil; +import org.mapstruct.intellij.util.SourceUtils; import static org.mapstruct.intellij.util.MapstructUtil.asLookup; import static org.mapstruct.intellij.util.MapstructUtil.findRecordComponent; @@ -133,21 +135,31 @@ Object[] getVariantsInternal(@NotNull PsiMethod mappingMethod) { @Override PsiType resolvedType() { PsiElement element = resolve(); - - if ( element instanceof PsiMethod psiMethod ) { - return psiMethod.getReturnType(); - } - else if ( element instanceof PsiParameter psiParameter ) { - return psiParameter.getType(); - } - else if ( element instanceof PsiRecordComponent psiRecordComponent ) { - return psiRecordComponent.getType(); - } - else if ( element instanceof PsiField psiField ) { - return psiField.getType(); + PsiType elementType = switch ( element ) { + case PsiMethod psiMethod -> psiMethod.getReturnType(); + case PsiParameter psiParameter -> psiParameter.getType(); + case PsiRecordComponent psiRecordComponent -> psiRecordComponent.getType(); + case PsiField psiField -> psiField.getType(); + case null, default -> null; + }; + + if ( elementType == null ) { + return null; } - return null; + PsiType contextType = Optional.ofNullable( getPrevious() ) + .map( MapstructBaseReference::resolvedType ) + .or( () -> Optional.ofNullable( this.getMappingMethod() ) + .map( MapstructUtil::getSourceParameters ) + .filter( params -> params.length == 1 ) + .map( psiParameters -> psiParameters[0] ) + .map( SourceUtils::getParameterType ) + ) + .orElse( null ); + + return PsiUtil.resolveGenericsClassInType( contextType ) + .getSubstitutor() + .substitute( elementType ); } /** diff --git a/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructTargetReference.java b/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructTargetReference.java index 10cdba5e..77bdc9e3 100644 --- a/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructTargetReference.java +++ b/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructTargetReference.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; import com.intellij.codeInsight.AnnotationUtil; @@ -27,7 +28,6 @@ import com.intellij.psi.util.PsiUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.mapstruct.Mapping; import org.mapstruct.intellij.util.MapStructVersion; import org.mapstruct.intellij.util.MapstructUtil; import org.mapstruct.intellij.util.TargetType; @@ -203,21 +203,28 @@ Object[] getVariantsInternal(@NotNull PsiMethod mappingMethod) { @Override PsiType resolvedType() { PsiElement element = resolve(); - - if ( element instanceof PsiMethod psiMethod ) { - return firstParameterPsiType( psiMethod ); - } - else if ( element instanceof PsiParameter psiParameter ) { - return psiParameter.getType(); - } - else if ( element instanceof PsiRecordComponent psiRecordComponent ) { - return psiRecordComponent.getType(); - } - else if ( element instanceof PsiField psiField ) { - return psiField.getType(); + PsiType elementType = switch ( element ) { + case PsiMethod psiMethod -> firstParameterPsiType( psiMethod ); + case PsiParameter psiParameter -> psiParameter.getType(); + case PsiRecordComponent psiRecordComponent -> psiRecordComponent.getType(); + case PsiField psiField -> psiField.getType(); + case null, default -> null; + }; + + if ( elementType == null ) { + return null; } - return null; + PsiType contextType = Optional.ofNullable( getPrevious() ) + .map( MapstructBaseReference::resolvedType ) + .or( () -> Optional.ofNullable( getMappingMethod() ) + .map( TargetUtils::getRelevantType ) + ) + .orElse( null ); + + return PsiUtil.resolveGenericsClassInType( contextType ) + .getSubstitutor() + .substitute( elementType ); } /** diff --git a/src/test/java/org/mapstruct/intellij/MapstructBaseCompletionTestCase.java b/src/test/java/org/mapstruct/intellij/MapstructBaseCompletionTestCase.java index 1a48ff9b..54fb0528 100644 --- a/src/test/java/org/mapstruct/intellij/MapstructBaseCompletionTestCase.java +++ b/src/test/java/org/mapstruct/intellij/MapstructBaseCompletionTestCase.java @@ -9,7 +9,6 @@ import com.intellij.codeInsight.completion.LightFixtureCompletionTestCase; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess; import com.intellij.testFramework.LightProjectDescriptor; import com.intellij.testFramework.PsiTestUtil; import com.intellij.util.PathUtil; @@ -29,7 +28,13 @@ protected void setUp() throws Exception { super.setUp(); final String mapstructLibPath = PathUtil.toSystemIndependentName( new File( BUILD_LIBS_DIRECTORY ) .getAbsolutePath() ); - VfsRootAccess.allowRootAccess( getTestRootDisposable(), mapstructLibPath ); + + allowAccessToDirsIfExists( + BUILD_LIBS_DIRECTORY, + "testData", + "build/test-libs" + ); + PsiTestUtil.addLibrary( myFixture.getProjectDisposable(), myFixture.getModule(), diff --git a/src/test/java/org/mapstruct/intellij/MapstructCompletionTestCase.java b/src/test/java/org/mapstruct/intellij/MapstructCompletionTestCase.java index 093b70db..d4844700 100644 --- a/src/test/java/org/mapstruct/intellij/MapstructCompletionTestCase.java +++ b/src/test/java/org/mapstruct/intellij/MapstructCompletionTestCase.java @@ -488,6 +488,115 @@ public void testFluentGenericTargetMapper() { ); } + public void testGenericCarWrapperSourceAutoCompleteAfterCar() { + configureByTestName(); + + assertThat( myItems ) + .extracting( LookupElement::getLookupString ) + .containsExactlyInAnyOrder( + "winCode" + ); + + assertThat( myItems ) + .extracting( LookupElementPresentation::renderElement ) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder( + createVariable( "winCode", "String" ) + ); + } + + public void testGenericCarWrapperTargetAutoCompleteAfterCar() { + configureByTestName(); + + assertThat( myItems ) + .extracting( LookupElement::getLookupString ) + .containsExactlyInAnyOrder( "winCode" ); + + assertThat( myItems ) + .extracting( LookupElementPresentation::renderElement ) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder( createVariable( "winCode", "String" ) ); + } + + public void testGenericCarWrapperSourceAutoCompleteAfterCarWithoutParameterPrefix() { + configureByTestName(); + + assertThat( myItems ) + .extracting( LookupElement::getLookupString ) + .containsExactlyInAnyOrder( "winCode" ); + + assertThat( myItems ) + .extracting( LookupElementPresentation::renderElement ) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder( createVariable( "winCode", "String" ) ); + } + + public void testGenericConstructorCarMapper() { + configureByTestName(); + + assertThat( myItems ) + .extracting( LookupElement::getLookupString ) + .containsExactlyInAnyOrder( "winCode" ); + + assertThat( myItems ) + .extracting( LookupElementPresentation::renderElement ) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder( createVariable( "winCode", "String" ) ); + } + + public void testGenericRecordConstructorTargetMapper() { + configureByTestName(); + + assertThat( myItems ) + .extracting( LookupElement::getLookupString ) + .containsExactlyInAnyOrder( "winCode" ); + + assertThat( myItems ) + .extracting( LookupElementPresentation::renderElement ) + .usingRecursiveFieldByFieldElementComparator() + .usingElementComparatorIgnoringFields( "myIcon", "myTail" ) + .containsExactlyInAnyOrder( createParameter( "winCode", "String" ) ); + } + + public void testGenericConstructorMappingTargetUpdateMapper() { + configureByTestName(); + + assertThat( myItems ) + .extracting( LookupElement::getLookupString ) + .containsExactlyInAnyOrder( "winCode" ); + + assertThat( myItems ) + .extracting( LookupElementPresentation::renderElement ) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder( createVariable( "winCode", "String" ) ); + } + + public void testGenericTwoTypeParamsConstructorTargetMapper() { + configureByTestName(); + + assertThat( myItems ) + .extracting( LookupElement::getLookupString ) + .containsExactlyInAnyOrder( "serial" ); + + assertThat( myItems ) + .extracting( LookupElementPresentation::renderElement ) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder( createVariable( "serial", "String" ) ); + } + + public void testGenericNestedGenericTargetMapper() { + configureByTestName(); + + assertThat( myItems ) + .extracting( LookupElement::getLookupString ) + .containsExactlyInAnyOrder( "winCode" ); + + assertThat( myItems ) + .extracting( LookupElementPresentation::renderElement ) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder( createVariable( "winCode", "String" ) ); + } + public void testVariantsCarMapperNoSourceClass() { myFixture.configureByFile( "CarMapperNoSourceClass.java" ); complete(); diff --git a/testData/mapping/GenericCarWrapperSourceAutoCompleteAfterCar.java b/testData/mapping/GenericCarWrapperSourceAutoCompleteAfterCar.java new file mode 100644 index 00000000..6ddf07ea --- /dev/null +++ b/testData/mapping/GenericCarWrapperSourceAutoCompleteAfterCar.java @@ -0,0 +1,56 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.complex; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper +public interface GenericCarWrapperMapper { + + @Mapping(target = "id", source = "wrapper.car.winCode") + CarEntity toCarDto(CarWrapper wrapper); +} + +class CarEntity { + + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + +} + +class Car { + + private String winCode; + + public String getWinCode() { + return winCode; + } + + public void setWinCode(String winCode) { + this.winCode = winCode; + } +} + +class CarWrapper { + + private T car; + + public T getCar() { + return car; + } + + public void setCar(T car) { + this.car = car; + } +} diff --git a/testData/mapping/GenericCarWrapperSourceAutoCompleteAfterCarWithoutParameterPrefix.java b/testData/mapping/GenericCarWrapperSourceAutoCompleteAfterCarWithoutParameterPrefix.java new file mode 100644 index 00000000..94275144 --- /dev/null +++ b/testData/mapping/GenericCarWrapperSourceAutoCompleteAfterCarWithoutParameterPrefix.java @@ -0,0 +1,45 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.complex; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper +public interface GenericCarWrapperSourceAutoCompleteAfterCarWithoutParameterPrefix { + @Mapping(target = "id", source = "car.winCode") + CarEntity toCarDto(CarWrapper wrapper); +} + +class CarEntity { + private String id; + public String getId() { + return id; + } + public void setId(String id) { + this.id = id; + } +} + +class Car { + private String winCode; + public String getWinCode() { + return winCode; + } + public void setWinCode(String winCode) { + this.winCode = winCode; + } +} + +class CarWrapper { + private T car; + public T getCar() { + return car; + } + public void setCar(T car) { + this.car = car; + } +} diff --git a/testData/mapping/GenericCarWrapperTargetAutoCompleteAfterCar.java b/testData/mapping/GenericCarWrapperTargetAutoCompleteAfterCar.java new file mode 100644 index 00000000..6bf5a034 --- /dev/null +++ b/testData/mapping/GenericCarWrapperTargetAutoCompleteAfterCar.java @@ -0,0 +1,56 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.complex; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +@Mapper +public interface GenericCarWrapperTargetMapper { + + @Mapping(source = "id", target = "wrapper.car.winCode") + void update(@MappingTarget CarWrapper wrapper, CarEntity entity); +} + +class CarEntity { + + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} + +class Car { + + private String winCode; + + public String getWinCode() { + return winCode; + } + + public void setWinCode(String winCode) { + this.winCode = winCode; + } +} + +class CarWrapper { + + private T car; + + public T getCar() { + return car; + } + + public void setCar(T car) { + this.car = car; + } +} diff --git a/testData/mapping/GenericConstructorCarMapper.java b/testData/mapping/GenericConstructorCarMapper.java new file mode 100644 index 00000000..329226d5 --- /dev/null +++ b/testData/mapping/GenericConstructorCarMapper.java @@ -0,0 +1,39 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.complex; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper +public interface GenericConstructorCarMapper { + @Mapping(target = "car.winCode", source = "winCode") + CarWrapperTarget toCarTarget(CarSource source); +} + +class CarSource { + private String winCode; + + public String getWinCode() { + return winCode; + } + + public void setWinCode(String winCode) { + this.winCode = winCode; + } +} + +class CarWrapperTarget { + private final T car; + + public CarWrapperTarget(T car) { + this.car = car; + } + + public T getCar() { + return car; + } +} diff --git a/testData/mapping/GenericConstructorMappingTargetUpdateMapper.java b/testData/mapping/GenericConstructorMappingTargetUpdateMapper.java new file mode 100644 index 00000000..daef83ee --- /dev/null +++ b/testData/mapping/GenericConstructorMappingTargetUpdateMapper.java @@ -0,0 +1,46 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.complex; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +@Mapper +public interface GenericConstructorMappingTargetUpdateMapper { + @Mapping(target = "car.winCode", source = "winCode") + void update(@MappingTarget CarWrapperTarget target, CarSource source); +} + +class CarSource { + private String winCode; + public String getWinCode() { + return winCode; + } + public void setWinCode(String winCode) { + this.winCode = winCode; + } +} + +class Car { + private String winCode; + public String getWinCode() { + return winCode; + } + public void setWinCode(String winCode) { + this.winCode = winCode; + } +} + +class CarWrapperTarget { + private final T car; + public CarWrapperTarget(T car) { + this.car = car; + } + public T getCar() { + return car; + } +} diff --git a/testData/mapping/GenericNestedGenericTargetMapper.java b/testData/mapping/GenericNestedGenericTargetMapper.java new file mode 100644 index 00000000..2105e862 --- /dev/null +++ b/testData/mapping/GenericNestedGenericTargetMapper.java @@ -0,0 +1,68 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.complex; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper +public interface GenericNestedGenericTargetMapper { + + @Mapping(target = "wrapper.car.winCode", source = "winCode") + OuterWrapper> toTarget(CarSource source); +} + +class CarSource { + + private String winCode; + + public String getWinCode() { + return winCode; + } + + public void setWinCode(String winCode) { + this.winCode = winCode; + } +} + +class Car { + + private String winCode; + + public String getWinCode() { + return winCode; + } + + public void setWinCode(String winCode) { + this.winCode = winCode; + } +} + +class CarWrapper { + + private final T car; + + public CarWrapper(T car) { + this.car = car; + } + + public T getCar() { + return car; + } +} + +class OuterWrapper { + + private final T wrapper; + + public OuterWrapper(T wrapper) { + this.wrapper = wrapper; + } + + public T getWrapper() { + return wrapper; + } +} diff --git a/testData/mapping/GenericRecordConstructorTargetMapper.java b/testData/mapping/GenericRecordConstructorTargetMapper.java new file mode 100644 index 00000000..3cf6f7e6 --- /dev/null +++ b/testData/mapping/GenericRecordConstructorTargetMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.complex; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper +public interface GenericRecordConstructorTargetMapper { + @Mapping(target = "car.winCode", source = "winCode") + CarWrapperTarget toCarTarget(CarSource source); +} + +class CarSource { + private String winCode; + public String getWinCode() { + return winCode; + } + public void setWinCode(String winCode) { + this.winCode = winCode; + } +} + +record Car(String winCode) { +} + +record CarWrapperTarget(T car) { +} diff --git a/testData/mapping/GenericTwoTypeParamsConstructorTargetMapper.java b/testData/mapping/GenericTwoTypeParamsConstructorTargetMapper.java new file mode 100644 index 00000000..9ba1ab7f --- /dev/null +++ b/testData/mapping/GenericTwoTypeParamsConstructorTargetMapper.java @@ -0,0 +1,84 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.complex; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper +public interface GenericTwoTypeParamsConstructorTargetMapper { + + @Mapping(target = "car.winCode", source = "winCode") + @Mapping(target = "engine.serial", source = "serial") + PairWrapperTarget toTarget(CarEngineSource source); +} + +class CarEngineSource { + + private String winCode; + private String serial; + + public String getWinCode() { + return winCode; + } + + public void setWinCode(String winCode) { + this.winCode = winCode; + } + + public String getSerial() { + return serial; + } + + public void setSerial(String serial) { + this.serial = serial; + } +} + +class Car { + + private String winCode; + + public String getWinCode() { + return winCode; + } + + public void setWinCode(String winCode) { + this.winCode = winCode; + } +} + +class Engine { + + private String serial; + + public String getSerial() { + return serial; + } + + public void setSerial(String serial) { + this.serial = serial; + } +} + +class PairWrapperTarget { + + private final A car; + private final B engine; + + public PairWrapperTarget(A car, B engine) { + this.car = car; + this.engine = engine; + } + + public A getCar() { + return car; + } + + public B getEngine() { + return engine; + } +}