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..66e9e110 100644 --- a/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructBaseReference.java +++ b/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructBaseReference.java @@ -18,6 +18,7 @@ import com.intellij.psi.PsiReferenceExpression; import com.intellij.psi.PsiType; import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiUtil; import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -148,6 +149,24 @@ public final Object[] getVariants() { @Nullable abstract PsiType resolvedType(); + /** + * Substitutes type parameters in {@code memberType} using the generic context from the previous reference in the + * property chain. + */ + @Nullable + protected PsiType substituteMemberType(@Nullable PsiType memberType) { + if ( memberType == null || previous == null ) { + return memberType; + } + + PsiType ownerType = previous.resolvedType(); + if ( ownerType == null ) { + return memberType; + } + + return PsiUtil.resolveGenericsClassInType( ownerType ).getSubstitutor().substitute( memberType ); + } + @Override public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException { PsiElement reference = 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..5dfae22f 100644 --- a/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructSourceReference.java +++ b/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructSourceReference.java @@ -135,16 +135,16 @@ PsiType resolvedType() { PsiElement element = resolve(); if ( element instanceof PsiMethod psiMethod ) { - return psiMethod.getReturnType(); + return substituteMemberType( psiMethod.getReturnType() ); } else if ( element instanceof PsiParameter psiParameter ) { return psiParameter.getType(); } else if ( element instanceof PsiRecordComponent psiRecordComponent ) { - return psiRecordComponent.getType(); + return substituteMemberType( psiRecordComponent.getType() ); } else if ( element instanceof PsiField psiField ) { - return psiField.getType(); + return substituteMemberType( psiField.getType() ); } return null; 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..b727c2f0 100644 --- a/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructTargetReference.java +++ b/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructTargetReference.java @@ -205,16 +205,16 @@ PsiType resolvedType() { PsiElement element = resolve(); if ( element instanceof PsiMethod psiMethod ) { - return firstParameterPsiType( psiMethod ); + return substituteMemberType( firstParameterPsiType( psiMethod ) ); } else if ( element instanceof PsiParameter psiParameter ) { return psiParameter.getType(); } else if ( element instanceof PsiRecordComponent psiRecordComponent ) { - return psiRecordComponent.getType(); + return substituteMemberType( psiRecordComponent.getType() ); } else if ( element instanceof PsiField psiField ) { - return psiField.getType(); + return substituteMemberType( psiField.getType() ); } return null; diff --git a/src/test/java/org/mapstruct/intellij/MapstructBaseCompletionTestCase.java b/src/test/java/org/mapstruct/intellij/MapstructBaseCompletionTestCase.java index 1a48ff9b..08757cd9 100644 --- a/src/test/java/org/mapstruct/intellij/MapstructBaseCompletionTestCase.java +++ b/src/test/java/org/mapstruct/intellij/MapstructBaseCompletionTestCase.java @@ -5,16 +5,15 @@ */ package org.mapstruct.intellij; -import java.io.File; - 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; import org.jetbrains.annotations.NotNull; +import java.io.File; + /** * Base completion test case for MapStruct. * @@ -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..dfc4e857 100644 --- a/src/test/java/org/mapstruct/intellij/MapstructCompletionTestCase.java +++ b/src/test/java/org/mapstruct/intellij/MapstructCompletionTestCase.java @@ -488,6 +488,36 @@ 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 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/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; + } +}